4. RTP序列号机制:序列号的作用、丢包检测、乱序重排、序列号回绕处理
各位好,我是老周。今天我们来聊聊RTP协议里一个看似简单、实则暗藏玄机的字段——序列号(Sequence Number)。
很多刚接触RTP的同学觉得序列号不就是个计数器嘛,从0开始,每发一个包加1,有什么好讲的?
嗯,如果你真这么想,那在实际项目中大概率会踩坑。我当年第一次做视频通话系统时,就被序列号回绕问题折腾了一整晚。后来我才明白,序列号机制是RTP传输可靠性的基石,丢包检测、乱序重排、抖动计算,全都依赖它。
4.1 序列号的作用:不只是计数器
RTP序列号是一个16位的无符号整数,取值范围0~65535。它的核心作用有三个:
- 包序标识:接收端根据序列号判断包的先后顺序
- 丢包检测:通过序列号间隙发现丢失的包
- 乱序重排:将因网络延迟到达的乱序包恢复正确顺序
说白了,序列号就是每个RTP包的“身份证号”。发送端按顺序发,接收端按顺序收——但网络环境复杂,包不一定按顺序到,这时候序列号就派上用场了。
关键点:RTP序列号是独立于时间戳的。序列号标识包序,时间戳标识播放时间。两者不能混用,否则音画同步会出大问题。
4.2 丢包检测:如何发现网络丢了包?
丢包检测的原理其实很简单:接收端维护一个期望序列号(expected_seq_num),每收到一个包,就检查它的序列号是否等于期望值。
- 如果相等:正常接收,期望序列号+1
- 如果大于期望值:说明中间有包丢失了,丢包数 = 当前序列号 - 期望序列号
- 如果小于期望值:可能是乱序包或重复包,需要进一步判断
举个例子:
期望序列号: 100
收到包序列号: 100 → 正常,期望变为101
收到包序列号: 102 → 检测到丢包!丢失了序列号101
收到包序列号: 101 → 乱序包,序列号101已标记为丢失,现在补到了
我在项目中遇到过一种情况:网络抖动导致大量乱序包,接收端误判为丢包,触发重传请求,反而加重了网络拥塞。所以丢包检测不能只看一次间隙,需要结合丢包率统计和超时机制综合判断。
实战建议:丢包检测的阈值不要设得太敏感。我个人习惯是连续检测到3个以上序列号间隙,才判定为真实丢包。单个间隙可能是乱序,等一等可能就收到了。
4.3 乱序重排:把打乱的包排回正确顺序
网络传输中,包走不同的路径到达接收端,先发的包可能后到,后发的包可能先到。这就是乱序。
接收端需要维护一个重排缓冲区(Reorder Buffer),把乱序的包暂存起来,等前面的包到齐后再按顺序提交给上层解码器。
重排缓冲区的工作原理:
- 收到一个包,检查它的序列号
- 如果序列号等于期望值,直接提交,期望值+1
- 如果序列号大于期望值,放入缓冲区等待
- 如果序列号小于期望值,可能是重复包或超时包,直接丢弃
- 每次提交后,检查缓冲区中是否有连续的包可以提交
你想想看,如果没有重排机制,解码器收到乱序的帧数据,画面就会出现花屏、马赛克。尤其是视频编码中依赖帧间预测的P帧和B帧,顺序错了根本解不出来。
注意:重排缓冲区不能无限等待。需要设置一个最大等待时间(通常20~100ms),超时后即使前面的包没到,也要把已收到的包提交上去。否则会造成播放延迟越来越大。
4.4 序列号回绕处理:16位不够用怎么办?
RTP序列号只有16位,最大值65535。如果发送端持续发包,序列号从65535加1后会变成0,这就是回绕(Wrap Around)。
回绕带来的问题很直接:接收端无法区分序列号0是新的包还是旧的包。比如期望序列号是65535,突然收到一个序列号0的包——它是回绕后的新包,还是网络延迟到达的旧包?
处理回绕的常见方法:
| 方法 | 原理 | 优缺点 |
|---|---|---|
| 扩展序列号 | 在接收端维护一个高位计数器,将16位序列号扩展为32位或48位 | 实现简单,但需要处理高位同步问题 |
| 时间戳辅助判断 | 结合RTP时间戳判断包的新旧 | 时间戳也可能回绕,需要联合处理 |
| 滑动窗口 | 维护一个序列号窗口,窗口内的包视为有效,窗口外的丢弃 | 窗口大小需要合理设置,太小会误判 |
我个人最推荐扩展序列号的方法。具体做法是:
// 接收端维护扩展序列号
uint32_t extended_seq = 0;
uint16_t last_seq = 0;
void on_receive_packet(uint16_t seq) {
// 检测回绕:当前序列号小于上一个序列号,且差值较大
if (seq < last_seq && (last_seq - seq) > 16384) {
// 发生了回绕,高位+1
extended_seq += 65536;
}
last_seq = seq;
uint32_t real_seq = extended_seq + seq;
// 用real_seq进行丢包检测和乱序重排
}
这里有个细节:为什么判断条件是差值大于16384(即65536/4)?因为网络乱序可能导致序列号小幅下降,比如序列号1000的包比999先到,差值只有1。只有差值超过半个序列号空间时,才判定为回绕。
避坑指南:我曾经在项目中直接用seq < last_seq判断回绕,结果网络抖动时频繁误触发。后来改成差值大于16384才判定回绕,问题就解决了。记住:回绕检测要容忍一定程度的乱序。
4.5 序列号机制整体流程
下面这张图展示了从发送端到接收端的完整序列号处理流程:
4.6 序列号与时间戳的配合
最后提一个容易混淆的点:序列号和时间戳的关系。
序列号和时间戳是独立递增的。一个RTP包可能包含多个音频采样或一个完整的视频帧,所以序列号加1时,时间戳可能增加多个单位(音频)或保持不变(视频帧分片)。
举个例子:
| 包序号 | 序列号 | 时间戳 | 说明 |
|---|---|---|---|
| 1 | 100 | 1600 | 第一个音频包,包含160个采样 |
| 2 | 101 | 1760 | 第二个音频包,时间戳+160 |
| 3 | 102 | 1920 | 第三个音频包,时间戳+160 |
如果网络丢包导致序列号101的包丢失,接收端检测到间隙后,会等待一段时间。如果超时未收到,就跳过这个包,用序列号102的包继续播放。这时候时间戳会出现跳跃,播放器需要做丢包隐藏(Packet Loss Concealment)来填补这段空白。
经验之谈:序列号和时间戳的配合是音画同步的基础。我建议你在实现接收端时,把序列号用于包序管理,时间戳用于播放调度,各司其职,不要混用。