5. 网络抗丢包技术:NACK与RTX重传机制、FEC前向纠错、带宽估计与拥塞控制(GCC、BBR)

各位好,欢迎来到第五章。

这一章,我们聊聊WebRTC里最硬核的部分——网络抗丢包

你想想看,实时通信最大的敌人是谁?不是编码器不够快,不是摄像头像素低,而是网络。丢包、抖动、带宽波动,任何一个都能让你的视频卡成PPT。我做了这么多年实时通信,见过太多项目死在网络适应性上。

WebRTC之所以能在公网上跑起来,靠的就是一套组合拳:NACK + RTXFECGCC/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,这三者不是孤立的。它们是一个完整的抗丢包体系

我画了一张图,帮你理清它们的关系:

WebRTC 网络抗丢包体系 发送端 接收端 网络(丢包、抖动) NACK(请求重传) FEC 冗余包 GCC / BBR 带宽估计 → 控制发送速率

从图上你可以看到:

  • FEC 是发送端主动加的冗余,接收端直接恢复丢包,不需要交互。
  • NACK 是接收端发现丢包后,请求发送端重传,需要一次往返。
  • GCC/BBR 是发送端根据网络反馈,动态调整发送速率,从源头减少丢包。

这三者形成了一个闭环:GCC/BBR控制速率 → 减少丢包 → FEC处理小丢包 → NACK处理大丢包 → 反馈给GCC/BBR调整速率。

总结:没有哪个技术是银弹。真正的抗丢包,是让这三者协同工作。我建议你在实际项目中,先调好GCC/BBR的参数,再根据丢包率动态调整FEC的冗余比例,最后用NACK兜底。这样,你的WebRTC应用才能在公网上稳定运行。


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