# ICE 总览 · 怎么在两个 NAT 后打通一条路

> 两个都藏在 NAT 后的端点，怎么找到一条能互通的路？这就是 ICE 要解决的核心难题。这一章讲清 NAT 穿越的困境、ICE 的「收集候选→配对→连通性检查→选路」四步思路，以及 p2p/ 里那张核心类地图：P2PTransportChannel、Port、Candidate、Connection、PortAllocator、IceController。

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/webrtc/ice-overview
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# ICE 总览 · 怎么在两个 NAT 后打通一条路

第 2 部分，进入 WebRTC 最硬核的子系统之一：`p2p/`。[会话协商](https://daiw.org/manual/webrtc/offer-answer-handler)谈定后，一个艰巨的任务摆在面前——**两个可能都藏在 NAT / 防火墙后的端点，怎么找到一条能互相发包的路？** 解决它的框架叫 **ICE**。这一章先建立全局观：难在哪、思路是什么、有哪些核心类。

## 难题：NAT 让直连几乎不可能

家里、公司、手机网络，几乎每台设备都在 **NAT（网络地址转换）** 后面——你的设备只有个私网地址（如 `192.168.1.5`），对外共享一个公网地址。NAT 带来两个麻烦：

- **你不知道自己的公网地址**：应用只看得到私网 IP，把它写进 [SDP](https://daiw.org/manual/webrtc/sdp-and-jsep) 发给对端也没用——对端没法用你的私网地址找到你。
- **NAT 默认拒绝主动入站**：对端想连你，包到了你的 NAT 就被丢弃（没有对应的出站记录）。防火墙更严。

于是**两个 NAT 后的端点直连，默认是通不了的**。ICE 就是为攻克这个难题而生的框架。

## ICE 的思路：多备选，逐个试

**ICE（Interactive Connectivity Establishment，交互式连通性建立）** 不指望「一条路走到黑」，而是**广撒网、逐个试**：

<Callout type="info">
  **ICE 四步走：① 收集候选 → ② 交换 → ③ 配对做连通性检查 → ④ 选一条能通的用。**

  1. **收集候选（candidates）** —— 每一端尽可能多地找出「我可能被联系到的地址」：本机网卡地址、经 [STUN](https://daiw.org/manual/webrtc/stun-protocol) 问出的公网地址、[TURN](https://daiw.org/manual/webrtc/source-map) 中继地址……（下一章细说四种候选）。
  2. **交换候选** —— 通过[信令](https://daiw.org/manual/webrtc/call-lifecycle)把候选发给对端（和 SDP 一样，信令是应用的事）。
  3. **配对 + 连通性检查** —— 把「我的候选」和「对端的候选」**两两配对**，对每一对发 [STUN 探测包](https://daiw.org/manual/webrtc/stun-protocol)试试通不通。
  4. **选路** —— 从通了的配对里，选一条最优的用来传媒体。之后还持续探测，以防路况变化。

  核心洞察：**与其猜哪条路能通，不如把所有可能的路都试一遍，让「能通」这件事自己浮现出来。** 这是一种务实的、面向真实互联网（充满 NAT 和防火墙）的工程哲学。
</Callout>

## 核心类地图

ICE 的实现，是 `p2p/` 里一组类的协作。项目文档 `g3doc/ice.md` 给出了权威的角色划分，记住这张地图，整个第 2 部分你都不会迷路：

| 类 | canonical 路径 | 角色 |
| --- | --- | --- |
| `P2PTransportChannel` | `p2p/base/p2p_transport_channel.h` | **总指挥**（实现 `IceTransportInternal`） |
| `PortAllocator` | `p2p/base/port_allocator.h` | 创建 Port、[采集本地候选](https://daiw.org/manual/webrtc/port-allocator) |
| `Port`（4 种实现） | `p2p/base/port.h` | 一种[本地通信机制](https://daiw.org/manual/webrtc/port-allocator)，产出候选 |
| `Candidate` | `api/candidate.h` | 一个[可达地址](https://daiw.org/manual/webrtc/candidates)（本地或远端） |
| `Connection` | `p2p/base/connection.h` | 管理一个[候选对](https://daiw.org/manual/webrtc/source-map)，发 STUN 探测 |
| `IceController` | `p2p/base/ice_controller_interface.h` | [决定用哪条 `Connection`](https://daiw.org/manual/webrtc/source-map) 发数据 |

它们的协作关系：

```mermaid
flowchart TD
    PC["P2PTransportChannel(总指挥)"]
    PC -->|"用它采集候选"| PA["PortAllocator → BasicPortAllocator"]
    PA -->|"创建"| PORT["Port(UDPPort/StunPort/TcpPort/TurnPort)"]
    PORT -->|"产出"| CAND["Candidate(host/srflx/prflx/relay)"]
    PC -->|"本地候选 × 远端候选 配对"| CONN["Connection(候选对,发 STUN 探测)"]
    PC -->|"用它选路"| IC["IceController(决定用哪条 Connection)"]
    CONN --> IC
```

一句话概括各自的活：**`PortAllocator` 造 `Port`、`Port` 产 `Candidate`；`P2PTransportChannel` 把本地和远端候选配成 `Connection`；`Connection` 发 [STUN 探测](https://daiw.org/manual/webrtc/stun-protocol)；`IceController` 从中选路。** 而 `P2PTransportChannel` 是把这一切串起来的总指挥——它实现了 `IceTransportInternal` 接口，是[上一部分 `JsepTransport` 里那个 ICE 传输](https://daiw.org/manual/webrtc/jsep-transport-controller)的真身。

## 建连五步（后续详解）

`g3doc/ice.md` 还给出了一次成功建连的五步时序，它们全由 `PeerConnection` 的动作触发。这里先给个骨架，后面各章逐个展开：

1. **`MaybeStartGathering`** —— [`SetLocalDescription`](https://daiw.org/manual/webrtc/offer-answer-handler) 触发，开始[采集候选](https://daiw.org/manual/webrtc/port-allocator)。
2. **`SignalCandidateGathered`** —— 采到一个本地候选，通过[回调](https://daiw.org/manual/webrtc/threading-model)报给应用（`OnIceCandidate`），应用经信令发给对端。
3. **`AddRemoteCandidate`** —— 收到对端候选，和本地候选**配对**成 `Connection`，交给 `IceController`。
4. **`SignalCandidatePairChanged`** —— 某个 `Connection` 的 [STUN 探测](https://daiw.org/manual/webrtc/stun-protocol)得到回应，`IceController` 选它来传数据。
5. **`SignalIceTransportStateChanged`** —— 选定连接使状态变化，触发 [DTLS 握手](https://daiw.org/manual/webrtc/source-map)开始（[第 3 部分](https://daiw.org/manual/webrtc/source-map)）。

## 小结与下一章

**ICE 解决「两个 NAT 后的端点怎么直连」**：因为设备不知道自己的公网地址、NAT 又拒绝主动入站，直连默认通不了。ICE 的思路是**收集候选 → 交换 → 配对做[连通性检查](https://daiw.org/manual/webrtc/stun-protocol) → 选一条能通的**。核心类：**`P2PTransportChannel`**（总指挥）用 **`PortAllocator`** 造 **`Port`**、`Port` 产 **`Candidate`**，把本地 × 远端候选配成 **`Connection`**（发 STUN 探测），再由 **`IceController`** 选路。

第一件事是**候选**——它到底有哪四种，分别怎么来？下一章细看。👉 [候选：host / srflx / prflx / relay](https://daiw.org/manual/webrtc/candidates)。

## 参考资料

- [1] libwebrtc 源码.`g3doc/ice.md`（ICE 类地图与建连五步）。
- [2] libwebrtc 源码.`p2p/base/p2p_transport_channel.h`、`p2p/base/port.h`、`api/candidate.h`、`p2p/base/connection.h`。
- [3] IETF RFC 8445「Interactive Connectivity Establishment （ICE）」—— ICE 规范。
