WebRTC 全景实战 (5):ICE、STUN、TURN 与 NAT 穿透
"70% 的 WebRTC bug 与 NAT 有关。" — 业界经验总结
Ch4 SDP 中的 a=ice-ufrag、a=ice-pwd 只是凭证;真正让两个浏览器在 NAT 后面找到彼此、建立 UDP 通道的,是 ICE(Interactive Connectivity Establishment)——RFC 8445 定义的标准算法。
1990 年代 MBONE 多播时代,发送者只需向多播组发一次包,路由器负责复制给所有订阅者。Ron Frederick 在 WebRTC for the Curious — 历史 中回忆:他和同事都是 IP 多播研究者,用 nv 工具向整个 Internet 广播会议视频——一份数据包,数百个子网同时接收。Serge Lachapelle 则描述了另一条演进路线:他创办的 Marratech 最初也依赖多播网络,「服务器可以非常简单,因为网络负责把视频包复制给通话中的每一个人」——但「必须设计网络以适配多播模式」这一致命缺点,最终推动行业从多播转向 SFU(packet shufflers)。
WebRTC 运行在没有多播的公网上。IPv4 地址耗尽催生了 NAT 的大规模部署,每个参与者必须找到与对端通信的具体 IP:Port 路径。从「一对多广播」到「点对点单播」的设计转折,是理解 ICE 存在原因的关键背景。
本章深入 ICE 状态机、Candidate 类型、STUN/TURN 协议细节、Trickle ICE 优化,以及生产环境中最常见的穿透失败排查路径。
配套 Lab:examples/webrtc-lab/docker/coturn/ + client/ch02-p2p-basic/
本篇术语表
| 术语 | 英文 | 解释 |
|---|---|---|
| ICE | Interactive Connectivity Establishment | 在多个 Candidate 地址之间做连通性检查,选出最优传输路径的标准算法 |
| Candidate | ICE Candidate | 一个可用的网络地址(IP:Port + 类型 + 优先级),供 ICE 检查 |
| host | Host Candidate | 本机网卡直接绑定的地址,如 192.168.1.10:54321 |
| srflx | Server Reflexive | 经 STUN 服务器反射得到的公网映射地址 |
| prflx | Peer Reflexive | 连通性检查过程中动态发现的远端映射地址 |
| relay | Relay Candidate | TURN 服务器分配的中继地址,P2P 失败时的兜底 |
| STUN | Session Traversal Utilities for NAT | RFC 5389,帮助客户端发现自己的公网映射 |
| TURN | Traversal Using Relays around NAT | RFC 5766,在服务端分配中继地址转发流量 |
| Connectivity Check | — | ICE 用 STUN Binding Request 探测 Candidate Pair 是否可达 |
| Nomination | 提名 | ICE 选出「最终使用」的 Candidate Pair 的过程 |
| Trickle ICE | — | 边收集 Candidate 边通过信令发送,而非等全部收集完 |
| NAT | Network Address Translation | 将私网地址映射到公网地址/端口的中间设备 |
| Symmetric NAT | — | 每个目标地址分配不同外部端口,P2P 几乎必然失败 |
| CGNAT | Carrier-Grade NAT | 运营商级 NAT,多层 NAT 嵌套,穿透难度更高 |
| ice-ufrag / ice-pwd | — | SDP 中的 ICE 凭证,用于连通性检查的身份验证 |
| Candidate Pair | — | 本地 Candidate + 远端 Candidate 的组合,ICE 逐个检查 |
| iceTransportPolicy | — | all(默认)或 relay(强制走 TURN) |
| ICE-CONTROLLING | — | 连通性检查中拥有提名权的一方(Offer 方通常为 controlling) |
| ICE-Lite | — | 简化版 ICE 实现,仅被动响应检查,不主动探测 |
一、从多播到单播:为什么 ICE 存在
Curious 历史章节 勾勒出一条清晰的技术演进线:
| 时代 | 模型 | 带宽效率 | 网络要求 |
|---|---|---|---|
MBONE / nv (1992) | IP 多播 | 发送方只发一份 | 全网支持多播 |
| Marratech (2000s) | 多播 + 简单服务器 | 高 | 企业网多播 |
| SFU / WebRTC (2010+) | 单播 P2P 或 SFU 转发 | 每跳一份 | 仅需 UDP 出站 |
Ron Frederick 坦言:「有时我希望我们之前能更加努力地推动 IP 多播的应用……如果我们这么做了,可能早就可以看到有线电视过渡到基于 Internet 的音频和视频。」但现实是公网 ISP 几乎不转发多播,IPv4 地址耗尽又催生了 NAT 的大规模部署。
每个 WebRTC 参与者可能有多个网络接口(Wi-Fi、以太网、VPN、IPv6),每个接口经 NAT 映射后又有不同的公网地址。ICE 的任务就是:枚举所有可能的路径,逐个探测,选出延迟最低、可用的那条。
Serge Lachapelle 在 Google 收购 Global IP Solutions(GIPS)后,把 VoIP 技术栈搬进浏览器——GIPS 的 libjingle 提供了 ICE/STUN/TURN 的成熟实现,这正是今天 Chrome 内置 ICE Agent 的根基。
二、ICE 在连接建立中的位置
WebRTC 连接建立是一个多层协议栈的叠加过程。ICE 位于 SDP 协商之后、DTLS 握手之前:
| 阶段 | 协议 | 本章是否涉及 |
|---|---|---|
| 信令 | WebSocket / 自定义 | 间接(传递 Candidate) |
| 媒体协商 | SDP Offer/Answer | Ch4 |
| 地址发现与选路 | ICE + STUN + TURN | 本章 |
| 加密 | DTLS | Ch7 |
| 媒体传输 | SRTP / SCTP | Ch6/Ch8 |
ICE 不传输任何应用数据——它只负责在 UDP(偶尔 TCP)上找到一条可达的五元组 (srcIP, srcPort, dstIP, dstPort, UDP)。找到之后,DTLS 在同一五元组上握手,SRTP/SCTP 复用该通道。
三、ICE 状态机详解
浏览器通过 RTCPeerConnection.iceConnectionState 暴露 ICE 连接状态:
3.1 各状态含义
| 状态 | 含义 | 典型持续时间 |
|---|---|---|
new | 尚未开始 ICE 检查 | — |
checking | 正在对 Candidate Pair 做连通性检查 | 数百 ms ~ 数秒 |
connected | 至少一个 Pair 成功,媒体可传输 | — |
completed | 提名完成,不再切换 Pair | 稳定连接后 |
disconnected | 临时断连,可能自动恢复 | 数秒 |
failed | 所有 Pair 失败,需人工介入 | 终止 |
3.2 iceGatheringState 与 connectionState
ICE 涉及三个平行的状态维度,调试时缺一不可:
| 属性 | 反映层次 | 关键值 |
|---|---|---|
iceGatheringState | Candidate 收集进度 | new → gathering → complete |
iceConnectionState | ICE 连通性 | checking → connected / failed |
connectionState | ICE + DTLS 整体 | connecting → connected / failed |
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
pc.oniceconnectionstatechange = () => {
const state = pc.iceConnectionState;
console.log("[ICE]", state);
switch (state) {
case "checking":
console.log("正在探测 Candidate Pair…");
break;
case "connected":
case "completed":
console.log("ICE 连通,DTLS 应已开始");
break;
case "disconnected":
console.warn("临时断连,等待恢复或触发 failed");
break;
case "failed":
console.error("ICE 失败 — 检查 TURN 配置、防火墙、Candidate 类型");
break;
}
};
pc.onicegatheringstatechange = () => {
console.log("[Gathering]", pc.iceGatheringState);
};
pc.onconnectionstatechange = () => {
console.log("[Connection]", pc.connectionState);
// connectionState=failed 但 iceConnectionState=connected → DTLS 问题,见 Ch7
};
pc.onicecandidate = (event) => {
if (event.candidate) {
signaling.send({ type: "candidate", candidate: event.candidate });
} else {
console.log("ICE gathering complete (null candidate)");
}
};
iceConnectionState 只反映 ICE 层;connectionState 还包含 DTLS 状态。调试时两者都要看——可能出现 ICE connected 但 connectionState=failed(DTLS 证书/指纹不匹配)。
四、Candidate 类型与优先级
ICE 为每个 Candidate 计算优先级,host > srflx > relay(同类型内还有协议、接口偏好等细粒度排序):
4.1 Candidate SDP 格式
a=candidate:842163049 1 udp 2130706431 192.168.1.10 54321 typ host
a=candidate:842163049 2 udp 2130706431 192.168.1.10 54321 typ host
a=candidate:1234567890 1 udp 1694498815 1.2.3.4 54321 typ srflx raddr 192.168.1.10 rport 54321
a=candidate:9876543210 1 udp 16777215 5.6.7.8 60000 typ relay raddr 1.2.3.4 rport 54321
a=candidate:1111111111 1 udp 2130706431 abcd1234-5678-90ab.local 54321 typ host
| 字段 | 示例 | 含义 |
|---|---|---|
| foundation | 842163049 | 相同类型+地址的 Candidate 共享 foundation |
| component | 1 / 2 | 1=RTP,2=RTCP(WebRTC 用 rtcp-mux 时通常只有 1) |
| transport | udp / tcp | 传输协议 |
| priority | 2130706431 | 优先级数值,越大越优先 |
| address | 192.168.1.10 | IP 地址 |
| port | 54321 | 端口 |
| typ | host / srflx / relay / prflx | Candidate 类型 |
| raddr / rport | srflx/relay 附带 | 本地映射前的地址 |
4.2 优先级计算公式(RFC 8445)
优先级是一个 32 位无符号整数,公式为:
priority = (2^24) × (126 - typePreference)
+ (2^8) × (256 - localPreference)
+ (256 - componentId)
| typePreference | 类型 | 值 |
|---|---|---|
| host | 本机 | 126 |
| prflx | 对端反射 | 110 |
| srflx | 服务器反射 | 100 |
| relay | 中继 | 0 |
因此 host candidate 的 priority 天然高于 srflx,srflx 高于 relay。浏览器自动计算,开发者通常无需手动干预。
4.3 mDNS Host Candidate
Chrome 从 M94 起默认用 mDNS 隐藏本地 IP:host candidate 的地址显示为 xxxx-xxxx.local 而非真实 192.168.x.x。这是隐私保护特性,不影响 srflx/relay 的收集和 ICE 选路。在 chrome://webrtc-internals 中你会看到这类 candidate,属于正常现象。
4.4 配置 iceServers
const ICE_SERVERS = [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
{
urls: [
"turn:turn.example.com:3478?transport=udp",
"turn:turn.example.com:3478?transport=tcp",
"turns:turn.example.com:5349?transport=tcp",
],
username: "webrtc-user",
credential: "secret-token",
credentialType: "password",
},
];
const pc = new RTCPeerConnection({
iceServers: ICE_SERVERS,
iceCandidatePoolSize: 4,
iceTransportPolicy: "all",
bundlePolicy: "max-bundle",
});
| 配置项 | 作用 |
|---|---|
iceServers | STUN/TURN 服务器列表 |
iceCandidatePoolSize | 在 createOffer 前预收集 N 个 Candidate,加速首连 |
iceTransportPolicy: "relay" | 隐私模式或对称 NAT 测试,禁用 host/srflx |
bundlePolicy: "max-bundle" | 所有 m-line 复用同一 ICE 传输(WebRTC 默认行为) |
五、STUN 协议深度解析(RFC 5389)
STUN 的核心消息只有几种:Binding Request / Binding Response,用于地址发现。Allocate / Send / Data 等属于 TURN 扩展(RFC 5766)。
5.1 Binding 交互时序
5.2 STUN 消息结构(简化)
RFC 5389 固定头 20 字节,后跟可变长 Attributes(TURN Allocate 等复用同一头部格式)。首 16 bit 的 Message Type 按位域编码(§6.1):
| Class (C0C1) | 值 | 典型消息 |
|---|---|---|
00 | Request | STUN Binding Request、TURN Allocate |
01 | Indication | TURN Send/Data Indication |
10 | Success Response | Binding Success Response |
11 | Error Response | 401 Unauthorized 等 |
Method 12 bit 标识具体操作,例如 Binding = 0x001、Allocate = 0x003(TURN)。Class + Method 组合成线上 16 bit Message Type 字段——前两 bit 恒为 0 是 STUN 与旧版 STUN 的兼容标记,不可省略。
常见 Attribute:
| Attribute | 用途 |
|---|---|
XOR-MAPPED-ADDRESS | 客户端的公网映射地址(防 NAT 篡改) |
USERNAME / MESSAGE-INTEGRITY | ICE 连通性检查时的身份验证 |
PRIORITY / USE-CANDIDATE | ICE 提名与优先级 |
ICE-CONTROLLED / ICE-CONTROLLING | 决定哪端执行提名 |
5.3 ICE 连通性检查中的 STUN
Candidate 收集阶段的 STUN Binding 不带 ICE 凭证;连通性检查阶段的 STUN Binding 则携带 SDP 中的 ice-ufrag 和 ice-pwd:
- ICE-CONTROLLING(通常为 Offer 方):有权发送
USE-CANDIDATE提名 - ICE-CONTROLLED(通常为 Answer 方):被动响应,接受提名
5.4 公共 STUN 服务器
| 提供者 | URL | 备注 |
|---|---|---|
stun:stun.l.google.com:19302 | 免费,无 SLA | |
| Cloudflare | stun:stun.cloudflare.com:3478 | 免费 |
| 自建 | stun:your-domain:3478 | 生产推荐(coturn 同时提供 STUN+TURN) |
STUN 只回答「你的公网地址是什么」。它不参与 RTP/SCTP 数据转发。如果 P2P 打洞失败,必须靠 TURN。
六、TURN 中继协议(RFC 5766)
当双方都是 Symmetric NAT,或企业防火墙只允许出站 UDP 时,P2P 必然失败。TURN 在服务端分配中继地址(relay candidate),所有媒体经服务器转发。
6.1 TURN Allocate 时序
6.2 TURN 认证机制
生产 TURN 通常使用 长期凭证(long-term credentials) 或 REST API 临时凭证(Twilio、Xirsys 模式):
// 长期凭证(开发/内网)
{
urls: "turn:turn.example.com:3478",
username: "test",
credential: "test123",
}
// REST 临时凭证(生产推荐)—— 服务端生成
const crypto = require("crypto");
function generateTurnCredentials(secret, ttl = 86400) {
const timestamp = Math.floor(Date.now() / 1000) + ttl;
const username = `${timestamp}:userId123`;
const hmac = crypto.createHmac("sha1", secret);
hmac.update(username);
const credential = hmac.digest("base64");
return { username, credential };
}
// 客户端消费
const { username, credential } = await fetch("/api/turn-credentials").then(r => r.json());
const pc = new RTCPeerConnection({
iceServers: [{
urls: "turn:turn.example.com:3478",
username,
credential,
}],
});
临时凭证的优势:即使泄露,凭证在 TTL 后自动失效;可按用户/会话隔离。
6.3 coturn Docker 本地部署
examples/webrtc-lab/docker/coturn/docker-compose.yml:
services:
coturn:
image: coturn/coturn:latest
ports:
- "3478:3478/udp"
- "3478:3478/tcp"
- "49152-49200:49152-49200/udp"
command: >
-n --log-file=stdout
--lt-cred-mech
--user=test:test123
--realm=webrtc.lab
--min-port=49152
--max-port=49200
启动与验证:
cd examples/webrtc-lab/docker/coturn
docker compose up -d
# 验证 STUN 响应(需安装 stuntman)
stunclient localhost 3478
# 查看 coturn 日志
docker compose logs -f coturn
浏览器端配置:
const LOCAL_TURN = {
urls: [
"turn:localhost:3478?transport=udp",
"turn:localhost:3478?transport=tcp",
],
username: "test",
credential: "test123",
};
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
LOCAL_TURN,
],
});
部署成功后,在 chrome://webrtc-internals 中应能看到 typ relay 的 Candidate。生产集群扩展见 Ch14 TURN 集群。
6.4 TURN 带宽成本
| 模式 | 路径 | 服务器带宽 |
|---|---|---|
| P2P (srflx) | A ↔ B 直连 | 0 |
| TURN relay | A → TURN → B | 发送量 × 2 |
| SFU | 每人 → SFU → 每人 | N × 发送量 |
TURN 是兜底方案,但大流量场景(视频会议、文件传输)中 relay 比例直接影响成本。监控 relay 使用率是生产运维的关键指标。
七、Trickle ICE vs Full ICE
早期 ICE 实现等所有 Candidate 收集完毕才交换 SDP,首连延迟高。Trickle ICE(RFC 8838)边收集边发送,是现代 WebRTC 的标准做法。
| 模式 | 行为 | 首连延迟 | 信令复杂度 |
|---|---|---|---|
| Full ICE | 等 iceGatheringState=complete 再发 SDP | 较慢(2~5s) | 低 |
| Trickle ICE | onicecandidate 实时转发 | 快(<1s 常见) | 中 |
| ICE Restart | 断连后 restartIce() 重新收集 | 取决于网络 | 高 |
examples/webrtc-lab/client/ch02-p2p-basic/ 使用 Full ICE(waitIceGatheringComplete),适合理解流程;生产信令服务器(Ch3)应实现 Trickle ICE。
SDP 中 a=ice-options:trickle 表示支持 Trickle ICE。信令层需处理 candidate 消息在 Offer/Answer 之前或之后到达的乱序情况:
const pendingCandidates = [];
async function onRemoteCandidate(candidate) {
if (pc.remoteDescription) {
await pc.addIceCandidate(candidate);
} else {
pendingCandidates.push(candidate);
}
}
async function onRemoteAnswer(answer) {
await pc.setRemoteDescription(answer);
for (const c of pendingCandidates) {
await pc.addIceCandidate(c);
}
pendingCandidates.length = 0;
}
Trickle ICE 结束时,发起方会发送 candidate: null(即 onicecandidate 中 event.candidate === null)。现代浏览器不再需要显式调用 addIceCandidate(null),但信令协议应能识别这一信号。
八、NAT 类型与穿透策略
RFC 4787 定义了 NAT 行为分类。理解 NAT 类型有助于预测 P2P 成功率:
| NAT 类型 | 映射行为 | 过滤行为 | P2P 成功率 |
|---|---|---|---|
| Full Cone | 固定映射 | 任何源可入 | 高 |
| Restricted Cone | 固定映射 | 仅曾发送过的 IP | 中 |
| Port Restricted | 固定映射 | 仅曾发送过的 IP:Port | 中低 |
| Symmetric | 每个目标不同映射 | 严格 | 极低,必须 TURN |
8.1 CGNAT(运营商级 NAT)
移动网络(4G/5G)和许多家庭宽带使用 CGNAT:你的「公网 IP」其实是运营商内网地址,外面还有一层 NAT。这意味着:
- srflx candidate 拿到的是运营商 NAT 的外部地址,不是真正的公网 IP
- 两层 NAT 叠加,打洞成功率进一步下降
- 始终配置 TURN 是移动场景的生产标配
8.2 现实建议
不要试图在客户端检测 NAT 类型——结果不可靠且各浏览器实现不同。生产环境的正确策略是:
- 始终配置 STUN + TURN
- 让 ICE 自动选路
- 监控 relay 使用率,优化 TURN 集群部署(Ch14)
- 对隐私敏感场景,可用
iceTransportPolicy: "relay"隐藏真实 IP
九、Candidate Pair 状态机
ICE 为每对 (local, remote) candidate 维护独立状态:
| Pair 状态 | 含义 |
|---|---|
frozen | 初始状态,等待解冻 |
waiting | 已解冻,排队等待检查 |
in-progress | 正在发送 STUN Binding |
succeeded | 检查成功,可被提名 |
failed | 检查失败 |
nominated | 被选为最终传输路径 |
ICE Agent 按优先级从高到低逐个检查 Pair。第一个成功的 host-host 或 srflx-srflx Pair 通常延迟最低,会被优先提名。
十、ICE 失败诊断决策树
当 iceConnectionState === "failed" 时,按以下决策树排查:
10.1 常见失败原因
| 现象 | 根因 | 修复 |
|---|---|---|
只有 host,无 srflx | STUN 服务器不可达或被墙 | 换 STUN / 检查出站 UDP |
有 srflx 无 relay | 未配置 TURN 或认证失败 | 检查 username/credential |
有 relay 但仍 failed | TURN 端口范围被防火墙拦截 | 开放 49152-65535/UDP |
| 本地通、跨网 failed | 对称 NAT 或运营商 CGNAT | 必须 TURN |
| 连上后频繁 disconnected | Wi-Fi 切换 / 网络抖动 | ICE Restart + 断线重连 |
| mDNS host candidate | Chrome 隐私特性 .local | 正常,不影响 srflx/relay |
| TURN 凭证过期 | REST 临时凭证 TTL 到期 | 重新获取凭证 / 延长 TTL |
10.2 chrome://webrtc-internals 使用指南
- 打开
chrome://webrtc-internals(连接建立前打开,可捕获完整过程) - 建立 WebRTC 连接
- 选择对应的 PeerConnection
- 查看 Stats →
candidate-pair→ 找selected=true或state=succeeded的行 - 查看 ICE candidate grid → 确认本地/远端 Candidate 类型
- 点击 Create Dump 导出完整 JSON 用于离线分析
关键字段:
| 字段 | 含义 |
|---|---|
selected / nominated | 是否为当前选中 Pair |
localCandidateType | host / srflx / relay / prflx |
remoteCandidateType | 对端 Candidate 类型 |
bytesSent / bytesReceived | 确认有实际流量 |
currentRoundTripTime | RTT 估算 |
state | Pair 状态(succeeded / failed / in-progress) |
async function exportIceDiagnostics(pc) {
const stats = await pc.getStats();
const report = {
iceConnectionState: pc.iceConnectionState,
iceGatheringState: pc.iceGatheringState,
connectionState: pc.connectionState,
candidates: { local: [], remote: [] },
pairs: [],
selectedPair: null,
};
stats.forEach((s) => {
if (s.type === "local-candidate") {
report.candidates.local.push({
type: s.candidateType,
address: s.address || s.ip,
port: s.port,
protocol: s.protocol,
});
}
if (s.type === "remote-candidate") {
report.candidates.remote.push({
type: s.candidateType,
address: s.address || s.ip,
port: s.port,
});
}
if (s.type === "candidate-pair") {
report.pairs.push({
state: s.state,
nominated: s.nominated,
bytesSent: s.bytesSent,
bytesReceived: s.bytesReceived,
rtt: s.currentRoundTripTime,
});
if (s.selected || s.nominated) {
report.selectedPair = s;
}
}
});
console.log(JSON.stringify(report, null, 2));
return report;
}
十一、ICE Restart 与断线恢复
网络切换(Wi-Fi → 4G)或 NAT 映射过期时,ICE 可能从 connected 变为 disconnected 甚至 failed。WebRTC 支持 ICE Restart 在不重建整个 PeerConnection 的情况下重新收集 Candidate:
async function handleIceFailure() {
if (pc.iceConnectionState !== "failed") return;
const offer = await pc.createOffer({ iceRestart: true });
await pc.setLocalDescription(offer);
signaling.send({ type: "offer", sdp: offer, iceRestart: true });
}
async function onIceRestartOffer(offer) {
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
signaling.send({ type: "answer", sdp: answer });
}
ICE Restart 会生成新的 ice-ufrag / ice-pwd,旧 Candidate 全部作废。信令层需重新交换 Candidate。配合 Ch3 的重连状态机,这是生产系统断线恢复的标准手段。
disconnected 状态可能持续数秒后自动恢复为 connected(如短暂网络抖动)。不要立即触发 ICE Restart——建议等待 5~10 秒,确认无法恢复后再重启。
十二、实战 Lab
Lab 1:仅 STUN,观察 Candidate 类型
- 启动
examples/webrtc-lab/signaling/信令服务器(或直接用 ch02 手动交换 SDP) - 打开两个 Tab 运行
client/ch02-p2p-basic/ iceServers只配 Google STUN,不配 TURN- 打开
chrome://webrtc-internals,确认出现host+srflx,无relay - 记录 selected pair 的类型和 RTT
Lab 2:部署 coturn,验证 relay
cd examples/webrtc-lab/docker/coturn && docker compose up -d- 客户端添加 localhost TURN 配置(见第六节)
- 重新建立连接,确认出现
typ relayCandidate - 对比 selected pair:P2P 成功时通常选 srflx
Lab 3:强制 TURN 中继
const pc = new RTCPeerConnection({
iceServers: [LOCAL_TURN],
iceTransportPolicy: "relay",
});
- 设置
iceTransportPolicy: "relay" - 建立连接,确认 selected pair 的 local/remote 类型均为
relay - 测量 RTT,对比 P2P 模式下的差异
Lab 4:Trickle ICE 时序观察
- 在
onicecandidate中加performance.now()时间戳日志 - 对比各 Candidate 到达顺序 vs
iceConnectionState变化时间 - 验证:第一个 srflx candidate 到达后,
checking状态是否很快出现
Lab 5:故障注入
| 实验 | 操作 | 预期 |
|---|---|---|
| 无 TURN | 移除 TURN 配置 | 大多数家庭网络仍可 P2P |
| 错误 TURN 密码 | credential 故意写错 | 无 relay candidate,coturn 日志显示认证失败 |
| 阻断 UDP | 防火墙禁出站 UDP | ICE failed |
| 强制 relay | iceTransportPolicy: "relay" | 只有 relay pair 成功 |
| 错误 STUN 地址 | urls 指向不存在的主机 | 无 srflx,仅 host candidate |
Lab 6:导出诊断报告
- 在连接失败时调用
exportIceDiagnostics(pc) - 检查
candidates.local中是否有 relay 类型 - 检查
pairs中是否有state: "succeeded"的条目 - 将 JSON 报告与
chrome://webrtc-internals的 Dump 交叉验证
下一篇(Ch6):Data Channel 与 SCTP over DTLS——ICE 连通后,如何在 DTLS 之上传输任意 P2P 数据。
十三、本章小结
| 概念 | 要点 |
|---|---|
| ICE | 枚举 + 检查 + 提名,RFC 8445 |
| STUN | 发现公网映射,RFC 5389,不转发数据 |
| TURN | 中继兜底,RFC 5766,带宽成本 ×2 |
| Trickle ICE | 边收集边发送,降低首连延迟 |
| 生产策略 | 始终 STUN + TURN,监控 relay 比例 |
Phase 2(连接建立)继续深入:Ch6 Data Channel 将在 ICE 选出的路径上传输任意 P2P 数据。
系列导航
章节 主题 状态 0 架构全景与协议栈地图 ✅ 已发布 1 浏览器媒体 API 与设备管理 ✅ 已发布 2 第一个 P2P 视频通话 ✅ 已发布 3 信令服务器设计与会话状态机 ✅ 已发布 4 SDP 解剖与媒体协商 ✅ 已发布 5 ICE、STUN、TURN 与 NAT 穿透 ✅ 已发布 6 Data Channel 与 SCTP over DTLS ✅ 已发布 7 DTLS 握手与 SRTP 加密体系 ✅ 已发布 8 RTP/RTCP 媒体传输与 QoS ✅ 已发布 9 音视频编解码与 Simulcast 入门 ✅ 已发布 10 带宽估计与拥塞控制 GCC ✅ 已发布 11 Simulcast、SVC 与选择性订阅 ✅ 已发布 12 SFU/MCU/Mesh 架构与 Pion 实战 ✅ 已发布 13 调试工具链与可观测性 ✅ 已发布 14 TURN 集群部署与多区域扩展 ✅ 已发布 15 Capstone 生产级视频会议系统 ✅ 已发布
References
- RFC 8445 — ICE: Interactive Connectivity Establishment
- RFC 5389 — STUN: Session Traversal Utilities for NAT
- RFC 5766 — TURN: Traversal Using Relays around NAT
- RFC 4787 — NAT Behavioral Requirements for Unicast UDP
- RFC 8838 — Trickle ICE
- RFC 8839 — ICE in WebRTC
- WebRTC for the Curious — ICE
- WebRTC for the Curious — 历史 / 多播 vs 单播
- MDN — RTCPeerConnection.iceConnectionState
- coturn — GitHub