14、连接状态管理与重连:监听ICE连接状态、处理ICE断开事件、实现自动重连逻辑
WebRTC 的连接,说白了就是一条脆弱的“网线”。
你想想看,用户的网络环境千奇百怪——地铁里信号断断续续、Wi-Fi 突然切换、甚至手机从 4G 切到 5G 的瞬间,ICE 连接都可能说断就断。我在项目中遇到过最夸张的一次,用户开着视频会议从办公室走到电梯,连接断了 7 次。嗯,从那以后我就把重连机制当成了 WebRTC 应用的标配。
ICE 连接状态:你的“网络晴雨表”
ICE 连接状态,就是 WebRTC 告诉你“我现在连得好不好”的信号。它通过 RTCPeerConnection.iceConnectionState 属性暴露出来,同时会触发 oniceconnectionstatechange 事件。
我个人习惯把状态变化比作“交通信号灯”:
| 状态值 | 含义 | 我的经验 |
|---|---|---|
new |
刚创建,还没开始收集候选者 | 这时候啥也别干,等着就行 |
checking |
正在检查候选者对是否可用 | 如果卡在这里超过 10 秒,大概率是 NAT 穿透失败 |
connected |
至少找到一条可用路径 | 好消息,但还不是最终状态 |
completed |
所有候选者都检查完了 | 稳定连接,可以放心了 |
disconnected |
连接暂时丢失,但可能恢复 | ⚠️ 这是重连的黄金窗口期 |
failed |
连接彻底挂了 | 必须重建 PeerConnection |
closed |
连接被主动关闭 | 正常退出,不需要重连 |
核心要点:disconnected 和 failed 是重连逻辑的两个关键入口。前者是“可能还能救”,后者是“没救了,重新来吧”。
监听 ICE 连接状态:写一个“状态监听器”
代码其实很简单,但要注意一个坑——oniceconnectionstatechange 在有些浏览器里触发频率很高,别在里面做太重的操作。
const pc = new RTCPeerConnection(config);
pc.oniceconnectionstatechange = () => {
const state = pc.iceConnectionState;
console.log(`[ICE状态] ${state}`);
switch (state) {
case 'disconnected':
// 连接暂时断开,启动重连计时器
handleDisconnected();
break;
case 'failed':
// 连接彻底失败,重建连接
handleFailed();
break;
case 'connected':
case 'completed':
// 连接恢复,清除重连计时器
clearReconnectTimer();
break;
default:
break;
}
};
我的小技巧:除了监听 iceConnectionState,我还会同时监听 connectionState(这是更上层的状态,包含 ICE + DTLS)。两个一起看,判断更准确。
处理 ICE 断开事件:别急着重建
我曾经犯过一个错误——看到 disconnected 就立刻重建 PeerConnection。结果用户网络只是抖了一下,重建过程反而造成了更长的中断。
正确的做法是:给网络一点“缓冲时间”。
let reconnectTimer = null;
const RECONNECT_DELAY = 3000; // 3秒缓冲
function handleDisconnected() {
// 如果已经有计时器在跑,就别重复设置了
if (reconnectTimer) return;
console.log('[重连] 检测到断开,等待3秒看是否自动恢复...');
reconnectTimer = setTimeout(() => {
// 3秒后还是 disconnected,尝试重连
if (pc.iceConnectionState === 'disconnected') {
console.log('[重连] 未自动恢复,开始重连');
startReconnect();
}
}, RECONNECT_DELAY);
}
function clearReconnectTimer() {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
console.log('[重连] 连接已恢复,取消重连');
}
}
注意:这个缓冲时间不是固定的。我在项目中会根据网络类型动态调整——Wi-Fi 下给 5 秒,4G/5G 下给 2 秒。因为移动网络切换基站时,断开时间通常更短。
实现自动重连逻辑:三步走策略
自动重连说白了就三步:拆旧、重建、续传。我把它总结成一张流程图:
重建 PeerConnection 的代码,我建议封装成一个独立函数:
async function startReconnect() {
console.log('[重连] 开始重建连接...');
// 第一步:关闭旧连接
if (pc) {
pc.close();
}
// 第二步:创建新连接
const newPc = new RTCPeerConnection(config);
// ... 重新添加本地流、设置事件监听等
// 第三步:重新发起信令协商
const offer = await newPc.createOffer();
await newPc.setLocalDescription(offer);
// 发送 offer 到远端...
console.log('[重连] 新连接已建立');
}
关键点:重建时别忘了把之前的数据通道(DataChannel)也重新创建。我踩过这个坑——视频恢复了,但数据传输通道没重建,用户以为功能坏了。
重连策略的进阶优化
基本的重连逻辑写完了,但实际项目中还需要考虑几个问题:
- 重连次数限制:不能无限重连。我一般设 3 次上限,超过就提示用户“网络异常,请检查网络”。
- 指数退避:第一次重连等 3 秒,第二次等 6 秒,第三次等 12 秒。避免频繁重连浪费资源。
- 状态同步:重连成功后,需要把断开期间的数据补发。这个在数据通道里尤其重要。
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 3;
function getReconnectDelay() {
// 指数退避:3s, 6s, 12s
return Math.min(3000 * Math.pow(2, reconnectAttempts), 30000);
}
async function startReconnect() {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error('[重连] 已达最大重连次数,放弃');
showUserMessage('网络连接异常,请检查网络后刷新页面');
return;
}
reconnectAttempts++;
const delay = getReconnectDelay();
console.log(`[重连] 第 ${reconnectAttempts} 次尝试,等待 ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
// ... 执行重建逻辑
}
我的经验:重连成功后,最好重置 reconnectAttempts 为 0。否则用户网络连续抖动几次,很快就达到上限了。
数据通道的特殊处理
如果你用 DataChannel 传文件或实时数据,重连时有个大坑——数据丢失。
我建议的做法是:
- 发送方维护一个“已发送但未确认”的队列
- 接收方收到数据后回复 ACK
- 重连成功后,发送方重新发送队列中未确认的数据
// 发送方
const pendingQueue = [];
function sendData(data) {
const packet = { id: generateId(), data, timestamp: Date.now() };
pendingQueue.push(packet);
dataChannel.send(JSON.stringify(packet));
}
// 收到 ACK 后移除
function onAck(packetId) {
const idx = pendingQueue.findIndex(p => p.id === packetId);
if (idx !== -1) pendingQueue.splice(idx, 1);
}
// 重连后重发
function resendPending() {
console.log(`[重连] 重发 ${pendingQueue.length} 个未确认数据包`);
pendingQueue.forEach(packet => {
dataChannel.send(JSON.stringify(packet));
});
}
注意:这个队列不能无限增长。我一般设 1000 条上限,超过就丢弃最旧的数据。毕竟实时数据讲究的是“时效性”,太旧的数据发了也没意义。
写在最后
连接状态管理和重连,说白了就是“防患于未然”和“亡羊补牢”的结合。监听状态是防患,自动重连是补牢。两者缺一不可。
我个人习惯在项目初期就把重连机制写好,而不是等测试发现了再补。因为——你想想看,用户不会在乎你的 WebRTC 实现有多优雅,他只在乎“视频能不能一直通”。
嗯,这就是我踩过无数坑后总结出来的经验。