2、搭建信令服务器:信令的作用、WebSocket基础、Node.js搭建简易信令服务器、信令消息格式设计
好,咱们进入第二章。这一章要聊的是信令服务器——说白了,就是让两个浏览器互相找到对方、商量好怎么通话的那个“中间人”。
我记得刚接触WebRTC时,有个很大的困惑:明明WebRTC是点对点的,为什么还需要一个服务器?后来踩了坑才明白,没有信令服务器,两个Peer根本连对方在哪都不知道。就像你想打电话,但不知道对方的号码——这电话怎么打?
2.1 信令的作用
信令,本质上就是“握手前的悄悄话”。它负责传递三类关键信息:
- 会话控制消息:谁发起通话、谁挂断、谁拒绝
- 媒体协商信息:SDP(Session Description Protocol),包含编解码器、分辨率、IP端口等
- 网络路径信息:ICE候选者(Candidate),告诉对方“我有哪些可能的网络入口”
你想想看,浏览器A要和浏览器B通话,A得先告诉B:“我支持H264编码,我的IP是192.168.1.100,端口3478”。这些信息怎么传?WebRTC没规定,所以需要我们自己搭信令服务器来转发。
核心要点:信令服务器只负责转发,不负责理解消息内容。它就是个“邮递员”,把A的信封原封不动递给B。
我在项目中遇到过一种情况:有人把信令服务器和媒体服务器混为一谈。信令服务器不处理音视频数据,只处理控制消息。如果它挂了,通话不会立刻断,但新用户进不来,也挂不了电话——嗯,这很尴尬。
2.2 WebSocket基础
为什么选WebSocket?因为HTTP是“请求-响应”模式,服务器不能主动给客户端发消息。而WebSocket是全双工的,服务器可以随时“推”数据给客户端。信令场景下,当B加入房间时,服务器要主动通知A——“有新伙伴来了”,这用HTTP轮询就太笨了。
WebSocket的握手很简单:客户端发一个HTTP Upgrade请求,服务器同意后,连接就升级为WebSocket。之后双方可以互相发送文本或二进制帧。
小技巧:我个人习惯用 ws:// 协议,生产环境一定要用 wss://(WebSocket Secure),否则浏览器会报安全错误。
一个最简单的WebSocket客户端示例:
// 客户端连接
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
console.log('连接已建立');
socket.send('Hello Server!');
};
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
socket.onclose = () => {
console.log('连接已关闭');
};
服务端呢?Node.js里用 ws 库最省事。我刚开始用 socket.io,后来发现太重了,信令场景只需要最基础的WebSocket就够了。
2.3 Node.js搭建简易信令服务器
好,咱们动手搭一个。先安装依赖:
npm init -y
npm install ws
然后写一个简单的信令服务器:
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
// 存储所有连接的客户端
const clients = new Map();
server.on('connection', (ws) => {
// 给每个客户端分配一个唯一ID
const clientId = Date.now().toString(36);
clients.set(clientId, ws);
console.log(`客户端 ${clientId} 已连接`);
// 收到消息后广播给其他客户端
ws.on('message', (message) => {
const data = JSON.parse(message);
console.log('收到消息:', data);
// 根据消息类型做不同处理
switch (data.type) {
case 'join':
// 加入房间逻辑
broadcast(clientId, {
type: 'user-joined',
userId: clientId
});
break;
case 'offer':
// 转发SDP offer
sendTo(data.targetId, {
type: 'offer',
sdp: data.sdp,
from: clientId
});
break;
case 'candidate':
// 转发ICE候选者
sendTo(data.targetId, {
type: 'candidate',
candidate: data.candidate,
from: clientId
});
break;
default:
console.log('未知消息类型:', data.type);
}
});
ws.on('close', () => {
console.log(`客户端 ${clientId} 已断开`);
clients.delete(clientId);
broadcast(clientId, {
type: 'user-left',
userId: clientId
});
});
});
function broadcast(senderId, message) {
clients.forEach((ws, id) => {
if (id !== senderId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
});
}
function sendTo(targetId, message) {
const ws = clients.get(targetId);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}
console.log('信令服务器已启动,监听端口 8080');
注意:这个服务器没有做房间管理,所有消息都是全局广播。生产环境需要加房间ID,只转发给同房间的客户端。
我曾经犯过一个低级错误:忘记做 JSON.parse,直接拿字符串当对象用,结果 data.type 一直是 undefined。排查了半天才发现——嗯,这种坑踩一次就记住了。
2.4 信令消息格式设计
消息格式设计得好,后面写代码就顺畅。我建议统一用JSON,结构清晰,扩展方便。
基本的消息结构:
{
"type": "消息类型",
"from": "发送者ID",
"to": "接收者ID(可选)",
"payload": {} // 具体数据
}
常用的消息类型:
| 消息类型 | 方向 | 说明 |
|---|---|---|
join |
客户端→服务器 | 请求加入房间 |
user-joined |
服务器→客户端 | 通知有新用户加入 |
offer |
客户端→客户端 | 发送SDP offer |
answer |
客户端→客户端 | 回复SDP answer |
candidate |
客户端→客户端 | 发送ICE候选者 |
leave |
客户端→服务器 | 离开房间 |
user-left |
服务器→客户端 | 通知有用户离开 |
举个例子,A向B发送offer:
{
"type": "offer",
"from": "user-A",
"to": "user-B",
"payload": {
"sdp": "v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\n..."
}
}
ICE候选者消息:
{
"type": "candidate",
"from": "user-A",
"to": "user-B",
"payload": {
"candidate": "candidate:1 1 UDP 2122252543 192.168.1.100 3478 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
避坑指南:我曾经把SDP和ICE候选者放在同一个消息里发,结果对方解析时顺序乱了,导致连接失败。后来老老实实分开发,先发offer/answer,再逐个发candidate。顺序很重要!
下面这张图展示了信令交互的完整流程:
你看,整个流程就是“A告诉服务器,服务器告诉B,B回复,服务器再告诉A”。信令服务器就是个透明的管道,不修改消息内容。
嗯,到这里信令服务器的核心内容就讲完了。记住三点:信令只转发不处理、WebSocket全双工通信、消息格式统一用JSON。下一节咱们会把这些代码整合到实际项目中,跑通一个完整的通话流程。