资讯动态

WebRTC ICE candidate属性详解:从优先级计算到连通性排查实战

发布时间:2026/10/18 5:24:33 来源:尧图企业网站定制
1. 从一次线上事故说起candidate属性为什么值得单独拎出来讲去年冬天团队里一个刚转正的同学在群里发了一张截图线上环境里 ICE 协商卡在checking状态整整四十秒最后超时失败。日志里翻来覆去就是几行local candidate和remote candidate的打印看起来一切正常但连接就是建不起来。当时我们几个人围着屏幕看了半天最后发现问题出在一个谁都没注意的地方——候选地址的优先级计算里type preference和local preference的取值逻辑被改错了导致一对本该优先配对的 host 候选被排到了最后而 relay 候选又因为网络策略压根不可达。这件事之后我就养成了一个习惯凡是涉及 ICE 的排查第一件事就是把双方所有 candidate 的完整属性打出来逐字段过一遍。很多人觉得 candidate 不就是个 IP 加端口吗有什么好看的。但恰恰是这个没什么好看的东西藏着 ICE 能不能连通、多快连通、走哪条路径的全部秘密。这篇内容就是围绕candidate 属性展开的。我会把 candidate 的每一个字段拆开讲清楚它是什么、为什么这么设计、实际抓包时长什么样、哪些字段最容易出问题、怎么根据这些字段快速定位连通性故障。适合正在做实时音视频、WebRTC 相关开发的同学也适合那些被 ICE 连接问题折磨过、想彻底搞明白底层逻辑的人。读完之后你至少能做到拿到一份 candidate 列表能判断出配对顺序合不合理能看出哪条路径大概率能通能定位到是采集、信令还是网络层的问题。需要说明的是ICE 本身是一套标准化的协商框架不同实现浏览器、移动端 SDK、服务端网关在细节上会有差异。下面讲的内容以通用机制为主具体参数取值我会标注出常见实践你对照自己用的实现去验证即可。2. candidate 属性的完整字段拆解2.1 一行 candidate 到底包含哪些信息先看一条真实的 candidate 字符串长什么样。这是从一次抓包里摘出来的格式是标准的 SDP 属性行acandidate:1 1 udp 2130706431 192.168.1.100 54321 typ host acandidate:2 1 udp 1694498815 203.0.113.45 12345 typ srflx raddr 192.168.1.100 rport 54321 acandidate:3 1 udp 41885439 198.51.100.7 3478 typ relay raddr 203.0.113.45 rport 12345别看这一行不长它其实塞了七八个关键信息。我按位置逐个拆位置示例值字段名含义11foundation候选基础标识相同 foundation 的候选来自同一类网络接口21component-id组件 ID1 通常代表 RTP2 代表 RTCP3udptransport传输协议常见 udp也有 tcp42130706431priority优先级决定配对顺序的核心数字5192.168.1.100connection-address候选的 IP 地址654321port候选的端口7typ hostcandidate-type候选类型host / srflx / prflx / relay8raddr/rport相关地址仅 srflx 和 relay 有表示该候选映射前的本地地址这里最容易被人忽略的是foundation和priority这两个字段。foundation 看起来只是个标识但它在 ICE 的冗余消除机制里扮演关键角色——相同 foundation 的候选被认为是同源的在配对时会被特殊处理避免重复计算。而 priority 直接决定了谁先被配对、谁后配对是影响连通速度的头号因素。2.2 priority 的计算公式与常见取值priority 不是一个随便填的数字它有明确的计算规则。标准里给出的公式是priority (2^24) * type_preference (2^8) * local_preference (256 - component_id)三个部分各有讲究。type_preference反映候选类型的质量等级host 最高因为它是本机直连地址不需要经过任何中转srflx 次之它经过了 NAT 映射但至少是点对点的prflx 再次relay 最低因为它要经过中继服务器转发延迟和成本都最高。常见的取值是host126prflx110srflx100relay0local_preference是本地策略的调节项通常和网络接口的优先级挂钩。比如同时有有线和无线网卡有线接口的 local_preference 会被设得更高这样在配对时会优先尝试有线路径。这个值在不同实现里差异很大有的直接用接口索引有的用一套自定义的权重表。component_id那一项(256 - component_id)是为了让 RTP 组件id1比 RTCP 组件id2稍微优先一点差值只有 1影响很小但存在。拿上面那条 host 候选算一下type_preference126local_preference 假设是 65535最大值component_id1那么 priority 126 * 16777216 65535 * 256 255 2113929216 16776960 255 2130706431。和抓包里的数字完全对得上。你可以用这个公式反推任何一条候选的 type_preference 和 local_preference这在排查为什么这条候选没被优先选中时特别有用。2.3 foundation 的生成逻辑与冗余消除foundation 的生成规则是相同类型、相同基础地址、相同传输协议的候选共享同一个 foundation。注意这里说的是基础地址base对于 host 候选就是它自己的 IP对于 srflx 和 relay 则是它们映射前的本地地址也就是 raddr。举个例子一台机器有两个网卡分别拿到 192.168.1.100 和 10.0.0.50那么它会生成两条 host 候选foundation 不同因为基础地址不同。但如果它通过同一个 STUN 服务器拿到了两个 srflx 候选且这两个 srflx 都映射自 192.168.1.100那么它们的 foundation 就是相同的。为什么要有 foundation因为 ICE 在配对时如果发现两条候选的 foundation 相同就可以认为它们同源在冗余检查时可以直接跳过一部分计算。更实际的作用是当一条候选的连通性检查失败时同 foundation 的其他候选大概率也会失败可以快速剪枝减少无效的检查次数。这个机制在候选数量多的时候能显著降低协商开销。我遇到过一种情况某移动端设备在弱网下频繁切换网络导致 foundation 不断变化ICE 每次都要重新做全量配对协商时间从正常的几百毫秒涨到三四秒。后来在信令层做了 foundation 的稳定性优化把同一物理接口的 foundation 固定下来问题才缓解。所以 foundation 不只是个标识它直接影响协商效率。3. 四种候选类型的本质区别与适用场景3.1 host 候选最直接但也最受限host 候选就是本机网络接口上的地址它不经过任何 NAT 映射是最原始的地址。在局域网内host 候选之间的配对几乎总能成功延迟也最低。但一旦跨公网host 候选基本就废了——因为对方的 host 地址在你的网络里根本路由不到。很多人有个误区觉得 host 候选优先级最高就一定要用它。实际上在公网场景下host 候选的连通性检查大概率是失败的只是因为它优先级高会被优先尝试反而拖慢了整体协商速度。所以有些实现会在检测到对端不在同一网段时主动降低 host 候选的 local_preference让 srflx 或 relay 先被尝试。这是一个典型的优先级高不等于好用的例子。host 候选的另一个特点是数量可能很多。一台开发机如果有虚拟网卡、Docker 网桥、多个物理网卡host 候选能列出七八条。这里面很多是无效的比如 Docker 的 172.17.x.x如果不做过滤会白白增加配对组合数。常见做法是在采集阶段就根据接口类型做一轮筛选把明显不可达的接口排除掉。3.2 srflx 候选NAT 映射后的公网地址srflx 是 server reflexive 的缩写意思是通过服务器反射得到的地址。它的获取过程是客户端向 STUN 服务器发一个请求服务器看到的是客户端经过 NAT 映射后的公网地址和端口然后把这个地址回给客户端。客户端拿到后就生成一条 srflx 候选。srflx 候选的价值在于它让两个都在 NAT 后面的客户端有机会直接通信。只要双方的 NAT 允许这种打洞行为srflx 之间的配对就能成功不需要经过中继。这是 P2P 通信最理想的情况。但 srflx 有个关键限制它只对锥形 NAT 有效。如果一方是对称 NAT那么它每次向不同目标发请求时NAT 分配的端口都不一样STUN 服务器看到的端口和实际通信时用的端口对不上srflx 候选就失效了。这种情况下只能退回到 relay。判断是否对称 NAT 的方法是向两个不同的 STUN 服务器发请求比较返回的映射端口是否一致。一致就是锥形不一致就是对称。这个检测在很多 SDK 里是内置的但检测本身也要花时间所以有些实现会跳过检测直接采集 srflx等配对失败再降级。3.3 prflx 候选被动发现的映射地址prflx 是 peer reflexive 的缩写它不是在采集阶段主动获取的而是在连通性检查过程中被动发现的。具体来说当 A 向 B 的某个候选地址发检查请求时B 收到的请求里带着 A 的源地址如果这个源地址不在 B 已知的候选列表里B 就会把它作为一条 prflx 候选加进来。prflx 的存在是为了处理一种特殊情况双方的 NAT 映射在采集之后发生了变化或者某些候选在采集时没被发现。它相当于一个兜底机制让协商过程能适应动态变化的网络环境。prflx 的优先级通常介于 srflx 和 relay 之间。它不需要中继但因为是事后发现的可靠性不如主动采集的 srflx。在实际排查中如果看到大量 prflx 候选往往说明网络环境不稳定或者采集阶段的 STUN 请求没有覆盖到所有接口。3.4 relay 候选最后的保底通道relay 候选是通过中继服务器TURN分配的地址。客户端向 TURN 服务器申请一个中继地址之后所有发往这个地址的数据都会由 TURN 服务器转发。它的优点是几乎总能连通——因为双方都是主动连到 TURN 服务器的不存在 NAT 穿透问题。缺点是延迟高、带宽成本高而且所有流量都要经过中继对服务器压力大。relay 候选的优先级最低这是合理的能直连就直连直连不行才走中继。但在实际部署中relay 的可用性直接决定了最坏情况下能不能通。我见过一些项目为了省成本TURN 服务器配置得很弱结果在对称 NAT 场景下大量连接失败。所以 relay 虽然优先级低但绝对不能省。四种候选类型的关系可以用一句话概括host 是理想srflx 是常态prflx 是补充relay 是底线。一个健壮的 ICE 实现应该四种都支持并根据实际网络情况动态调整它们的优先级。4. 候选配对与优先级排序的实战逻辑4.1 配对组合是怎么产生的ICE 的配对逻辑说起来简单把本地的每一条候选和远端的每一条候选两两组合形成候选对candidate pair然后按优先级排序依次做连通性检查。但实际组合数量是本地候选数乘以远端候选数如果双方各有 10 条候选就是 100 个候选对每个都要发检查包开销不小。所以实际实现里会做大量剪枝。第一轮剪枝是按foundation去重相同 foundation 的组合只保留一个代表。第二轮是按组件 ID过滤RTP 的候选只和 RTP 的候选配对不会和 RTCP 混。第三轮是按传输协议过滤udp 和 tcp 不会互相配对。经过这几轮候选对数量通常能降到原来的三分之一甚至更少。候选对的优先级计算也有公式pair_priority 2^32 * MIN(G, D) 2 * MAX(G, D) (G D ? 1 : 0)其中 G 是本地候选的 priorityD 是远端候选的 priority。这个公式的核心思想是取双方优先级中较小的那个作为主导。也就是说一条候选对的质量取决于木桶最短的那块板。如果本地是 host高优先级但远端是 relay低优先级那这个候选对的优先级就由 relay 决定不会因为本地是 host 就被优先尝试。这个设计很合理避免了一厢情愿式的配对。4.2 连通性检查的顺序与超时控制候选对排好序之后ICE 会按顺序发起连通性检查。每个检查是一个 STUN Binding 请求发出去之后等响应。如果收到响应这个候选对就被标记为 succeeded如果超时标记为 failed继续下一个。这里有个关键参数检查的超时时间。设得太短网络稍微抖一下就误判失败设得太长协商时间被拖长。常见做法是采用指数退避第一次等 500ms没响应就等 1s再没响应等 2s最多重试几次。整个 ICE 协商还有一个总超时通常是 10 到 30 秒超过就整体失败。我在排查时经常关注一个指标首个成功候选对的出现时间。如果这个时间很短几百毫秒说明网络环境好、候选质量高如果接近总超时说明大部分候选都失败了最后靠 relay 兜底。这个指标比最终是否连通更能反映网络质量。还有一个容易被忽略的点检查的并发度。如果串行检查100 个候选对逐个试时间根本不够。所以实现里通常会并发发起多个检查但并发数也不能太高否则会给网络和对方造成压力。常见的并发窗口是 5 到 10 个。这个值的调优需要根据实际网络情况来没有万能数字。4.3 提名机制谁来决定用哪条路径连通性检查成功后并不意味着这条路径就会被使用。ICE 还有一个提名nomination过程由控制方通常是发起方从所有成功的候选对里选一个最优的提名它作为正式的数据通道。提名分两种模式常规提名和激进提名。常规提名是等所有检查都做完从成功的候选对里选优先级最高的激进提名是一旦有候选对成功立刻提名它不等其他检查完成。激进提名的好处是快坏处是可能选到一条不是最优的路径。常规提名更稳但慢。实际项目里怎么选我的经验是对延迟敏感的场景比如实时通话用激进提名对质量敏感的场景比如文件传输用常规提名。有些实现还支持先激进提名保证连通再后台继续检查发现更优路径就切换的混合策略这个比较复杂但效果最好。提名之后双方会用选定的候选对建立数据通道后续的媒体流就走这条路径。如果这条路径后来失效了比如网络切换ICE 会重新进入检查流程这个过程叫ICE restart。restart 时 foundation 和 priority 会重新计算所以前面讲的这些属性又会被重新用到。5. 从抓包看 candidate 属性的真实表现5.1 一次成功协商的完整候选交换过程光讲理论不够直观我拿一次实际的抓包记录来走一遍。场景是两个客户端在同一局域网内通过信令服务器交换 SDP。第一步A 采集本地候选生成三条一条 host192.168.1.100:54321一条 srflx203.0.113.45:12345一条 relay198.51.100.7:3478。这三条通过信令发给 B。第二步B 也采集三条host192.168.1.101:54322srflx203.0.113.46:12346relay198.51.100.8:3479。发给 A。第三步双方各自做配对。A 这边产生 3×39 个候选对按优先级排序后最前面的是 host-host 对192.168.1.100 对 192.168.1.101因为双方都是 host优先级最高。第四步A 向 B 的 host 候选发 STUN 检查。因为是同一局域网请求直接到达B 回响应。这个候选对标记为 succeeded。第五步B 也向 A 的 host 候选发检查同样成功。双方都确认 host-host 路径可用。第六步提名阶段A 作为控制方提名 host-host 对。数据通道建立后续媒体流走这条路径。整个过程从开始到连通大概 200 毫秒。抓包里能看到的关键信息是检查请求的源地址、目标地址、以及响应里的 XOR-MAPPED-ADDRESS。如果响应里的映射地址和请求的目标地址一致说明路径是直的没有额外的 NAT 映射。5.2 协商失败时的典型日志特征再看一个失败的例子。场景是 A 在公司内网对称 NATB 在家庭网络锥形 NAT。A 采集到的候选host10.0.0.5:5000srflx198.51.100.20:6000relay198.51.100.7:3478。注意 A 是对称 NAT所以它的 srflx 端口 6000 只在向 STUN 服务器通信时有效向 B 发检查时 NAT 会分配一个新端口导致 B 收到的源端口不是 6000。B 采集到的候选host192.168.0.10:7000srflx203.0.113.50:8000relay198.51.100.8:3479。配对后A 先试 host-host失败跨网段。再试 host-srflxA 的 host 是内网地址B 根本路由不到失败。再试 srflx-srflxA 发出去的检查包源端口被 NAT 改成了别的B 回的响应到不了 A失败。最后试 relay-relay双方都连到各自的 TURN 服务器TURN 服务器之间转发成功。日志里的典型特征是前几个候选对的检查都是timeout最后一个 relay 对是succeeded。整体协商时间可能到 5 秒以上。如果 TURN 服务器配置有问题relay 也失败那整个 ICE 就彻底失败了日志里全是 timeout没有任何 succeeded。5.3 用属性字段快速定位问题层级拿到一份 candidate 列表和检查日志怎么快速判断问题出在哪我总结了一个排查顺序先看候选类型是否齐全。如果只有 host 没有 srflx说明 STUN 采集失败可能是 STUN 服务器地址配错、端口被封、或者网络策略拦截了 UDP。如果连 relay 都没有说明 TURN 也没配上那跨网段基本没戏。再看priority 是否合理。如果 relay 的 priority 比 srflx 还高那肯定是计算逻辑写错了会导致优先走中继浪费带宽还增加延迟。正常情况下 host srflx prflx relay。再看foundation 是否重复异常。如果所有候选的 foundation 都一样说明生成逻辑有问题冗余消除会失效配对数量暴涨。如果 foundation 频繁变化说明网络接口不稳定。最后看检查日志的失败模式。全是 timeout 说明网络层不通部分 succeeded 但提名失败说明提名逻辑有问题succeeded 后很快 failed 说明路径不稳定可能是 NAT 映射超时。这套顺序能覆盖大部分常见问题比盲目翻日志高效得多。6. 那些年我在 candidate 上踩过的坑6.1 优先级计算里的整数溢出前面给的 priority 公式里(2^24) * type_preference这一项如果 type_preference 取 126结果是 2113929216接近 32 位有符号整数的上限 2147483647。如果实现里用的是有符号整数再加上 local_preference 的部分很容易溢出变成负数。负数在排序时会被排到最后导致本该优先的 host 候选反而最后才被尝试。这个坑我在两个不同项目里都遇到过。一次是 C 实现里用了int而不是uint32_t另一次是 JavaScript 里位运算默认按 32 位有符号处理。修复方法很简单把所有涉及 priority 的变量都改成无符号类型或者在计算时用更大的数据类型再截断。但发现这个问题的过程很痛苦因为日志里打印出来的 priority 看起来就是个数字不特意去算根本看不出溢出。提示如果你在排查host 候选明明存在却不被优先尝试的问题第一件事就是检查 priority 的计算类型确认没有溢出。6.2 多网卡环境下的候选爆炸开发机通常有多个网络接口有线、无线、虚拟网卡、Docker 网桥、回环。每个接口都可能生成 host 候选再加上 srflx 和 relay候选数量轻松上双。如果双方都是这种环境候选对数量就是几十乘以几十上千个组合。虽然剪枝能砍掉一部分但剩下的检查量依然很大协商时间被拖到十几秒。解决办法是在采集阶段做接口过滤。具体策略是排除回环接口、排除明显是虚拟网桥的接口比如 172.17.x.x、169.254.x.x、排除没有默认路由的接口。有些实现还支持配置白名单只采集指定接口。这个过滤逻辑看起来简单但能显著减少候选数量是优化协商速度最有效的手段之一。我在一个项目里做过对比过滤前平均候选数是 12过滤后降到 4协商时间从平均 3.2 秒降到 1.1 秒。效果非常明显。6.3 srflx 采集的时序陷阱srflx 候选的采集依赖 STUN 请求的响应。如果采集逻辑是发请求后立即返回不等响应那 srflx 候选就会缺失。如果采集逻辑是等所有 STUN 响应都回来再继续那在网络差的时候会卡很久。正确的做法是设置一个合理的等待窗口比如 500ms在这个窗口内收到的 STUN 响应都用来生成 srflx 候选超时的就放弃。这样既不会漏掉正常的响应也不会被慢响应拖死。有些实现还会对同一个 STUN 服务器重试但重试次数不宜过多否则窗口会被拉长。还有一个细节STUN 请求应该并发发往多个服务器。如果只用一个服务器那个服务器挂了或者被限流srflx 就采集不到。用两到三个不同地址的服务器能提高采集成功率。但也不能太多否则请求量太大反而容易被判定为异常流量。6.4 relay 候选的按需分配relay 候选的获取需要向 TURN 服务器申请这个过程比 STUN 慢而且会占用服务器资源。如果每次协商都无条件申请 relay服务器压力会很大。所以很多实现采用按需分配策略先只采集 host 和 srflx如果配对检查失败再回头申请 relay。这个策略的优点是省资源缺点是增加了协商时间——因为要等前面的检查都失败了才去申请 relay整体时间变长。折中方案是设置一个阈值比如前 3 个候选对都失败了就立即申请 relay不等全部检查完。这个阈值需要根据实际网络情况调没有标准答案。我在一个用户量比较大的项目里见过另一种做法根据历史数据预测。如果某个用户之前的协商记录里 relay 使用率很高就提前申请如果一直是直连成功就延后申请。这种数据驱动的策略效果不错但实现复杂度高适合有足够数据积累的场景。7. 让 candidate 属性真正为你所用的几个实操建议7.1 建立一份自己的候选检查清单排查 ICE 问题时最怕的是没有章法东看一点西看一点。我建议你整理一份自己的检查清单每次按顺序过一遍。我的清单大概是这样候选类型是否齐全host / srflx / relay 至少各有一条priority 数值是否在合理范围host 接近 2.1e9relay 接近 4e7foundation 是否有异常重复或频繁变化候选数量是否过多超过 10 条就要考虑过滤检查日志里首个 succeeded 的时间最终提名的是哪条候选对是否符合预期是否有 prflx 候选出现出现频率如何这份清单看起来简单但能覆盖 80% 以上的常见问题。每次排查都走一遍慢慢就形成肌肉记忆了。7.2 在日志里把关键属性打全很多项目的日志只打印候选的 IP 和端口这是不够的。排查时你需要看到完整的属性行包括 foundation、priority、type、raddr。我的做法是在采集和配对的关键节点把候选的完整字符串打出来并且加上时间戳和阶段标记。这样事后分析时能清楚地看到每个阶段候选的变化。日志级别也要注意。正常运行时可以只打摘要但一旦进入排查模式就要能动态调整到详细级别。有些实现支持通过配置或接口动态开启详细日志这个能力在线上排查时非常有用。7.3 用模拟工具验证优先级逻辑优先级计算和配对排序的逻辑光靠看代码很难验证对不对。我习惯写一个小工具输入一组候选输出排序后的候选对列表然后手工核对前几个是否符合预期。这个工具不需要多复杂几百行代码就能搞定但能帮你快速发现计算错误。更进一步可以用模拟网络环境来测试不同 NAT 类型下的表现。比如用软件模拟对称 NAT、锥形 NAT、全锥 NAT观察候选采集和配对的结果是否符合预期。这种测试在项目早期做能避免很多上线后才发现的问题。7.4 关注候选属性随时间的动态变化candidate 不是一成不变的。网络切换、NAT 映射超时、接口状态变化都会导致候选列表变化。如果实现里没有处理这些变化就会出现协商时好好的用着用着断了的情况。我的经验是在连接建立后仍然要定期检查候选的有效性。比如每隔一段时间发一个保活包如果连续几次没响应就触发 ICE restart。restart 时会重新采集候选重新配对相当于把整个流程走一遍。这个过程对用户应该是无感的但底层要做不少工作。还有一个细节restart 时 foundation 会重新生成如果 foundation 变化太频繁会导致冗余消除失效。所以有些实现会在 restart 时尽量保持 foundation 的稳定性只更新必要的字段。这个优化对协商速度有正面影响。8. 关于 candidate 属性我最后想说的几点体会搞 ICE 这些年我最大的体会是candidate 属性是 ICE 的体检报告。一份完整的候选列表加上检查日志基本能还原出整个协商过程的全貌。问题往往不在那些复杂的状态机逻辑里而在某个字段的取值、某个计算的类型、某个采集的时序上。另一个体会是不要迷信默认值。不同实现、不同版本对 priority 的计算、foundation 的生成、候选的过滤策略都有差异。默认配置在大多数场景下能用但在特定网络环境下可能就不行。真正靠谱的做法是理解每个字段的含义然后根据实际场景去调。最后ICE 的调试很依赖工具和日志。花时间把日志打全、把工具做好比反复读标准文档更有效。标准告诉你应该怎样但实际网络会告诉你实际怎样两者之间的差距就是经验的价值所在。如果你正在被某个 ICE 连接问题困扰不妨先把双方的 candidate 完整列表打出来逐字段过一遍。很多时候答案就藏在那几行看起来平平无奇的属性里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑