8、基于丢包的拥塞控制(Loss-based):丢包率计算、RTCP RR报文解析、丢包阈值设计、与延迟控制器的协作

聊到基于丢包的拥塞控制,我得先说说我的一个心路历程。刚接触WebRTC那会儿,我总觉得丢包检测这事儿挺简单的——数数包没了不就行了?后来在项目中踩了坑才发现,这里面的门道比想象中深得多。你想想看,网络环境那么复杂,偶尔丢一两个包到底是拥塞造成的,还是单纯的线路抖动?这问题要是搞不清楚,带宽估计就会像过山车一样忽高忽低。

这一章,我们就来把基于丢包的拥塞控制彻底拆开揉碎。我会从最底层的RTCP RR报文讲起,再到丢包率的精确计算、阈值设计的艺术,最后聊聊它怎么跟延迟控制器配合工作。嗯,这些都是我在实际项目中反复调优过的经验,希望能帮你少走弯路。

8.1 RTCP RR报文:拥塞控制的数据基石

基于丢包的拥塞控制,第一步就是拿到准确的丢包数据。这个数据从哪来?RTCP RR报文。

RTCP RR,全称是Receiver Report。接收端每隔一段时间就会给发送端发一个RR报文,里面装着接收质量的关键指标。我个人习惯把RR报文看作是网络的「体检报告」——它告诉你网络当前的健康状况。

RR报文的核心字段如下:

字段 长度 说明
SSRC of sender 32 bit 发送端的同步源标识
SSRC of source 32 bit 接收端报告的源标识
Fraction lost 8 bit 上一个报告周期内的丢包率(小数部分)
Cumulative lost 24 bit 从开始到现在的累计丢包数
Extended highest sequence number 32 bit 收到的最高序列号(含扩展位)
Interarrival jitter 32 bit 到达时间抖动
Last SR timestamp 32 bit 最近一次发送SR的时间戳
Delay since last SR 32 bit 从上次SR到现在的延迟

这里最关键的字段就是 Fraction lostCumulative lost。Fraction lost 是8位定点小数,取值范围0到255,对应丢包率0%到100%。比如值是25,就代表丢包率大约是9.8%(25/255)。

我在项目中遇到过一个问题:有些接收端实现的RR报文里,Fraction lost字段总是0,但Cumulative lost却在增长。后来排查发现,是接收端把Fraction lost的计算周期搞错了。所以我的建议是:不要完全信任Fraction lost,最好自己根据Cumulative lost和序列号差值重新算一遍丢包率

8.2 丢包率计算:从原始数据到可用指标

拿到RR报文后,怎么算出真正可用的丢包率?这里有几个关键点。

第一,计算周期要对齐。 RR报文不是每个包都发,而是每隔一段时间发一次。WebRTC中默认的RTCP间隔是5秒。你算丢包率的时候,必须用当前RR和上一个RR之间的差值,而不是从0开始算。

计算公式很简单:

// 伪代码示例
uint32_t lost_since_last = current_cumulative_lost - last_cumulative_lost;
uint32_t expected_since_last = (current_extended_max_seq - last_extended_max_seq);
float loss_rate = (float)lost_since_last / (float)expected_since_last;

第二,序列号回绕问题。 RTP序列号是16位的,范围0到65535。超过65535后会回绕到0。如果你不做处理,算出来的丢包率会变成负数或者巨大的正数。

我曾经在一个跨国项目中遇到过这个问题。当时视频通话突然卡顿,查了半天发现是序列号回绕导致丢包率计算错误,触发了带宽骤降。解决方案是使用扩展序列号——在16位序列号前面加一个扩展位,变成32位,这样就不会回绕了。

核心要点: 丢包率计算必须基于扩展序列号,并且使用差分方式计算,避免累积误差和回绕问题。

第三,平滑处理。 单次计算的丢包率抖动很大,直接用它来做决策会导致带宽频繁变化。我一般会用一个指数移动平均(EMA)来做平滑:

// 平滑丢包率计算
const float kAlpha = 0.1f;  // 平滑因子,值越小越平滑
smoothed_loss_rate = kAlpha * current_loss_rate + (1 - kAlpha) * smoothed_loss_rate;

平滑因子kAlpha怎么选?我个人的经验是:网络稳定的环境下用0.05,网络抖动大的环境下用0.15。前者更平滑但反应慢,后者反应快但容易误判。没有完美的值,只有适合你场景的值。

8.3 丢包阈值设计:什么时候该降速?

丢包率算出来了,接下来就是决策问题:丢包率到多少的时候,我应该降低发送速率?

WebRTC中常用的阈值设计是这样的:

丢包率范围 行为 说明
0% - 2% 保持当前速率 正常网络波动,无需处理
2% - 10% 适度降低速率 按比例降速,通常降5%-10%
10% - 20% 大幅降低速率 降速20%-30%,进入保守模式
> 20% 紧急降速 降速50%以上,甚至暂停发送

这些阈值不是拍脑袋定的。我记得在Google的WebRTC源码中,默认的丢包阈值是2%和10%。为什么是2%?因为实验表明,在大多数有线网络中,正常情况下的丢包率都低于1%。2%已经是一个明显的拥塞信号了。

但这里有个坑:无线网络(Wi-Fi、4G/5G)的丢包率本身就比较高,经常在1%-3%之间波动。如果你用2%作为阈值,在无线网络下就会频繁触发降速,导致用户体验很差。

避坑指南: 我曾经在一个移动端项目中,用户反馈在4G网络下视频经常卡顿。排查后发现是丢包阈值设置得太低,导致频繁降速。后来我们把阈值从2%调整到5%,并增加了延迟判断——只有丢包率超过5%且延迟也升高时,才触发降速。效果好了很多。

所以我的建议是:不要死守固定阈值。可以根据网络类型动态调整阈值。比如检测到是Wi-Fi网络,用2%阈值;检测到是蜂窝网络,用5%阈值。或者更高级的做法:根据历史数据自适应调整阈值。

8.4 与延迟控制器的协作:双剑合璧

基于丢包的拥塞控制有个天然缺陷:它反应太慢了。等你检测到丢包的时候,网络其实已经拥塞了一段时间了。这就像你开车看到前方堵车了才踩刹车,已经晚了。

所以WebRTC中,基于丢包的控制器通常和基于延迟的控制器(比如GCC中的延迟控制器)配合使用。它们的关系是这样的:

基于丢包与基于延迟的拥塞控制协作流程 网络数据包 接收端统计模块 基于延迟的控制器 (延迟梯度、抖动检测) 基于丢包的控制器 (丢包率、RTCP RR) 带宽估算器 最终带宽估计值 延迟控制器优先响应,丢包控制器作为后备

从上面的流程图可以看出,两个控制器是并行工作的。但它们的优先级不同:

  • 延迟控制器优先:延迟变化能更快地反映网络状态,所以它先做决策。
  • 丢包控制器作为后备:当延迟控制器无法判断时(比如网络突然拥塞导致大量丢包),丢包控制器介入。

具体协作逻辑是这样的:

  1. 延迟控制器持续监控延迟梯度。如果延迟持续上升,说明网络开始拥塞,它会先降低发送速率。
  2. 如果延迟控制器已经降速了,但丢包率仍然在上升,说明拥塞很严重,丢包控制器会进一步降速。
  3. 如果延迟控制器判断网络恢复正常(延迟下降),它会尝试提升速率。但丢包控制器会检查丢包率是否也下降了——如果丢包率还很高,它会阻止速率提升。

说白了,延迟控制器负责「快反应」,丢包控制器负责「稳判断」。两者配合,才能既快速响应网络变化,又避免误判。

个人经验: 我在调优这两个控制器的协作时,发现一个关键参数——两个控制器输出带宽估计值的「融合权重」。延迟控制器的权重应该高一些(比如0.7),丢包控制器的权重低一些(0.3)。这样既能保证快速响应,又能防止丢包抖动带来的误调整。

8.5 实际项目中的避坑指南

最后,分享几个我在实际项目中踩过的坑,希望能帮你避开:

  • 坑一:RTCP RR报文延迟。RR报文不是实时发送的,它有自己的发送间隔。如果你在RR报文到达之前就做决策,可能会基于过时的数据。我的做法是:缓存最近两次RR报文,只有当新的RR到达时才更新丢包率。
  • 坑二:丢包率与重传包的混淆。有些场景下,接收端会请求重传丢失的包。重传成功后,丢包率应该下降。但如果你计算丢包率时没有排除重传包,就会导致丢包率虚高。我建议在计算丢包率时,只统计原始发送的包,不统计重传包。
  • 坑三:阈值滞后设计。我曾经发现,当丢包率在阈值附近来回波动时,带宽会频繁变化。解决方案是引入滞后(hysteresis):比如降速阈值是2%,但恢复阈值设为1.5%。这样丢包率在1.5%-2%之间波动时,不会触发频繁调整。

嗯,关于基于丢包的拥塞控制,核心内容就是这些了。记住一句话:丢包是结果,不是原因。真正要解决的是导致丢包的拥塞问题,而不是单纯地应对丢包本身。理解了这一点,你就能更好地设计你的拥塞控制算法了。


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