11. ICE Candidate 交换:ICE 协议简介、Candidate 消息处理、Trickle ICE 实现
好,咱们进入第11章。说实话,ICE 这块是 WebRTC 里最容易让人头大的部分之一。我刚接触那会儿,看着一堆 Candidate 信息在信令通道里飞来飞去,完全搞不清它们在干嘛。后来踩了不少坑,才慢慢摸清楚门道。
这一章,咱们就聊聊 ICE Candidate 的交换过程。我会把 ICE 协议的核心逻辑、Candidate 消息怎么处理、以及 Trickle ICE 的实现细节,掰开揉碎了讲清楚。
11.1 ICE 协议简介:它到底在解决什么问题?
ICE,全称 Interactive Connectivity Establishment,交互式连接建立。名字挺长,说白了就是:帮两个浏览器找到一条能通的路。
你想想看,两个设备可能在不同的局域网后面,中间有 NAT 路由器、有防火墙。A 想直接连 B,但 A 的 IP 是内网的 192.168.x.x,B 也是内网地址。它们怎么知道对方的公网地址?ICE 就是干这个的。
ICE 协议的核心思路是:收集所有可能的连接方式,然后逐个尝试,找到能用的那条。
这些“连接方式”就是 Candidate。每个 Candidate 包含一个 IP 地址和端口号,以及传输协议(通常是 UDP)。ICE 会把这些 Candidate 分成三类:
| Candidate 类型 | 来源 | 说明 |
|---|---|---|
| host | 本机网卡 | 内网 IP,比如 192.168.1.100:3478 |
| srflx | STUN 服务器 | NAT 映射后的公网 IP,比如 203.0.113.50:5000 |
| relay | TURN 服务器 | 中继地址,用于对称 NAT 或防火墙限制的情况 |
我个人习惯把 ICE 比作“找路游戏”。host 是家门口的路,srflx 是小区大门外的路,relay 是绕远路但一定能走通的高速公路。ICE 会优先尝试 host,不行再试 srflx,最后才用 relay。
11.2 Candidate 消息处理:信令通道里传的是什么?
ICE Candidate 的交换,是通过信令服务器中转的。A 收集到自己的 Candidate 后,打包成一条消息发给信令服务器,服务器再转发给 B。反过来也一样。
一条典型的 Candidate 消息长什么样?看代码:
{
"type": "candidate",
"candidate": {
"candidate": "candidate:1 1 UDP 2122252543 192.168.1.100 3478 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
这里每个字段都有含义:
- candidate 字符串:核心信息,包含优先级、IP、端口、类型等
- sdpMid:对应 SDP 中的媒体描述标识,比如 "0" 表示音频,"1" 表示视频
- sdpMLineIndex:媒体行的索引,从 0 开始
我在项目中遇到过一个问题:两个 Candidate 的 sdpMid 和 sdpMLineIndex 对不上,结果 ICE 连接一直失败。排查了半天才发现是信令服务器转发时把字段搞乱了。所以,处理 Candidate 消息时,一定要严格校验这两个字段。
11.3 Trickle ICE 实现:不等了,边收集边发
传统的 ICE 流程是:先收集所有 Candidate,全部收集完再一次性发给对方。这个过程可能很慢,尤其是 relay 类型的 Candidate,需要跟 TURN 服务器交互,耗时可能几秒钟。
Trickle ICE 的思路是:不等了,收集到一个就发一个。这样对方可以尽早开始连通性检查,整体连接速度会快很多。
实现 Trickle ICE,信令服务器需要支持增量式的 Candidate 转发。客户端代码大致是这样:
// 创建 RTCPeerConnection
const pc = new RTCPeerConnection(config);
// 监听 ICE Candidate 事件
pc.onicecandidate = (event) => {
if (event.candidate) {
// 收集到一个 Candidate,立即通过信令通道发送
sendToSignalingServer({
type: 'candidate',
candidate: event.candidate
});
} else {
// candidate 为 null 表示收集完成
console.log('所有 Candidate 已收集完毕');
}
};
// 收到对方的 Candidate 时,添加到连接中
function handleRemoteCandidate(candidateMsg) {
const candidate = new RTCIceCandidate(candidateMsg.candidate);
pc.addIceCandidate(candidate).catch(e => {
console.error('添加远程 Candidate 失败:', e);
});
}
这里有个细节要注意:onicecandidate 事件在 Candidate 收集完成时会触发一次,此时 event.candidate 为 null。很多新手会忽略这个信号,导致逻辑不完整。
11.4 ICE 连接状态与 Candidate 优先级
ICE 在尝试连接时,会给每个 Candidate 对(本地 + 远程)计算优先级。优先级越高,越先尝试。计算公式比较复杂,但你可以简单理解为:host 类型优先级最高,relay 类型最低。
ICE 连接状态的变化,可以通过 oniceconnectionstatechange 监听:
pc.oniceconnectionstatechange = () => {
console.log('ICE 状态:', pc.iceConnectionState);
// 可能的状态: new, checking, connected, completed, failed, disconnected, closed
};
嗯,这里要注意:failed 状态不一定是最终结果。有时候网络抖动会导致短暂失败,ICE 会自动重试。我建议在 failed 状态持续超过 5 秒后再做重连处理。
11.5 一张图看懂 ICE Candidate 交换流程
下面这张 SVG 图,展示了完整的 ICE Candidate 交换过程。从收集到发送,再到连通性检查,一目了然。
11.6 实战中的几个关键点
最后,分享几个我在实际项目中总结的经验:
- Candidate 去重很重要:同一个 Candidate 可能因为网络波动被多次触发,信令服务器要做好去重,否则对方会收到大量重复消息。
- 超时处理不能少:如果 30 秒内 ICE 还没建立连接,我建议主动触发重连。别傻等。
- 日志要详细:ICE 失败的原因千奇百怪,把 Candidate 信息、状态变化都记下来,排查问题时会省很多时间。
- Trickle ICE 不是万能的:有些老旧浏览器不支持 Trickle ICE,需要 fallback 到传统模式。信令服务器最好兼容两种方式。
核心要点回顾:
- ICE 协议负责找到两个设备之间的可用网络路径
- Candidate 有三种类型:host、srflx、relay,优先级依次降低
- Trickle ICE 边收集边发送,能显著缩短连接建立时间
- 信令服务器需要处理 Candidate 的转发、去重和顺序问题
- ICE 连接状态变化要监听,失败后要有重试机制