信令基础:信令的作用、信令协议设计(JSON over WebSocket)、信令服务器搭建(Node.js + Socket.IO)

聊到 WebRTC,很多人第一反应是「浏览器之间直接传视频,不需要服务器」。这话对了一半。媒体数据确实走的是 P2P 通道,但建立这个通道之前,有个关键角色——信令

说白了,信令就是让两个浏览器先「认识一下」的过程。你想想看,A 要跟 B 通话,A 得先知道 B 在哪、B 支持什么编解码、B 的网络环境怎么样。这些信息怎么交换?靠信令。

核心观点:WebRTC 规范只定义了媒体传输,信令部分完全交给开发者自己设计。这是坑,也是自由。

信令到底在做什么?

我刚开始做 WebRTC 时,以为信令就是传个「你好,我要通话」的消息。后来踩了坑才发现,信令要干的事比想象中多得多。

具体来说,信令负责这几件事:

  • 会话控制:谁发起通话?谁挂断?能不能加人?
  • 媒体协商:交换 SDP(Session Description Protocol),告诉对方「我支持 H264,分辨率 1280x720,音频用 Opus」。
  • 网络协商:交换 ICE 候选者(Candidate),告诉对方「我可能在 192.168.1.100,也可能在公网 8.8.8.8:3478」。
  • 房间管理:谁在哪个房间?房间里有几个人?

嗯,这里要注意:SDP 和 ICE 候选者都是 WebRTC 内部生成的,你不需要自己构造。你要做的只是把它们通过信令通道传给对方。

为什么选 JSON over WebSocket?

信令协议的设计,说白了就是选「消息格式」和「传输通道」。我个人习惯用 JSON over WebSocket,原因很简单:

方案 优点 缺点
JSON over WebSocket 双向实时、结构清晰、调试方便 需要自己处理重连
HTTP 轮询 实现简单 延迟高、浪费带宽
WebSocket + Protobuf 体积小、解析快 调试麻烦、开发成本高

我在项目中遇到过用 Protobuf 做信令的团队,最后调试时不得不写个 JSON 转换器。何必呢?WebRTC 信令本身消息量不大,JSON 完全够用。

我的建议:先用 JSON 快速验证,如果真遇到性能瓶颈再换二进制协议。大多数场景下,JSON 就是最优解。

信令消息结构设计

信令消息不需要太复杂。我一般用这样的结构:

{
  "type": "offer",        // 消息类型
  "from": "user_a",       // 发送者
  "to": "user_b",         // 接收者(可选,用于点对点)
  "room": "room_123",     // 房间 ID
  "data": { ... }         // 具体内容(SDP、ICE 等)
}

常用的消息类型:

  • join / leave — 加入/离开房间
  • offer / answer — 交换 SDP
  • ice_candidate — 交换 ICE 候选者
  • hangup — 挂断通话

你可能会问:为什么不用现成的信令协议?比如 SIP?嗯,SIP 太重量级了,而且跟 WebRTC 的浏览器环境不太搭。JSON over WebSocket 轻量、灵活,更适合前端场景。

信令服务器搭建:Node.js + Socket.IO

选 Node.js 是因为它跟 JavaScript 一脉相承,前端工程师上手快。Socket.IO 则封装了 WebSocket 的重连、房间、广播等机制,省了不少事。

先装依赖:

npm init -y
npm install socket.io express

然后写一个最简单的信令服务器:

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: { origin: '*' }
});

// 房间管理:记录每个房间的用户
const rooms = {};

io.on('connection', (socket) => {
  console.log(`用户连接: ${socket.id}`);

  // 加入房间
  socket.on('join', (roomId) => {
    socket.join(roomId);
    if (!rooms[roomId]) rooms[roomId] = [];
    rooms[roomId].push(socket.id);

    // 通知房间内其他用户
    socket.to(roomId).emit('user_joined', socket.id);
    console.log(`${socket.id} 加入了房间 ${roomId}`);
  });

  // 转发 offer
  socket.on('offer', (data) => {
    socket.to(data.target).emit('offer', {
      from: socket.id,
      sdp: data.sdp
    });
  });

  // 转发 answer
  socket.on('answer', (data) => {
    socket.to(data.target).emit('answer', {
      from: socket.id,
      sdp: data.sdp
    });
  });

  // 转发 ICE 候选者
  socket.on('ice_candidate', (data) => {
    socket.to(data.target).emit('ice_candidate', {
      from: socket.id,
      candidate: data.candidate
    });
  });

  // 离开房间
  socket.on('leave', (roomId) => {
    socket.leave(roomId);
    if (rooms[roomId]) {
      rooms[roomId] = rooms[roomId].filter(id => id !== socket.id);
    }
    socket.to(roomId).emit('user_left', socket.id);
  });

  socket.on('disconnect', () => {
    // 清理所有房间
    for (const roomId in rooms) {
      rooms[roomId] = rooms[roomId].filter(id => id !== socket.id);
      socket.to(roomId).emit('user_left', socket.id);
    }
    console.log(`用户断开: ${socket.id}`);
  });
});

server.listen(3000, () => {
  console.log('信令服务器运行在 http://localhost:3000');
});
注意:这段代码只是演示用。生产环境一定要加身份验证、房间密码、消息校验等安全措施。我曾经见过有人直接把信令服务器暴露在公网上,结果被刷了十几万条垃圾消息。

信令流程:从连接到通话

完整的信令流程大概是这样的:

  1. 用户 A 连接信令服务器,加入房间「room_123」。
  2. 用户 B 也连接服务器,加入同一个房间。
  3. 服务器通知 A:「B 来了」。
  4. A 创建 PeerConnection,生成 offer(SDP),通过信令发给 B。
  5. B 收到 offer,创建自己的 PeerConnection,生成 answer,发回给 A。
  6. 双方交换 ICE 候选者,找到最优路径。
  7. 连接建立,媒体开始传输。

这个过程看起来步骤多,但实际代码量很少。信令服务器只负责转发,不碰媒体数据。

信令流程核心图

下面这张图展示了信令在 WebRTC 通话中的位置和作用:

WebRTC 信令流程 用户 A(浏览器) PeerConnection 信令服务器 Node.js + Socket.IO 用户 B(浏览器) PeerConnection offer / answer ICE 候选者 转发消息 媒体通道(P2P) 音频 / 视频 / 数据通道 信令步骤: ① A 加入房间 → ② B 加入房间 → ③ A 创建 offer 并发送 ④ 服务器转发 offer 给 B → ⑤ B 创建 answer 并发送 ⑥ 服务器转发 answer 给 A → ⑦ 交换 ICE 候选者 → ⑧ P2P 连接建立 信令通道 媒体通道

避坑指南

信令这部分坑不少,我挑几个常见的说说:

  • 消息顺序问题:WebSocket 虽然是顺序的,但网络延迟可能导致 offer 比 join 先到。我习惯在客户端加一个状态机,确保消息按流程处理。
  • 房间清理:用户断线后一定要清理房间列表。否则下次别人加入时,会发现房间里有个「幽灵用户」。
  • ICE 候选者积压:如果网络环境复杂,ICE 候选者可能很多。不要一股脑全转发,可以等收集完再批量发送。
  • 不要信任客户端:所有信令消息都要在服务端做校验。我曾经遇到过有人伪造「加入房间」消息,把别人的通话搅黄了。
小技巧:开发阶段可以用 Socket.IO 的调试模式 io.set('log level', 3),能看到所有消息的收发情况。上线前记得关掉。

总结

信令是 WebRTC 应用的「粘合剂」。它不负责传输媒体,但决定了整个通话流程能否顺畅运行。JSON over WebSocket 是目前最实用的方案,Node.js + Socket.IO 能让你快速搭建一个可用的信令服务器。

记住一句话:信令设计得越简单,后期维护越省心。别想着一步到位搞个分布式高可用信令集群,先跑通再说。


公众号:蓝海资料掘金营,微信 deep3321