18、WebSocket信令:信令流程设计、房间管理、信令消息格式
聊到WebRTC,很多人第一反应是P2P、是音视频流。但说实话,真正让WebRTC跑起来的,是信令。没有信令,两个浏览器就是两个孤岛,谁也找不到谁。
我刚开始做WebRTC的时候,也犯过这个错误——花了大把时间调编码参数、折腾NAT穿透,结果发现连最基本的“谁邀请谁”都没想清楚。嗯,信令才是整个系统的粘合剂。
信令的核心作用
信令说白了就是“握手”。它负责三件事:
- 交换媒体能力:你支持什么编码?我支持什么编码?咱俩得先对齐一下。
- 交换网络信息:你的IP和端口是多少?我的Candidate有哪些?
- 控制会话状态:谁加入、谁离开、谁静音了。
WebRTC标准里并没有规定信令必须用什么协议。你可以用WebSocket、HTTP轮询、甚至用鸽子送信(理论上)。但实际项目中,WebSocket几乎是唯一的选择。为什么?因为它双向、实时、低延迟。你想想看,如果每次交换SDP都要发个HTTP请求,那延迟得多感人。
核心观点:信令不是WebRTC标准的一部分,但它是WebRTC应用必不可少的一部分。选对信令方案,项目就成功了一半。
信令流程设计
一个典型的WebRTC信令流程长什么样?我画了一张图,你看完就明白了。
流程其实不复杂。我拆解一下:
- 创建房间:发起方A告诉服务器,我要开个房间。服务器返回一个roomId。
- 加入房间:接收方B拿着roomId加入。服务器通知A:有人来了。
- 交换Offer/Answer:A创建Offer(包含SDP),通过服务器转发给B。B创建Answer,再转发回来。
- ICE Candidate交换:双方各自收集网络Candidate,通过信令通道交换。这一步是双向的,而且可能来回很多次。
我的经验:ICE Candidate交换不要等SDP交换完才开始。我习惯在创建Offer/Answer的同时就开始收集Candidate,然后边收集边发送。这样能节省至少几百毫秒的连接建立时间。
房间管理
房间管理是信令服务器的核心功能。说白了,就是维护一个“谁在哪个房间”的映射表。
我见过很多新手直接把房间信息存在内存里,用个Map搞定。小规模没问题,但一旦服务重启,所有房间就丢了。我曾经在一个项目中吃过这个亏——线上服务重启,几十个正在进行的会议全部断连,用户直接炸了。
所以,房间管理要考虑这几个维度:
房间生命周期
| 阶段 | 操作 | 说明 |
|---|---|---|
| 创建 | 生成唯一roomId,设置最大人数 | 我习惯用UUID+时间戳,避免冲突 |
| 加入 | 校验roomId是否存在,人数是否满 | 满了就返回错误码,别让用户傻等 |
| 离开 | 从房间移除用户,通知其他人 | 如果房间空了,可以延迟销毁 |
| 销毁 | 清理房间资源 | 建议加个超时机制,防止僵尸房间 |
房间数据结构
我一般这样设计房间的数据结构:
{
"roomId": "room_abc123",
"createdAt": 1700000000000,
"maxMembers": 4,
"members": {
"user_001": {
"userId": "user_001",
"userName": "Alice",
"joinedAt": 1700000001000,
"mediaStatus": {
"audio": true,
"video": true
}
},
"user_002": {
"userId": "user_002",
"userName": "Bob",
"joinedAt": 1700000002000,
"mediaStatus": {
"audio": true,
"video": false
}
}
}
}
注意:不要把敏感信息(如IP地址、ICE Candidate)直接暴露在房间信息里。这些应该通过信令消息单独传递,而且只发给目标用户。
信令消息格式
信令消息的格式,我建议统一用JSON。简单、可读、好扩展。别搞什么二进制协议,除非你的场景对带宽极度敏感。
我总结了一套消息格式,经过多个项目验证,你可以直接拿来用:
基础消息结构
{
"type": "message_type",
"from": "user_id",
"to": "user_id | room_id",
"payload": {},
"timestamp": 1700000000000,
"msgId": "msg_unique_id"
}
每个字段的含义:
- type:消息类型,后面会详细列举
- from:发送方ID
- to:接收方ID或房间ID。如果是房间广播,填roomId
- payload:具体数据,根据type不同而变化
- timestamp:时间戳,用于消息排序和去重
- msgId:消息唯一ID,防止重复处理
消息类型定义
| 类型 | 方向 | 说明 |
|---|---|---|
| join | client → server | 加入房间请求 |
| joined | server → client | 加入成功通知 |
| new_peer | server → client | 有新成员加入,通知房间内其他人 |
| offer | client → server → client | 转发SDP Offer |
| answer | client → server → client | 转发SDP Answer |
| ice_candidate | client → server → client | 转发ICE Candidate |
| leave | client → server | 离开房间 |
| peer_left | server → client | 通知其他人有人离开 |
| media_status | client → server → client | 通知音视频状态变化(静音/取消静音) |
| error | server → client | 错误信息 |
几个关键消息的payload示例
offer 消息:
{
"type": "offer",
"from": "user_001",
"to": "user_002",
"payload": {
"sdp": "v=0\r\no=- ...",
"type": "offer"
},
"timestamp": 1700000000000,
"msgId": "msg_001"
}
ice_candidate 消息:
{
"type": "ice_candidate",
"from": "user_001",
"to": "user_002",
"payload": {
"candidate": "candidate:1 1 UDP 2122252543 192.168.1.1 54321 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
},
"timestamp": 1700000000000,
"msgId": "msg_002"
}
避坑指南:我曾经遇到过一个问题——ICE Candidate发得太快,对方还没设置好远端SDP就收到了。结果Candidate被丢弃了。解决方案很简单:在信令服务器端做个缓冲,等对方设置完SDP后再转发Candidate。或者客户端收到Candidate后先缓存,等setRemoteDescription完成后再处理。
信令服务器的实现要点
最后聊几个实现上的关键点:
- 连接管理:每个WebSocket连接对应一个用户。断开时要及时清理房间成员信息。
- 消息路由:根据to字段判断是单播还是广播。单播直接转发给目标用户,广播则遍历房间内所有成员。
- 心跳机制:WebSocket可能意外断开。我一般每30秒发一次ping,60秒没收到pong就判定断开。
- 消息去重:网络抖动可能导致消息重复。用msgId做去重,服务端维护一个最近消息ID的缓存。
- 错误处理:用户加入不存在的房间、房间已满、消息格式错误……都要返回明确的错误码和描述。
信令这东西,看起来简单,但细节决定成败。我见过太多项目因为信令设计不合理,导致连接不稳定、消息丢失、状态混乱。花点时间把信令流程和消息格式设计好,后面会省很多事。
公众号:蓝海资料掘金营,微信deep3321