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检查经常超时。后来我们做了两件事:
- 延长超时时间:通过ICE服务器配置,把超时时间从30秒延长到60秒
- 增加重试机制:第一次失败后,自动重新发起连接
// 设置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状态流转关系,画了半小时,希望能帮你理清思路:
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