WebRTC 只管流,不管控——自研信令服务器的状态机设计

视频流是 WebRTC 的事。谁发起、谁接听、谁踢人、谁旁观——这些是信令的事。


一、问题

WebRTC 搞定了音视频传输。两个浏览器之间怎么建 PeerConnection、怎么传递 SDP、怎么走 ICE 打洞——这些都是现成的。

但一个视频帮办系统不只是"两个人能看见对方"。还需要:

  • 坐席发起通话,参保人接听
  • 坐席可以邀请第三方加入旁观
  • 坐席可以踢人
  • 多方加入时,每个人知道房间里有哪些人
  • 有人断线,其他人要知道
  • 通话结束后,知道是谁挂断的、挂了多久

这些 WebRTC 不管。得自己搭一层信令服务器


二、自研信令协议

设计一套轻量 JSON 信令。WebSocket 传输,每条消息一个类型:

信令方向 说明 谁来 谁收
create 坐席创建房间成功 服务器 坐席
joined 有人加入房间后,给此人发的确认(带了所有在房间的人列表) 服务器 加入者
called 有人申请加入,通知坐席是否允许 服务器 坐席
allow 坐席允许加入 坐席 服务器
deny 坐席拒绝加入 坐席 服务器
refused 服务器通知申请者被拒绝 服务器 申请者
otherjoin 有其他人加入房间(通知给已经在房间里的人) 服务器 其他人
bye 有人主动离开 离开者 服务器
leaved 有人离开,服务器通知房间里其他人 服务器 其他人
kick 坐席踢人 坐席 服务器
kicked 被踢的人收到通知 服务器 被踢者

关键设计决策:所有业务操作不走 P2P,全部经过信令服务器中转。 坐席踢人不是直接发给参保人,是发给服务器 → 服务器发给被踢者 + 通知其他人。单一控制点,状态不会分叉。


三、房间状态机

一个房间的生命周期:

        ┌──────────┐
        │   idle   │  等待坐席创建
        └────┬─────┘
             │ create
             ▼
        ┌──────────┐
        │ created  │  房间已建,等参保人加入
        └────┬─────┘
             │ join
             ▼
        ┌──────────┐
        │  joined  │  参保人已加入,等坐席允许
        └────┬─────┘
             │ allow
             ▼
        ┌──────────┐
        │  called  │  通话中(可能多人)
        └────┬─────┘
             │ bye / kick / timeout
             ▼
        ┌──────────┐
        │   ended  │  所有人离开,房间销毁
        └──────────┘

每个状态允许的操作不同:

状态 允许的操作
created 参保人加入、坐席取消
joined 坐席允许/拒绝、第三方加入
called 踢人、邀请第三方、正常挂断

状态机保护了异常路径——比如坐席在 created 状态还没人加入就发踢人指令,服务器直接拒绝,不会崩溃。


四、三种角色的加入和退出逻辑

坐席

  • 建房:发 create → 服务器创建房间,分配 roomId
  • 退出:发 bye → 服务器通知所有人 leaved → 房间销毁

参保人

  • 加入:发 join(带 roomId)→ 服务器暂存 → 发 called 给坐席
  • 被允许:服务器发 joined → 参保人开始建 P2P 连接
  • 被拒绝:服务器发 refused → 参保人收到拒绝原因
  • 被踢:坐席发 kick → 服务器发 kicked 给参保人 → 通知其他人 leaved

第三方(旁观)

  • 加入:同参保人,但坐席收到 called 时能看到是"旁观"角色
  • 退出:发 bye 或坐席踢,逻辑同参保人
  • 差别:不参与音视频流,只收到其他人的存在通知

五、一次完整通话的信令时序

以坐席 → 参保人一对一通话为例:

坐席                    信令服务器                   参保人
 │                         │                          │
 │──────── create ────────▶│                          │
 │◀───── create OK ────────│                          │
 │                                                      │
 │                                 ◀────── join ───────│
 │◀───── called ───────────│                          │
 │                                                      │
 │──────── allow ──────────▶│                          │
 │◀───── allow OK ──────────│                          │
 │                          │────── otherjoin ────────▶│
 │                          │                          │
 │════════════ WebRTC P2P 音视频流 ═══════════════════│
 │                          │                          │
 │                                 ◀─────── bye ───────│
 │◀───── leaved ────────────│                          │

Notes:

  1. 坐席建房后,房间处于 created 状态,等待参保人。
  2. 参保人 join 后,状态变为 joined,坐席决定 allow 还是 deny。
  3. allow 后状态变为 called,视频流开始。
  4. 参保人主动挂断(bye),服务器通知坐席(leaved),房间结束。
  5. 如果是坐席主动 bye,服务器直接销毁房间,通知所有人 leaved。

六、异常处理:断线、超时、重复加入

断线

WebSocket 断连时,服务器检测到连接关闭,等同于该用户主动 bye。处理逻辑:

  • 走正常离开流程,通知其他人 leaved
  • 保留房间 N 分钟,等待重连
  • 超时没重连的人,从房间列表移除

超时

joined 状态等待坐席 allow 超过 N 分钟 → 服务器自动给参保人发 refused,房源回退到 created

重复加入

同一个 roomId,同一个 userId 重复发 join → 服务器忽略,返回"已在房间中"。


七、总结

WebRTC 搞定了音视频流的传输。但它不负责:

  • 谁有权建房
  • 谁有权加入
  • 谁有权踢人
  • 旁观和发言有什么区别
  • 断线了怎么处理

这些是信令服务器的活。

这套自研协议只有 10 条左右信令,覆盖了远程帮办的全部业务场景。核心设计原则是:所有状态变更必须经过服务器——不是性能最优的方案,但状态一致性最高。政务场景不需要百万人并发,但绝对不能在通话中丢了状态。

信令层的代码不复杂。复杂的是想清楚"每个状态下谁可以做什么"。这个想清楚了,代码是自然推导出来的。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐