# 候选 · host / srflx / prflx / relay

> ICE 的第一步是收集候选——每一个「我可能被联系到的地址」。这一章读 api/candidate.h 的 IceCandidateType，讲清四种候选：主机候选、服务器反射候选、对端反射候选、中继候选，它们各自怎么来、优先级如何，以及为什么要同时准备这么多条备选。

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

# 候选 · host / srflx / prflx / relay

[ICE 四步](https://daiw.org/manual/webrtc/ice-overview)的第一步是「收集候选」。**候选（candidate）** 就是一个「我可能被对端联系到的传输地址」——一个 IP、端口、协议的组合。ICE 的哲学是[广撒网](https://daiw.org/manual/webrtc/ice-overview)，所以每一端都会尽可能多地收集候选。这一章讲清候选的**四种类型**，它们是理解 ICE 的基础词汇。

## 四种类型

`api/candidate.h` 里的 `IceCandidateType` 枚举，干净利落地给出四种：

```cpp
// api/candidate.h
enum class IceCandidateType : int { kHost, kSrflx, kPrflx, kRelay };
```

它们对应四种「怎么得到这个地址」的方式，从「最直接」到「最兜底」：

<Callout type="info">
  **四种候选，四种「找到我」的路径：**

  - **`kHost`（主机候选）** —— 设备**本机网卡的地址**（如 `192.168.1.5:50000`）。最直接：如果两端在同一局域网，用它就能直连，零绕路。但跨 NAT 时它对外没用。
  - **`kSrflx`（服务器反射候选，server-reflexive）** —— 设备经 [STUN 服务器](https://daiw.org/manual/webrtc/stun-protocol)问出的**公网地址**。设备向 STUN 服务器发个包，服务器回答「我看到你来自 `203.0.113.7:60000`」——这个「NAT 外面看到的地址」就是 srflx 候选。**这是穿越 NAT 的主力**。
  - **`kPrflx`（对端反射候选，peer-reflexive）** —— 在[连通性检查过程中](https://daiw.org/manual/webrtc/stun-protocol)**意外发现**的地址。有时对端探测包来自一个双方都没预料到的地址（某些 NAT 行为），这个新地址就地成为一个 prflx 候选。它是「检查中冒出来的惊喜」。
  - **`kRelay`（中继候选，relay）** —— [TURN 中继服务器](https://daiw.org/manual/webrtc/source-map)分配的地址。当直连怎么都不通（对称 NAT、严格防火墙），就让**媒体经 TURN 服务器转发**。它最兜底、也最费（绕一道服务器、增延迟、耗服务器带宽），但**保证能通**——只要两端都连得上 TURN。
</Callout>

## 本地候选 vs 远端候选

候选分两个来源（`g3doc/ice.md`）：

- **本地候选**：自己的 [`Port` 采集](https://daiw.org/manual/webrtc/port-allocator)出来的，通过[信令](https://daiw.org/manual/webrtc/call-lifecycle)发给对端。
- **远端候选**：对端发来的，经信令收到后，用 [`P2PTransportChannel::AddRemoteCandidate`](https://daiw.org/manual/webrtc/ice-overview) 加进来。

注意：**候选的交换不必等 SDP 全部完成**。现代 WebRTC 用 **Trickle ICE**——候选一边采集、一边**陆续**通过信令发给对端（而非等全部采完再一起发），这样连通性检查能更早开始，[建连更快](https://daiw.org/manual/webrtc/ice-overview)。你在 [`PeerConnectionObserver`](https://daiw.org/manual/webrtc/call-lifecycle) 里反复收到的 `OnIceCandidate` 回调，就是候选在「trickle」。

## 优先级：先试快的

一端往往有好几个候选，先试哪个？ICE 给每个候选算一个**优先级**，配成 [`Connection`](https://daiw.org/manual/webrtc/source-map) 后按优先级高低排序检查。大致的偏好顺序：

```text
host(最快,同网直连)  >  srflx(穿 NAT 直连)  >  relay(中继,最慢但保底)
```

`Candidate`（`api/candidate.h`）结构里带着 `type()`、地址、协议（UDP/TCP）、`priority()`、以及一个 `foundation`（用于把「同类同源」的候选归组）。优先级的计算综合了类型、网络接口、协议等因素——目标是**让延迟低、代价小的路先被选中**，中继只在别无选择时才上。

## 为什么要准备这么多

初学者常问：既然 host 通不了就用 srflx、srflx 不行再用 relay，为什么不按顺序试，非要一次全备齐？

<Callout type="info">
  **因为你事先不知道哪条能通——NAT 的行为千奇百怪。** 有的 NAT 让 srflx 直连轻松成功，有的（对称 NAT）会让 srflx 也失败、只能走 relay；两端 NAT 类型的**组合**更是难以预测。ICE 的智慧就在于**不预测、全备齐、并行试**：同时收集 host / srflx / relay，同时对所有配对做[连通性检查](https://daiw.org/manual/webrtc/stun-protocol)，让「哪条能通、哪条最快」在实测中自然分出胜负。这比「猜一条、失败再换」快得多、也可靠得多。代价是要维护一堆候选和连接，但[换来的是高穿透率和低建连延迟](https://daiw.org/manual/webrtc/ice-overview)——非常值。
</Callout>

## 小结与下一章

**候选是「我可能被联系到的地址」**，四种（`IceCandidateType`）：**`kHost`**（本机网卡，同网直连）、**`kSrflx`**（STUN 问出的公网地址，穿 NAT 主力）、**`kPrflx`**（检查中意外发现）、**`kRelay`**（TURN 中继，最兜底保底）。候选分本地 / 远端，用 **Trickle ICE** 边采边发（`OnIceCandidate` 回调）。每个候选有**优先级**（host > srflx > relay），让快的先试。ICE **全备齐、并行试**，不靠预测靠实测。

候选从哪来？靠 **`Port`** 去采集，而 `Port` 由 **`PortAllocator`** 创建。下一章看这套采集机制，以及四种 `Port` 分别产哪种候选。👉 [PortAllocator 与候选采集](https://daiw.org/manual/webrtc/port-allocator)。

## 参考资料

- [1] libwebrtc 源码.`api/candidate.h`（`IceCandidateType`、`Candidate` 结构）。
- [2] libwebrtc 源码.`g3doc/ice.md`（候选类型与本地 / 远端来源）。
- [3] IETF RFC 8445「ICE」§ Candidates;RFC 8838「Trickle ICE」。
