24、信令服务器:WebSocket信令实现、房间管理、用户加入/离开通知
好,咱们今天来聊聊信令服务器。说实话,很多做WebRTC的同学,一开始都把精力花在音视频流上,觉得信令就是个传消息的,没什么技术含量。我以前也这么想,直到有一次线上环境出了个诡异的bug——用户A能看到B,但B看不到A,查了半天才发现是信令消息的顺序乱了。嗯,从那以后,我再也不敢小看信令服务器了。
信令服务器说白了就是WebRTC的“调度中心”。它不传音视频数据,但它负责让两个浏览器“认识”对方。没有它,你的摄像头和麦克风再好也白搭。
24.1 为什么需要信令服务器?
WebRTC的P2P连接,有个很尴尬的问题:两个浏览器在公网上,怎么找到对方?你想想看,你家电脑的IP是私有的,对方也是私有的,中间隔着NAT、防火墙。这时候就需要一个“中间人”来交换信息。
这个中间人就是信令服务器。它主要干三件事:
- 交换SDP:告诉对方你支持什么编码、什么分辨率
- 交换ICE候选:告诉对方你的公网IP和端口
- 管理房间:谁进来了、谁出去了、房间里都有谁
我个人的习惯是,把信令服务器和业务服务器分开。信令只做信令的事,别掺和业务逻辑。这样出了问题,排查起来也快。
24.2 WebSocket信令实现
WebSocket是信令传输最常用的方式。为什么?因为它全双工、低延迟,而且浏览器原生支持。我见过有人用HTTP轮询做信令,结果延迟高得离谱,用户体验极差。
下面是一个简单的WebSocket信令服务器实现,我用Node.js + ws库来写:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 存储所有连接
const clients = new Map();
wss.on('connection', (ws, req) => {
const userId = generateUserId();
clients.set(userId, ws);
console.log(`用户 ${userId} 连接成功`);
// 处理收到的消息
ws.on('message', (data) => {
const message = JSON.parse(data);
handleMessage(userId, message);
});
// 处理断开连接
ws.on('close', () => {
console.log(`用户 ${userId} 断开连接`);
clients.delete(userId);
// 通知房间内其他用户
notifyRoomLeave(userId);
});
});
function handleMessage(userId, message) {
switch (message.type) {
case 'join':
handleJoin(userId, message.roomId);
break;
case 'offer':
forwardMessage(userId, message.targetId, message);
break;
case 'answer':
forwardMessage(userId, message.targetId, message);
break;
case 'ice-candidate':
forwardMessage(userId, message.targetId, message);
break;
default:
console.warn('未知消息类型:', message.type);
}
}
24.3 房间管理
房间管理是信令服务器的核心功能。说白了,就是维护一个“谁在哪个房间”的映射表。我见过有人用全局变量存,也有人用Redis。小项目用内存就够了,但如果是生产环境,我建议用Redis,方便横向扩展。
下面是一个房间管理的数据结构:
// 房间数据结构
const rooms = new Map();
// 创建房间
function createRoom(roomId) {
if (!rooms.has(roomId)) {
rooms.set(roomId, {
id: roomId,
users: new Map(), // userId -> { ws, joinTime }
createdAt: Date.now()
});
console.log(`房间 ${roomId} 创建成功`);
}
return rooms.get(roomId);
}
// 加入房间
function joinRoom(userId, roomId, ws) {
let room = rooms.get(roomId);
if (!room) {
room = createRoom(roomId);
}
// 检查房间是否已满(假设最多4人)
if (room.users.size >= 4) {
ws.send(JSON.stringify({
type: 'error',
message: '房间已满'
}));
return false;
}
room.users.set(userId, { ws, joinTime: Date.now() });
console.log(`用户 ${userId} 加入房间 ${roomId}`);
return true;
}
// 离开房间
function leaveRoom(userId, roomId) {
const room = rooms.get(roomId);
if (!room) return;
room.users.delete(userId);
console.log(`用户 ${userId} 离开房间 ${roomId}`);
// 如果房间没人了,就销毁
if (room.users.size === 0) {
rooms.delete(roomId);
console.log(`房间 ${roomId} 已销毁`);
}
}
24.4 用户加入/离开通知
用户加入或离开房间时,需要通知房间内的其他用户。这个逻辑看似简单,但实现不好容易出问题。我见过最典型的bug是:用户A离开后,用户B收到通知,但用户B的UI没有及时更新,导致B还在跟空气说话。
通知的实现其实就两步:
- 构造通知消息
- 遍历房间内其他用户的WebSocket连接,发送消息
// 通知用户加入
function notifyRoomJoin(newUserId, roomId) {
const room = rooms.get(roomId);
if (!room) return;
const joinMessage = {
type: 'user-joined',
userId: newUserId,
roomId: roomId,
userCount: room.users.size
};
// 遍历房间内所有用户
room.users.forEach((user, userId) => {
if (userId !== newUserId) {
user.ws.send(JSON.stringify(joinMessage));
}
});
// 给新用户发送当前房间用户列表
const userList = Array.from(room.users.keys());
const newUserWs = room.users.get(newUserId).ws;
newUserWs.send(JSON.stringify({
type: 'room-info',
roomId: roomId,
users: userList,
userCount: userList.length
}));
}
// 通知用户离开
function notifyRoomLeave(userId, roomId) {
const room = rooms.get(roomId);
if (!room) return;
const leaveMessage = {
type: 'user-left',
userId: userId,
roomId: roomId,
userCount: room.users.size - 1 // 注意:此时用户还没从房间移除
};
room.users.forEach((user, uid) => {
if (uid !== userId) {
user.ws.send(JSON.stringify(leaveMessage));
}
});
}
24.5 信令消息协议设计
信令消息的格式,我建议统一用JSON。虽然有人用Protobuf,但JSON调试起来方便,而且浏览器原生支持。下面是我常用的消息协议:
| 消息类型 | 方向 | 说明 | 关键字段 |
|---|---|---|---|
| join | 客户端→服务端 | 请求加入房间 | roomId |
| leave | 客户端→服务端 | 请求离开房间 | roomId |
| offer | 客户端→服务端→客户端 | 转发SDP Offer | targetId, sdp |
| answer | 客户端→服务端→客户端 | 转发SDP Answer | targetId, sdp |
| ice-candidate | 客户端→服务端→客户端 | 转发ICE候选 | targetId, candidate |
| user-joined | 服务端→客户端 | 通知有新用户加入 | userId, roomId, userCount |
| user-left | 服务端→客户端 | 通知有用户离开 | userId, roomId, userCount |
| room-info | 服务端→客户端 | 发送房间当前状态 | roomId, users[], userCount |
这个协议看起来简单,但够用了。我见过有人把协议设计得特别复杂,什么消息优先级、消息确认、重传机制都加上去了。其实没必要,WebSocket本身已经保证了消息的有序性和可靠性。你想想看,信令消息丢了,大不了重新连接一次,又不是传音视频流。
24.6 信令服务器的整体流程
为了让你更直观地理解整个信令流程,我画了一张图:
这张图展示了完整的信令流程。从加入房间,到交换SDP,再到交换ICE候选,最后建立P2P连接。每一步都离不开信令服务器的转发。
24.7 避坑指南
最后,分享几个我踩过的坑:
- 消息顺序问题:我曾经遇到过offer还没到,answer先到了的情况。解决方案是在客户端加一个状态机,只有收到offer后才能发answer。
- 连接断开重连:用户网络闪断,WebSocket重连后,房间信息需要重新同步。我建议在重连成功后,客户端主动发一个sync请求,服务端把房间状态全量发回去。
- 多房间并发:一个用户同时加入多个房间?虽然不常见,但最好在服务端做限制,一个用户同一时间只能在一个房间里。
- 日志记录:信令服务器的日志一定要详细。我每次排查问题,第一件事就是看信令日志。谁什么时候加入了哪个房间,发了什么消息,一目了然。
好了,信令服务器的核心内容就这些。说白了,它就是个消息中转站,但要做好也不容易。下一节我们会聊更具体的东西,先把今天的内容消化掉。
公众号:蓝海资料掘金营,微信deep3321