ICE提名机制:常规提名与激进提名

ICE提名,说白了就是决定「用哪条路」的最后一步。

我在做WebRTC开发的头两年,一直觉得提名是个很玄乎的东西。直到有一次排查一个通话卡顿问题,发现两边明明连通了,却一直在换路径——嗯,就是提名机制没搞对。

今天咱们就把这个机制彻底讲清楚。

提名是什么?为什么需要它?

ICE流程走到最后,候选者对(Candidate Pair)已经排序好了。但排序归排序,到底用哪一对,得有个最终确认的动作。这个动作,就叫提名(Nomination)。

你可以这么理解:

  • 连通性检查(Connectivity Check)是「探路」
  • 排序(Priority)是「给路打分」
  • 提名(Nomination)是「拍板定路」

没有提名,ICE永远在「待定」状态。只有提名完成,媒体流才能真正开始跑。

核心要点:提名是ICE从「检查阶段」进入「完成阶段」的开关。

两种提名模式:常规 vs 激进

RFC 5245里定义了两种提名方式。我刚开始看文档时觉得区别不大,直到自己动手实现了一遍才明白——这俩完全是不同的哲学。

特性 常规提名(Regular) 激进提名(Aggressive)
提名时机 所有检查完成后,选最优的提名 边检查边提名,发现可用就提名
速度 慢,但稳定 快,但可能反复
提名次数 通常只提名一次 可能多次提名不同候选者
适用场景 对稳定性要求高的场景 对连接速度要求高的场景

常规提名(Regular Nomination)

常规提名的逻辑很简单:先做完所有连通性检查,等所有候选者对都验证过了,再从中挑一个最优的进行提名。

我习惯把这种方式叫做「先看全貌,再做决定」。

流程是这样的:

  1. 发起方发送所有候选者的STUN绑定请求(Binding Request)
  2. 等所有响应都回来了,候选者对排序完成
  3. 从排序列表顶部选一个,发送带提名标志的请求
  4. 接收方确认后,ICE完成

我的经验:常规提名在P2P场景下表现很好。但如果你在用TURN中继,常规提名可能会让连接建立时间翻倍。我曾经在一个项目中,就因为用了常规提名,用户等了3秒才听到对方声音——后来改成激进提名,1秒内就通了。

激进提名(Aggressive Nomination)

激进提名就激进多了。它不等所有检查完成,只要发现一个可用的候选者对,立马提名。

说白了就是「先上车,再补票」。

流程是这样的:

  1. 发起方发送STUN绑定请求,同时带上提名标志
  2. 如果收到成功响应,立即提名这个候选者对
  3. 如果后续发现更好的候选者对,可以重新提名

注意:激进提名可能导致「提名抖动」——刚提名了一个,又发现更好的,又提名另一个。媒体流可能会因此短暂中断。我遇到过最夸张的情况,一次通话中提名切换了7次。

提名与ICE完成的关系

这里有个容易混淆的点:提名完成不等于ICE完成。

ICE完成需要满足两个条件:

  • 至少有一个候选者对完成了提名
  • 所有候选者对都已经检查完毕(或超时)

换句话说:

  • 提名完成 → 可以开始发媒体了
  • ICE完成 → 所有候选者都检查过了,不会再变了

我见过不少开发者把这两个混为一谈。其实你想想看,激进提名下,提名完成得很早,但ICE可能还没完成——因为还有别的候选者在排队检查呢。

关键区别:提名完成是「能用」,ICE完成是「稳定」。

提名冲突处理

提名冲突,说白了就是两边同时提名了不同的候选者对。

举个例子:

  • Alice提名了候选者对A
  • Bob提名了候选者对B
  • A和B不是同一个候选者对

这时候怎么办?

RFC里规定了一套规则:

  1. 如果收到对方的提名请求,且自己还没提名,就接受对方的提名
  2. 如果双方都提名了,比较提名候选者的优先级,选优先级高的
  3. 如果优先级一样,比较双方的角色(控制方 vs 被控制方),控制方说了算

避坑指南:我曾经在一个跨平台项目中,Android端用激进提名,iOS端用常规提名,结果两边提名冲突了。Android端提名了一个低延迟的P2P候选者,iOS端提名了一个稳定的TURN候选者。最后因为角色判定问题,两边各执一词,ICE状态卡住了。解决方案是统一两端的提名策略。

提名流程的SVG图解

下面这张图展示了常规提名和激进提名的完整流程对比:

ICE提名流程对比 常规提名 激进提名 1. 发送所有候选者的检查请求 2. 等待所有响应返回 3. 排序候选者对,选最优 4. 发送带提名标志的请求 ICE完成 1. 发送检查请求+提名标志 2. 收到响应立即提名 3. 媒体开始传输 4. 继续检查其他候选者 ICE完成(可能延迟) vs

提名相关的STUN消息格式

提名是通过STUN消息中的标志位来传递的。具体来说,是在STUN绑定请求中设置一个特定的属性。

// STUN消息中的提名标志
// ICE-CONTROLLED 或 ICE-CONTROLLING 属性中携带提名信息

// 常规提名:先发不带提名标志的请求,再发带提名标志的请求
// 激进提名:直接在请求中设置提名标志

// 伪代码示例
if (nomination_mode == REGULAR) {
    // 先做普通检查
    send_binding_request(candidate_pair, NOMINATION_FLAG_NONE);
    // 等所有检查完成,再提名
    send_binding_request(best_pair, NOMINATION_FLAG_SET);
} else if (nomination_mode == AGGRESSIVE) {
    // 直接带提名标志
    send_binding_request(candidate_pair, NOMINATION_FLAG_SET);
}

实际项目中的选择建议:

  • 如果用户对连接速度敏感(比如直播、实时通话),用激进提名
  • 如果用户对稳定性敏感(比如视频会议、远程医疗),用常规提名
  • 如果两端设备性能差异大,建议统一提名策略,避免冲突

提名状态机

ICE提名涉及几个关键状态:

  • Waiting:等待检查结果,还没提名
  • InProgress:提名请求已发送,等待响应
  • Succeeded:提名成功,候选者对可用
  • Failed:提名失败,需要重试或换候选者

我习惯在代码里用状态机来管理提名流程,这样不容易出错。

我的一个小技巧:在调试ICE提名问题时,我会在日志里打印每个候选者对的状态变化。这样一旦出现提名冲突或卡住,一眼就能看出问题出在哪一步。

总结

ICE提名机制,说白了就是「选路」的最后一步。常规提名稳但慢,激进提名快但可能抖。提名完成不等于ICE完成,这个区别一定要记住。提名冲突时,靠优先级和角色来裁决。

实际项目中,我建议根据场景灵活选择提名策略。没有银弹,只有最适合的。


公众号:蓝海资料掘金营,微信deep3321