信令基础:信令的作用、信令协议设计(JSON over WebSocket)、信令服务器搭建(Node.js + Socket.IO)
聊到 WebRTC,很多人第一反应是「浏览器之间直接传视频,不需要服务器」。这话对了一半。媒体数据确实走的是 P2P 通道,但建立这个通道之前,有个关键角色——信令。
说白了,信令就是让两个浏览器先「认识一下」的过程。你想想看,A 要跟 B 通话,A 得先知道 B 在哪、B 支持什么编解码、B 的网络环境怎么样。这些信息怎么交换?靠信令。
信令到底在做什么?
我刚开始做 WebRTC 时,以为信令就是传个「你好,我要通话」的消息。后来踩了坑才发现,信令要干的事比想象中多得多。
具体来说,信令负责这几件事:
- 会话控制:谁发起通话?谁挂断?能不能加人?
- 媒体协商:交换 SDP(Session Description Protocol),告诉对方「我支持 H264,分辨率 1280x720,音频用 Opus」。
- 网络协商:交换 ICE 候选者(Candidate),告诉对方「我可能在 192.168.1.100,也可能在公网 8.8.8.8:3478」。
- 房间管理:谁在哪个房间?房间里有几个人?
嗯,这里要注意:SDP 和 ICE 候选者都是 WebRTC 内部生成的,你不需要自己构造。你要做的只是把它们通过信令通道传给对方。
为什么选 JSON over WebSocket?
信令协议的设计,说白了就是选「消息格式」和「传输通道」。我个人习惯用 JSON over WebSocket,原因很简单:
| 方案 | 优点 | 缺点 |
|---|---|---|
| JSON over WebSocket | 双向实时、结构清晰、调试方便 | 需要自己处理重连 |
| HTTP 轮询 | 实现简单 | 延迟高、浪费带宽 |
| WebSocket + Protobuf | 体积小、解析快 | 调试麻烦、开发成本高 |
我在项目中遇到过用 Protobuf 做信令的团队,最后调试时不得不写个 JSON 转换器。何必呢?WebRTC 信令本身消息量不大,JSON 完全够用。
信令消息结构设计
信令消息不需要太复杂。我一般用这样的结构:
{
"type": "offer", // 消息类型
"from": "user_a", // 发送者
"to": "user_b", // 接收者(可选,用于点对点)
"room": "room_123", // 房间 ID
"data": { ... } // 具体内容(SDP、ICE 等)
}
常用的消息类型:
join/leave— 加入/离开房间offer/answer— 交换 SDPice_candidate— 交换 ICE 候选者hangup— 挂断通话
你可能会问:为什么不用现成的信令协议?比如 SIP?嗯,SIP 太重量级了,而且跟 WebRTC 的浏览器环境不太搭。JSON over WebSocket 轻量、灵活,更适合前端场景。
信令服务器搭建:Node.js + Socket.IO
选 Node.js 是因为它跟 JavaScript 一脉相承,前端工程师上手快。Socket.IO 则封装了 WebSocket 的重连、房间、广播等机制,省了不少事。
先装依赖:
npm init -y
npm install socket.io express
然后写一个最简单的信令服务器:
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: '*' }
});
// 房间管理:记录每个房间的用户
const rooms = {};
io.on('connection', (socket) => {
console.log(`用户连接: ${socket.id}`);
// 加入房间
socket.on('join', (roomId) => {
socket.join(roomId);
if (!rooms[roomId]) rooms[roomId] = [];
rooms[roomId].push(socket.id);
// 通知房间内其他用户
socket.to(roomId).emit('user_joined', socket.id);
console.log(`${socket.id} 加入了房间 ${roomId}`);
});
// 转发 offer
socket.on('offer', (data) => {
socket.to(data.target).emit('offer', {
from: socket.id,
sdp: data.sdp
});
});
// 转发 answer
socket.on('answer', (data) => {
socket.to(data.target).emit('answer', {
from: socket.id,
sdp: data.sdp
});
});
// 转发 ICE 候选者
socket.on('ice_candidate', (data) => {
socket.to(data.target).emit('ice_candidate', {
from: socket.id,
candidate: data.candidate
});
});
// 离开房间
socket.on('leave', (roomId) => {
socket.leave(roomId);
if (rooms[roomId]) {
rooms[roomId] = rooms[roomId].filter(id => id !== socket.id);
}
socket.to(roomId).emit('user_left', socket.id);
});
socket.on('disconnect', () => {
// 清理所有房间
for (const roomId in rooms) {
rooms[roomId] = rooms[roomId].filter(id => id !== socket.id);
socket.to(roomId).emit('user_left', socket.id);
}
console.log(`用户断开: ${socket.id}`);
});
});
server.listen(3000, () => {
console.log('信令服务器运行在 http://localhost:3000');
});
信令流程:从连接到通话
完整的信令流程大概是这样的:
- 用户 A 连接信令服务器,加入房间「room_123」。
- 用户 B 也连接服务器,加入同一个房间。
- 服务器通知 A:「B 来了」。
- A 创建 PeerConnection,生成 offer(SDP),通过信令发给 B。
- B 收到 offer,创建自己的 PeerConnection,生成 answer,发回给 A。
- 双方交换 ICE 候选者,找到最优路径。
- 连接建立,媒体开始传输。
这个过程看起来步骤多,但实际代码量很少。信令服务器只负责转发,不碰媒体数据。
信令流程核心图
下面这张图展示了信令在 WebRTC 通话中的位置和作用:
避坑指南
信令这部分坑不少,我挑几个常见的说说:
- 消息顺序问题:WebSocket 虽然是顺序的,但网络延迟可能导致 offer 比 join 先到。我习惯在客户端加一个状态机,确保消息按流程处理。
- 房间清理:用户断线后一定要清理房间列表。否则下次别人加入时,会发现房间里有个「幽灵用户」。
- ICE 候选者积压:如果网络环境复杂,ICE 候选者可能很多。不要一股脑全转发,可以等收集完再批量发送。
- 不要信任客户端:所有信令消息都要在服务端做校验。我曾经遇到过有人伪造「加入房间」消息,把别人的通话搅黄了。
io.set('log level', 3),能看到所有消息的收发情况。上线前记得关掉。
总结
信令是 WebRTC 应用的「粘合剂」。它不负责传输媒体,但决定了整个通话流程能否顺畅运行。JSON over WebSocket 是目前最实用的方案,Node.js + Socket.IO 能让你快速搭建一个可用的信令服务器。
记住一句话:信令设计得越简单,后期维护越省心。别想着一步到位搞个分布式高可用信令集群,先跑通再说。