6、信令服务搭建(上):信令的作用与设计原则、WebSocket基础回顾、Node.js搭建简易信令服务器
好,咱们进入第六章。前面几章我们把 WebRTC 的底层原理、媒体协商、ICE 流程都捋了一遍。你可能会想:“道理我都懂,但代码怎么写?”
别急,这一章开始,我们就要动手搭一个真正的信令服务。我个人的习惯是,先搞清楚“为什么需要它”,再动手写代码。这样踩坑的时候,你才知道坑在哪。
6.1 信令到底在干什么?
先问一个问题:WebRTC 能不能自己找到对方?
答案是不能。WebRTC 只负责媒体数据的传输,它不负责“发现对方”。就像两个人打电话,你得先知道对方的号码,才能拨号。信令就是那个“拨号”的过程。
信令的核心职责:
- 交换 SDP(会话描述协议)——告诉对方“我能收什么格式的视频/音频”
- 交换 ICE 候选地址——告诉对方“我有哪些 IP 和端口可以连”
- 管理房间/用户状态——谁进来了、谁出去了
我在项目中遇到过最典型的坑:有人把媒体数据也塞进信令通道里。结果信令服务器直接崩了。记住,信令只传控制信息,不传音视频流。这是设计红线。
6.2 信令服务的设计原则
说白了,信令服务就是一个“中间人”。它不需要多聪明,但必须可靠。我总结了三条原则,你写代码时照着来就行。
| 原则 | 说明 | 我的经验 |
|---|---|---|
| 轻量 | 只做消息转发,不做复杂业务逻辑 | 曾经把用户认证也塞进去,后来拆出来了 |
| 可靠 | 消息必须按顺序到达,不能丢 | WebSocket 天然有序,但要注意重连机制 |
| 无状态 | 服务器不保存会话状态,挂了重启也不怕 | 用 Redis 存房间信息,服务器本身无状态 |
你想想看,如果信令服务器挂了,正在通话的两个人其实不受影响——因为媒体流是 P2P 的。但新用户就进不来了。所以信令服务的高可用,主要影响“接入”阶段。
6.3 WebSocket 基础回顾
WebRTC 的信令通道,最常用的就是 WebSocket。为什么不用 HTTP?因为 HTTP 是“请求-响应”模式,服务器没法主动推消息。而信令需要双向实时通信。
WebSocket 的核心就四个字:全双工。连接建立后,两端随时可以发消息。我刚开始用的时候,总觉得它跟 HTTP 差不多,其实完全不是一回事。
小提示:WebSocket 的 URL 以 ws:// 或 wss:// 开头。wss 是加密版本,生产环境必须用。我见过有人把 ws 直接部署到公网,结果被中间人攻击了。
来看一个最简单的 WebSocket 客户端代码:
// 客户端连接
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
console.log('连接成功');
socket.send('你好,服务器');
};
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
socket.onclose = () => {
console.log('连接关闭');
};
嗯,这里要注意:onmessage 收到的是字符串。如果你要传 JSON,记得 JSON.parse。我曾经犯过一个错,直接拿 event.data 当对象用,结果报了一堆 undefined。
6.4 Node.js 搭建简易信令服务器
好,动手时间。我们用 Node.js 的 ws 库,搭一个最简信令服务器。这个服务器只做一件事:收到消息后,广播给房间里的其他人。
先安装依赖:
npm init -y
npm install ws
然后写服务器代码:
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
// 存储所有连接,按房间分组
const rooms = new Map();
server.on('connection', (ws) => {
console.log('新用户连接');
ws.on('message', (message) => {
const data = JSON.parse(message);
const { type, room, payload } = data;
switch (type) {
case 'join':
// 用户加入房间
if (!rooms.has(room)) {
rooms.set(room, new Set());
}
rooms.get(room).add(ws);
ws.room = room;
console.log(`用户加入房间: ${room}`);
break;
case 'message':
// 转发消息给同房间其他人
const peers = rooms.get(ws.room);
if (peers) {
peers.forEach((peer) => {
if (peer !== ws && peer.readyState === WebSocket.OPEN) {
peer.send(JSON.stringify(payload));
}
});
}
break;
case 'leave':
// 用户离开房间
if (ws.room && rooms.has(ws.room)) {
rooms.get(ws.room).delete(ws);
if (rooms.get(ws.room).size === 0) {
rooms.delete(ws.room);
}
}
break;
}
});
ws.on('close', () => {
// 断开时自动离开房间
if (ws.room && rooms.has(ws.room)) {
rooms.get(ws.room).delete(ws);
if (rooms.get(ws.room).size === 0) {
rooms.delete(ws.room);
}
}
console.log('用户断开');
});
});
console.log('信令服务器已启动,端口: 8080');
注意:这段代码只是演示原理。生产环境你还需要处理:
- 心跳检测(防止僵尸连接)
- 消息格式校验(防止恶意数据)
- 房间密码/权限控制
我曾经在生产环境忘了加心跳,结果一堆死连接占着房间名额,新用户进不来。
6.5 信令流程可视化
为了让你更直观地理解信令的流转过程,我画了一张图。这张图展示了两个浏览器通过信令服务器交换 SDP 和 ICE 候选的完整流程。
这张图你看懂了吗?核心就两点:
- SDP 交换(①-④)是“我能提供什么、我需要什么”的协商
- ICE 候选交换(⑤-⑧)是“我在哪、怎么连我”的地址交换
等这两步走完,浏览器之间就直接建立 P2P 连接了。信令服务器功成身退,后面的事它就不管了。
6.6 避坑指南
最后分享几个我踩过的坑,你写代码时注意避开:
- 消息顺序问题:WebSocket 虽然有序,但如果你同时发多条消息,接收端可能乱序处理。建议每条消息带一个序列号。
- 房间清理:用户断线时一定要清理房间。我曾经忘了做,结果房间里堆满了死连接,新用户加不进来。
- 消息大小限制:SDP 有时候会很大(尤其是带多个视频轨),WebSocket 默认限制是 1MB。如果超过,需要分片发送。
- 不要信任客户端:所有消息格式都要做校验。我见过有人故意发畸形数据,导致服务器崩溃。
好了,这一章我们搞清楚了信令的作用、设计原则,还用 Node.js 搭了一个能跑起来的信令服务器。下一章我们会在这个基础上,加入房间管理和更完善的消息路由。你先把这段代码跑起来,感受一下信令的流转过程。
练习建议:打开两个浏览器窗口,都连上你的信令服务器。在控制台手动发送 join 和 message 消息,看看能不能互相收到。这是理解信令最好的方式。
公众号:蓝海资料掘金营,微信deep3321