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 连接被主动关闭 正常退出,不需要重连

核心要点:disconnectedfailed 是重连逻辑的两个关键入口。前者是“可能还能救”,后者是“没救了,重新来吧”。

监听 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 秒。因为移动网络切换基站时,断开时间通常更短。

实现自动重连逻辑:三步走策略

自动重连说白了就三步:拆旧、重建、续传。我把它总结成一张流程图:

自动重连逻辑流程图 检测到 disconnected 等待缓冲时间 (3s) 状态仍为 disconnected? ✅ 连接恢复,继续使用 🔄 重建 PeerConnection 否(已恢复) 是(仍断开)

重建 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 传文件或实时数据,重连时有个大坑——数据丢失

我建议的做法是:

  1. 发送方维护一个“已发送但未确认”的队列
  2. 接收方收到数据后回复 ACK
  3. 重连成功后,发送方重新发送队列中未确认的数据
// 发送方
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 实现有多优雅,他只在乎“视频能不能一直通”。

嗯,这就是我踩过无数坑后总结出来的经验。


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