5、信令服务器实战(Node.js):WebSocket信令设计、房间管理、用户加入/离开逻辑、信令消息协议定义
聊到WebRTC,很多人第一反应是P2P、是音视频流。但有个东西绕不开——信令服务器。说白了,它就是整个通话的“总机”。没有它,两个浏览器连对方在哪都不知道。
我个人习惯把信令服务器比作“媒人”。它不参与实际的音视频数据传输,但负责牵线搭桥。今天我们就用Node.js把它撸出来。
5.1 为什么非得要信令?
你想想看,浏览器A要和浏览器B建立P2P连接。A得先知道B在哪吧?但浏览器没有公网IP,也不知道对方的网络情况。这时候就需要一个中间人——信令服务器。
它负责交换SDP(会话描述协议)和ICE候选地址。这些信息交换完了,P2P连接建立,信令服务器就可以退居二线了。
我在项目中遇到过一个问题:有人试图把信令服务器做成“万能中转站”,连音视频数据都走它。结果服务器带宽直接被打满,延迟高得离谱。记住,信令只传控制信息,不传媒体数据。
5.2 WebSocket vs HTTP轮询
信令传输有两种主流方式:WebSocket和HTTP轮询。我个人强烈推荐WebSocket。为什么?
- 实时性:WebSocket是全双工通信,服务器可以主动推送消息。HTTP轮询需要客户端不断请求,延迟高、浪费带宽。
- 连接持久:WebSocket建立后保持连接,适合频繁交换SDP和ICE候选的场景。
- 开销小:WebSocket头部只有2字节,HTTP请求头动不动几百字节。
我在早期的一个项目中用过HTTP轮询,结果用户一多,服务器CPU直接飙到90%。换成WebSocket后,同样的机器扛住了三倍的用户量。
5.3 信令消息协议定义
这是整个信令设计的核心。没有统一的协议,客户端和服务器就是鸡同鸭讲。我习惯用JSON格式,结构清晰,扩展方便。
下面是我常用的消息协议定义:
{
"type": "message_type", // 消息类型
"from": "sender_id", // 发送者ID
"to": "receiver_id", // 接收者ID(可选,广播时省略)
"room": "room_id", // 房间ID
"data": {} // 具体数据
}
消息类型我定义为以下几种:
| 消息类型 | 方向 | 说明 |
|---|---|---|
| join | 客户端→服务器 | 用户请求加入房间 |
| joined | 服务器→客户端 | 通知用户已加入房间 |
| leave | 客户端→服务器 | 用户请求离开房间 |
| leaved | 服务器→客户端 | 通知用户已离开房间 |
| offer | 客户端→客户端 | 发送SDP offer |
| answer | 客户端→客户端 | 发送SDP answer |
| ice_candidate | 客户端→客户端 | 发送ICE候选地址 |
| user_joined | 服务器→客户端 | 通知房间内其他用户有新用户加入 |
| user_leaved | 服务器→客户端 | 通知房间内其他用户有用户离开 |
5.4 房间管理逻辑
房间是WebRTC通话的基本单元。一个房间就是一个通话组。我设计房间管理时,主要考虑以下几点:
- 房间创建:第一个用户加入时自动创建房间
- 房间销毁:最后一个用户离开时自动销毁房间
- 房间容量:限制每个房间的最大用户数(通常2-4人)
- 房间ID:支持自定义或自动生成
下面是我常用的房间数据结构:
class Room {
constructor(roomId) {
this.roomId = roomId;
this.users = new Map(); // key: userId, value: WebSocket连接
this.createdAt = Date.now();
}
addUser(userId, ws) {
this.users.set(userId, ws);
}
removeUser(userId) {
this.users.delete(userId);
}
getUserCount() {
return this.users.size;
}
broadcast(message, excludeUserId = null) {
this.users.forEach((ws, userId) => {
if (userId !== excludeUserId) {
ws.send(JSON.stringify(message));
}
});
}
}
5.5 用户加入/离开逻辑
用户加入和离开是信令服务器最核心的两个操作。处理不好,整个通话系统就会出问题。
用户加入流程
- 客户端发送
join消息,包含房间ID和用户ID - 服务器检查房间是否存在,不存在则创建
- 服务器检查房间是否已满,已满则拒绝加入
- 服务器将用户加入房间,保存WebSocket连接
- 服务器向该用户发送
joined消息,包含房间内现有用户列表 - 服务器向房间内其他用户广播
user_joined消息
用户离开流程
- 客户端发送
leave消息,或WebSocket连接断开 - 服务器从房间中移除该用户
- 服务器向房间内其他用户广播
user_leaved消息 - 如果房间内没有用户了,销毁房间
这里有个坑:WebSocket断开不一定是用户主动离开,可能是网络波动。我建议加一个心跳机制,客户端每隔30秒发一次ping,服务器回复pong。如果连续3次没收到心跳,再判定用户离线。
5.6 核心代码实现
下面是一个简化版的信令服务器核心逻辑。我用的是Node.js + ws库,轻量又好用。
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const rooms = new Map(); // key: roomId, value: Room实例
wss.on('connection', (ws) => {
let currentRoom = null;
let currentUserId = null;
ws.on('message', (message) => {
const msg = JSON.parse(message);
switch (msg.type) {
case 'join':
handleJoin(ws, msg);
break;
case 'leave':
handleLeave(ws);
break;
case 'offer':
case 'answer':
case 'ice_candidate':
forwardMessage(msg);
break;
default:
console.warn('未知消息类型:', msg.type);
}
});
ws.on('close', () => {
handleLeave(ws);
});
function handleJoin(ws, msg) {
const { room: roomId, from: userId } = msg;
if (!rooms.has(roomId)) {
rooms.set(roomId, new Room(roomId));
}
const room = rooms.get(roomId);
if (room.getUserCount() >= 4) {
ws.send(JSON.stringify({ type: 'error', data: '房间已满' }));
return;
}
room.addUser(userId, ws);
currentRoom = roomId;
currentUserId = userId;
// 通知用户加入成功
ws.send(JSON.stringify({
type: 'joined',
room: roomId,
data: { users: Array.from(room.users.keys()) }
}));
// 广播给其他用户
room.broadcast({
type: 'user_joined',
room: roomId,
data: { userId: userId }
}, userId);
}
function handleLeave(ws) {
if (!currentRoom || !currentUserId) return;
const room = rooms.get(currentRoom);
if (!room) return;
room.removeUser(currentUserId);
// 广播给其他用户
room.broadcast({
type: 'user_leaved',
room: currentRoom,
data: { userId: currentUserId }
});
// 房间空了就销毁
if (room.getUserCount() === 0) {
rooms.delete(currentRoom);
}
currentRoom = null;
currentUserId = null;
}
function forwardMessage(msg) {
if (!currentRoom) return;
const room = rooms.get(currentRoom);
if (!room) return;
const targetWs = room.users.get(msg.to);
if (targetWs) {
targetWs.send(JSON.stringify(msg));
}
}
});
5.7 信令服务器架构图
下面我用一张SVG图来展示信令服务器的整体架构和消息流转:
这张图展示了信令服务器的核心架构。客户端A和B通过WebSocket连接到信令服务器。服务器内部维护房间管理和消息转发两个模块。所有信令消息都经过服务器中转,但音视频数据是P2P直连的。
5.8 总结与避坑
信令服务器看起来简单,但实际开发中坑不少。我总结几个经验:
- 连接状态管理:一定要处理好WebSocket的open、close、error事件。用户断网重连时,要能恢复之前的房间状态。
- 消息顺序:SDP和ICE候选的到达顺序很重要。我建议客户端在收到所有ICE候选后再开始建立连接,避免丢包。
- 安全性:生产环境一定要加鉴权。我见过有人直接把信令服务器暴露在公网上,结果被恶意用户刷爆了房间。
- 日志:信令服务器的日志要详细。每次用户加入、离开、转发消息都要记录。排查问题的时候,日志就是救命稻草。
公众号:蓝海资料掘金营,微信deep3321