4. RTP 序列号管理:序列号的作用、乱序与丢包检测、序列号回绕处理
聊到 RTP,序列号(Sequence Number)绝对是个绕不开的核心角色。很多人刚接触时觉得它就是个简单的计数器,每发一个包加 1 嘛。但实际用起来,坑不少。
我个人习惯把序列号比作快递单号。你寄了一堆包裹,每个上面贴了唯一编号。收件人拿到后,一看编号就知道:哪个先发的、哪个后发的、中间有没有丢件、甚至包裹是不是绕路晚到了。RTP 的序列号干的就是这个活。
4.1 序列号到底在干什么?
序列号是 RTP 固定头部里的 16 位字段。发送端每发出一个 RTP 包,序列号就递增 1。初始值是随机的,不是从 0 开始。为什么?安全考虑。防止某些攻击者猜出序列号规律。
它的核心职责有三点:
- 包序重建:接收端根据序列号把乱序到达的包排回正确顺序。
- 丢包检测:发现序列号跳变,就知道中间有包丢了。
- 报文去重:同一个序列号出现两次,说明是重复包,直接丢弃。
关键认知:序列号只保证唯一性和递增性,不保证到达顺序。网络是乱序的,序列号就是那个「秩序维护者」。
4.2 乱序检测与处理
实际网络中,包乱序太常见了。尤其是走不同路径的包,晚发的可能先到。我在做视频会议系统时遇到过:发送端按顺序发了 1、2、3、4,接收端收到的顺序是 1、3、2、4。如果直接按到达顺序播放,画面就花了。
怎么判断乱序?接收端维护一个 expected_sequence_number,每收到一个包,比较当前序列号和期望值:
- 当前序列号 大于 期望值 → 可能是跳变(丢包),也可能是乱序。
- 当前序列号 小于 期望值 → 基本是乱序包(或者重复包)。
具体实现时,我建议用「滑动窗口」+「阈值判断」。举个例子:
// 伪代码:乱序判断逻辑
#define MAX_MISORDER 100 // 最大容忍乱序范围
int16_t diff = (int16_t)(seq - expected_seq);
if (diff > 0) {
// 序列号向前跳了,可能是丢包
if (diff > MAX_MISORDER) {
// 跳变太大,认为是序列号回绕或异常,重置期望值
expected_seq = seq;
} else {
// 标记丢包,把当前包放入乱序缓冲区等待
}
} else if (diff < 0) {
// 序列号回退,肯定是乱序包
// 放入乱序缓冲区,等待前面的包到齐
} else {
// diff == 0,重复包,丢弃
}
我的经验:乱序缓冲区不要设太大。我曾经设了 500 个包的缓冲区,结果内存暴涨,延迟也上去了。后来改成 50-100 个包,配合 50ms 超时,效果好了很多。
4.3 丢包检测:怎么知道包丢了?
丢包检测说白了就是看序列号有没有「跳号」。比如收到 1、2、4,3 没来,那就是丢了。但这里有个细节:你怎么确定 3 是真的丢了,而不是还在路上?
答案是:等一会儿。接收端会维护一个超时机制。如果某个序列号迟迟不到,超过阈值就判定为丢包。
常用的丢包检测算法:
- 基于期望值:收到包后,检查当前序列号与期望值的差值。差值大于 1,说明中间有包丢失。
- 基于位图(Bitmap):维护一个位图,记录最近 N 个包的到达状态。位图中出现空洞,就是丢包。
- 基于 RTCP 反馈:接收端通过 RTCP 的 RR(Receiver Report)告诉发送端丢包率。这是端到端的统计,适合做流控。
我在做直播系统时,丢包检测最头疼的是「假丢包」。网络抖动大,包晚到了几毫秒,就被判定为丢包,然后触发重传。结果重传包和原包一起到了,反而浪费带宽。后来我加了一个「延迟判定」机制:收到乱序包后,先等 20-30ms,再决定是否丢包。效果很明显。
注意:丢包检测的阈值不能太死。WiFi 环境下抖动大,阈值要放宽;有线网络可以收紧。我曾经在 WiFi 场景下用 10ms 阈值,结果丢包率虚高到 30%,后来改成 50ms 才正常。
4.4 序列号回绕:16 位不够用怎么办?
RTP 序列号只有 16 位,范围是 0 到 65535。超过 65535 就回绕到 0。你想想看,如果视频是 30fps,每秒发 30 个包,65535 个包能撑多久?大约 36 分钟。一场直播动辄几小时,回绕是必然的。
回绕带来的问题很直接:接收端分不清新包和旧包。比如序列号从 65535 回绕到 0,接收端看到 0,会以为这是最早的包,结果乱序缓冲区就炸了。
怎么处理?我常用的方法有两种:
| 方法 | 原理 | 优缺点 |
|---|---|---|
| 模比较法 | 用有符号 16 位整数比较,差值在 -32768 到 32767 之间,自动处理回绕 | 简单高效,但回绕周期内只能区分一半范围 |
| 扩展序列号法 | 在接收端维护一个 32 位或 64 位的扩展序列号,高位记录回绕次数 | 无范围限制,但需要额外逻辑判断回绕边界 |
我个人更推荐扩展序列号法。实现起来也不复杂:
// 扩展序列号:32 位,高 16 位记录回绕次数
uint32_t extended_seq = 0;
uint16_t last_seq = 0;
void on_receive_packet(uint16_t seq) {
// 检测回绕:当前序列号比上一次小很多,说明回绕了
if (seq < last_seq && (last_seq - seq) > 32768) {
// 回绕一次,高位加 1
extended_seq += 65536;
}
// 更新低位
extended_seq = (extended_seq & 0xFFFF0000) | seq;
last_seq = seq;
// 现在 extended_seq 是单调递增的 32 位值,随便比较
}
避坑指南:我曾经在回绕检测时用了简单的「seq < last_seq」判断,结果网络抖动导致乱序包被误判为回绕。后来加了阈值 32768,只有差值超过这个值才认为是回绕。这个阈值怎么来的?16 位有符号整型的一半,刚好能区分正常乱序和回绕。
4.5 序列号管理的整体流程
说了这么多,画张图把整个流程串起来。接收端从收到 RTP 包到最终交给解码器,序列号管理是中间的关键环节。
从图里能看出来,乱序检测和丢包检测是并行的。乱序包进缓冲区等排序,丢包则触发重传或记录。最后统一经过回绕处理,得到正确的扩展序列号,再交给解码器。这个流程我在多个项目中验证过,稳定可靠。
4.6 小结
序列号管理,说白了就是三件事:排好序、找丢包、防回绕。每件事都不难,但合在一起就容易出问题。我见过不少团队在回绕处理上翻车,导致视频卡死或花屏。嗯,这里要提醒一句:千万别把序列号当成简单的计数器,它背后承载的是整个 RTP 流的秩序。