9、重传机制(NACK):NACK 的工作原理、RTCP NACK 包格式、选择性重传策略
说到 WebRTC 的 QoS,重传机制绝对是个绕不开的话题。我刚开始做实时通信那会儿,总觉得丢包嘛,重传一下不就完了?后来踩了坑才发现——怎么传、什么时候传、传哪些包,这里面的门道深着呢。
今天咱们就聊聊 NACK(Negative Acknowledgment),也就是否定确认。说白了,就是接收端告诉发送端:“嘿,我丢包了,赶紧补发一下!”
9.1 NACK 的工作原理
先讲个最简单的场景。你想想看,视频通话的时候,网络偶尔抖一下,丢几个包很正常。如果每个丢包都让发送端重传,那带宽压力就大了。但如果不重传,画面就会花掉、卡住。
NACK 的思路很直接:接收端发现丢包了,就发一个 NACK 报文回去,告诉发送端“把第 X 号包再发一次”。
具体流程是这样的:
- 发送端按顺序发送 RTP 包,每个包都有一个唯一的序列号(Sequence Number)。
- 接收端收到 RTP 包后,检查序列号是否连续。
- 如果发现跳号了(比如收到了 100 和 102,但没收到 101),就判定 101 丢了。
- 接收端构造一个 RTCP NACK 包,里面填上丢失的序列号范围。
- 发送端收到 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 帧)的包优先重传,非关键帧的包可以放弃。
我在实际项目中用的是混合策略。简单来说:
- 先判断这个包是否还在发送缓冲区里。如果已经被清除了,直接跳过。
- 再判断这个包的发送时间是否在 150ms 以内。超过 150ms 的包,重传意义不大。
- 最后判断这个包是否属于关键帧。如果是关键帧的包,即使超过 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 的完整链路串起来了。从接收端检测丢包,到发送端选择性重传,再到与 FEC 的配合,每一步都有讲究。你想想看,如果没有这些策略,随便一个网络抖动就能让视频通话卡成幻灯片。
好了,NACK 的内容就聊到这里。下一节咱们聊聊 FEC 前向纠错,看看它是怎么在不需要重传的情况下修复丢包的。
公众号:蓝海资料掘金营,微信deep3321