11、数据通道状态机:connecting、open、closing、closed 状态转换、异常断开处理

数据通道的状态机,说白了就是一条连接的「生命周期」。我刚开始接触 WebRTC 时,觉得状态机就是个简单的开关——要么通,要么断。后来在项目中踩过几次坑才发现,这玩意儿比想象中要细腻得多。

今天我们就来彻底搞懂它。

状态机概览

RTCDataChannel 一共有四种状态:

状态 含义 触发时机
connecting 正在建立连接 创建通道后、底层传输就绪前
open 连接已建立,可收发数据 底层 SCTP 关联建立成功
closing 正在关闭连接 调用 close() 或对端发起关闭
closed 连接已关闭 关闭流程完成或连接异常断开

这四种状态是单向流转的,不会往回跳。也就是说,一旦进入 closed,就不可能再回到 open。嗯,这里要注意——有些新手会尝试在 closed 后重新 send(),那肯定会报错。

状态转换流程

我习惯把状态转换画成一条直线:

connecting open closing closed SCTP 就绪 close() 调用 关闭完成 异常断开(网络中断、对端崩溃)

你看,正常流程是 connecting → open → closing → closed。但异常情况下,open 可以直接跳到 closed。我在项目中遇到过好几次这种情况——网络突然断了,连 closing 的机会都没有,直接就 closed 了。

监听状态变化

代码怎么写?很简单,监听 onopen、onclose、onerror 这几个事件就行。

const dc = peerConnection.createDataChannel('myChannel');

dc.onopen = () => {
  console.log('通道已打开,可以发数据了');
  dc.send('Hello!');
};

dc.onclose = () => {
  console.log('通道已关闭');
};

dc.onerror = (err) => {
  console.error('通道出错了:', err);
};

// 也可以主动检查状态
console.log(dc.readyState); // 'connecting' 或 'open' 等

我个人习惯在 onopen 里做一次「握手确认」——发一条特定消息,等对端回复。这样能确保双向通信都正常。你想想看,如果只监听 onopen,那只能说明本地认为通道建好了,但对端可能还没准备好。

异常断开处理

这是实战中最容易出问题的地方。我总结了几种常见场景:

  • 网络闪断:Wi-Fi 信号不稳,几秒后又恢复。这时候通道可能已经 closed 了,需要重新创建。
  • 对端崩溃:对方浏览器或页面挂了,你这边会收到 onclose 事件。
  • ICE 重启:网络切换时触发 ICE restart,数据通道会经历短暂的断开再重建。
⚠️ 注意: 通道一旦 closed,就不能再复用。必须重新 createDataChannel 并等待新的 open 事件。不要尝试在 closed 后调用 send(),会抛出异常。

我曾经在一个实时协作项目里,用户频繁切换网络,导致数据通道反复断开。一开始我简单地在 onclose 里重新创建通道,结果发现消息丢失了——因为重建需要时间,中间的消息没人收。

后来我加了一个「消息队列 + 重连缓冲」机制:

let messageQueue = [];
let isReconnecting = false;

dc.onclose = () => {
  isReconnecting = true;
  // 启动重连逻辑
  reconnectDataChannel();
};

function reconnectDataChannel() {
  const newDc = pc.createDataChannel('reconnectChannel');
  newDc.onopen = () => {
    isReconnecting = false;
    // 把队列里的消息发出去
    while (messageQueue.length) {
      newDc.send(messageQueue.shift());
    }
  };
}

function safeSend(data) {
  if (dc.readyState === 'open') {
    dc.send(data);
  } else {
    // 通道还没好,先缓存
    messageQueue.push(data);
  }
}
💡 小技巧: 可以用一个定时器来检测「长时间未收到对端消息」的情况。比如每 5 秒发一次 ping,如果 15 秒没收到 pong,就主动关闭并重建通道。这比单纯依赖 onclose 更可靠。

closing 状态的细节

closing 这个状态很多人会忽略。它其实是一个「优雅关闭」的过渡阶段。调用 close() 后,通道会进入 closing,此时:

  • 不能再发送新数据(send() 会失败)
  • 但尚未发送完的数据会继续传输
  • 等待对端确认关闭后,才进入 closed

说白了,closing 就是给双方一个「把话说完」的机会。我建议在关闭通道前,先发一条「我要关了」的消息,让对方有个准备。

function gracefulClose(dc) {
  if (dc.readyState !== 'open') return;
  
  // 先通知对端
  dc.send('__CLOSE__');
  
  // 等 200ms 再真正关闭
  setTimeout(() => {
    dc.close();
  }, 200);
}

状态机与重连策略

实际项目中,很少只用一条通道从头用到尾。更常见的做法是:

  1. 监听 onclose 和 onerror
  2. 一旦触发,立即启动重连流程
  3. 重连时重新创建 PeerConnection 和数据通道
  4. 恢复之前的状态(比如未发送的消息)

我记得有个项目,用户反馈「偶尔会丢消息」。查了半天,发现是重连时新旧通道混用了——旧通道已经 closed,但代码还在往上面 send。从那以后,我每次重连都会把旧通道的引用置为 null,强制代码走新通道。

核心要点回顾:

  • 四种状态:connecting → open → closing → closed,单向流转
  • 异常断开会直接从 open 跳到 closed
  • closed 后必须重建通道,不能复用
  • 用消息队列 + 重连缓冲处理断连期间的数据
  • closing 是优雅关闭阶段,给双方时间处理剩余数据

状态机这东西,看着简单,但每个状态背后都有对应的网络行为。理解了它,你就能在数据通道出问题时快速定位——到底是没连上、正在关、还是已经断了。嗯,下次遇到通道异常,先看看 readyState,你就知道该往哪个方向查了。

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