11、数据通道状态机:connecting、open、closing、closed 状态转换、异常断开处理
数据通道的状态机,说白了就是一条连接的「生命周期」。我刚开始接触 WebRTC 时,觉得状态机就是个简单的开关——要么通,要么断。后来在项目中踩过几次坑才发现,这玩意儿比想象中要细腻得多。
今天我们就来彻底搞懂它。
状态机概览
RTCDataChannel 一共有四种状态:
| 状态 | 含义 | 触发时机 |
|---|---|---|
| connecting | 正在建立连接 | 创建通道后、底层传输就绪前 |
| open | 连接已建立,可收发数据 | 底层 SCTP 关联建立成功 |
| closing | 正在关闭连接 | 调用 close() 或对端发起关闭 |
| closed | 连接已关闭 | 关闭流程完成或连接异常断开 |
这四种状态是单向流转的,不会往回跳。也就是说,一旦进入 closed,就不可能再回到 open。嗯,这里要注意——有些新手会尝试在 closed 后重新 send(),那肯定会报错。
状态转换流程
我习惯把状态转换画成一条直线:
你看,正常流程是 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,数据通道会经历短暂的断开再重建。
我曾经在一个实时协作项目里,用户频繁切换网络,导致数据通道反复断开。一开始我简单地在 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);
}
}
closing 状态的细节
closing 这个状态很多人会忽略。它其实是一个「优雅关闭」的过渡阶段。调用 close() 后,通道会进入 closing,此时:
- 不能再发送新数据(send() 会失败)
- 但尚未发送完的数据会继续传输
- 等待对端确认关闭后,才进入 closed
说白了,closing 就是给双方一个「把话说完」的机会。我建议在关闭通道前,先发一条「我要关了」的消息,让对方有个准备。
function gracefulClose(dc) {
if (dc.readyState !== 'open') return;
// 先通知对端
dc.send('__CLOSE__');
// 等 200ms 再真正关闭
setTimeout(() => {
dc.close();
}, 200);
}
状态机与重连策略
实际项目中,很少只用一条通道从头用到尾。更常见的做法是:
- 监听 onclose 和 onerror
- 一旦触发,立即启动重连流程
- 重连时重新创建 PeerConnection 和数据通道
- 恢复之前的状态(比如未发送的消息)
我记得有个项目,用户反馈「偶尔会丢消息」。查了半天,发现是重连时新旧通道混用了——旧通道已经 closed,但代码还在往上面 send。从那以后,我每次重连都会把旧通道的引用置为 null,强制代码走新通道。
核心要点回顾:
- 四种状态:connecting → open → closing → closed,单向流转
- 异常断开会直接从 open 跳到 closed
- closed 后必须重建通道,不能复用
- 用消息队列 + 重连缓冲处理断连期间的数据
- closing 是优雅关闭阶段,给双方时间处理剩余数据
状态机这东西,看着简单,但每个状态背后都有对应的网络行为。理解了它,你就能在数据通道出问题时快速定位——到底是没连上、正在关、还是已经断了。嗯,下次遇到通道异常,先看看 readyState,你就知道该往哪个方向查了。