24、WebRTC与WebSocket协同:WebSocket信令与WebRTC媒体通道分离、心跳机制、重连策略、状态同步

这一章,我们来聊聊WebRTC和WebSocket这对黄金搭档。

很多人刚开始接触WebRTC时,会误以为WebSocket就是用来传音视频的。其实不是。WebSocket传的是信令——也就是控制指令。真正的媒体数据走的是UDP上的SRTP/SCTP通道。这两条路,各司其职。

核心原则:信令通道(WebSocket)负责“说”,媒体通道(WebRTC)负责“做”。两者分离,互不干扰。

为什么非要分离?

我刚开始做第一个WebRTC项目时,图省事,把信令和媒体混在一起走WebSocket。结果呢?一旦网络抖动,信令延迟,ICE连接也跟着乱套。后来我学乖了——信令和媒体必须分开。

原因有三:

  • 可靠性不同:信令需要可靠传输(TCP),媒体可以容忍丢包(UDP)。混在一起,信令会被媒体丢包拖累。
  • 优先级不同:信令消息必须优先处理,比如“挂断”指令不能因为视频卡顿就延迟执行。
  • 扩展性不同:信令服务器可以轻量部署,媒体服务器需要高性能。分开后,各自独立扩缩容。

我的习惯:信令通道用WebSocket + JSON,媒体通道用WebRTC原生API。两者之间通过一个状态管理器同步。这样调试时,信令日志和媒体日志分开看,问题定位快很多。

心跳机制:别让连接“假死”

WebSocket连接看起来还活着,实际上已经断了——这种情况我遇到过太多次了。尤其是移动端网络切换时,TCP连接可能已经断开,但客户端和服务端都不知道。

解决方案就是心跳。

我常用的心跳策略:

  • 客户端定时发送Ping:每5秒发一次,服务端收到后回复Pong。
  • 服务端超时检测:如果10秒内没收到任何消息,判定连接断开。
  • 客户端超时检测:如果发送Ping后3秒没收到Pong,主动断开重连。
// 客户端心跳示例
const HEARTBEAT_INTERVAL = 5000;
const HEARTBEAT_TIMEOUT = 3000;

let heartbeatTimer = null;
let pongTimer = null;

function startHeartbeat(ws) {
  heartbeatTimer = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
      // 设置超时检测
      pongTimer = setTimeout(() => {
        console.warn('心跳超时,主动断开');
        ws.close();
      }, HEARTBEAT_TIMEOUT);
    }
  }, HEARTBEAT_INTERVAL);
}

// 收到pong时清除超时
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'pong') {
    clearTimeout(pongTimer);
  }
};

注意:心跳间隔不要太短,否则会增加服务端压力。5秒是比较折中的值。我曾经在一个项目中设成1秒,结果服务端CPU直接飙到80%。

重连策略:断了怎么办?

WebSocket断了,WebRTC的媒体通道不一定断。但信令断了,你就没法做后续操作了——比如切换摄像头、调整码率、甚至挂断。

所以重连是必须的。但怎么重连,有讲究。

我总结了一套“阶梯式重连”策略:

  1. 立即重连:第一次断开后,马上尝试重连。
  2. 递增延迟:如果失败,等待1秒、2秒、4秒、8秒……最多30秒。
  3. 最大次数:尝试5次后,如果还连不上,提示用户网络异常。
  4. 恢复状态:重连成功后,重新同步当前房间状态、媒体状态。
// 阶梯式重连示例
class ReconnectManager {
  constructor(url) {
    this.url = url;
    this.maxRetries = 5;
    this.baseDelay = 1000;
    this.retryCount = 0;
  }

  async connect() {
    while (this.retryCount < this.maxRetries) {
      try {
        const ws = new WebSocket(this.url);
        await this.waitForOpen(ws);
        console.log('重连成功');
        this.retryCount = 0;
        return ws;
      } catch (err) {
        this.retryCount++;
        const delay = Math.min(
          this.baseDelay * Math.pow(2, this.retryCount - 1),
          30000
        );
        console.warn(`重连失败,${delay}ms后重试`);
        await this.sleep(delay);
      }
    }
    throw new Error('重连次数已达上限');
  }

  waitForOpen(ws) {
    return new Promise((resolve, reject) => {
      ws.onopen = resolve;
      ws.onerror = reject;
    });
  }

  sleep(ms) {
    return new Promise(r => setTimeout(r, ms));
  }
}

避坑指南:我曾经在重连时忘记恢复ICE状态,结果媒体通道虽然还在,但信令丢了,导致无法切换音视频设备。重连后一定要做一次完整的状态同步。

状态同步:大家都在同一个页面

WebRTC通信中,状态同步是个容易被忽略的点。说白了,就是确保所有客户端对当前通话状态的理解是一致的。

需要同步的状态包括:

  • 房间状态:谁在房间里?谁在说话?
  • 媒体状态:谁开启了摄像头?谁静音了?
  • 连接状态:ICE连接是否稳定?是否在重连?
  • 用户操作:谁发起了屏幕共享?谁在录制?

我常用的同步方式:

  • 全量同步:每次重连后,服务端把当前房间的完整状态推送给客户端。
  • 增量同步:状态变化时,只推送变化的部分。
  • 确认机制:客户端收到状态更新后,回复确认。服务端没收到确认就重推。
// 状态同步消息示例
// 服务端推送全量状态
{
  "type": "state_sync",
  "roomId": "room123",
  "users": [
    {
      "userId": "user1",
      "audio": true,
      "video": true,
      "screenShare": false
    },
    {
      "userId": "user2",
      "audio": false,
      "video": true,
      "screenShare": true
    }
  ],
  "timestamp": 1695000000000
}

// 客户端确认
{
  "type": "state_ack",
  "timestamp": 1695000000000
}

整体架构图

下面这张图展示了WebSocket信令通道和WebRTC媒体通道的分离关系,以及心跳、重连、状态同步在整个流程中的位置。

WebSocket + WebRTC 协同架构 客户端 A WebSocket 信令 WebRTC 媒体 心跳检测 状态同步 客户端 B WebSocket 信令 WebRTC 媒体 心跳检测 状态同步 信令服务器 (WebSocket) 信令转发 · 心跳管理 · 状态同步 重连协调 · 房间管理 媒体服务器 (WebRTC) 媒体转发 · 编码转码 · 合流录制 信令 信令 媒体 媒体 控制 信令通道 (WebSocket) 媒体通道 (WebRTC) 控制指令

实际项目中的坑

最后分享几个我踩过的坑:

  • 心跳和重连的时序问题:有一次我把心跳超时设成3秒,重连延迟设成5秒。结果心跳刚超时,重连还没触发,用户已经手动刷新页面了。后来我把重连延迟改成2秒,心跳超时改成5秒,才稳定下来。
  • 状态同步的并发问题:两个用户同时静音,服务端收到两个增量更新,但处理顺序错了,导致状态不一致。解决方案是给每个状态更新加一个序列号,服务端按序处理。
  • 重连后的ICE重启:WebSocket重连后,如果网络环境变了(比如从WiFi切到4G),原来的ICE候选可能失效。这时候需要触发ICE restart,重新建立媒体通道。

重要提醒:不要以为WebSocket重连成功就万事大吉了。一定要检查媒体通道的状态。如果媒体通道断了,信令通道再好也没用。

好了,这一章的内容就到这里。WebSocket和WebRTC的协同,说白了就是两条腿走路——一条负责控制,一条负责数据。两条腿配合好了,才能走得稳、走得远。