8、ICE与网络穿透:ICE候选者、NAT穿透原理、STUN协议详解、TURN中继服务
WebRTC 最让人头疼的问题是什么?
不是编解码,不是信令,而是——连不上。
你写好了 P2P 通信代码,本地测试一切正常,一放到公网环境就歇菜。为什么?因为现实世界的网络环境太复杂了:公司内网、家庭路由器、运营商级 NAT、防火墙……两个设备之间可能隔着好几层 NAT 网关,根本找不到对方。
ICE(Interactive Connectivity Establishment)就是来解决这个问题的。它不是一个单独的协议,而是一套候选者收集 + 连通性检测 + 优先级排序的框架。说白了,ICE 帮你回答三个问题:
- 我能用哪些地址连对方?
- 这些地址里哪个能真正通?
- 如果都不通,我该找谁帮忙?
8.1 ICE 候选者:三种身份,一个目标
ICE 的核心概念是 Candidate(候选者)。每个候选者代表一个可能的通信端点。WebRTC 会收集三类候选者:
| 候选者类型 | 来源 | 优先级 | 典型场景 |
|---|---|---|---|
| host | 本机网卡 IP | 最高 | 局域网内通信 |
| srflx | STUN 反射地址 | 中等 | 普通 NAT 穿透 |
| relay | TURN 中继地址 | 最低 | 对称 NAT / 防火墙 |
ICE 的优先级策略很简单:能直连就别绕路。host 候选者延迟最低,优先尝试;srflx 次之;relay 是最后的手段——毕竟走中继服务器会增加延迟和带宽成本。
优先级计算公式(RFC 5245):
priority = (2^24) * type_preference + (2^8) * local_preference + (2^0) * (256 - component_id)
其中 type_preference 对于 host 是 126,srflx 是 100,relay 是 0。
我在项目中遇到过一个问题:两个设备都在同一个局域网内,按理说 host 候选者应该直接连通,但实际却走了 relay。排查了半天,发现是 STUN 服务器返回的 srflx 地址优先级被错误地设得比 host 还高。嗯,这里要注意:候选者优先级不是客户端随意定的,必须严格遵循 RFC 规范。
8.2 NAT 穿透原理:为什么你的设备找不到自己?
NAT(网络地址转换)是家庭和企业网络中常见的设备。它的作用是把内网私有 IP 映射成公网 IP。但问题来了:NAT 只允许内网主动发起的连接通过,外网主动发来的数据包会被丢弃。
WebRTC 要做的,就是让两个都在 NAT 后面的设备建立直接连接。这听起来像是不可能完成的任务,对吧?
其实原理并不复杂。我们来看一个典型的场景:
- 设备 A(内网 192.168.1.10)想连接设备 B(内网 10.0.0.5)
- A 向 STUN 服务器发送请求,获取自己的公网映射地址(比如 203.0.113.10:3478)
- B 也做同样的事,获取自己的公网映射地址
- 双方交换这些候选者信息
- A 尝试向 B 的公网地址发送数据包
- 如果 NAT 允许,连接建立成功
但这里有个坑:NAT 的行为模式各不相同。有的 NAT 会为每个目标 IP 分配不同的端口(对称 NAT),有的则复用同一个端口(锥形 NAT)。ICE 的连通性检查就是用来探测这些细节的。
我曾经踩过的坑:有一次在客户现场部署,设备 A 能 ping 通设备 B 的公网 IP,但 WebRTC 就是连不上。后来发现 A 的 NAT 是端口限制型锥形 NAT,它只允许来自 B 的原始端口的数据通过。而 B 的 STUN 反射地址在 ICE 协商过程中发生了变化——因为 B 重新发送了 STUN 请求,端口变了。解决方案是让 B 在 ICE 过程中保持同一个 STUN 会话。
8.3 STUN 协议详解:你的公网地址是什么?
STUN(Session Traversal Utilities for NAT)是一个轻量级的 UDP 协议。它的工作流程简单到令人发指:
- 客户端向 STUN 服务器发送一个 Binding Request
- 服务器收到请求后,查看 UDP 数据包的源 IP 和端口
- 服务器把这些信息封装在 Binding Response 中返回给客户端
- 客户端就知道自己的公网映射地址了
就这么简单?对,就这么简单。但 STUN 协议本身还有一些细节值得注意:
- 事务 ID:每个请求都有一个唯一的事务 ID,用于匹配请求和响应
- 属性(Attribute):STUN 消息体由一系列 TLV(Type-Length-Value)属性组成,比如 MAPPED-ADDRESS、XOR-MAPPED-ADDRESS、SOFTWARE 等
- 鉴权:STUN 支持简单的用户名/密码鉴权,防止恶意请求
小技巧:你可以用 stunclient 命令行工具手动测试 STUN 服务器:
stunclient stun.l.google.com:19302
返回结果会显示你的公网 IP 和端口。我经常用这个来快速验证网络环境。
STUN 有一个重要的变种叫 XOR-MAPPED-ADDRESS。为什么需要 XOR?因为有些 NAT 会修改 UDP 负载中的 IP 地址(比如 ALG 功能),XOR 编码可以防止这种干扰。RFC 5389 强制要求使用 XOR-MAPPED-ADDRESS 而不是传统的 MAPPED-ADDRESS。
8.4 TURN 中继服务:最后的救命稻草
STUN 不是万能的。当遇到对称 NAT 或者防火墙阻止 UDP 时,STUN 就无能为力了。这时候需要 TURN(Traversal Using Relays around NAT)出场。
TURN 的原理很简单:所有数据都经过一个公网服务器中转。客户端向 TURN 服务器申请一个中继地址,然后把数据发送到 TURN 服务器,服务器再转发给对端。
你想想看,这其实违背了 WebRTC 的 P2P 初衷。但没办法,有些网络环境就是不允许直连。TURN 是最后的保障。
| 特性 | STUN | TURN |
|---|---|---|
| 数据传输 | 不传输媒体数据 | 中转所有媒体数据 |
| 服务器负载 | 极低 | 高(带宽消耗大) |
| 延迟 | 无额外延迟 | 增加一跳延迟 |
| 适用场景 | 锥形 NAT | 对称 NAT / 防火墙 |
TURN 协议的核心操作包括:
- Allocate:客户端请求分配一个中继地址
- Refresh:定期刷新中继地址的租约
- Send / Data:通过中继地址发送和接收数据
- CreatePermission:授权对端通过中继地址通信
实际部署建议:
我建议你在生产环境中至少部署两个 TURN 服务器,分布在不同的地理区域。因为 TURN 的带宽成本很高(每路视频流可能消耗 2-5 Mbps),如果所有用户都挤在一个服务器上,很快就会被流量费压垮。
另外,TURN 服务器需要开启 UDP 和 TCP 两种传输方式。有些企业防火墙只允许 TCP 出站,如果 TURN 只支持 UDP,这些用户就彻底连不上了。
8.5 ICE 完整流程:从收集到连接
现在我们把所有知识点串起来,看看 ICE 的完整工作流程:
- 候选者收集:每个 Peer 收集自己的 host、srflx、relay 候选者
- 候选者交换:通过信令通道交换候选者列表
- 连通性检查:每个 Peer 向对方的候选者发送 STUN Binding Request,检查是否可达
- 候选者排序:根据优先级和连通性结果,选择最优的候选者对
- 媒体传输:使用选中的候选者对进行音视频数据传输
ICE 还有一个重要的概念叫 ICE Restart。当网络环境发生变化时(比如 Wi-Fi 切换到 4G),ICE 需要重新开始整个流程。WebRTC 提供了 createOffer({ iceRestart: true }) 来触发重启。
我曾经踩过的坑:有一次用户反馈视频通话突然中断,但网络连接看起来是正常的。排查后发现是用户的手机从 Wi-Fi 切换到 4G 时,ICE 没有触发重启。原因是我们的代码中 oniceconnectionstatechange 事件处理得不够及时。解决方案是监听 iceconnectionstate 为 disconnected 或 failed 时,立即触发 ICE Restart。
8.6 知识体系总览
下面这张图总结了 ICE 与网络穿透的核心知识结构:
ICE 与网络穿透是 WebRTC 中最具挑战性的部分。它不像编解码那样有固定的算法,也不像信令那样有清晰的协议流程。它需要你理解 NAT 的行为模式,熟悉 STUN/TURN 的协议细节,还要能处理各种边界情况。
我个人觉得,掌握 ICE 最好的方法就是动手抓包分析。用 Wireshark 抓取 WebRTC 通信过程中的 STUN 消息,看看候选者是怎么交换的,连通性检查是怎么进行的。当你亲眼看到那些 UDP 数据包在 NAT 之间穿梭时,很多抽象的概念就变得具体了。
推荐工具:
- Wireshark:抓包分析 STUN/TURN 消息
- stuntman:自建 STUN/TURN 服务器进行测试
- Chrome webrtc-internals:查看 ICE 候选者和连接状态
好了,这一章的内容就到这里。记住一句话:ICE 不是魔法,它只是一套系统化的尝试和回退策略。理解了这一点,你就能在遇到连接问题时,有条不紊地排查和解决。
公众号:蓝海资料掘金营,微信deep3321