4、连接状态调试:监控 iceConnectionState 和 iceGatheringState 的变化,定位连接失败原因
做 WebRTC 开发,最头疼的是什么?
我个人觉得,就是看着两个端点明明都在线,但就是连不上。没有报错,没有崩溃,就是卡在那里不动。这时候,iceConnectionState 和 iceGatheringState 就是你最好的朋友。
说白了,这两个状态机就是 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 状态变化流程图
下面这张图是我自己总结的,每次调试时都会在脑子里过一遍。你想想看,如果连接失败了,你至少要知道它卡在哪一步。
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 状态也是一样的道理。