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 变量,表示下一个期望收到的序列号。每次收到包:

  1. 如果 seq == expected_seq,一切正常,expected_seq 加 1。
  2. 如果 seq > expected_seq,说明中间有包丢了。丢包数 = seq - expected_seq。
  3. 如果 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 序列号机制整体流程

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

RTP序列号机制流程图 RTP序列号机制流程图 发送方 序列号生成 (0-65535) 网络传输 接收方 回绕检测 扩展序列号 (32位) 丢包检测 包排序 / 去重 说明 序列号从发送方生成,经过网络传输到接收方。接收方先检测回绕,生成32位扩展序列号, 然后基于扩展序列号进行丢包检测、包排序和去重。

4.5 总结

序列号是 RTP 协议里最基础但也最容易出错的字段。16 位的范围注定了回绕是常态,不是异常。接收方必须正确处理回绕,否则排序和丢包检测都会出错。

我个人建议:

  • 始终使用 32 位扩展序列号做内部处理。
  • 丢包检测时,要区分乱序包和真正的丢包。
  • 回绕阈值不要设得太小,30000 左右比较安全。

嗯,序列号这部分就讲到这里。记住一句话:序列号是 RTP 的命根子,搞懂了它,RTP 协议你就掌握了一半。