4、RTP序列号(SN)机制:序列号的作用、序列号回绕处理、序列号在丢包检测中的应用
RTP 序列号,说白了就是每个 RTP 包的身份证号。发送方每发一个包,序列号就加 1。接收方拿到包,第一件事就是看这个号。它能干嘛?排序、去重、检测丢包,全指着它。
我刚开始做 WebRTC 的时候,觉得序列号不就是个计数器吗?有什么好研究的。后来线上出了个诡异的问题——视频突然卡住,但过一会儿又自己好了。查了半天,发现是序列号回绕导致的。嗯,从那以后我再也不敢小看这个 16 位的字段了。
4.1 序列号的核心作用
RTP 头部里,序列号占 16 位。范围是 0 到 65535。每发一个包,发送方就把序列号加 1。接收方拿到包后,用它做三件事:
- 包排序:网络会把包打乱顺序。接收方根据序列号,把包重新排好。
- 包去重:同一个包可能因为重传到达两次。序列号一样,直接丢弃重复的。
- 丢包检测:如果收到的序列号不连续,中间跳了几个号,那就是丢包了。
举个例子。发送方发了 5 个包,序列号分别是 100、101、102、103、104。接收方只收到了 100、101、103、104。一看,102 丢了。这就是最基础的丢包检测。
关键点:序列号是 RTP 接收端重建媒体流的基石。没有它,接收方根本不知道哪个包先到、哪个包后到,更别提检测丢包了。
4.2 序列号回绕问题
16 位能表示的最大值是 65535。超过这个数,序列号就会从 0 重新开始。这叫回绕(wrapping)。
你想想看,一个视频流每秒可能发几千个包。65535 个包,几十秒就用完了。回绕是必然发生的。如果不处理,接收方会以为新来的包是旧的,或者把回绕后的包当成乱序包丢弃。
我在项目中遇到过一个问题:通话持续了半小时后,画面突然出现大量花屏。排查发现,接收方把回绕后的包当成了乱序包,直接扔掉了。结果就是大量丢包,画面崩了。
4.2.1 回绕检测算法
怎么判断序列号是回绕了,还是真的乱序?业界常用的方法是:
- 维护一个
max_seq变量,记录当前收到的最大序列号。 - 新来的序列号
seq如果比max_seq小很多(比如小 30000 以上),就认为是回绕了。 - 如果只是小一点点(比如小 1 到 100),那就是正常的乱序包。
代码实现大概是这样的:
bool IsNewerSequenceNumber(uint16_t seq, uint16_t last_seq) {
// 判断 seq 是否比 last_seq 更新
// 使用有符号差值,避免回绕问题
int16_t diff = static_cast<int16_t>(seq - last_seq);
return diff > 0;
}
这个函数的核心思想:把差值转成有符号 16 位整数。如果差值是正数,说明 seq 更新。如果差值是负数,说明 seq 更旧或者回绕了。但要注意,这个函数只能处理一次回绕。多次回绕需要结合扩展序列号来处理。
个人经验:我习惯在接收端维护一个 32 位的扩展序列号。低 16 位就是 RTP 包里的序列号,高 16 位记录回绕次数。每次检测到回绕,高 16 位加 1。这样就能精确表示 0 到 2^32-1 的范围,基本不会出问题。
4.2.2 扩展序列号
扩展序列号的实现很简单:
uint32_t extended_seq = (wrap_count << 16) | rtp_seq;
其中 wrap_count 是回绕次数,rtp_seq 是 RTP 包里的 16 位序列号。接收方每次收到包,都先判断是否回绕,然后更新 wrap_count,最后得到 extended_seq。后续的排序、丢包检测,全部基于这个 32 位的扩展序列号。
注意:回绕检测不能只看绝对值。比如当前 max_seq 是 65530,新来的 seq 是 10。差值看起来是 65520,但实际是回绕了 1 次。正确的做法是看有符号差值,或者设定一个阈值(比如 30000)。超过阈值就认为是回绕。
4.3 序列号在丢包检测中的应用
丢包检测是序列号最重要的应用之一。WebRTC 的丢包检测机制,说白了就是看序列号有没有跳变。
4.3.1 基础丢包检测
接收方维护一个 expected_seq 变量,表示下一个期望收到的序列号。每次收到包:
- 如果
seq == expected_seq,一切正常,expected_seq加 1。 - 如果
seq > expected_seq,说明中间有包丢了。丢包数 =seq - expected_seq。 - 如果
seq < expected_seq,说明是乱序包或者重复包,不处理。
举个例子:
| 收到序列号 | expected_seq | 丢包数 | 说明 |
|---|---|---|---|
| 100 | 101 | 0 | 正常 |
| 101 | 102 | 0 | 正常 |
| 103 | 104 | 1 | 102 丢了 |
| 104 | 105 | 0 | 正常 |
| 102 | 105 | 0 | 乱序包,不计数 |
注意最后一行。102 虽然比 104 晚到,但它不是丢包,只是乱序。接收方不能把它算成丢包。
4.3.2 基于序列号的丢包率计算
WebRTC 会实时计算丢包率,用于调整码率和发送策略。丢包率的计算公式:
丢包率 = 丢失包数 / (已收到包数 + 丢失包数)
其中丢失包数就是通过序列号跳变算出来的。WebRTC 的 RTCP Receiver Report 里有一个字段叫 fraction lost,就是基于这个计算的。
我曾经调试过一个案例:用户反馈视频越看越模糊。查了日志,发现丢包率在 5% 左右波动。WebRTC 的拥塞控制检测到丢包,自动降低了发送码率。结果就是视频质量下降。后来优化了网络,丢包率降到 1% 以下,视频就恢复了。
4.3.3 丢包检测的陷阱
丢包检测看起来简单,但有几个坑要注意:
- 乱序包误判:如果乱序包延迟太大,接收方可能已经把它算成丢包了。等它到了,又得纠正。这会导致丢包率抖动。
- 回绕误判:回绕时如果不处理,接收方会以为丢了 65535 个包。这会导致拥塞控制误触发,码率骤降。
- 重复包干扰:重传的包序列号不变。接收方要能识别重复包,不把它算成新包。
避坑指南:我曾经在实现丢包检测时,忘了处理乱序包。结果丢包率忽高忽低,拥塞控制一直在震荡。后来加了一个乱序窗口(比如 100ms 内的包都算乱序),问题就解决了。
4.4 序列号机制整体流程
下面这张图展示了序列号从发送到接收的完整流程:
4.5 总结
序列号是 RTP 协议里最基础但也最容易出错的字段。16 位的范围注定了回绕是常态,不是异常。接收方必须正确处理回绕,否则排序和丢包检测都会出错。
我个人建议:
- 始终使用 32 位扩展序列号做内部处理。
- 丢包检测时,要区分乱序包和真正的丢包。
- 回绕阈值不要设得太小,30000 左右比较安全。
嗯,序列号这部分就讲到这里。记住一句话:序列号是 RTP 的命根子,搞懂了它,RTP 协议你就掌握了一半。