4、ICE协议与NAT穿透:NAT类型、ICE工作原理、STUN与TURN服务器、Candidate优先级与类型
WebRTC 要解决的核心问题,说白了就是:两个浏览器怎么在复杂的网络环境下找到对方,并建立一条能用的数据通道。
现实世界里的网络,大部分设备都躲在 NAT(网络地址转换)后面。你想想看,你的电脑拿到的往往是 192.168.x.x 这样的内网 IP,而互联网上的其他设备根本不知道这个地址在哪。ICE 协议就是专门解决这个问题的。
我个人习惯把 ICE 比作一个“网络侦探”——它负责收集所有可能的连接方式,然后逐一尝试,直到找到一条能通的路。
4.1 NAT 类型:你的网络到底有多“封闭”?
在讲 ICE 之前,得先搞清楚 NAT 的分类。不同的 NAT 行为,决定了穿透的难度。
我在项目中遇到过最头疼的情况,就是对称型 NAT。这种 NAT 几乎无法用单纯的 STUN 穿透。
| NAT 类型 | 行为特征 | 穿透难度 |
|---|---|---|
| 完全锥型 | 映射后的公网 IP:Port 对任何外部主机都开放 | 容易 |
| 限制锥型 | 只允许曾经发过包的目标 IP 回包 | 中等 |
| 端口限制锥型 | 限制更严格,必须 IP 和 Port 都匹配 | 较难 |
| 对称型 | 每次发往不同目标,都使用不同的公网端口 | 极难 |
4.2 ICE 工作原理:从“猜”到“通”
ICE(Interactive Connectivity Establishment)的核心逻辑其实不复杂:收集候选者 → 排序 → 连通性检查 → 选定最终路径。
嗯,这里要注意,ICE 并不是一股脑把所有候选者都试一遍。它有一个优先级机制,会优先尝试理论上成功率最高的组合。
下面这张图展示了 ICE 的完整工作流程:
ICE 的检查策略是“先快后慢”。它会先尝试延迟最低的候选者对,比如两台机器在同一个局域网内。如果不行,再尝试通过 STUN 打洞。最后实在不行,才走 TURN 中继。
4.3 STUN 与 TURN 服务器:打洞与中继
STUN 和 TURN 是 ICE 的两大“武器”。它们的作用完全不同。
- STUN(Session Traversal Utilities for NAT):帮设备发现自己的公网 IP 和端口。说白了就是“照镜子”。
- TURN(Traversal Using Relays around NAT):当打洞失败时,作为中继转发数据。代价是带宽和延迟。
我记得有一次做跨国 WebRTC 通话测试,两边都是对称型 NAT,STUN 完全失效。最后只能走 TURN 中继,延迟从 20ms 飙升到 200ms。虽然能通,但体验确实差了不少。
关键区别:
- STUN 只负责“发现地址”,不转发媒体数据。
- TURN 负责“转发数据”,但会消耗服务器带宽。
- ICE 会优先尝试 STUN 打洞,TURN 是最后的手段。
4.4 Candidate 优先级与类型
ICE 候选者(Candidate)分为三种类型,每种都有自己的优先级计算公式。
| 候选者类型 | 代码标识 | 来源 | 优先级范围 |
|---|---|---|---|
| Host | host | 本地网卡 IP(内网地址) | 最高(2130706431 ~ 1694498815) |
| Server Reflexive | srflx | STUN 服务器返回的公网映射地址 | 中等(1694498815 ~ 1007054847) |
| Relay | relay | TURN 服务器分配的中继地址 | 最低(1007054847 ~ 0) |
优先级计算公式是这样的:
priority = (2^24) * type_preference + (2^8) * local_preference + (2^0) * component_id
其中 type_preference 对于 host 是 126,srflx 是 100,relay 是 0。这个公式确保了 host 候选者永远优先于 srflx,srflx 永远优先于 relay。
我个人的调试习惯:在 Chrome 的 chrome://webrtc-internals 页面里,可以直接看到每个候选者的优先级数值。如果发现 relay 候选者被选中了,基本可以断定是 NAT 穿透失败。
最后提一下 ICE 提名(Nomination)。当连通性检查通过后,ICE 会“提名”一个候选者对作为最终路径。这个过程分为两种模式:
- 常规提名:等所有检查完成后再选最优的。
- 激进提名:发现第一个可用的就直接用,不等了。
WebRTC 默认使用激进提名。为什么?因为快。用户体验上,早一秒建立连接比晚一秒但路径稍微好一点更重要。我在项目中验证过,激进提名在大多数场景下都能在 200ms 内完成连接建立。