7、ICE连接状态机:从new到failed的完整旅程

ICE状态机,说白了就是WebRTC连接的生命周期管理。我刚开始接触WebRTC时,觉得这东西就是个黑盒子——丢个SDP进去,连上了就开心,连不上就抓瞎。后来踩了不少坑才明白,理解ICE状态机是排查连接问题的基本功。

7.1 ICE状态的五个阶段

ICE连接状态一共分为5个:new、checking、connected、completed、failed。还有一个disconnected状态,这个我们后面单独聊。

状态 含义 触发时机
new 刚创建,还没开始干活 创建RTCPeerConnection后
checking 正在收集候选者、发起连通性检查 调用setLocalDescription后
connected 至少找到一条可用路径 首次连通性检查成功
completed 所有候选者检查完毕 所有连通性检查完成
failed 所有候选者都连不上 超时或全部检查失败

核心要点:connected和completed的区别在于——connected表示"能用",completed表示"检查完了"。很多时候connected之后就直接用了,completed只是告诉你"没有更好的路径了"。

7.2 状态变化回调:oniceconnectionstatechange

这个回调是排查连接问题的第一道防线。我个人习惯在项目里这么写:

peerConnection.oniceconnectionstatechange = (event) => {
  const state = peerConnection.iceConnectionState;
  console.log(`[ICE状态变化] ${state}`);

  switch(state) {
    case 'new':
      // 刚创建,啥也没干
      break;
    case 'checking':
      // 正在努力找路,别急
      break;
    case 'connected':
      // 通了!但可能还有更好的路径
      break;
    case 'completed':
      // 所有候选者都检查完了
      break;
    case 'failed':
      // 出问题了,需要处理
      handleICEFAilure();
      break;
    case 'disconnected':
      // 临时断连,可能还能恢复
      break;
  }
};

我的经验:千万别只监听failed状态。我在项目中遇到过一种情况——网络抖动导致状态在connected和disconnected之间反复横跳,如果只监听failed,根本抓不到这个现象。建议把每个状态变化都打日志,方便事后分析。

7.3 连接超时处理:别让用户干等

ICE连接超时是个很头疼的问题。默认情况下,ICE会在30秒左右判定失败。但实际场景中,有些网络环境可能需要更长时间。

我曾经在一个跨国项目中遇到:用户从东南亚连到美国服务器,网络延迟本身就高,再加上丢包,ICE检查经常超时。后来我们做了两件事:

  1. 延长超时时间:通过ICE服务器配置,把超时时间从30秒延长到60秒
  2. 增加重试机制:第一次失败后,自动重新发起连接
// 设置ICE服务器和超时参数
const iceConfig = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }
  ],
  iceTransportPolicy: 'all',
  // 注意:浏览器没有直接暴露超时配置的API
  // 但可以通过ICE服务器间接控制
};

// 手动实现超时逻辑
const ICE_TIMEOUT = 30000; // 30秒
let iceTimer = null;

peerConnection.oniceconnectionstatechange = () => {
  const state = peerConnection.iceConnectionState;
  
  if (state === 'checking') {
    // 开始计时
    iceTimer = setTimeout(() => {
      console.warn('ICE连接超时,准备重试');
      restartICE();
    }, ICE_TIMEOUT);
  } else if (state === 'connected' || state === 'completed') {
    // 连接成功,取消计时
    clearTimeout(iceTimer);
  } else if (state === 'failed') {
    // 连接失败,清理计时器
    clearTimeout(iceTimer);
  }
};

function restartICE() {
  // 重新创建连接或触发ICE重启
  peerConnection.createOffer()
    .then(offer => peerConnection.setLocalDescription(offer))
    .catch(err => console.error('ICE重启失败:', err));
}

注意:不要频繁重启ICE。我曾经见过一个项目,每5秒重启一次ICE,结果把STUN服务器打爆了。建议至少间隔30秒以上,并且限制重试次数(比如3次)。

7.4 ICE状态机流程图

下面这张图是我自己整理的ICE状态流转关系,画了半小时,希望能帮你理清思路:

ICE连接状态机流转图 new 初始状态 checking 正在检查 connected 已连通 completed 检查完成 failed 连接失败 disconnected 临时断连 setLocalDescription 连通性检查成功 所有候选者检查完毕 全部检查失败 网络抖动/丢包 自动恢复 超时未恢复 图例: 初始 检查中 已连通 失败 断连 正常流转 异常/恢复流转

7.5 实际项目中的避坑指南

说了这么多理论,来点实际的。我在项目中遇到过几个典型问题:

坑1:connected之后就不管了

很多人看到connected就觉得万事大吉。但网络是会变的!用户从WiFi切到4G,或者中间有个路由器重启了,connected状态可能就变成disconnected了。我建议在connected之后,仍然监听状态变化,做好重连准备。

坑2:failed之后直接放弃

我曾经在一个项目中,用户反馈"视频打不开"。查了半天发现是ICE failed之后直接弹了个错误提示,没有重试。后来改成failed之后自动重启ICE,成功率从70%提升到了95%。

我的建议:在生产环境中,至少保留3次ICE重试机会。每次重试间隔递增(比如10秒、20秒、40秒),避免短时间内大量请求。

7.6 状态机调试技巧

调试ICE状态机,我一般用这招:

// 在控制台实时查看ICE状态
const pc = new RTCPeerConnection();
pc.oniceconnectionstatechange = () => {
  console.table({
    'ICE状态': pc.iceConnectionState,
    '收集状态': pc.iceGatheringState,
    '信号状态': pc.signalingState,
    '连接时间': new Date().toISOString()
  });
};

// 查看候选者信息
pc.onicecandidate = (event) => {
  if (event.candidate) {
    console.log('候选者:', event.candidate.candidate);
  }
};

嗯,ICE状态机这块内容不少,但核心就一句话:理解状态变化,做好异常处理。你想想看,如果连状态机都搞不清楚,出了问题连从哪下手都不知道,那多尴尬。


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