9、重传机制(NACK):NACK 的工作原理、RTCP NACK 包格式、选择性重传策略

说到 WebRTC 的 QoS,重传机制绝对是个绕不开的话题。我刚开始做实时通信那会儿,总觉得丢包嘛,重传一下不就完了?后来踩了坑才发现——怎么传、什么时候传、传哪些包,这里面的门道深着呢。

今天咱们就聊聊 NACK(Negative Acknowledgment),也就是否定确认。说白了,就是接收端告诉发送端:“嘿,我丢包了,赶紧补发一下!”

9.1 NACK 的工作原理

先讲个最简单的场景。你想想看,视频通话的时候,网络偶尔抖一下,丢几个包很正常。如果每个丢包都让发送端重传,那带宽压力就大了。但如果不重传,画面就会花掉、卡住。

NACK 的思路很直接:接收端发现丢包了,就发一个 NACK 报文回去,告诉发送端“把第 X 号包再发一次”

具体流程是这样的:

  1. 发送端按顺序发送 RTP 包,每个包都有一个唯一的序列号(Sequence Number)。
  2. 接收端收到 RTP 包后,检查序列号是否连续。
  3. 如果发现跳号了(比如收到了 100 和 102,但没收到 101),就判定 101 丢了。
  4. 接收端构造一个 RTCP NACK 包,里面填上丢失的序列号范围。
  5. 发送端收到 NACK 后,从发送缓冲区里找到对应的包,重新发送。

嗯,这里要注意:发送端不能无限期地保留所有已发送的包。缓冲区大小是有限的,太老的包会被丢弃。所以 NACK 重传有个时效性——如果丢包发生得太久,重传也没意义了。

核心要点:NACK 是接收端驱动的重传机制。它只在检测到丢包时才触发,不会像 ACK 那样每个包都确认。这种设计节省了带宽,特别适合实时通信这种对延迟敏感的场景。

9.2 RTCP NACK 包格式

NACK 报文走的是 RTCP 通道,不是 RTP。RTCP 本身是控制协议,专门用来传这些反馈信息的。我记得第一次抓包看 NACK 格式的时候,觉得 RFC 4585 写得挺绕,后来自己动手解析了一遍才彻底搞明白。

RTCP NACK 包属于 Payload-Specific Feedback Message,PT(Payload Type)固定为 206。它的结构是这样的:

字段 长度 说明
V(版本) 2 bits 固定为 2
P(填充) 1 bit 一般置 0
FMT(反馈消息类型) 5 bits NACK 的 FMT 值为 1
PT(负载类型) 8 bits 固定为 206
长度 16 bits 整个 RTCP 包的长度(以 32 位字为单位)
SSRC(发送者标识) 32 bits 发送这个 NACK 的接收端 SSRC
SSRC(媒体源标识) 32 bits 被反馈的媒体流的 SSRC
丢失序列号 16 bits 第一个丢失的 RTP 包序列号
位掩码(BLP) 16 bits 指示后续 16 个包中哪些也丢了

这个位掩码(BLP)设计得很巧妙。你想想看,如果每个丢包都单独发一个 NACK,那网络里全是小包,效率太低了。BLP 用 16 个 bit 来表示“从丢失序列号开始,后面 16 个包中哪些也丢了”。bit 为 1 表示丢了,0 表示收到了。

举个例子:

丢失序列号 = 100
BLP = 0b0000000000000101

解释:
- 包 100 丢了(由丢失序列号字段表示)
- 包 101 收到了(bit 0 = 0)
- 包 102 丢了(bit 1 = 1)
- 包 103 收到了(bit 2 = 0)
- 包 104 丢了(bit 3 = 1)
- 包 105~116 都收到了(bit 4~15 = 0)

所以这个 NACK 实际上请求重传:包 100、102、104

小技巧:一个 NACK 包最多可以报告 17 个丢包(1 个序列号 + 16 个位掩码)。如果丢包超过 17 个,就需要发多个 NACK 包。我在项目中遇到过极端情况——网络抖动时一次丢了 50 多个包,NACK 包发了一大串,反而加剧了网络拥塞。后来我们加了限速逻辑,NACK 发送频率不能超过 50ms 一次。

9.3 选择性重传策略

好,现在接收端发了 NACK,发送端收到了。是不是所有丢包都要重传?

不一定。你想想看,有些包即使重传了,也赶不上播放时间了。比如视频帧的 P 帧依赖前面的 I 帧,如果 I 帧丢了,后面的 P 帧重传也没用。这就是选择性重传要解决的问题。

常见的策略有这几种:

  • 全部重传:收到 NACK 就重传,不管三七二十一。实现简单,但浪费带宽。
  • 基于时间戳的重传:检查包的发送时间戳,如果已经超过某个阈值(比如 200ms),就不重传了。因为即使重传了,接收端也来不及解码播放。
  • 基于依赖关系的重传:检查包的编码依赖关系。比如 H.264 中,如果某个包属于 P 帧,而它依赖的 I 帧已经丢了,那这个 P 帧重传也没意义。
  • 基于优先级的重传:给不同类型的包分配优先级。关键帧(I 帧)的包优先重传,非关键帧的包可以放弃。

我在实际项目中用的是混合策略。简单来说:

  1. 先判断这个包是否还在发送缓冲区里。如果已经被清除了,直接跳过。
  2. 再判断这个包的发送时间是否在 150ms 以内。超过 150ms 的包,重传意义不大。
  3. 最后判断这个包是否属于关键帧。如果是关键帧的包,即使超过 150ms,也尝试重传一次。

避坑指南:我曾经在项目中犯过一个错误——没有限制 NACK 重传的次数。结果网络一抖动,发送端反复重传同一个包,接收端收到了重复包,解码器直接崩溃了。后来我们加了个限制:同一个包最多重传 3 次。超过 3 次就不再重传,让解码器自己处理丢包。

9.4 NACK 与 FEC 的配合

说到重传,就不得不提 FEC(前向纠错)。NACK 是后向纠错,FEC 是前向纠错。两者各有优劣:

特性 NACK FEC
延迟 高(需要一次 RTT) 低(无需等待)
带宽开销 低(只在丢包时发送) 高(始终发送冗余数据)
适用场景 低丢包率、低延迟要求 高丢包率、低延迟要求

我个人的习惯是:丢包率低于 5% 时,只用 NACK;丢包率在 5%~15% 时,NACK + FEC 混合使用;丢包率超过 15% 时,优先用 FEC,NACK 作为补充

为什么?因为丢包率高了以后,NACK 本身也会丢。你发出去的 NACK 包如果丢了,发送端根本不知道要重传。这时候 FEC 的冗余数据反而更可靠。

9.5 知识体系总览

最后,我用一张图来总结 NACK 的核心逻辑。这张图涵盖了 NACK 的触发条件、报文格式、重传策略以及与其他机制的配合关系。

NACK 重传机制知识体系 接收端 发送端 RTCP NACK 包 重传 RTP 包 接收端处理流程 1. 接收 RTP 包,检查序列号 2. 发现序列号不连续 3. 构造 NACK 包(序列号+BLP) 4. 通过 RTCP 通道发送 发送端处理流程 1. 收到 NACK 包 2. 解析丢失序列号+BLP 3. 从缓冲区查找对应包 4. 选择性重传决策 选择性重传策略 ✅ 基于时间戳:超过阈值(如 150ms)不重传 ✅ 基于依赖关系:关键帧优先 ✅ 基于优先级:I 帧 > P 帧 > B 帧 ✅ 重传次数限制:最多 3 次 配合 FEC 使用 低丢包率用 NACK 缓冲区管理 限制缓冲区大小

这张图把 NACK 的完整链路串起来了。从接收端检测丢包,到发送端选择性重传,再到与 FEC 的配合,每一步都有讲究。你想想看,如果没有这些策略,随便一个网络抖动就能让视频通话卡成幻灯片。

好了,NACK 的内容就聊到这里。下一节咱们聊聊 FEC 前向纠错,看看它是怎么在不需要重传的情况下修复丢包的。


公众号:蓝海资料掘金营,微信deep3321