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. 收到一个包,检查它的序列号
  2. 如果序列号等于期望值,直接提交,期望值+1
  3. 如果序列号大于期望值,放入缓冲区等待
  4. 如果序列号小于期望值,可能是重复包或超时包,直接丢弃
  5. 每次提交后,检查缓冲区中是否有连续的包可以提交

你想想看,如果没有重排机制,解码器收到乱序的帧数据,画面就会出现花屏、马赛克。尤其是视频编码中依赖帧间预测的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 序列号机制整体流程

下面这张图展示了从发送端到接收端的完整序列号处理流程:

RTP序列号处理流程 RTP序列号处理流程 发送端 序列号自增(+1) 网络传输 可能丢包 可能乱序 接收端 步骤1:提取序列号 步骤2:回绕检测 步骤3:丢包检测 步骤4:乱序重排 步骤5:提交给解码器 关键参数 • 序列号范围:0~65535 • 重排超时:20~100ms • 丢包阈值:连续3个间隙

4.6 序列号与时间戳的配合

最后提一个容易混淆的点:序列号和时间戳的关系。

序列号和时间戳是独立递增的。一个RTP包可能包含多个音频采样或一个完整的视频帧,所以序列号加1时,时间戳可能增加多个单位(音频)或保持不变(视频帧分片)。

举个例子:

包序号 序列号 时间戳 说明
1 100 1600 第一个音频包,包含160个采样
2 101 1760 第二个音频包,时间戳+160
3 102 1920 第三个音频包,时间戳+160

如果网络丢包导致序列号101的包丢失,接收端检测到间隙后,会等待一段时间。如果超时未收到,就跳过这个包,用序列号102的包继续播放。这时候时间戳会出现跳跃,播放器需要做丢包隐藏(Packet Loss Concealment)来填补这段空白。

经验之谈:序列号和时间戳的配合是音画同步的基础。我建议你在实现接收端时,把序列号用于包序管理,时间戳用于播放调度,各司其职,不要混用。