跳到主要内容

本篇是 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 的流程:收集 → 配对 → 探测 → 选定

  1. 收集(gathering):双方各收集 host / srflx(STUN) / relay(TURN) 候选。带 --use- 启动参数的场景不算,这里指真实网络。收集过程边发现边发(trickle,RFC 8838),不等收完就开始后面步骤——这就是 onicecandidate 被触发很多次的原因。
  2. 配对(pairing):把双方候选两两组成 candidate pair(我 host × 你 srflx、我 relay × 你 host……)。
  3. 探测(connectivity checks)每一对都要"试通"——从我的候选 A 向你的候选 B 发一个 STUN binding 请求,收到回复即该 pair 可达。这是 ICE 真正"打洞"的动作:那个往外发的探测包,恰好会让两边 NAT 各自开一个"临时洞"。
  4. 选定(nomination):两边里有一个是 controlling agent(默认 offerer),它从所有"试通且优先级最高"的 pair 里提名最终路径,另一方(controlled)跟随。媒体随后就沿着这条选中的路走。
  5. 保鲜(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 卡住时看什么

  1. 打开 chrome://webrtc-internals(Chromium),重开会话,看 ICE 相关的日志与 candidate-pair 统计;
  2. pc.iceConnectionState 停在哪个值:checking = 探测没成功(多半是没 TURN 且网络穿不过);disconnected/failed = 中途断了;
  3. getStats()candidate-pair:看 state(succeeded/failed)、nominatedcurrentRoundTripTime——能确认最终走的到底是 host/srflx/relay 哪条路,以及延迟多少;
  4. 最常用的一招:临时把 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)。

参考与来源