一、信令的作用与设计原则

信令,说白了就是让两个浏览器互相找到对方、商量好怎么聊天的过程。很多人刚开始学WebRTC时,容易把信令和媒体传输混在一起。其实它们是两码事——信令只管“握手”,媒体才是真正的音视频数据流。

我记得第一次做WebRTC项目时,老板问我:“信令不就是发个消息吗?用WebSocket随便搞搞不就行了?”嗯,后来踩了不少坑才明白,信令远不止“发消息”这么简单。

1.1 信令到底在做什么?

信令的核心任务有三个:

  • 会话控制:谁发起通话?谁挂断?要不要加人?
  • 媒体协商:你支持什么编码?我支持什么分辨率?咱俩得统一一下。
  • 网络协商:你的IP是什么?我的NAT类型是啥?能不能直接连?

你想想看,如果没有信令,两个浏览器就像两个哑巴,互相不知道对方的存在。WebRTC本身没有定义信令协议,它只定义了SDP(会话描述协议)和ICE(交互式连接建立)的格式。怎么把这些信息传过去,完全由你决定。

关键认知:信令不是WebRTC标准的一部分,但它是WebRTC应用必不可少的一环。没有信令,WebRTC就是一堆没法用的API。

1.2 信令的设计原则

我在项目中总结了几条信令设计的原则,分享给你:

  1. 轻量:信令只传控制信息,别把大文件、视频流塞进去。我曾经见过有人把整个日志文件当信令发……
  2. 可靠:信令必须保证送达,而且顺序要对。WebSocket或长轮询都可以,但别用UDP。
  3. 安全:信令通道容易被攻击,一定要加密。我习惯用WSS(WebSocket Secure)或HTTPS。
  4. 异步:信令是事件驱动的,别搞同步阻塞。你想想看,如果等对方回复才做下一步,网络一卡就全卡死了。

避坑指南:我曾经在一个项目中用了HTTP短轮询做信令,结果用户一多,服务器直接被打爆。后来换成WebSocket,问题就解决了。记住,信令通道要尽量保持长连接。

二、主流信令协议对比

信令协议有很多种,选哪个取决于你的场景。我帮你梳理一下最常见的三种:SIP、XMPP和自定义协议。

2.1 SIP(会话发起协议)

SIP是电信行业的老大哥,VoIP时代就广泛使用。它的优点是成熟、标准化程度高,适合与PSTN(传统电话网)互通。

但说实话,SIP在WebRTC里用起来有点重。它的消息格式是文本的,解析起来麻烦,而且很多特性在浏览器里用不上。我个人不太推荐纯前端项目用SIP,除非你要对接运营商。

协议 优点 缺点 适用场景
SIP 标准化、互通性好 消息格式重、浏览器支持差 与PSTN互通、企业级通信
XMPP 扩展性强、支持即时消息 XML解析开销大、实时性一般 即时通信+音视频、社交应用
自定义 轻量、灵活、可控 需要自己实现、无标准化 纯WebRTC应用、内部系统

2.2 XMPP(可扩展消息与存在协议)

XMPP是IM(即时通信)领域的经典协议。它基于XML,扩展性很强,可以加各种自定义字段。很多早期的WebRTC应用都用XMPP做信令。

但问题也出在XML上——解析开销大,消息体臃肿。你想想看,一个简单的“hello”消息,用XMPP可能要包好几层标签。在移动端或弱网环境下,这种开销是不能接受的。

2.3 自定义信令协议

我个人最推荐的方式。说白了,就是用JSON或Protobuf定义一套自己的消息格式,通过WebSocket传输。这样做的好处是:

  • 极简:只传必要字段,没有冗余
  • 灵活:想加什么字段就加什么字段
  • 可控:出了问题自己就能排查,不用翻协议文档

我在做直播项目时,就自己定义了一套信令协议,核心消息只有5种:JOIN、OFFER、ANSWER、ICE_CANDIDATE、LEAVE。整个信令通道的代码不到200行,跑得很稳。

注意:自定义协议虽然灵活,但要做好版本管理。我曾经因为没加版本号,导致新旧客户端信令不兼容,线上出了事故。从那以后,我每个信令消息都带一个version字段。

三、信令状态机设计

信令状态机是WebRTC应用的灵魂。没有状态机,信令就是一盘散沙。我习惯把状态机画出来再写代码,这样逻辑清晰,不容易漏掉边界情况。

3.1 核心状态定义

一个典型的WebRTC信令状态机包含以下状态:

  • IDLE:空闲状态,没有通话
  • INVITING:发起呼叫,等待对方应答
  • RINGING:对方振铃中
  • CONNECTING:正在建立媒体连接
  • CONNECTED:通话中
  • DISCONNECTING:正在挂断

你想想看,如果没有状态机,用户连续点了两次“挂断”按钮,会发生什么?可能会发出两个挂断消息,导致状态混乱。状态机就是用来防止这种问题的。

3.2 状态转换图

下面是我用SVG画的一个信令状态机图,展示了核心的状态流转:

WebRTC 信令状态机 IDLE INVITING RINGING CONNECTING CONNECTED DISCONNECTING 发起呼叫 收到180 接听 直接接听 媒体就绪 挂断 清理完成 正常流转 异常/挂断

3.3 状态机实现要点

写状态机代码时,我习惯用有限状态机(FSM)模式。下面是一个简单的JavaScript实现示例:

class SignalingStateMachine {
  constructor() {
    this.state = 'IDLE';
    this.transitions = {
      'IDLE': { 'CALL': 'INVITING' },
      'INVITING': { 'RINGING': 'RINGING', 'ACCEPT': 'CONNECTING', 'HANGUP': 'IDLE' },
      'RINGING': { 'ACCEPT': 'CONNECTING', 'HANGUP': 'IDLE' },
      'CONNECTING': { 'MEDIA_READY': 'CONNECTED', 'FAIL': 'IDLE' },
      'CONNECTED': { 'HANGUP': 'DISCONNECTING' },
      'DISCONNECTING': { 'CLEANUP': 'IDLE' }
    };
  }

  transition(event) {
    const nextState = this.transitions[this.state][event];
    if (!nextState) {
      console.warn(`非法转换:${this.state} -> ${event}`);
      return false;
    }
    console.log(`状态转换:${this.state} -> ${nextState}`);
    this.state = nextState;
    return true;
  }
}

经验之谈:状态机一定要加非法转换的日志。我曾经在生产环境遇到过状态卡死的问题,就是因为没有日志,排查了半天。加上日志后,5分钟就定位到了问题。

3.4 边界情况处理

信令状态机最怕的就是边界情况。我列几个常见的坑:

  • 重复消息:对方发了两次OFFER怎么办?状态机应该忽略第二次。
  • 超时:INVITING状态超过30秒没响应,应该自动回到IDLE。
  • 并发操作:用户同时点了“接听”和“挂断”,状态机应该按顺序处理。
  • 重连:WebSocket断了,状态机要能恢复到之前的状态。

嗯,这里要注意:状态机不是万能的。它只能保证单次通话的逻辑正确。如果你要做多人会议,还需要一个全局的会话管理器来协调多个状态机实例。

总结一下:信令是WebRTC应用的骨架,状态机是信令的灵魂。选对协议、设计好状态机,你的WebRTC应用就成功了一半。剩下的,就是处理那些让人头疼的边界情况了。

公众号:蓝海资料掘金营,微信deep3321