本篇是 WebRTC 101 系列 第 3 篇,讲 WebRTC 最"魔法"也最常被问的部分:两个躲在路由器后面的浏览器,到底怎么互相找到并打通。原理为主。
NAT 与 ICE:STUN / TURN 是怎么"打洞"的
第 2 篇末尾说过:iceConnectionState 卡在 checking = 网络层没通。这篇就是"网络层怎么通"的全过程。先看为什么这事这么难。
1. 问题:你们俩都没有"可被对方直接连到的地址"
你家路由器(NAT)会把你设备的私有地址(192.168.x.x)映射成一个公网出口。问题是:
- 你不知道自己在这个公网上"看起来"是哪个地址和端口;
- 对方更不知道;
- 更糟的是,很多 NAT 只允许"由内向外发起的连接"的回包通过——外部想先连你?会被路由器直接丢弃。
所以"拿到对方公网 IP 然后直连"这招根本行不通。需要一层层的候选 + 探测,这就是 ICE 干的活。ICE 的名字很直白:Interactive Connectivity Establishment——交互式地建立连接(RFC 8445)。
2. 三种"候选地址"(candidate)与三个协议对应
ICE 的输入是 SDP 里协商后,双方各自收集的一组候选地址。每个候选就是"我身上可能能被连到的一个地址",分四类:
| 候选类型 | 来源 | 协议/谁给 | 优先级(默认) | 说明 |
|---|---|---|---|---|
| host | 本机网卡地址 | 自己 | 高 | 我的 192.168.x.x 或真实公网 IP |
| server-reflexive (srflx) | NAT 外的映射地址 | STUN 反射 | 中 | "我在公网长这样" |
| peer-reflexive (prflx) | 探测中临时发现 | 对方 | — | 连通性检查过程中被"撞"出来的地址 |
| relay | 中继服务器分配的地址 | TURN 分配 | 低 | 最兜底:所有包经服务器转发 |
三个协议于是各司其职:
| 协议 (RFC) | 干什么 | 一句话 |
|---|---|---|
| STUN(RFC 8489) | 向公网服务器问"我的映射地址是什么" | "帮我照镜子" |
| TURN(RFC 8656) | 在服务器上分配中继地址,媒体走服务器转发 | "帮我传话" |
| ICE(RFC 8445) | 编排上述候选与探测,挑出最终那条路 | "总导演" |
新手最容易混淆的点:STUN 只是"查地址",TURN 才真正转发流量。用 TURN = 流量先到你的服务器再转发,所以它烧服务器带宽、延迟更高,但一定能通(只要网络允许到 TURN 服务器)。
3. 一个典型配置长什么样
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" }, // 免费公网 STUN(见"国内坑")
{ urls: "turn:turn.example.com:3478", // 自建/云厂商 TURN
username: "…", credential: "…" }, // 短期凭证, 生产用 REST 接口发
],
iceTransportPolicy: "all", // 或 "relay": 只走 TURN(保隐私/可预期)
});
几个工程事实:
- STUN 通常可公开免费,TURN 必须有账号凭证(服务器要认领流量);
- 生产环境 TURN 凭证走短期票据(带时间戳的用户名,如 coturn 的
use-auth-secret),几分钟过期,别写死长密钥; iceTransportPolicy: "relay"会让连接只走 TURN——慢但绝不泄露真实 IP,对强隐私/被墙场景有用,代价是成本和带宽。
4. ICE 的流程:收集 → 配对 → 探测 → 选定
- 收集(gathering):双方各收集 host / srflx(STUN) / relay(TURN) 候选。带
--use-启动参数的场景不算,这里指真实网络。收集过程边发现边发(trickle,RFC 8838),不等收完就开始后面步骤——这就是onicecandidate被触发很多次的原因。 - 配对(pairing):把双方候选两两组成 candidate pair(我 host × 你 srflx、我 relay × 你 host……)。
- 探测(connectivity checks):每一对都要"试通"——从我的候选 A 向你的候选 B 发一个 STUN binding 请求,收到回复即该 pair 可达。这是 ICE 真正"打洞"的动作:那个往外发的探测包,恰好会让两边 NAT 各自开一个"临时洞"。
- 选定(nomination):两边里有一个是 controlling agent(默认 offerer),它从所有"试通且优先级最高"的 pair 里提名最终路径,另一方(controlled)跟随。媒体随后就沿着这条选中的路走。
- 保鲜(consent freshness,RFC 7675):连接期间持续用轻量 STUN 探测确认"这条路还活着",断了才切候选。
一个经常被误解的点:最终很可能不是双方的最佳直连。ICE 会选"优先级最高且能通"的 pair——通常默认 host 优先,但现代实现常把 relay 的优先级调高,或干脆由策略决定(比如企业网络里 host/srflx 全都不通,只能 relay)。
5. 为什么有的网络"必须靠 TURN"
能不能直连取决于 NAT 的行为。简化的 NAT 分类:
| NAT 类型 | 对外表现 | 直连难度 |
|---|---|---|
| 全锥型 / 受限锥型 | 对"谁进来的"较宽松 | 好穿 |
| 端口受限锥型 | 只认自己连过的那台 | 中等 |
| 对称型 | 每次对外都换新映射 | 基本穿不了,只能 TURN 中继 |
两个都躲在对称 NAT / 严格防火墙后面(很多企业网、部分运营商大网、以及两边都用 4G 出口时),直连成功率很低——这不是代码 bug,是网络现实。这也是为什么任何要"保证能通"的 WebRTC 产品都必须配 TURN。
6. 排查:checking 卡住时看什么
- 打开
chrome://webrtc-internals(Chromium),重开会话,看 ICE 相关的日志与 candidate-pair 统计; - 看
pc.iceConnectionState停在哪个值:checking= 探测没成功(多半是没 TURN 且网络穿不过);disconnected/failed= 中途断了; - 用
getStats()查candidate-pair:看state(succeeded/failed)、nominated、currentRoundTripTime——能确认最终走的到底是 host/srflx/relay 哪条路,以及延迟多少; - 最常用的一招:临时把
iceTransportPolicy设为relay。如果 relay 能通而默认配置不通,就证明是直连被 NAT/防火墙挡了,去配 TURN 即可——这个二分法能快速把问题定位到"网络"还是"代码"。
7. 隐私与安全:一张双刃剑
- host 候选会泄露你的真实内网/公网 IP,这是 WebRTC 的著名隐私特性(第 0 篇提到)。现代浏览器默认对 host 候选做 mDNS 混淆(把你的主机名伪装成
xxx.local),降低泄露面;但你主动getDisplayMedia或某些实现仍可能暴露真实地址。 - 想彻底不暴露:
iceTransportPolicy: "relay"(全走 TURN),代价是延迟与成本。 - 仅当心把真实设备 IP 打到日志/统计里——排查时注意脱敏。
8. 国内环境的特定坑(对中文读者额外提醒)
stun.l.google.com:19302这类公共 STUN/TURN 在大陆常连不通或极不稳定,依赖它会间歇性卡在checking;- 面向国内用户的产品:自建/使用国内可达的 TURN(coturn 一台云主机即可起步,见第 6 篇),并做"STUN/TURN 可直达"的启动自检;
- 弱网 + UDP 被 QoS 丢的时候,TURN 记得开 TCP/443 模式兜底(部分企业/校园网禁 UDP)。