7、ICE连接建立:ICE Candidate是什么、收集Candidate、Candidate优先级、ICE连接状态变化(iceConnectionState)、连接建立成功回调
ICE,全称 Interactive Connectivity Establishment,翻译过来叫「交互式连接建立」。名字挺长,但说白了,它就是帮两个浏览器找到一条能通的路。
我刚开始接触 WebRTC 时,觉得 ICE 这层特别玄乎。后来踩了几次坑才明白——没有 ICE,你的音视频数据根本发不出去。为什么?因为现实网络太复杂了,有 NAT、有防火墙、有各种奇怪的网络拓扑。两个设备要直接通信,中间得翻多少座山?
嗯,ICE 就是那个翻山越岭的向导。
7.1 ICE Candidate 是什么
先说说 Candidate。Candidate 这个词直译是「候选人」,在 ICE 语境下,它代表一个「可能的连接地址」。每个 Candidate 包含:IP 地址、端口号、传输协议(UDP/TCP),以及一个类型标识。
类型分三种:
- host:本机网卡地址。比如你电脑的局域网 IP,192.168.1.100 这种。这是优先级最高的,因为走内网最快。
- srflx:Server Reflexive,服务器反射地址。说白了就是你的公网 IP,通过 STUN 服务器查到的。我在项目中遇到过,有些公司内网策略很严,srflx 地址死活拿不到,后来发现是 UDP 端口被防火墙封了。
- relay:中继地址,通过 TURN 服务器转发。这是最后的手段,延迟高,但一定能通。我曾经调试过一个跨国会议,两边网络都走不了 P2P,最后全靠 relay 撑着。
每个 Candidate 就是一个 JSON 结构,大概长这样:
{
"candidate": "candidate:1 1 UDP 2122252543 192.168.1.100 54321 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
}
注意那个长长的数字「2122252543」,那是优先级。后面会细说。
7.2 收集 Candidate
收集 Candidate 的过程,其实就是 ICE Agent 在背后默默干活。你调用 createOffer() 或 createAnswer() 之后,浏览器就开始收集了。
流程大致是这样:
- 先收集本机所有网卡地址(host 类型)。
- 然后向 STUN 服务器发请求,拿到公网映射地址(srflx 类型)。
- 如果配置了 TURN 服务器,还会申请中继地址(relay 类型)。
每个 Candidate 收集到后,会通过 onicecandidate 回调通知你。注意,这个回调会触发多次,每次携带一个 Candidate。当所有 Candidate 收集完毕时,会触发一次 null 回调。
我个人的习惯是,在 onicecandidate 里直接把 Candidate 通过信令通道发给远端。不要等全部收集完再发,那样会拖慢连接建立的速度。
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
// 立即发送给远端
signalingChannel.send({
type: 'candidate',
candidate: event.candidate
});
} else {
console.log('所有 Candidate 收集完毕');
}
};
7.3 Candidate 优先级
ICE 收集到一堆 Candidate 后,怎么选?答案是按优先级排序。优先级是一个 32 位整数,值越大越优先。
优先级的计算公式如下:
priority = (2^24) * type_preference + (2^8) * local_preference + (2^0) * (256 - component_id)
其中:
- type_preference:类型偏好。host 是 126,srflx 是 100,relay 是 0。
- local_preference:本地偏好,通常跟网卡优先级有关。有线网卡一般比无线网卡高。
- component_id:组件 ID。RTP 是 1,RTCP 是 2。
你想想看,host 类型的优先级天然就比 relay 高出一大截。所以只要内网能通,ICE 绝不会走中继。这个设计很合理——能直连就别绕路。
我见过一个案例:某次线上会议,用户反馈视频卡顿。排查后发现,ICE 选了一条 srflx 的路径,但实际两台机器在同一个局域网。为什么没选 host?因为其中一台机器的 host Candidate 被防火墙拦截了。后来调整了网络策略,host 路径才通。
7.4 ICE 连接状态变化(iceConnectionState)
ICE 连接建立不是一蹴而就的,它有一个状态机。你可以通过 oniceconnectionstatechange 回调来监听状态变化。
状态一共有 7 种:
| 状态 | 说明 |
|---|---|
new |
刚创建,还没开始收集 Candidate |
checking |
正在检查 Candidate 对是否可用 |
connected |
至少找到一条可用路径,但还在尝试其他路径 |
completed |
所有 Candidate 对都检查完毕,选出了最终路径 |
failed |
所有 Candidate 对都试过了,没有一条能通 |
disconnected |
连接丢失,但还在尝试恢复 |
closed |
连接被主动关闭 |
实际开发中,我最关注的是 connected 和 failed 这两个状态。前者表示连接通了,后者表示彻底没戏了。
peerConnection.oniceconnectionstatechange = () => {
const state = peerConnection.iceConnectionState;
console.log('ICE 状态:', state);
switch (state) {
case 'connected':
console.log('ICE 连接建立成功!');
break;
case 'failed':
console.error('ICE 连接失败,尝试重启 ICE');
peerConnection.restartIce();
break;
case 'disconnected':
console.warn('ICE 连接断开,等待恢复...');
break;
}
};
disconnected 状态不一定是永久性的。ICE 会尝试用 STUN 心跳来恢复连接。如果一段时间后还没恢复,才会变成 failed。所以不要一看到 disconnected 就立刻重启 ICE,给它一点时间。
7.5 连接建立成功回调
当 ICE 连接建立成功后,你会收到 oniceconnectionstatechange 回调,状态变为 connected 或 completed。但注意,这并不代表媒体流已经可以传输了。
为什么?因为 ICE 只是负责「通路」,媒体流还需要经过 SDP 协商、编解码器初始化等步骤。真正可以开始传输音视频数据的标志,是 ontrack 回调被触发。
我个人的经验是,在 connected 状态时,可以做一些 UI 上的提示,比如显示「连接中...」。等到 ontrack 触发后,再显示「已连接」,并开始渲染视频。
peerConnection.ontrack = (event) => {
console.log('收到远端媒体流');
const video = document.getElementById('remoteVideo');
video.srcObject = event.streams[0];
};
还有一个回调值得关注——onicecandidateerror。这个回调会在 Candidate 收集或连接过程中发生错误时触发。比如 STUN 服务器超时、TURN 服务器认证失败等。
peerConnection.onicecandidateerror = (event) => {
console.error('ICE Candidate 错误:', event.errorCode, event.errorText);
};
嗯,这个回调我一开始没留意,直到有一次线上环境频繁出现连接失败,排查了半天才发现是 TURN 服务器的凭证过期了。从那以后,我每个项目都会加上这个监听。
7.6 知识体系总览
下面这张图总结了 ICE 连接建立的核心流程:
这张图把 ICE 的三大阶段和状态机串在了一起。你可以看到,从收集 Candidate 开始,到优先级排序,再到连通性检查,最后进入状态机循环。一旦进入 connected 或 completed,就说明通路已经打通了。
最后说一句,ICE 这块调试起来确实有点麻烦。我建议你在开发阶段,把 onicecandidate、oniceconnectionstatechange、onicecandidateerror 这三个回调的日志都打出来。出问题时,看日志就能定位到是 Candidate 没收集到,还是连通性检查失败了。
嗯,ICE 连接建立就讲到这里。记住一句话:ICE 不是魔法,它只是一套严谨的选路和打洞机制。理解了它的工作原理,你就能驾驭它。