4、连接状态调试:监控 iceConnectionState 和 iceGatheringState 的变化,定位连接失败原因

做 WebRTC 开发,最头疼的是什么?

我个人觉得,就是看着两个端点明明都在线,但就是连不上。没有报错,没有崩溃,就是卡在那里不动。这时候,iceConnectionStateiceGatheringState 就是你最好的朋友。

说白了,这两个状态机就是 WebRTC 连接过程的「仪表盘」。你开车不能不看仪表盘吧?调试 WebRTC 也一样。

4.1 两个状态机,搞懂它们就成功了一半

先说说 iceGatheringState。它描述的是本地端收集候选地址的过程。我刚开始做的时候,以为只要拿到一个候选地址就够了。后来才发现,这玩意儿是动态的,会不断发现新的地址。

状态 含义 常见触发场景
new 还没开始收集 刚创建 RTCPeerConnection
gathering 正在收集候选地址 调用 setLocalDescription 后
complete 收集完毕 所有候选地址已生成

再来看 iceConnectionState。这个更关键,它反映的是两端之间连接的实际状态。嗯,这里要注意:connected 不代表媒体流已经通了,它只代表 ICE 协商成功了。

状态 含义 调试时重点关注
new 初始状态 检查是否调用了 createOffer
checking 正在检查候选对 如果卡在这里,说明候选地址不匹配
connected 至少一个候选对通了 恭喜,但还要看媒体流
completed 所有候选对检查完毕 正常状态
failed 连接失败 最需要分析的状态
disconnected 连接断开 网络波动或超时
closed 连接关闭 主动调用 close()

核心经验:我一般会同时监听这两个状态。如果 gathering 已经 complete 了,但 connection 还卡在 checking,那问题大概率出在 NAT 穿透上。

4.2 实战:写一个状态监控器

光说不练假把式。来看看我平时怎么监控这两个状态的。这段代码我几乎每个项目都会用,稍微改改就能直接跑。

const pc = new RTCPeerConnection(config);

// 监控 ICE 连接状态
pc.oniceconnectionstatechange = () => {
  console.log(`[连接状态] ${pc.iceConnectionState}`);
  
  if (pc.iceConnectionState === 'failed') {
    console.error('连接失败!开始排查...');
    // 这里可以触发重连逻辑
  }
  
  if (pc.iceConnectionState === 'disconnected') {
    console.warn('连接断开,可能是网络波动');
    // 可以尝试重新 ICE 重启
  }
};

// 监控 ICE 候选收集状态
pc.onicegatheringstatechange = () => {
  console.log(`[收集状态] ${pc.iceGatheringState}`);
  
  if (pc.iceGatheringState === 'complete') {
    console.log('候选地址收集完毕,共收集到:', 
      pc.localDescription.sdp.match(/a=candidate/g)?.length || 0, '个');
  }
};

// 监控每个候选地址的生成
pc.onicecandidate = (event) => {
  if (event.candidate) {
    console.log(`新候选地址: ${event.candidate.candidate}`);
  } else {
    console.log('候选地址生成结束');
  }
};

我的习惯:我会把状态变化加上时间戳,输出到控制台。这样回头排查问题时,能清楚地看到时间线。比如「3秒时 gathering 完成,5秒时 connection 失败」——这个时间差就是关键线索。

4.3 状态变化流程图

下面这张图是我自己总结的,每次调试时都会在脑子里过一遍。你想想看,如果连接失败了,你至少要知道它卡在哪一步。

ICE 连接状态调试流程图 创建 PeerConnection iceGatheringState: gathering iceConnectionState: checking connected / completed failed 排查方向:检查 STUN/TURN 配置 | 查看候选地址类型 | 确认防火墙规则 常见原因:NAT 穿透失败 | TURN 服务器不可用 | SDP 不匹配

4.4 定位失败原因:我的三板斧

iceConnectionState 变成 failed 时,别慌。我有一套固定的排查流程,分享给你。

第一板斧:看候选地址类型

onicecandidate 回调里,每个候选地址都带类型信息。我一般会这样过滤:

pc.onicecandidate = (event) => {
  if (event.candidate) {
    const type = event.candidate.type; // host / srflx / relay
    const priority = event.candidate.priority;
    console.log(`类型: ${type}, 优先级: ${priority}`);
    
    // 如果只有 host 类型,说明 NAT 穿透可能有问题
    if (type === 'host') {
      console.warn('只有本地地址,检查 STUN 配置');
    }
  }
};

我曾经踩过的坑:有一次线上环境连接失败,排查了半天,发现是 STUN 服务器地址写错了。更坑的是,控制台没有任何报错,只是 silently 地失败了。所以我现在都会在初始化时打印一下 ICE 服务器配置,确认地址正确。

第二板斧:检查 SDP 交换

连接失败,很多时候是 SDP 没传对。我会在两端都打印 SDP 的关键字段:

// 发送端
pc.createOffer().then(offer => {
  console.log('本地 SDP 类型:', offer.type);
  console.log('ICE 候选数量:', offer.sdp.match(/a=candidate/g)?.length);
  // 检查是否有 ICE 相关字段
  if (!offer.sdp.includes('ice-ufrag')) {
    console.error('SDP 缺少 ice-ufrag 字段!');
  }
});

第三板斧:看日志时间线

我个人习惯把状态变化和时间戳一起记录下来。比如:

  • 0ms:创建 PeerConnection
  • 50ms:gathering 开始
  • 200ms:gathering 完成(收集到 5 个候选)
  • 300ms:connection 进入 checking
  • 5000ms:connection 变成 failed

看到没?从 checking 到 failed 花了 4.7 秒。这个时间就是 ICE 在尝试所有候选对的时间。如果候选对很多,但都失败了,那基本可以确定是网络层面的问题。

避坑指南:我曾经遇到过一个 case,connection 状态在 connected 和 disconnected 之间反复横跳。后来发现是 Wi-Fi 信号不稳定,导致候选地址频繁失效。解决方案是加一个重连机制,当 disconnected 时触发 ICE restart。

4.5 一个完整的调试模板

最后,给你一个我常用的调试模板。直接复制到项目里,连接问题基本都能定位到。

class ConnectionDebugger {
  constructor(pc) {
    this.pc = pc;
    this.logs = [];
    this.startTime = Date.now();
    
    pc.oniceconnectionstatechange = () => this.logState('connection');
    pc.onicegatheringstatechange = () => this.logState('gathering');
    pc.onicecandidate = (e) => this.logCandidate(e);
  }
  
  logState(type) {
    const state = type === 'connection' 
      ? this.pc.iceConnectionState 
      : this.pc.iceGatheringState;
    const elapsed = Date.now() - this.startTime;
    this.logs.push(`[${elapsed}ms] ${type}: ${state}`);
    console.log(`[调试] ${this.logs[this.logs.length-1]}`);
    
    if (state === 'failed') {
      console.error('连接失败,日志如下:');
      console.table(this.logs);
    }
  }
  
  logCandidate(event) {
    if (event.candidate) {
      console.log(`候选: ${event.candidate.type} | ${event.candidate.ip}:${event.candidate.port}`);
    }
  }
}

// 使用方式
const pc = new RTCPeerConnection(config);
const debugger = new ConnectionDebugger(pc);

这个模板我用了快两年了。说白了,调试 WebRTC 连接,就是跟这两个状态机打交道。你只要把它们的每个变化都记录下来,连接失败的原因基本就浮出水面了。

嗯,记住一点:不要只看最终状态,要看变化过程。就像看病一样,医生不光看你的体温,还要看体温的变化曲线。ICE 状态也是一样的道理。


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