搭建第一个信令服务器:为什么需要信令、WebSocket 基础、Node.js 搭建简易信令服务器、信令消息格式设计
好,咱们正式开始动手了。这一章,我们要搞定整个 WebRTC 应用的“神经中枢”——信令服务器。
你可能会有疑问:WebRTC 不是号称点对点通信吗?为什么还要搞个服务器?嗯,这个问题我当年刚接触时也困惑过。说白了,两个浏览器之间要建立连接,总得先互相认识一下吧?信令服务器就是那个“介绍人”。
为什么需要信令?
WebRTC 的媒体和数据通道确实是 P2P 的,但建立连接的过程,双方需要交换一些“元信息”。这些信息包括:
- 会话描述(SDP):你的浏览器支持哪些编解码器?网络带宽多少?这些信息编码在 SDP 里。
- 网络候选(ICE Candidate):你的公网 IP 是什么?用了什么端口?有没有 NAT 穿透能力?
这些信息必须通过一个双方都能访问的“中间人”来转发。这个中间人,就是信令服务器。
核心观点:信令服务器只负责“牵线搭桥”,不负责传输媒体数据。一旦连接建立,它就可以功成身退了。
我在项目中遇到过一种情况:有人试图把媒体数据也通过信令服务器转发,结果服务器瞬间崩溃。记住,信令只传控制信息,别让它干重活。
WebSocket 基础
选什么协议来实现信令?HTTP 行不行?行,但不推荐。为什么呢?
HTTP 是“请求-响应”模式,客户端问一句,服务器答一句。但信令通信往往是双向的、实时的——比如对方突然断线了,服务器得主动通知你。HTTP 做这个就很别扭,得轮询,效率低。
WebSocket 就不同了。它建立一次连接后,双方可以随时互发消息。这简直就是为信令量身定做的。
小提示:WebSocket 的 ws:// 协议和 HTTP 的 80 端口可以共用,部署起来很方便。生产环境建议用 wss://(加密版),就像 HTTPS 一样。
WebSocket 的消息格式很简单,就是字符串或二进制数据。我们通常用 JSON 字符串来封装信令消息,这样解析方便,结构清晰。
Node.js 搭建简易信令服务器
好,理论说完了,咱们直接上代码。我用 Node.js 加一个叫 ws 的库来搭服务器。这个库很轻量,几行代码就能跑起来。
先安装依赖:
npm init -y
npm install ws
然后写一个 server.js:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 存储所有连接的客户端
const clients = new Map();
wss.on('connection', (ws, req) => {
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
clients.set(id, ws);
console.log(`客户端 ${id} 已连接`);
// 通知客户端它的 ID
ws.send(JSON.stringify({ type: 'welcome', id }));
ws.on('message', (data) => {
try {
const msg = JSON.parse(data);
handleMessage(id, ws, msg);
} catch (e) {
console.error('消息解析失败:', e);
}
});
ws.on('close', () => {
clients.delete(id);
console.log(`客户端 ${id} 已断开`);
// 通知其他客户端
broadcast({ type: 'peer-disconnected', id });
});
});
function handleMessage(senderId, ws, msg) {
switch (msg.type) {
case 'join-room':
// 加入房间逻辑
break;
case 'offer':
case 'answer':
case 'ice-candidate':
// 转发给目标客户端
const target = clients.get(msg.targetId);
if (target) {
target.send(JSON.stringify({ ...msg, senderId }));
}
break;
default:
console.log('未知消息类型:', msg.type);
}
}
function broadcast(msg) {
const str = JSON.stringify(msg);
clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(str);
}
});
}
console.log('信令服务器已启动,端口: 8080');
这段代码做了几件事:
- 每个客户端连接时,分配一个唯一 ID
- 收到消息后,根据
type字段做不同处理 - 如果是
offer、answer、ice-candidate,就转发给目标客户端 - 客户端断开时,通知其他人
注意:这个服务器没有做房间管理,所有客户端都在一个“大房间”里。实际项目中,你需要加房间逻辑,不然人一多就乱套了。
我曾经在一个 demo 里忘了加房间隔离,结果三个浏览器同时测试,信令全串了。嗯,从那以后我写信令服务器第一件事就是加房间。
信令消息格式设计
消息格式看起来简单,但设计不好后面会很难受。我建议统一用 JSON 对象,包含以下几个核心字段:
| 字段 | 类型 | 说明 |
|---|---|---|
type |
string | 消息类型,如 offer、answer、ice-candidate |
senderId |
string | 发送者 ID |
targetId |
string | 目标客户端 ID(如果是点对点消息) |
data |
object | 具体内容,比如 SDP 或 ICE 候选 |
roomId |
string | 房间 ID(可选,用于房间隔离) |
举个例子,一个 offer 消息长这样:
{
"type": "offer",
"senderId": "abc123",
"targetId": "def456",
"roomId": "room-001",
"data": {
"sdp": "v=0\r\no=- ...",
"type": "offer"
}
}
为什么要把 senderId 和 targetId 分开?因为服务器转发时需要知道谁发的、发给谁。你想想看,如果只传 targetId,服务器还得自己猜发送者是谁,容易出错。
我的习惯:所有消息都带上 roomId,哪怕当前只有一个房间。这样以后扩展多房间时,代码不用大改。
信令流程全景图
下面这张图展示了整个信令交互的流程,从客户端连接到建立 P2P 连接:
从图上可以看得很清楚:信令服务器就是个“中转站”,所有信令消息都经过它转发。一旦 P2P 连接建立(第⑩步),数据就不再经过服务器了。
避坑指南:我曾经在生产环境中遇到一个问题——ICE 候选消息太多,信令服务器成了瓶颈。后来加了消息合并和限流才解决。如果你的应用有很多候选(比如有 10 个以上 ICE 候选),建议批量发送,别一条一条发。
好了,到这里你已经有了一个能跑的信令服务器。虽然简陋,但核心逻辑都在了。下一章我们会在这个基础上,真正把 WebRTC 的数据通道跑起来。