3、信令服务器搭建:WebSocket基础、Node.js信令服务器实现、房间管理逻辑、信令消息协议设计
信令服务器,说白了就是 WebRTC 通话的「总调度室」。没有它,两个浏览器之间连招呼都打不上。我记得刚接触 WebRTC 时,以为只要拿到媒体流就能直接 P2P 通信,结果发现连对方的 IP 地址都不知道——嗯,这就是信令要解决的问题。
3.1 WebSocket 基础:为什么选它?
WebRTC 本身不规定信令传输方式。你可以用 HTTP 轮询、长轮询、甚至用鸽子送信(开玩笑)。但实际项目中,WebSocket 几乎是唯一选择。为什么?
- 全双工:服务器可以主动推消息给客户端,不用等客户端轮询。
- 低延迟:建立连接后,消息走 TCP 长连接,省去 HTTP 握手开销。
- 浏览器原生支持:WebSocket API 是 W3C 标准,不用额外库。
我在项目中遇到过用 Socket.IO 做信令的团队,后来发现多了一层自定义协议,调试时反而麻烦。我个人习惯直接用原生 WebSocket,简单可控。
小提示:WebSocket 的 URL 使用 ws:// 或 wss://(加密)。生产环境务必用 wss://,否则浏览器会报安全错误。
3.2 Node.js 信令服务器实现
我们用 Node.js 的 ws 库来实现。安装很简单:
npm install ws
一个最简的信令服务器长这样:
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', (ws) => {
console.log('新客户端连接');
ws.on('message', (message) => {
const data = JSON.parse(message);
// 处理信令消息
handleSignaling(data, ws);
});
ws.on('close', () => {
console.log('客户端断开');
// 清理房间状态
});
});
function handleSignaling(data, ws) {
// 根据消息类型分发
switch (data.type) {
case 'join':
// 加入房间逻辑
break;
case 'offer':
// 转发 SDP offer
break;
case 'ice-candidate':
// 转发 ICE 候选
break;
default:
console.warn('未知消息类型:', data.type);
}
}
你想想看,这个服务器本身不处理媒体流,它只做一件事:转发。把 A 的 SDP 和 ICE 候选转发给 B,把 B 的转发给 A。
注意:千万不要在信令服务器里做媒体数据的编解码或混音。信令服务器只负责控制信令,不做媒体处理。我曾经见过有人试图在信令服务器里做音频转码,结果延迟飙升到 2 秒以上。
3.3 房间管理逻辑
房间管理是信令服务器的核心。一个房间就是一个通话组,通常包含 2 个或更多参与者。我习惯用 Map 来管理:
// 房间数据结构
const rooms = new Map();
// 房间对象结构
class Room {
constructor(roomId) {
this.roomId = roomId;
this.participants = new Map(); // userId -> WebSocket
this.maxParticipants = 2; // 默认双人通话
}
addParticipant(userId, ws) {
if (this.participants.size >= this.maxParticipants) {
return { success: false, error: '房间已满' };
}
this.participants.set(userId, ws);
return { success: true };
}
removeParticipant(userId) {
this.participants.delete(userId);
if (this.participants.size === 0) {
rooms.delete(this.roomId); // 房间空了就销毁
}
}
broadcast(message, excludeUserId = null) {
this.participants.forEach((ws, userId) => {
if (userId !== excludeUserId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
});
}
}
房间管理的几个关键点:
- 房间创建:第一个用户加入时自动创建。
- 房间销毁:最后一个用户离开时自动清理。
- 容量控制:双人通话限制 2 人,多人会议可以放宽。
- 心跳检测:定期 ping/pong 检测客户端是否存活。
避坑指南:我曾经在生产环境中遇到一个问题:用户断网重连后,旧 WebSocket 连接还在房间里,导致新连接无法加入。解决方案是在用户重连时,先踢掉旧连接再允许新连接加入。
3.4 信令消息协议设计
信令协议就是客户端和服务器之间的「对话规则」。我设计了一套简洁的 JSON 协议:
| 消息类型 | 方向 | 说明 | 关键字段 |
|---|---|---|---|
join |
客户端 → 服务器 | 请求加入房间 | roomId, userId |
joined |
服务器 → 客户端 | 加入成功通知 | roomId, participants |
offer |
客户端 → 服务器 → 对端 | 转发 SDP offer | sdp, targetUserId |
answer |
客户端 → 服务器 → 对端 | 转发 SDP answer | sdp, targetUserId |
ice-candidate |
客户端 → 服务器 → 对端 | 转发 ICE 候选 | candidate, targetUserId |
leave |
客户端 → 服务器 | 离开房间 | roomId |
error |
服务器 → 客户端 | 错误通知 | code, message |
消息格式示例:
// 客户端发送 offer
{
"type": "offer",
"roomId": "room-123",
"targetUserId": "user-b",
"sdp": {
"type": "offer",
"sdp": "v=0\r\no=- ..."
}
}
// 服务器转发给目标用户
{
"type": "offer",
"fromUserId": "user-a",
"sdp": {
"type": "offer",
"sdp": "v=0\r\no=- ..."
}
}
协议设计原则:
- 类型驱动:每个消息必须有
type字段,服务器根据类型分发。 - 目标明确:转发类消息必须指定
targetUserId,避免广播给所有人。 - 错误可追溯:错误消息带
code和message,方便客户端定位问题。 - 版本兼容:在消息中加入
version字段,方便协议升级。
经验之谈:我建议在协议中加入
timestamp 字段,用于调试时追踪消息延迟。另外,ICE 候选可能很多(尤其是移动端),可以考虑批量发送,减少信令消息数量。
3.5 信令流程全景图
下面这张图展示了完整的信令交互流程:
流程总结:
- 加入房间:A 发送
join,服务器通知 B 有新用户。 - SDP 交换:A 创建 offer,经服务器转发给 B;B 创建 answer,经服务器转发给 A。
- ICE 候选交换:双方收集 ICE 候选,通过服务器转发给对方。
- P2P 连接建立:双方 ICE 协商成功,媒体流直连。
核心要点:信令服务器只做转发,不做媒体处理。SDP 和 ICE 候选的生成与解析完全在客户端完成。服务器一旦完成转发任务,后续媒体流就走 P2P 了,信令服务器不再参与。
3.6 实战中的坑与优化
最后分享几个我在实际项目中踩过的坑:
- WebSocket 重连:客户端断网后要自动重连,重连后重新加入房间。我建议用指数退避策略,避免服务器被打爆。
- 消息顺序:WebSocket 保证 TCP 顺序,但多个客户端之间的消息到达顺序不确定。比如 B 的
answer可能比 A 的ice-candidate先到。客户端要做好状态机,忽略乱序消息。 - 房间超时清理:如果用户异常断开(比如直接关浏览器),服务器可能收不到
close事件。我习惯加一个定时器,定期清理长时间无心跳的房间。 - 日志记录:每个信令消息都打日志,包括时间戳、消息类型、用户 ID。排查问题时,日志就是你的救命稻草。
重要:生产环境的信令服务器一定要做鉴权。最简单的做法是在
join 消息里带上 token,服务器验证通过后才允许加入房间。否则任何人都能乱入你的通话。