信令协议设计:消息类型与JSON格式
好,咱们接着聊信令协议的设计。说实话,这部分是整个信令服务器的「灵魂」。你想想看,WebRTC 的媒体数据是走 P2P 的,但建立连接前的「握手」全靠信令。如果信令协议设计得乱七八糟,后面调试起来会非常痛苦。
我在早期的一个项目中,就吃过信令格式不统一的亏。当时团队里有人用 snake_case,有人用 camelCase,结果解析的时候各种踩坑。后来我痛定思痛,定了一套规范,再也没出过类似问题。
信令消息类型定义
我们先明确一下,信令服务器需要处理哪些消息。说白了,就是客户端之间要交换哪些信息才能把 P2P 连接建起来。
我个人习惯把消息类型分为两大类:媒体协商类和房间管理类。下面这张图可以帮你快速理解整体流程:
核心消息类型详解
我们一个一个来看。每种消息都有它存在的意义,缺一不可。
1. join —— 加入房间
这是客户端进入房间的第一步。客户端发送 join 消息,告诉服务器「我要进哪个房间」。服务器收到后,会把客户端加入房间列表,并通知房间里其他人。
关键点:join 消息里必须包含房间 ID 和用户标识。我个人习惯用 userId 而不是 username,因为 userId 更稳定,不会因为改名而出问题。
2. leave —— 离开房间
客户端主动断开连接时发送 leave。注意,如果客户端直接断网(比如浏览器崩溃),服务器应该通过 WebSocket 的 close 事件来检测,然后自动触发 leave 逻辑。
我曾经踩过的坑:早期我设计时只处理了主动 leave,没处理异常断开。结果用户浏览器崩溃后,房间里还残留着他的「幽灵」状态,其他用户一直尝试连接一个不存在的 peer。后来我加了心跳检测和超时清理,才算彻底解决。
3. offer —— 发起媒体协商
offer 消息携带的是发起方的 SDP(Session Description Protocol)。说白了,就是告诉对方「我支持哪些编解码器、我的网络情况如何」。这是 WebRTC 连接建立的起点。
4. answer —— 响应媒体协商
answer 是接收方对 offer 的回应。它同样携带 SDP,但内容是接收方根据自己的能力做出的选择。比如发起方说「我支持 H.264 和 VP8」,接收方可能回答「我用 H.264」。
5. candidate —— ICE 候选者交换
candidate 消息携带的是 ICE 候选者信息。嗯,这里要注意,candidate 可能不止一个。实际上,一个客户端可能会发现多个候选者(本地 IP、公网 IP、TURN 服务器地址等),每个都要通过信令服务器转发给对方。
小技巧:candidate 的传输顺序不重要,但一定要全部传完。我建议在客户端收集完所有候选者后再统一发送,而不是收集一个发一个。这样可以减少信令服务器的转发压力。
JSON 消息格式设计
格式统一是信令协议设计的重中之重。我推荐使用以下结构:
{
"type": "消息类型",
"roomId": "房间ID",
"userId": "用户ID",
"payload": {
// 具体数据
},
"timestamp": 1699000000000
}
为什么这么设计?我给你拆解一下:
- type:必填字段,标识消息类型。取值就是上面说的那五种:join、leave、offer、answer、candidate。
- roomId:必填字段。服务器需要知道这条消息属于哪个房间。
- userId:必填字段。标识发送者是谁。
- payload:可选字段,存放具体数据。不同类型的消息,payload 结构不同。
- timestamp:可选字段,但强烈建议加上。调试时非常有用,可以追踪消息延迟。
各消息类型的 payload 设计
不同类型的消息,payload 里装的东西不一样。我们来看具体设计:
| 消息类型 | payload 字段 | 说明 |
|---|---|---|
| join | 无(或可选 displayName) | 加入房间,可附带显示名称 |
| leave | 无 | 离开房间,无需额外数据 |
| offer | sdp: string | 发起方的 SDP 字符串 |
| answer | sdp: string | 接收方的 SDP 字符串 |
| candidate | candidate: string, sdpMid: string, sdpMLineIndex: number | ICE 候选者信息 |
来看几个完整的示例:
join 消息示例
{
"type": "join",
"roomId": "room-001",
"userId": "user-alice",
"payload": {
"displayName": "Alice"
},
"timestamp": 1699000000000
}
offer 消息示例
{
"type": "offer",
"roomId": "room-001",
"userId": "user-alice",
"payload": {
"sdp": "v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n..."
},
"timestamp": 1699000001000
}
candidate 消息示例
{
"type": "candidate",
"roomId": "room-001",
"userId": "user-alice",
"payload": {
"candidate": "candidate:1 1 UDP 2122252543 192.168.1.100 54321 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
},
"timestamp": 1699000002000
}
服务器广播消息设计
除了客户端之间的点对点消息,服务器还需要广播一些系统消息。比如当有人加入或离开房间时,服务器要通知房间里所有人。
广播消息的格式和客户端消息保持一致,只是 type 字段会有所不同:
{
"type": "user_joined",
"roomId": "room-001",
"userId": "user-alice",
"payload": {
"displayName": "Alice"
},
"timestamp": 1699000000000
}
核心原则:所有消息格式必须统一。不管是客户端发的,还是服务器广播的,都遵循同一个 JSON 结构。这样客户端解析时只需要一套代码,省心省力。
设计时的几个注意事项
最后,分享几个我在实际项目中总结的经验:
- 字段命名要一致:别一会儿用 userId,一会儿用 user_id。我统一用 camelCase,因为 JavaScript 解析起来最自然。
- 预留扩展字段:payload 设计成对象而不是直接放字符串,就是为了以后扩展。比如 offer 消息以后可能加个 codec 优先级字段,直接往 payload 里加就行。
- 消息大小要控制:SDP 字符串可能很长,但别超过 WebSocket 的消息大小限制。一般来说 64KB 以内是安全的。
- 加个消息 ID:虽然上面没列出来,但我建议在顶层加个 messageId 字段。这样客户端可以做去重处理,防止消息重复执行。
好了,信令协议的设计就聊到这里。这套 JSON 格式我在多个项目中验证过,稳定可靠。你直接拿去用,基本不会出大问题。