5. 网络抗丢包技术:NACK与RTX重传机制、FEC前向纠错、带宽估计与拥塞控制(GCC、BBR)
各位好,欢迎来到第五章。
这一章,我们聊聊WebRTC里最硬核的部分——网络抗丢包。
你想想看,实时通信最大的敌人是谁?不是编码器不够快,不是摄像头像素低,而是网络。丢包、抖动、带宽波动,任何一个都能让你的视频卡成PPT。我做了这么多年实时通信,见过太多项目死在网络适应性上。
WebRTC之所以能在公网上跑起来,靠的就是一套组合拳:NACK + RTX、FEC、GCC/BBR。这三者缺一不可。今天我就把这套拳法拆开,一招一招讲清楚。
5.1 NACK与RTX重传机制
先说NACK。全称是Negative Acknowledgment,说白了就是接收端告诉发送端:“我丢包了,快补发!”
WebRTC里,接收端会持续检测RTP包的序列号。如果发现序列号不连续,比如收到了10、11、13,那12号包大概率丢了。这时候接收端会发一个RTCP NACK包回去,里面带上丢失的序列号。
发送端收到NACK后,会从历史缓冲区里找到对应的包,重新发送。这个重传的包,走的是RTX通道。
核心要点:RTX重传使用独立的SSRC和payload type。这样做的好处是,接收端可以区分原始包和重传包,方便做去重和统计。
我在项目中遇到过一个问题:NACK风暴。当网络突然恶化时,接收端会疯狂发NACK,发送端拼命重传,结果带宽全被重传占满,正常数据反而过不去。嗯,这里要注意——NACK不是无限制的。
WebRTC里有个参数叫max_nack_count,默认限制每个包最多重传3次。超过次数就直接放弃。另外,重传也有超时时间,一般设为200ms。如果200ms内没收到重传,接收端就认为这个包彻底丢了,直接跳过。
// 伪代码:NACK处理逻辑
void OnNackReceived(uint16_t seq_num) {
if (retransmit_count[seq_num] >= 3) {
// 放弃重传
return;
}
if (now - first_nack_time[seq_num] > 200) {
// 超时,放弃
return;
}
RetransmitPacket(seq_num);
retransmit_count[seq_num]++;
}
避坑指南:我曾经在弱网环境下忘记限制重传次数,结果带宽被重传占满,正常视频帧反而过不去。后来加了重传次数限制和超时机制,问题才解决。
5.2 FEC前向纠错
NACK有个致命问题——需要往返时间。如果RTT是100ms,那重传一次就要100ms。对于实时通信来说,这个延迟可能已经不可接受了。
这时候就需要FEC(Forward Error Correction)上场了。
FEC的思路很简单:发送端在发送原始数据的同时,额外发送一些冗余包。接收端即使丢了一部分包,也能通过冗余包恢复出原始数据。整个过程不需要等待重传,延迟极低。
WebRTC里最常用的是ULP FEC(Uneven Level Protection)。它支持对不同重要性的数据包提供不同级别的保护。比如,关键帧的保护级别可以设高一些,非关键帧可以低一些。
| 保护级别 | 冗余比例 | 适用场景 |
|---|---|---|
| 低 | 10% | P帧、B帧 |
| 中 | 20% | 普通I帧 |
| 高 | 50% | 关键I帧、音频 |
但FEC不是万能的。冗余包本身也会占用带宽。如果网络丢包率是50%,你加50%的冗余,那总带宽就翻倍了。说白了,FEC是用带宽换延迟。
注意:FEC和NACK不是互斥的。实际项目中,两者通常配合使用。FEC处理小规模丢包(比如5%以内),NACK处理大规模丢包。我习惯的做法是:丢包率低于10%时只用FEC,超过10%时启用NACK辅助。
5.3 带宽估计与拥塞控制(GCC、BBR)
有了NACK和FEC,我们解决了丢包问题。但还有一个更根本的问题没解决——带宽。
你发送的数据量超过了网络容量,丢包是必然的。所以,真正的抗丢包,是从源头控制发送速率。
WebRTC里有两个主流的拥塞控制算法:GCC(Google Congestion Control)和BBR(Bottleneck Bandwidth and Round-trip propagation time)。
5.3.1 GCC
GCC是WebRTC的默认算法。它基于延迟梯度来检测拥塞。
原理是这样的:发送端每隔一段时间发送一组探测包,接收端测量这些包的到达时间间隔。如果间隔变大,说明网络开始拥塞了。GCC会据此降低发送速率。
// GCC核心逻辑(简化)
void GccRateController::UpdateRate(double delay_gradient) {
if (delay_gradient > threshold) {
// 拥塞,降低速率
bitrate_ *= 0.85;
} else {
// 空闲,增加速率
bitrate_ *= 1.08;
}
}
GCC的优点是响应快,对突发拥塞很敏感。但缺点也很明显——容易误判。比如,WiFi信号波动会导致延迟抖动,GCC可能误以为是拥塞,从而不必要地降低码率。
个人经验:我在做移动端项目时,发现GCC在4G网络下表现不稳定。后来加了自适应阈值,根据网络波动动态调整判断门限,效果好了很多。
5.3.2 BBR
BBR是Google开发的另一个拥塞控制算法,最初用在TCP上。WebRTC后来也引入了BBR。
BBR的思路和GCC完全不同。它不依赖丢包或延迟,而是直接测量瓶颈带宽和最小RTT。
BBR会周期性地发送探测包,测量网络的实际吞吐量。然后根据测量结果,动态调整发送速率。它的核心公式是:
发送速率 = 瓶颈带宽 × 1.25(探测阶段)
发送速率 = 瓶颈带宽 × 0.75(收敛阶段)
BBR的优势是公平性好,不会因为延迟抖动而误判。但它的缺点是收敛慢,在网络条件剧烈变化时,可能需要几秒钟才能调整到位。
| 特性 | GCC | BBR |
|---|---|---|
| 检测依据 | 延迟梯度 | 瓶颈带宽 + RTT |
| 响应速度 | 快 | 慢 |
| 公平性 | 一般 | 好 |
| 适用场景 | 低延迟场景 | 高吞吐场景 |
5.4 三者如何配合?
讲到这里,你可能已经明白了:NACK、FEC、GCC/BBR,这三者不是孤立的。它们是一个完整的抗丢包体系。
我画了一张图,帮你理清它们的关系:
从图上你可以看到:
- FEC 是发送端主动加的冗余,接收端直接恢复丢包,不需要交互。
- NACK 是接收端发现丢包后,请求发送端重传,需要一次往返。
- GCC/BBR 是发送端根据网络反馈,动态调整发送速率,从源头减少丢包。
这三者形成了一个闭环:GCC/BBR控制速率 → 减少丢包 → FEC处理小丢包 → NACK处理大丢包 → 反馈给GCC/BBR调整速率。
总结:没有哪个技术是银弹。真正的抗丢包,是让这三者协同工作。我建议你在实际项目中,先调好GCC/BBR的参数,再根据丢包率动态调整FEC的冗余比例,最后用NACK兜底。这样,你的WebRTC应用才能在公网上稳定运行。
公众号:蓝海资料掘金营,微信deep3321