【P2P音视频通信系统】WebRTC 之 ICE 详解

1. ICE 概述

1.1 什么是 ICE

ICE (Interactive Connectivity Establishment) 全称是"交互式连接建立"。简单来说,ICE 就是帮助两个设备在复杂的网络环境中找到对方并建立连接的技术。

生活中的类比
复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 的生活类比                                            │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  想象你要给一个新朋友寄快递,但你只知道他的名字:

  场景 1:你们在同一个小区
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  你:我住在 A 栋 101                                                                    │
  │  朋友:我住在 B 栋 202                                                                  │
  │  结果:直接送过去就行 → 这就是 host 候选者(直连)                                       │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  场景 2:你们在不同城市
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  你:我在北京,地址是 xxx                                                               │
  │  朋友:我在上海,地址是 yyy                                                             │
  │  结果:通过快递公司中转 → 这就是 relay 候选者(中转)                                    │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  场景 3:你不知道自己的公网地址
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  你:我不知道我的公网地址是什么                                                         │
  │  STUN 服务器:我来告诉你,你的公网地址是 xxx                                            │
  │  结果:知道了公网地址 → 这就是 srflx 候选者(服务器反射)                                │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

ICE 的工作就是收集所有可能的"地址",然后尝试每一个,找到能连通的那一个。

1.2 为什么需要 ICE

在 WebRTC 视频通话中,双方通常处于不同的网络环境:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    网络连接的挑战                                            │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  用户 A(家里)                              用户 B(公司)
  ┌─────────────┐                            ┌─────────────┐
  │   手机      │                            │   电脑      │
  └──────┬──────┘                            └──────┬──────┘
         │                                          │
         ▼                                          ▼
  ┌─────────────┐                            ┌─────────────┐
  │   WiFi路由器 │                            │ 公司防火墙   │
  │ 192.168.1.x │                            │ 10.0.0.x    │
  └──────┬──────┘                            └──────┬──────┘
         │                                          │
         ▼                                          ▼
  ┌─────────────┐                            ┌─────────────┐
  │   运营商NAT  │                            │ 公司NAT     │
  │ 公网IP A    │                            │ 公网IP B    │
  └──────┬──────┘                            └──────┬──────┘
         │                                          │
         └──────────────────┬───────────────────────┘
                            │
                            ▼
                     ┌─────────────┐
                     │   互联网     │
                     └─────────────┘

  挑战:
  1. A 不知道自己的公网 IP(被 NAT 隐藏了)
  2. B 在公司防火墙后面,外部无法主动连接
  3. 双方都在 NAT 后面,无法直接通信

  ICE 的解决方案:
  1. 通过 STUN 服务器获取公网 IP
  2. 尝试各种可能的连接方式
  3. 如果直连不通,使用 TURN 服务器中转

1.3 ICE 的核心作用

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 的三大核心作用                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  1️⃣ 候选者收集 - "我有哪些地址?"                                                          │
│                                                                                             │
│     收集所有可能的网络地址:                                                                │
│     - 本地地址(局域网 IP)                                                                │
│     - 公网地址(通过 STUN 获取)                                                           │
│     - 中转地址(通过 TURN 获取)                                                           │
│                                                                                             │
│     通俗理解:列出所有能联系到我的方式                                                      │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  2️⃣ 连接检查 - "哪个地址能用?"                                                            │
│                                                                                             │
│     尝试每一个候选者对,检查是否能连通:                                                    │
│     - 发送 STUN 绑定请求                                                                   │
│     - 接收 STUN 绑定响应                                                                   │
│     - 验证双向连通性                                                                       │
│                                                                                             │
│     通俗理解:挨个试,看哪个能打通                                                          │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  3️⃣ 连接选择 - "用哪个最好?"                                                              │
│                                                                                             │
│     根据优先级选择最佳连接:                                                               │
│     - host > srflx > prflx > relay                                                        │
│     - 直连优先,中转最后                                                                   │
│                                                                                             │
│     通俗理解:能直连就直连,实在不行才中转                                                  │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2. ICE 候选者类型详解

2.1 四种候选者类型

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 候选者类型                                            │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  类型      │ 英文全称              │ 通俗解释              │ 优先级     │ 示例              │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  host     │ Host Candidate        │ 本地地址              │ 最高 ⭐⭐⭐ │ 192.168.1.100    │
│  srflx    │ Server Reflexive      │ 服务器反射地址        │ 高 ⭐⭐    │ 203.0.113.10     │
│  prflx    │ Peer Reflexive        │ 对等反射地址          │ 高 ⭐⭐    │ 动态发现          │
│  relay    │ Relayed Candidate     │ 中继地址              │ 低 ⭐      │ TURN服务器地址    │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2.2 Host 候选者(本地地址)

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Host 候选者详解                                           │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  什么是 Host 候选者?
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  设备本地的网络地址,通常是局域网 IP                                                     │
  │                                                                                         │
  │  通俗理解:你家里的"门牌号"                                                            │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  示例:
  a=candidate:1 1 UDP 2122260223 192.168.1.100 5000 typ host

  分解:
  ├── foundation: 1          (候选者标识)
  ├── component-id: 1        (1=RTP, 2=RTCP)
  ├── transport: UDP         (传输协议)
  ├── priority: 2122260223   (优先级,最高)
  ├── IP: 192.168.1.100      (本地 IP 地址)
  ├── port: 5000             (本地端口)
  └── typ: host              (类型:本地地址)

  使用场景:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ✅ 双方在同一个局域网内(如同一个 WiFi)                                               │
  │  ✅ 可以直接通信,不需要经过 NAT                                                       │
  │  ✅ 延迟最低,速度最快                                                                 │
  │                                                                                         │
  │  通俗理解:邻居之间串门,直接走过去就行                                                │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  局限性:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ❌ 不同局域网之间无法使用                                                             │
  │  ❌ 无法穿透 NAT                                                                       │
  │                                                                                         │
  │  通俗理解:不能用来给外地朋友寄快递                                                    │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

2.3 Server Reflexive 候选者(服务器反射地址)

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    srflx 候选者详解                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  什么是 srflx 候选者?
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  通过 STUN 服务器获取的公网 IP 地址                                                     │
  │                                                                                         │
  │  通俗理解:STUN 服务器告诉你"你在互联网上的地址是什么"                                 │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  工作原理:
  ┌─────────────────┐                    ┌─────────────────┐
  │   客户端 A       │                    │   STUN 服务器   │
  │  192.168.1.100  │                    │  公网 IP        │
  │                 │                    │                 │
  │  "我的公网地址   │                    │                 │
  │   是什么?"     │ ──────────────────>│                 │
  │                 │                    │                 │
  │                 │  "你的公网地址是   │                 │
  │                 │   203.0.113.10"    │                 │
  │                 │ <──────────────────│                 │
  └─────────────────┘                    └─────────────────┘

  示例:
  a=candidate:2 1 UDP 1686052607 203.0.113.10 12345 typ srflx raddr 192.168.1.100 rport 5000

  分解:
  ├── IP: 203.0.113.10       (公网 IP 地址)
  ├── port: 12345            (公网端口,NAT 映射后的端口)
  ├── typ: srflx             (类型:服务器反射)
  ├── raddr: 192.168.1.100   (原始本地 IP)
  └── rport: 5000            (原始本地端口)

  使用场景:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ✅ 穿透 Cone NAT(锥形 NAT)                                                          │
  │  ✅ 不同网络之间通信                                                                   │
  │  ✅ 不需要中转服务器                                                                   │
  │                                                                                         │
  │  通俗理解:知道了公网地址,可以给外地朋友寄快递了                                      │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  局限性:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ❌ 无法穿透 Symmetric NAT(对称型 NAT)                                               │
  │  ❌ 某些防火墙可能阻止                                                                 │
  │                                                                                         │
  │  通俗理解:有些小区的门禁太严,还是进不去                                              │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

2.4 Peer Reflexive 候选者(对等反射地址)

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    prflx 候选者详解                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  什么是 prflx 候选者?
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  在 ICE 连接检查过程中,对方告诉你的地址                                                │
  │                                                                                         │
  │  通俗理解:对方说"我看到你的地址是 xxx",这是一个意外发现的地址                        │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  工作原理:
  ┌─────────────────┐                    ┌─────────────────┐
  │   客户端 A       │                    │   客户端 B       │
  │                 │                    │                 │
  │  发送连接请求    │ ──────────────────>│                 │
  │                 │                    │                 │
  │                 │  "我看到你的地址是  │                 │
  │                 │   203.0.113.20"    │                 │
  │                 │ <──────────────────│                 │
  │                 │                    │                 │
  │  "原来我还有这个│                    │                 │
  │   地址!"       │                    │                 │
  └─────────────────┘                    └─────────────────┘

  使用场景:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ✅ Symmetric NAT 环境下                                                               │
  │  ✅ NAT 映射与预期不同时                                                               │
  │  ✅ 意外发现的可连通地址                                                               │
  │                                                                                         │
  │  通俗理解:本来不知道这个地址,对方告诉我才知道                                         │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

2.5 Relay 候选者(中继地址)

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Relay 候选者详解                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  什么是 Relay 候选者?
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  通过 TURN 服务器中转的地址                                                             │
  │                                                                                         │
  │  通俗理解:实在连不上,就找个中间人帮忙传话                                            │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  工作原理:
  ┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
  │   客户端 A       │      │   TURN 服务器   │      │   客户端 B       │
  │                 │      │                 │      │                 │
  │  发送数据        │ ────>│  中转数据       │ ────>│  接收数据       │
  │                 │      │                 │      │                 │
  │  接收数据        │ <────│  中转数据       │ <────│  发送数据       │
  │                 │      │                 │      │                 │
  └─────────────────┘      └─────────────────┘      └─────────────────┘

  示例:
  a=candidate:3 1 UDP 41885439 198.51.100.10 60000 typ relay raddr 198.51.100.10 rport 60000

  分解:
  ├── IP: 198.51.100.10     (TURN 服务器的 IP)
  ├── port: 60000           (TURN 服务器分配的端口)
  ├── typ: relay            (类型:中继)
  └── priority: 41885439    (优先级,最低)

  使用场景:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ✅ 所有其他方式都失败时                                                                │
  │  ✅ 严格的 Symmetric NAT 环境                                                          │
  │  ✅ 企业防火墙阻止 P2P                                                                 │
  │                                                                                         │
  │  通俗理解:最后的选择,保证一定能连通                                                  │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  缺点:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ❌ 延迟较高(多经过一跳)                                                             │
  │  ❌ 服务器带宽成本高                                                                   │
  │  ❌ 不是真正的 P2P                                                                     │
  │                                                                                         │
  │  通俗理解:找中间人传话,总比直接说慢一点                                              │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

3. ICE 连接流程

3.1 完整的 ICE 连接流程

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 连接完整流程                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

     客户端 A                                          STUN/TURN 服务器                客户端 B
        │                                                    │                               │
        │                                                    │                               │
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │  阶段 1: 收集候选者 (Gathering)
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │                                                    │                               │
        │  1. 收集本地地址 (host)                            │                               │
        │     - 192.168.1.100                               │                               │
        │                                                    │                               │
        │  2. 向 STUN 服务器查询公网地址                     │                               │
        │ ─────────────────────────────────────────────────>│                               │
        │                                                    │                               │
        │                              "你的公网地址是 203.0.113.10"                         │
        │ <─────────────────────────────────────────────────│                               │
        │                                                    │                               │
        │  3. 收集 srflx 候选者                              │                               │
        │     - 203.0.113.10                                │                               │
        │                                                    │                               │
        │  4. (可选) 向 TURN 服务器请求中继地址              │                               │
        │ ─────────────────────────────────────────────────>│                               │
        │                                                    │                               │
        │                              "分配的中继地址是 198.51.100.10"                      │
        │ <─────────────────────────────────────────────────│                               │
        │                                                    │                               │
        │  5. 收集 relay 候选者                              │                               │
        │     - 198.51.100.10                               │                               │
        │                                                    │                               │
        ▼                                                    ▼                               ▼

     客户端 A                                          信令服务器                    客户端 B
        │                                                    │                               │
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │  阶段 2: 交换候选者 (Exchange)
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │                                                    │                               │
        │  发送候选者给 B                                    │                               │
        │  - host: 192.168.1.100                            │                               │
        │  - srflx: 203.0.113.10                            │                               │
        │  - relay: 198.51.100.10                           │                               │
        │ ─────────────────────────────────────────────────>│                               │
        │                                                    │  转发候选者给 B               │
        │                                                    │ ───────────────────────────────>│
        │                                                    │                               │
        │                                                    │  B 也收集并发送候选者         │
        │                                                    │ <───────────────────────────────│
        │  接收 B 的候选者                                   │                               │
        │ <─────────────────────────────────────────────────│                               │
        │                                                    │                               │
        ▼                                                    ▼                               ▼

     客户端 A                                                                          客户端 B
        │                                                                                    │
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │  阶段 3: 连接检查 (Connectivity Check)
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │                                                                                    │
        │  尝试所有候选者对:                                                                │
        │                                                                                    │
        │  A 的 host → B 的 host     ❌ 不同网络,不通                                       │
        │  A 的 host → B 的 srflx    ❌ NAT 阻挡                                             │
        │  A 的 srflx → B's host     ❌ NAT 阻挡                                             │
        │  A 的 srflx → B's srflx    ✅ 成功!                                               │
        │ ──────────────────────────────────────────────────────────────────────────────────>│
        │                                                                                    │
        │                              收到响应,确认连通                                     │
        │ <──────────────────────────────────────────────────────────────────────────────────│
        │                                                                                    │
        ▼                                                                                    ▼

        │  ═══════════════════════════════════════════════════════════════════════════════════
        │  阶段 4: 连接建立 (Connection Established)
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │                                                                                    │
        │  选择最佳候选者对:srflx ↔ srflx                                                   │
        │  开始传输音视频数据                                                                │
        │ ──────────────────────────────────────────────────────────────────────────────────>│
        │ <──────────────────────────────────────────────────────────────────────────────────│
        │                                                                                    │
        ▼                                                                                    ▼

3.2 ICE 状态机

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 连接状态                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│                           ┌─────────────────┐                                              │
│                           │     NEW         │                                              │
│                           │   (新建)        │                                              │
│                           └────────┬────────┘                                              │
│                                    │                                                        │
│                                    │ 开始收集候选者                                         │
│                                    ▼                                                        │
│                           ┌─────────────────┐                                              │
│                           │   GATHERING     │                                              │
│                           │   (收集中)      │                                              │
│                           └────────┬────────┘                                              │
│                                    │                                                        │
│                    ┌───────────────┼───────────────┐                                       │
│                    │               │               │                                        │
│                    ▼               ▼               ▼                                        │
│           ┌─────────────┐ ┌─────────────┐ ┌─────────────┐                                  │
│           │  CHECKING   │ │   CLOSED    │ │   FAILED    │                                  │
│           │  (检查中)   │ │   (已关闭)  │ │   (失败)    │                                  │
│           └──────┬──────┘ └─────────────┘ └─────────────┘                                  │
│                  │                                                                          │
│                  │ 至少一对候选者连通                                                       │
│                  ▼                                                                          │
│           ┌─────────────┐                                                                  │
│           │  CONNECTED  │                                                                  │
│           │  (已连接)   │                                                                  │
│           └──────┬──────┘                                                                  │
│                  │                                                                          │
│                  │ 所有候选者对检查完成                                                     │
│                  ▼                                                                          │
│           ┌─────────────┐                                                                  │
│           │  COMPLETED  │                                                                  │
│           │  (完成)     │                                                                  │
│           └──────┬──────┘                                                                  │
│                  │                                                                          │
│                  │ 连接断开                                                                 │
│                  ▼                                                                          │
│           ┌─────────────┐                                                                  │
│           │  DISCONNECTED│                                                                 │
│           │  (断开)      │                                                                 │
│           └─────────────┘                                                                  │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

状态说明:
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  状态          │ 通俗解释                    │ 说明                                       │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  NEW           │ 刚创建                      │ PeerConnection 刚创建                      │
│  GATHERING     │ 正在收集地址                │ 正在收集 ICE 候选者                        │
│  CHECKING      │ 正在尝试连接                │ 正在进行连接检查                           │
│  CONNECTED     │ 连上了                      │ 至少有一对候选者连通                       │
│  COMPLETED     │ 全部检查完了                │ 所有检查完成,选择了最佳路径               │
│  FAILED        │ 连接失败                    │ 所有候选者对都无法连通                     │
│  DISCONNECTED  │ 连接断开                    │ 之前连通,现在断了                         │
│  CLOSED        │ 已关闭                      │ PeerConnection 已关闭                      │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

4. STUN 协议详解

4.1 什么是 STUN

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    STUN 协议概述                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  STUN (Session Traversal Utilities for NAT)
  
  通俗理解:STUN 就像是一个"镜子",告诉你"你在互联网上是什么样子"

  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │                                                                                         │
  │  客户端                          STUN 服务器                                            │
  │  ┌─────────┐                     ┌─────────┐                                           │
  │  │ 本地IP: │                     │ 公网IP  │                                           │
  │  │ 192.168 │                     │ 已知    │                                           │
  │  │ .1.100  │                     │         │                                           │
  │  └────┬────┘                     └────┬────┘                                           │
  │       │                               │                                                 │
  │       │ "我的公网地址是什么?"         │                                                 │
  │       │ ─────────────────────────────>│                                                 │
  │       │                               │                                                 │
  │       │ "你的公网地址是 203.0.113.10" │                                                 │
  │       │ <─────────────────────────────│                                                 │
  │       │                               │                                                 │
  │       │ "知道了!"                    │                                                 │
  │       ▼                               ▼                                                 │
  │                                                                                         │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  STUN 的作用:
  1. 告诉客户端它的公网 IP 和端口
  2. 帮助穿透 NAT
  3. 检测 NAT 类型

4.2 STUN 消息格式

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    STUN 消息格式                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  STUN 消息结构:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │                                                                                         │
  │  0                   1                   2                   3                           │
  │  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3                   │
  │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                       │
  │ |0 0|     STUN Message Type     |         Message Length        |                       │
  │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                       │
  │ │                         Magic Cookie                          │                       │
  │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                       │
  │ │                                                               │                       │
  │ │                     Transaction ID (96 bits)                  │                       │
  │ │                                                               │                       │
  │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                       │
  │ │                             Attributes                        │                       │
  │ │                             ...                               │                       │
  │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                       │
  │                                                                                         │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  字段解释:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  字段              │ 长度    │ 通俗解释                                                │
  ├─────────────────────────────────────────────────────────────────────────────────────────┤
  │  Message Type     │ 2 字节  │ 消息类型(请求/响应/错误)                              │
  │  Message Length   │ 2 字节  │ 消息体长度                                              │
  │  Magic Cookie     │ 4 字节  │ 固定值 0x2112A442,用于识别 STUN 消息                   │
  │  Transaction ID   │ 12 字节 │ 请求标识符,用于匹配请求和响应                          │
  │  Attributes       │ 变长    │ 各种属性,如 IP 地址、端口等                            │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  常见消息类型:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  类型              │ 值      │ 通俗解释                                                │
  ├─────────────────────────────────────────────────────────────────────────────────────────┤
  │  Binding Request  │ 0x0001  │ "告诉我我的公网地址是什么"                              │
  │  Binding Response │ 0x0101  │ "你的公网地址是 xxx"                                    │
  │  Binding Error    │ 0x0111  │ "出错了"                                                │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

4.3 本项目中的 STUN 配置

kotlin 复制代码
// PeerConnectionManager.kt 中的 STUN 配置
private fun buildIceServers(config: JSONObject?): List<PeerConnection.IceServer> {
    val iceServers = mutableListOf<PeerConnection.IceServer>()
    
    config?.let { cfg ->
        val stunConfig = cfg.optJSONObject("stun")
        
        stunConfig?.let { stun ->
            if (stun.optBoolean("enabled", false)) {
                val server = stun.optString("server", "")
                val port = stun.optInt("port", 3478)
                val stunUrl = "stun:$server:$port"
                iceServers.add(PeerConnection.IceServer.builder(stunUrl).createIceServer())
                Log.i(TAG, "[webrtc] 添加STUN服务器: $stunUrl")
            }
        }
    }
    
    return iceServers
}

5. TURN 协议详解

5.1 什么是 TURN

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    TURN 协议概述                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  TURN (Traversal Using Relays around NAT)
  
  通俗理解:TURN 就像是"传话筒",当两个人无法直接对话时,通过中间人传话

  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │                                                                                         │
  │  客户端 A                        TURN 服务器                      客户端 B              │
  │  ┌─────────┐                     ┌─────────┐                     ┌─────────┐           │
  │  │ 无法直接│                     │  中转站  │                     │ 无法直接│           │
  │  │ 连接 B  │                     │         │                     │ 连接 A  │           │
  │  └────┬────┘                     └────┬────┘                     └────┬────┘           │
  │       │                               │                               │                 │
  │       │ "帮我转发数据给 B"            │                               │                 │
  │       │ ─────────────────────────────>│                               │                 │
  │       │                               │ "这是 A 发来的数据"           │                 │
  │       │                               │ ─────────────────────────────>│                 │
  │       │                               │                               │                 │
  │       │                               │ "这是 B 发来的数据"           │                 │
  │       │ <─────────────────────────────│ <─────────────────────────────│                 │
  │       │ "收到 B 的数据"               │                               │                 │
  │       ▼                               ▼                               ▼                 │
  │                                                                                         │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  TURN 的作用:
  1. 当 P2P 连接无法建立时,提供中转服务
  2. 保证 100% 的连通性
  3. 适用于严格的 NAT/防火墙环境

5.2 TURN 认证机制

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    TURN 认证流程                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  TURN 需要认证,防止滥用:

  ┌─────────────────┐                    ┌─────────────────┐
  │   客户端        │                    │   TURN 服务器   │
  │                 │                    │                 │
  │  用户名: user1  │                    │  预配置:        │
  │  密码: pass123  │                    │  user1/pass123  │
  │                 │                    │                 │
  │  1. 请求分配端口│                    │                 │
  │  (带认证信息)   │ ──────────────────>│                 │
  │                 │                    │                 │
  │                 │                    │  验证用户名密码 │
  │                 │                    │  计算 HMAC      │
  │                 │                    │                 │
  │                 │  2. 分配成功       │                 │
  │                 │  端口: 60000       │                 │
  │                 │ <──────────────────│                 │
  │                 │                    │                 │
  └─────────────────┘                    └─────────────────┘

  本项目中的 TURN 认证配置:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  // stun_turn.go                                                                       │
  │  turnServer, err := turn.NewServer(turn.ServerConfig{                                 │
  │      Realm: "videocall",                                                               │
  │      AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) {│
  │          if username == gConfig.Turn.Username {                                        │
  │              key := turn.GenerateAuthKey(username, realm, gConfig.Turn.Password)       │
  │              return key, true                                                           │
  │          }                                                                             │
  │          return nil, false                                                             │
  │      },                                                                                │
  │  })                                                                                    │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

5.3 本项目中的 TURN 配置

kotlin 复制代码
// PeerConnectionManager.kt 中的 TURN 配置
turnConfig?.let { turn ->
    if (turn.optBoolean("enabled", false)) {
        val server = turn.optString("server", "")
        val port = turn.optInt("port", 3479)
        val username = turn.optString("username", "")
        val password = turn.optString("password", "")
        val turnUrl = "turn:$server:$port"
        iceServers.add(
            PeerConnection.IceServer.builder(turnUrl)
                .setUsername(username)
                .setPassword(password)
                .createIceServer()
        )
        Log.i(TAG, "[webrtc] 添加TURN服务器: $turnUrl")
    }
}

6. NAT 类型与穿透策略

6.1 NAT 类型分类

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    NAT 类型详解                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  1️⃣ Full Cone NAT (完全锥形 NAT)                                                           │
│                                                                                             │
│     特点:一旦内部地址端口映射到外部地址端口,任何外部主机都可以发送数据                    │
│                                                                                             │
│     通俗理解:像是一个公开的信箱,任何人都可以往里投信                                     │
│                                                                                             │
│     内部地址:端口          外部地址:端口                                                   │
│     192.168.1.100:5000  →  203.0.113.10:12345                                             │
│                                                                                             │
│     任何外部主机 → 203.0.113.10:12345 → 192.168.1.100:5000 ✅                             │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  2️⃣ Restricted Cone NAT (限制锥形 NAT)                                                     │
│                                                                                             │
│     特点:只有内部主机曾经发送过数据的外部主机才能发送数据回来                              │
│                                                                                             │
│     通俗理解:像是一个有门禁的小区,只有你联系过的人才能进来                               │
│                                                                                             │
│     内部先发送数据给 A → A 可以回复                                                        │
│     内部未发送数据给 B → B 不能发送数据进来 ❌                                             │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  3️⃣ Port Restricted Cone NAT (端口限制锥形 NAT)                                            │
│                                                                                             │
│     特点:只有内部主机曾经发送过数据的外部主机和端口才能发送数据回来                        │
│                                                                                             │
│     通俗理解:像是一个更严格的门禁,不仅认人还认门                                         │
│                                                                                             │
│     内部发送给 A:5000 → A:5000 可以回复                                                    │
│     内部发送给 A:5000 → A:5001 不能回复 ❌                                                 │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  4️⃣ Symmetric NAT (对称型 NAT)                                                             │
│                                                                                             │
│     特点:对不同的目标地址,会分配不同的外部映射端口                                        │
│                                                                                             │
│     通俗理解:像是有多个分机,打给不同的人用不同的分机号                                   │
│                                                                                             │
│     内部发送给 A → 映射为 203.0.113.10:12345                                               │
│     内部发送给 B → 映射为 203.0.113.10:12346 (不同端口!)                                   │
│                                                                                             │
│     这导致 STUN 获取的地址对 B 来说可能不正确                                              │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

6.2 NAT 穿透策略

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    NAT 穿透策略                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  NAT 组合              │ 穿透方法              │ 成功率    │ 说明                          │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  Cone + Cone          │ STUN 即可             │ 高        │ 双方都能获取正确的公网地址   │
│  Cone + Symmetric     │ STUN + ICE            │ 中        │ 可能需要 prflx 候选者        │
│  Symmetric + Symmetric│ 需要 TURN             │ 低        │ 很难直连,需要中转           │
│  严格防火墙           │ 必须用 TURN           │ -         │ 只能中转                     │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

穿透优先级:
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  1️⃣ 优先尝试 host 候选者                                                                   │
│     - 同一局域网内可能直接连通                                                             │
│     - 通俗理解:先看看是不是邻居                                                           │
│                                                                                             │
│  2️⃣ 尝试 srflx 候选者                                                                      │
│     - 通过 STUN 获取公网地址                                                               │
│     - 通俗理解:用公网地址试试                                                             │
│                                                                                             │
│  3️⃣ 尝试 prflx 候选者                                                                      │
│     - 意外发现的地址                                                                       │
│     - 通俗理解:对方告诉我的新地址                                                         │
│                                                                                             │
│  4️⃣ 最后使用 relay 候选者                                                                  │
│     - 通过 TURN 中转                                                                       │
│     - 通俗理解:实在不行就找中间人                                                         │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

7. 本项目 ICE 实现

7.1 IceCandidateManager 实现

kotlin 复制代码
// IceCandidateManager.kt
class IceCandidateManager {
    
    private val pendingIceCandidates = mutableListOf<IceCandidate>()
    private val localIceCandidates = mutableListOf<IceCandidate>()
    var isRemoteDescriptionSet = false
    
    var onIceCandidateReady: ((String) -> Unit)? = null
    
    // 添加本地 ICE 候选者
    fun onLocalIceCandidate(candidate: IceCandidate) {
        localIceCandidates.add(candidate)
        
        // 转换为 JSON 格式发送给对端
        val candidateJson = JSONObject().apply {
            put("sdpMid", candidate.sdpMid)
            put("sdpMLineIndex", candidate.sdpMLineIndex)
            put("candidate", candidate.sdp)
        }
        
        onIceCandidateReady?.invoke(candidateJson.toString())
        
        logCandidateInfo(candidate, "本地")
    }
    
    // 处理远程 ICE 候选者
    fun handleRemoteIceCandidate(candidateJson: String) {
        val candidates = parseRemoteIceCandidates(candidateJson)
        
        candidates.forEach { candidate ->
            if (isRemoteDescriptionSet) {
                // 远程描述已设置,直接添加
                peerConnectionManager?.addIceCandidate(candidate)
            } else {
                // 远程描述未设置,先缓存
                addPendingIceCandidate(candidate)
            }
        }
    }
    
    // 处理缓存的候选者
    fun processPendingIceCandidates(peerConnectionManager: PeerConnectionManager) {
        pendingIceCandidates.forEach { candidate ->
            peerConnectionManager.addIceCandidate(candidate)
        }
        pendingIceCandidates.clear()
    }
}

7.2 ICE 候选者交换流程

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    本项目 ICE 交换流程                                       │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

     客户端 A                                          信令服务器                    客户端 B
        │                                                    │                               │
        │  1. setLocalDescription() 后开始收集 ICE           │                               │
        │                                                    │                               │
        │  2. 收集到 host 候选者                             │                               │
        │     onIceCandidate 回调                            │                               │
        │ ─────────────────────────────────────────────────>│                               │
        │                                                    │  转发                         │
        │                                                    │ ───────────────────────────────>│
        │                                                    │                               │
        │  3. 收集到 srflx 候选者                            │                               │
        │ ─────────────────────────────────────────────────>│                               │
        │                                                    │ ───────────────────────────────>│
        │                                                    │                               │
        │  4. 收集完成 (COMPLETE)                            │                               │
        │     发送 endOfCandidates 信号                      │                               │
        │ ─────────────────────────────────────────────────>│                               │
        │                                                    │ ───────────────────────────────>│
        │                                                    │                               │
        │  5. B 收到候选者后:                               │                               │
        │     - 如果远程描述已设置 → 直接添加                │                               │
        │     - 如果远程描述未设置 → 缓存等待                │                               │
        │                                                    │                               │
        ▼                                                    ▼                               ▼

7.3 ICE 连接状态处理

kotlin 复制代码
// WebRTCClient.kt 中的 ICE 状态处理
peerConnectionManager?.apply {
    onIceConnectionChange = { state ->
        Log.i(TAG, "[webrtc] ICE连接状态: $state")
        onConnectionStateChanged?.invoke(state)
        
        when (state) {
            PeerConnection.IceConnectionState.CONNECTED -> {
                // 连接成功
                Log.i(TAG, "[webrtc] ICE连接成功")
            }
            PeerConnection.IceConnectionState.FAILED -> {
                // 连接失败
                Log.e(TAG, "[webrtc] ICE连接失败")
            }
            PeerConnection.IceConnectionState.DISCONNECTED -> {
                // 连接断开
                Log.w(TAG, "[webrtc] ICE连接断开")
            }
            else -> {}
        }
    }
}

8. Trickle ICE

8.1 什么是 Trickle ICE

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Trickle ICE 机制                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  传统 ICE(等待所有候选者):
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │                                                                                         │
  │  客户端 A                                                                               │
  │  │                                                                                      │
  │  ├── 收集 host 候选者... 等待                                                          │
  │  ├── 收集 srflx 候选者... 等待                                                         │
  │  ├── 收集 relay 候选者... 等待                                                         │
  │  │                                                                                      │
  │  └── 全部收集完成,一次性发送所有候选者                                                 │
  │                                                                                         │
  │  问题:需要等待所有候选者收集完成,连接建立慢                                           │
  │  通俗理解:等所有快递都到了才一起发货,太慢了                                           │
  │                                                                                         │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  Trickle ICE(逐步发送):
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │                                                                                         │
  │  客户端 A                                                                               │
  │  │                                                                                      │
  │  ├── 收集到 host 候选者 → 立即发送 ────────────────────────────────────────> 对方       │
  │  │   通俗理解:本地地址找到了,先发过去                                                 │
  │  │                                                                                      │
  │  ├── 收集到 srflx 候选者 → 立即发送 ────────────────────────────────────────> 对方       │
  │  │   通俗理解:公网地址找到了,再发过去                                                 │
  │  │                                                                                      │
  │  └── 收集到 relay 候选者 → 立即发送 ────────────────────────────────────────> 对方       │
  │      通俗理解:中转地址找到了,最后发过去                                               │
  │                                                                                         │
  │  优势:可以立即开始连接检查,加快连接建立速度                                           │
  │  通俗理解:有一个地址就发一个,不用等                                                   │
  │                                                                                         │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

8.2 本项目中的 Trickle ICE 实现

kotlin 复制代码
// IceCandidateManager.kt
fun onLocalIceCandidate(candidate: IceCandidate) {
    // 每收集到一个候选者就立即发送
    localIceCandidates.add(candidate)
    
    val candidateJson = JSONObject().apply {
        put("sdpMid", candidate.sdpMid)
        put("sdpMLineIndex", candidate.sdpMLineIndex)
        put("candidate", candidate.sdp)
    }
    
    // 立即回调发送
    onIceCandidateReady?.invoke(candidateJson.toString())
}

// 收集完成时发送结束信号
fun onLocalIceCandidatesComplete() {
    val endSignal = JSONObject().apply {
        put("endOfCandidates", true)
    }
    onIceCandidateReady?.invoke(endSignal.toString())
}

9. ICE 调试技巧

9.1 日志分析

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 日志分析                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  本项目中的 ICE 日志:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  [webrtc] 添加STUN服务器: stun:videocall.itgcs.tech:3478                                │
  │  [webrtc] 添加TURN服务器: turn:videocall.itgcs.tech:3479                                │
  │  [ice][本地] 候选 | 类型: host | IP: 192.168.1.100 | 端口: 5000                         │
  │  [ice][本地] 候选 | 类型: srflx | IP: 203.0.113.10 | 端口: 12345                        │
  │  [webrtc][observer] ICE收集状态变化: COMPLETE                                           │
  │  [webrtc][observer] ICE连接状态变化: CHECKING                                           │
  │  [webrtc][observer] ICE连接状态变化: CONNECTED                                          │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  关键检查点:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  1️⃣ 检查 ICE 服务器配置                                                                │
  │     - STUN/TURN 服务器地址是否正确?                                                   │
  │     - TURN 用户名密码是否正确?                                                        │
  │                                                                                         │
  │  2️⃣ 检查候选者收集                                                                     │
  │     - 是否收集到 host 候选者?                                                         │
  │     - 是否收集到 srflx 候选者?(STUN 工作正常)                                       │
  │     - 是否收集到 relay 候选者?(TURN 工作正常)                                       │
  │                                                                                         │
  │  3️⃣ 检查连接状态                                                                       │
  │     - 是否进入 CHECKING 状态?                                                         │
  │     - 是否最终 CONNECTED?                                                             │
  │     - 如果 FAILED,检查候选者交换是否完整                                              │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

9.2 常见问题排查

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 常见问题排查                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

问题 1: ICE 状态一直是 CHECKING,无法 CONNECTED
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  可能原因:                                                                                 │
│  - 候选者没有正确交换                                                                       │
│  - NAT 类型导致无法直连                                                                     │
│  - 防火墙阻止 UDP 通信                                                                      │
│                                                                                             │
│  排查方法:                                                                                 │
│  1. 检查双方是否都收到了对方的候选者                                                       │
│  2. 检查是否有 srflx 或 relay 候选者                                                       │
│  3. 尝试配置 TURN 服务器                                                                   │
│                                                                                             │
│  通俗理解:双方都在尝试,但找不到对方的位置                                                 │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

问题 2: 只收集到 host 候选者
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  可能原因:                                                                                 │
│  - STUN 服务器不可达                                                                       │
│  - STUN 服务器配置错误                                                                     │
│  - 网络阻止了 STUN 请求                                                                    │
│                                                                                             │
│  排查方法:                                                                                 │
│  1. 检查 STUN 服务器地址和端口                                                             │
│  2. 使用工具测试 STUN 服务器连通性                                                         │
│  3. 检查防火墙设置                                                                         │
│                                                                                             │
│  通俗理解:STUN 服务器没响应,不知道公网地址                                               │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

问题 3: ICE 连接经常断开重连
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  可能原因:                                                                                 │
│  - 网络不稳定                                                                              │
│  - NAT 映射超时                                                                            │
│  - ICE 连接保活失败                                                                        │
│                                                                                             │
│  排查方法:                                                                                 │
│  1. 检查网络质量                                                                           │
│  2. 配置 ICE 保活参数                                                                      │
│  3. 考虑使用 TURN 中转提高稳定性                                                           │
│                                                                                             │
│  通俗理解:连接不稳定,经常掉线                                                            │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

10. ICE 最佳实践

10.1 配置建议

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 配置最佳实践                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

1️⃣ 同时配置 STUN 和 TURN
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - STUN 用于获取公网地址,成本低                                                          │
│  - TURN 作为备用,保证连通性                                                               │
│  - 通俗理解:先尝试自己解决,实在不行找中间人                                              │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2️⃣ 使用 Trickle ICE
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - 加快连接建立速度                                                                       │
│  - 边收集边发送边检查                                                                     │
│  - 通俗理解:有一个地址就试一个,不用等                                                   │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

3️⃣ 正确处理候选者时机
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - 远程描述设置后才能添加远程候选者                                                       │
│  - 使用缓存机制处理提前到达的候选者                                                       │
│  - 通俗理解:等对方准备好了,再告诉他你的地址                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

4️⃣ 监控 ICE 状态
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - 记录 ICE 状态变化                                                                      │
│  - 记录使用的候选者类型                                                                   │
│  - 记录连接建立时间                                                                       │
│  - 通俗理解:记录连接过程,方便排查问题                                                   │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

10.2 性能优化

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 性能优化建议                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

1️⃣ 减少 ICE 收集时间
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - 使用就近的 STUN/TURN 服务器                                                            │
│  - 配置合理的超时时间                                                                     │
│  - 通俗理解:服务器越近,响应越快                                                         │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2️⃣ 优先使用直连
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - host 候选者优先级最高                                                                  │
│  - 同一局域网内直连延迟最低                                                               │
│  - 通俗理解:能直连就直连,最快                                                           │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

3️⃣ 合理使用 TURN
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - TURN 会增加延迟和带宽成本                                                              │
│  - 只在必要时使用                                                                         │
│  - 通俗理解:中转是最后的选择                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

11. 参考资料

11.1 相关 RFC 文档

RFC 编号 标题 说明
RFC 8445 Interactive Connectivity Establishment (ICE) ICE 协议规范
RFC 8489 STUN STUN 协议规范
RFC 8656 TURN TURN 协议规范
RFC 8839 SDP Offer/Answer Procedures for ICE ICE 的 SDP 处理

11.2 本项目相关文件

文件路径 说明
IceCandidateManager.kt ICE 候选者管理,包含收集、缓存、发送
PeerConnectionManager.kt PeerConnection 管理,ICE 服务器配置
WebRTCClient.kt WebRTC 客户端,ICE 状态处理
stun_turn.go 服务端 STUN/TURN 服务器实现

本系列文章:
【P2P音视频通信系统】之方案架构详解
【P2P音视频通信系统】之呼叫完整时序图
【P2P音视频通信系统】之STUN服务详解
【P2P音视频通信系统】之TURN 服务详解
【P2P音视频通信系统】WebRTC ICE 候选类型详解:对等反射候选者(Peer Reflexive Candidate)
【P2P音视频通信系统】WebRTC 之 SDP 详解

相关推荐
TSINGSEE2 小时前
融合与重构:从EasyDSS一站式视频云平台看流媒体技术如何重塑企业交互边界
重构·音视频·视频编解码·智能摘要·智能字幕
弹简特2 小时前
【JavaEE12-后端部分】SpringMVC07-综合案例3-从留言板看前后端交互:接口文档与HTTP通信详解
spring boot·网络协议·spring·http·java-ee·交互
loong_XL2 小时前
qwen3.5 文字、图像、视频多模态openai接口案例
音视频·qwen·多模态大模型
YYDataV数据可视化2 小时前
【P2P音视频通信系统】之信令服务器详解
服务器·音视频·p2p·信令服务器
W|J2 小时前
websocket 的创建使用
websocket·网络协议
二十画~书生2 小时前
ESP32-S3音频板
经验分享·单片机·音视频·硬件工程·pcb工艺
zymill2 小时前
hysAnalyser和flvAnalyser对比
音视频·实时音视频·视频编解码·h.264·智能电视·视频分析·mpeg-2
野指针YZZ2 小时前
Gstreamer插入第三方plugins流程:rgaconvert
linux·音视频·rk3588
YYDataV数据可视化3 小时前
【P2P音视频通信系统】webrtc 之 SDP 详解
音视频·webrtc·sdp