4. ICE 与 NAT 穿透:ICE 框架、STUN/TURN 协议、Candidate 收集与连通性检查

WebRTC 要解决的核心问题之一,就是让两个身处不同内网的用户能直接连上。说白了,就是打穿 NAT。我早年做 VoIP 网关时,被 NAT 问题折磨得够呛——明明两边都能上网,就是连不通。后来 WebRTC 把 ICE 框架标准化了,才算是有了统一的解法。

这一节我们深入 ICE 的底层逻辑。你理解了 ICE,就理解了 WebRTC 连接建立的本质。

4.1 为什么需要 ICE?

先看一个典型场景:

  • 用户 A 在办公室内网(192.168.1.x)
  • 用户 B 在家里的内网(10.0.0.x)
  • 两边都通过 NAT 设备访问公网

如果 A 直接往 B 的私有 IP 发数据,路由器会直接丢弃。因为私有 IP 在公网上不可路由。ICE 的作用就是:帮双方找到一条能通的路径,不管是直连、通过 STUN 打洞,还是走 TURN 中继。

ICE 的核心思想:收集所有可能的网络路径(Candidate),然后两两测试,选出最优且可用的那条。

4.2 ICE 框架概览

ICE 不是一个单独的协议,而是一个框架。它整合了 STUN 和 TURN,再加上自己的连通性检查逻辑。整个流程可以概括为三步:

  1. 收集 Candidate——找出本机所有可能的 IP:Port 组合
  2. 交换 Candidate——通过信令通道把列表发给对端
  3. 连通性检查——双方互相发送 STUN Binding Request,验证哪些路径能通

下面这张图展示了 ICE 的整体流程:

ICE 框架核心流程 Peer A Peer B 信令服务器 Candidate 交换 Candidate 交换 阶段 1:收集 Candidate host / srflx / relay 阶段 2:排序与交换 按优先级排序 → 通过信令发送给对端 阶段 3:连通性检查 STUN Binding Request/Response → 验证路径可用性 阶段 4:选路与媒体传输 选择最优可用路径 → 开始 RTP/RTCP 传输 ICE 的核心价值:在不可靠的网络环境中,找到一条可靠的通信路径

4.3 STUN 协议:打洞的探针

STUN(Session Traversal Utilities for NAT)是 ICE 中最基础的组件。它的作用很简单:帮我看看我的公网 IP 和端口是什么。

工作原理:

  • 客户端向公网 STUN 服务器发送 Binding Request
  • STUN 服务器在响应中返回:你来自 X.X.X.X:YYYYY
  • 这个地址就是 srflx(Server Reflexive)Candidate

我在项目中遇到过一个问题:有些对称型 NAT 对 STUN 打洞不友好。每次请求拿到的端口都不一样,导致对端无法预测。这种情况就需要 TURN 上场了。

小技巧:STUN 默认使用 UDP 端口 3478。如果 UDP 被防火墙拦截,可以尝试 TLS 加密的 STUN(端口 5349)。

4.4 TURN 协议:最后的保底方案

当 STUN 打洞失败时,TURN(Traversal Using Relays around NAT)就派上用场了。它通过一个公网中继服务器转发媒体数据。

代价很明显:

  • 带宽成本:所有流量都经过 TURN 服务器
  • 延迟增加:多了一跳,延迟至少增加 10-30ms
  • 服务器压力:每个流都需要占用中继资源

所以 TURN 是最后的选择。ICE 框架会优先尝试直连和 STUN 打洞,只有都失败时才走 TURN。

特性 STUN TURN
主要作用 获取公网地址 中继转发媒体
服务器负载 极低(仅信令) 高(转发媒体)
延迟 无额外延迟 增加 10-30ms
适用场景 大部分 NAT 类型 对称 NAT / 防火墙限制
默认端口 3478 (UDP) 3478 (UDP/TCP)

4.5 Candidate 收集:三种类型

ICE 定义了三种 Candidate 类型,优先级从高到低:

  1. host:本机网卡地址(192.168.1.10:5000)——优先级最高
  2. srflx:STUN 获取的公网地址(203.0.113.5:5000)——优先级中等
  3. relay:TURN 服务器分配的中继地址(TURN 服务器 IP:端口)——优先级最低

收集过程大致如下:

// 伪代码:Candidate 收集流程
function collectCandidates() {
    // 1. 收集 host candidate
    for each local IP {
        addCandidate("host", ip, port, priority);
    }

    // 2. 向 STUN 服务器查询公网地址
    stunRequest(server: "stun.l.google.com:19302");
    onStunResponse(publicIp, publicPort) {
        addCandidate("srflx", publicIp, publicPort, priority);
    }

    // 3. 向 TURN 服务器申请中继地址
    turnAllocate(server: "turn.example.com:3478", credential);
    onTurnAllocate(relayIp, relayPort) {
        addCandidate("relay", relayIp, relayPort, priority);
    }
}

注意:我曾经遇到过一个坑——在 Docker 容器中运行 WebRTC 客户端时,host candidate 收集到的是容器内网 IP(172.17.0.x),而不是宿主机的 IP。这会导致对端无法直连。解决方案是在启动容器时使用 --network=host 模式,或者手动指定 ICE 的网卡绑定。

4.6 连通性检查:谁先通就用谁

Candidate 收集完成后,双方会通过信令交换各自的 Candidate 列表。然后 ICE 开始做连通性检查。

检查逻辑:

  • 每一对 Candidate(本地 vs 对端)组成一个 Check Pair
  • 双方互相发送 STUN Binding Request
  • 如果收到 STUN Binding Response,说明这对 Candidate 是通的
  • ICE 会按优先级顺序检查,一旦找到可用路径就停止

举个例子:

// Peer A 的 Candidate 列表
host: 192.168.1.10:5000 (优先级 100)
srflx: 203.0.113.5:5000 (优先级 80)
relay: 198.51.100.10:3478 (优先级 50)

// Peer B 的 Candidate 列表
host: 10.0.0.5:6000 (优先级 100)
srflx: 198.51.100.20:6000 (优先级 80)
relay: 198.51.100.10:3478 (优先级 50)

// 连通性检查顺序(按优先级组合):
// 1. A:host(192.168.1.10:5000) <-> B:host(10.0.0.5:6000) → 不通(内网隔离)
// 2. A:host(192.168.1.10:5000) <-> B:srflx(198.51.100.20:6000) → 不通
// 3. A:srflx(203.0.113.5:5000) <-> B:srflx(198.51.100.20:6000) → 通!使用此路径
// 4. 后续检查不再执行

嗯,这里要注意:连通性检查是有超时的。默认情况下,每个检查对会在 500ms 内重试 3 次。如果全部超时,就认为该路径不可用。

4.7 ICE 状态机

ICE 内部维护了一个状态机,用来跟踪连接建立的过程。主要状态有:

状态 说明
New ICE 刚创建,尚未开始收集
Gathering 正在收集 Candidate
Complete Candidate 收集完成
Checking 正在进行连通性检查
Connected 至少有一条路径可用
Failed 所有路径都不可用

我个人习惯在调试时监听 iceConnectionState 的变化。如果卡在 Checking 状态超过 5 秒,基本可以断定是 NAT 穿透出了问题。

4.8 避坑指南

最后分享几个我踩过的坑:

  • STUN 服务器不可达:有些企业防火墙会拦截 UDP 3478 端口。建议同时配置 TCP 回退方案。
  • TURN 凭证过期:TURN 的临时凭证(TURN REST API 生成)有时效性。如果连接建立时间过长,凭证可能失效。我建议在 ICE 开始前刷新凭证。
  • Candidate 数量过多:如果机器有多个虚拟网卡(VPN、Docker 等),Candidate 列表会非常长,导致连通性检查耗时增加。可以通过 iceCandidatePoolSize 控制并发检查数量。
  • IPv6 优先级:现代系统默认 IPv6 优先级高于 IPv4。但如果对端不支持 IPv6,会导致检查失败。建议在 ICE 配置中明确指定 iceTransportPolicy: "relay" 或 "all"。

总结一句话:ICE 的本质是穷举所有可能的路径,然后用 STUN 探针验证。直连不行就打洞,打洞不行就中继。理解了这个逻辑,你就能在遇到连接问题时快速定位瓶颈。