7、ICE连接建立:ICE Candidate是什么、收集Candidate、Candidate优先级、ICE连接状态变化(iceConnectionState)、连接建立成功回调

ICE,全称 Interactive Connectivity Establishment,翻译过来叫「交互式连接建立」。名字挺长,但说白了,它就是帮两个浏览器找到一条能通的路。

我刚开始接触 WebRTC 时,觉得 ICE 这层特别玄乎。后来踩了几次坑才明白——没有 ICE,你的音视频数据根本发不出去。为什么?因为现实网络太复杂了,有 NAT、有防火墙、有各种奇怪的网络拓扑。两个设备要直接通信,中间得翻多少座山?

嗯,ICE 就是那个翻山越岭的向导。

7.1 ICE Candidate 是什么

先说说 Candidate。Candidate 这个词直译是「候选人」,在 ICE 语境下,它代表一个「可能的连接地址」。每个 Candidate 包含:IP 地址、端口号、传输协议(UDP/TCP),以及一个类型标识。

类型分三种:

  • host:本机网卡地址。比如你电脑的局域网 IP,192.168.1.100 这种。这是优先级最高的,因为走内网最快。
  • srflx:Server Reflexive,服务器反射地址。说白了就是你的公网 IP,通过 STUN 服务器查到的。我在项目中遇到过,有些公司内网策略很严,srflx 地址死活拿不到,后来发现是 UDP 端口被防火墙封了。
  • relay:中继地址,通过 TURN 服务器转发。这是最后的手段,延迟高,但一定能通。我曾经调试过一个跨国会议,两边网络都走不了 P2P,最后全靠 relay 撑着。

每个 Candidate 就是一个 JSON 结构,大概长这样:

{
  "candidate": "candidate:1 1 UDP 2122252543 192.168.1.100 54321 typ host",
  "sdpMid": "0",
  "sdpMLineIndex": 0
}

注意那个长长的数字「2122252543」,那是优先级。后面会细说。

7.2 收集 Candidate

收集 Candidate 的过程,其实就是 ICE Agent 在背后默默干活。你调用 createOffer()createAnswer() 之后,浏览器就开始收集了。

流程大致是这样:

  1. 先收集本机所有网卡地址(host 类型)。
  2. 然后向 STUN 服务器发请求,拿到公网映射地址(srflx 类型)。
  3. 如果配置了 TURN 服务器,还会申请中继地址(relay 类型)。

每个 Candidate 收集到后,会通过 onicecandidate 回调通知你。注意,这个回调会触发多次,每次携带一个 Candidate。当所有 Candidate 收集完毕时,会触发一次 null 回调。

我个人的习惯是,在 onicecandidate 里直接把 Candidate 通过信令通道发给远端。不要等全部收集完再发,那样会拖慢连接建立的速度。

peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    // 立即发送给远端
    signalingChannel.send({
      type: 'candidate',
      candidate: event.candidate
    });
  } else {
    console.log('所有 Candidate 收集完毕');
  }
};
小技巧: 如果你发现 Candidate 收集特别慢,可以检查一下 STUN/TURN 服务器的配置。我曾经遇到一个坑——TURN 服务器域名解析超时,导致 relay 类型的 Candidate 迟迟拿不到,整个连接建立被拖慢了 5 秒。

7.3 Candidate 优先级

ICE 收集到一堆 Candidate 后,怎么选?答案是按优先级排序。优先级是一个 32 位整数,值越大越优先。

优先级的计算公式如下:

priority = (2^24) * type_preference + (2^8) * local_preference + (2^0) * (256 - component_id)

其中:

  • type_preference:类型偏好。host 是 126,srflx 是 100,relay 是 0。
  • local_preference:本地偏好,通常跟网卡优先级有关。有线网卡一般比无线网卡高。
  • component_id:组件 ID。RTP 是 1,RTCP 是 2。

你想想看,host 类型的优先级天然就比 relay 高出一大截。所以只要内网能通,ICE 绝不会走中继。这个设计很合理——能直连就别绕路。

我见过一个案例:某次线上会议,用户反馈视频卡顿。排查后发现,ICE 选了一条 srflx 的路径,但实际两台机器在同一个局域网。为什么没选 host?因为其中一台机器的 host Candidate 被防火墙拦截了。后来调整了网络策略,host 路径才通。

重点: 优先级决定了 ICE 的选路顺序,但不保证高优先级的 Candidate 一定能连通。ICE 会按优先级从高到低尝试,直到找到一条可用的路径。

7.4 ICE 连接状态变化(iceConnectionState)

ICE 连接建立不是一蹴而就的,它有一个状态机。你可以通过 oniceconnectionstatechange 回调来监听状态变化。

状态一共有 7 种:

状态 说明
new 刚创建,还没开始收集 Candidate
checking 正在检查 Candidate 对是否可用
connected 至少找到一条可用路径,但还在尝试其他路径
completed 所有 Candidate 对都检查完毕,选出了最终路径
failed 所有 Candidate 对都试过了,没有一条能通
disconnected 连接丢失,但还在尝试恢复
closed 连接被主动关闭

实际开发中,我最关注的是 connectedfailed 这两个状态。前者表示连接通了,后者表示彻底没戏了。

peerConnection.oniceconnectionstatechange = () => {
  const state = peerConnection.iceConnectionState;
  console.log('ICE 状态:', state);

  switch (state) {
    case 'connected':
      console.log('ICE 连接建立成功!');
      break;
    case 'failed':
      console.error('ICE 连接失败,尝试重启 ICE');
      peerConnection.restartIce();
      break;
    case 'disconnected':
      console.warn('ICE 连接断开,等待恢复...');
      break;
  }
};
注意: disconnected 状态不一定是永久性的。ICE 会尝试用 STUN 心跳来恢复连接。如果一段时间后还没恢复,才会变成 failed。所以不要一看到 disconnected 就立刻重启 ICE,给它一点时间。

7.5 连接建立成功回调

当 ICE 连接建立成功后,你会收到 oniceconnectionstatechange 回调,状态变为 connectedcompleted。但注意,这并不代表媒体流已经可以传输了。

为什么?因为 ICE 只是负责「通路」,媒体流还需要经过 SDP 协商、编解码器初始化等步骤。真正可以开始传输音视频数据的标志,是 ontrack 回调被触发。

我个人的经验是,在 connected 状态时,可以做一些 UI 上的提示,比如显示「连接中...」。等到 ontrack 触发后,再显示「已连接」,并开始渲染视频。

peerConnection.ontrack = (event) => {
  console.log('收到远端媒体流');
  const video = document.getElementById('remoteVideo');
  video.srcObject = event.streams[0];
};

还有一个回调值得关注——onicecandidateerror。这个回调会在 Candidate 收集或连接过程中发生错误时触发。比如 STUN 服务器超时、TURN 服务器认证失败等。

peerConnection.onicecandidateerror = (event) => {
  console.error('ICE Candidate 错误:', event.errorCode, event.errorText);
};

嗯,这个回调我一开始没留意,直到有一次线上环境频繁出现连接失败,排查了半天才发现是 TURN 服务器的凭证过期了。从那以后,我每个项目都会加上这个监听。

7.6 知识体系总览

下面这张图总结了 ICE 连接建立的核心流程:

ICE 连接建立核心流程 1. 收集 Candidate host(本机地址) srflx(公网映射) relay(TURN 中继) 2. 优先级排序 host > srflx > relay 按优先级从高到低 组成 Candidate 对 3. 连通性检查 STUN 打洞 连接测试 ICE 连接状态机 new checking connected completed failed disconnected → 正常流程 → 失败/断开 - - → 恢复尝试

这张图把 ICE 的三大阶段和状态机串在了一起。你可以看到,从收集 Candidate 开始,到优先级排序,再到连通性检查,最后进入状态机循环。一旦进入 connectedcompleted,就说明通路已经打通了。

最后说一句,ICE 这块调试起来确实有点麻烦。我建议你在开发阶段,把 onicecandidateoniceconnectionstatechangeonicecandidateerror 这三个回调的日志都打出来。出问题时,看日志就能定位到是 Candidate 没收集到,还是连通性检查失败了。

嗯,ICE 连接建立就讲到这里。记住一句话:ICE 不是魔法,它只是一套严谨的选路和打洞机制。理解了它的工作原理,你就能驾驭它。