3.1 PeerConnection 状态机 本章从骨架讲起:连接建立的一切动作都由状态机驱动,先看懂状态的推进与回退,后面两节的协商与穿越才有挂靠的框架。这也是排查"连不上"类工单的第一入口。 四组状态,别混为一谈 新手最容易犯的错误是把连接对象上的几组状态当成一回事。实际上是四组独立的机器在各自推进,读日志时先分清你说的是哪一组。信令状态描述会话描述的交换进度,只在设置本地或远端描述时跃迁; ICE 收集状态描述候选搜集的进度,从新开、搜集中到搜集完成;连接状态描述线路检查的结果,从新开、检查中、已连通到断开;聚合状态则把媒体层与线路层的状态合成为对应用最友好的总览。四组机器的触发源不同、节奏不同,健康建连时它们各自前进、最终汇合。
本章从骨架讲起:连接建立的一切动作都由状态机驱动,先看懂状态的推进与回退,后面两节的协商与穿越才有挂靠的框架。这也是排查"连不上"类工单的第一入口。
新手最容易犯的错误是把连接对象上的几组状态当成一回事。实际上是四组独立的机器在各自推进,读日志时先分清你说的是哪一组。信令状态描述会话描述的交换进度,只在设置本地或远端描述时跃迁; ICE 收集状态描述候选搜集的进度,从新开、搜集中到搜集完成;连接状态描述线路检查的结果,从新开、检查中、已连通到断开;聚合状态则把媒体层与线路层的状态合成为对应用最友好的总览。四组机器的触发源不同、节奏不同,健康建连时它们各自前进、最终汇合。
| 状态组 | 驱动事件 | 健康序列 |
|---|---|---|
| 信令状态 | 设置本地与远端描述 | 稳定、有本地提议、稳定、有远端提议、稳定 |
| 候选搜集 | 各类候选产出完毕 | 新开、搜集中、搜集完成 |
| 线路状态 | 连通性检查结果 | 新开、检查中、已连通、已完成 |
| 聚合状态 | 上两者合成 | 新开、连接中、已连接、已连接且加密完成 |
把四组分开的实用价值在于定位精度:信令状态没动,说明描述的设置环节出了问题,方向是协商;线路状态停在检查中,说明穿越受阻,方向是网络环境或中转配置;候选搜集早早完成但线路一直不通,八成是双方只产出了互相不可达的候选。一组状态一个排查方向,比"连不上"三个字有用得多。
引擎内部,协商相关的状态推进集中在连接层的会话应答处理器中,所有描述设置操作被序列化到信令线程排队执行,任何时刻只处理一个描述事务——这正是第二章线程纪律的兑现。状态变化通过观察者回调外送应用,回调同样固定在信令线程上触发,嵌入代码在回调里可以直接更新界面状态而无需加锁。
协商状态机的迁移规则由协议框架严格限定:只有处于稳定态才能生成新提议;收到对方提议时,本端必须先落定远端描述再产出应答;任何一步设置失败,状态回退并携带错误原因。引擎在每次设置描述时执行两段校验——语法层检查文本合法,语义层检查与当前状态兼容——两段都过才会真正提交。
上图还藏着一条对嵌入方极有价值的规则:提议可以在稳定态反复生成,但每次生成都会使前一次作废——如果应用层把"生成提议"和"送达对端"做成异步两步,中间又插了一次重新生成,送达的可能是已经作废的文本,对端就会在语义校验时拒绝。这类问题的日志特征非常固定:对端报出的错误指明收到了不匹配的事务序号。看到它,先查应用层有没有并发生成提议。
背景。某接入方反馈连接成功率大约只有一半,失败时没有任何应用层异常,日志里只有零星的回调记录。我们拿到两份日志:一份成功样本,一份失败样本。
操作。把两份日志按四组状态拆开对齐时间轴。成功样本里,候选搜集在协商完成前后陆续产出地址,线路状态从检查中推进到已连通,随后加密完成,聚合状态报出可用,全程不到两秒。失败样本里,信令状态正常走完,说明协商没问题;候选搜集也完成了;但线路状态长期停在检查中,直到超时。进一步看候选记录:双方都只产出了本机地址与公网映射地址,没有中转地址,而两侧的公网映射地址经检测属于对称型 NAT——映射随目标地址变化,探测出的映射地址对端根本够不着。
结果。结论明确:这批用户的网络环境必须依赖中转线路,而接入方部署时没有配置中转服务器。补上中转配置后重测,成功率回到预期。
解读。这个案例演示了状态分组排查法的完整闭环:先用四组状态的推进位置把问题切到"穿越"区域,再用候选构成把区域收窄到"缺中转",最后用 NAT 行为解释根因。全程没有读一行源码,但每一步判断都依赖对状态机的理解——这就是骨架知识的用处。
变式。如果失败样本的停点不同——比如信令状态就没走完——排查方向就完全换到协商侧,此时要对比双方引擎产出的会话描述文本,这正是下一节的主题。同一套状态骨架,停点不同,下一站的工具就不同。
状态通过观察者回调外送应用,听起来简单,时序上却有几个反复咬人的坑,值得集中排一次。第一,回调与调用同线程串行:你在信令线程里调用引擎方法,状态回调也会排到同一线程执行,因此回调里再调用引擎方法是安全的,但绝不能在回调里同步等待应用自己的另一条线程——那等于让引擎线程等人,死锁风险立刻出现。第二,回调可能合并:连续的状态跃迁可能合并成少量回调送达,应用侧不要假设"每次跃迁必有一次回调",要按当前状态设计逻辑而不是按差分事件。第三,失败也走回调:协商失败、建连失败都以状态与错误回调呈现,不设异常抛出,接入层必须处理失败分支,只写成功路径的代码在真实网络里活不过一周。
把这些陷阱与状态分组组合起来,可以得到一张"停点对应动作"的速查表,工单初判时照表走:
| 停点状态 | 含义方向 | 第一动作 |
|---|---|---|
| 信令状态未推进 | 描述设置失败 | 读错误回调的原因字段 |
| 搜集完成但候选少 | 网络受限或配置缺失 | 核对探测与中转配置 |
| 线路停在检查中 | 穿越受阻 | 查候选构成与中转可达性 |
| 已连通但无媒体 | 加密或协商问题 | 核对指纹与方向交集 |
再补一条关于"回退"的机制细节:状态回退不是原地消失,而是带着完整的错误上下文回退。设置描述失败时,引擎会把失败发生的校验阶段、冲突的具体字段、当前合法的迁移集合一并放进错误对象;可惜不少接入层把这个对象当作字符串打印完事,白白丢掉了最值钱的诊断信息。正确姿势是把错误对象的分阶段信息拆开入库——按阶段统计失败率,能直接看出你的信令实现在哪个环节最弱,这种统计比"总失败率"有用一个量级。
本节要点:连接对象上是四组独立状态机,先分清再说排查;协商事务在信令线程串行执行,状态回退必带原因;稳定态之外的重复提议会作废前次,异步信令实现要防并发生成;"时连时不连"先用状态停点切块,再下钻。下一节我们打开协商的核心文本,看共识是如何在两段描述之间达成的。