1. 流控基础回顾:为什么需要流控?
做流媒体传输这么多年,我见过太多「卡成幻灯片」的现场了。有一次给客户做直播保障,码率飙到 8Mbps,用户端 Wi-Fi 却只有 2Mbps 的吞吐。结果呢?缓冲区爆了,画面直接冻住,用户疯狂投诉。
这背后的问题,说白了就是:发送方不知道接收方到底能吞下多少数据。流控(Flow Control)就是来解决这个问题的——它让发送方根据接收方的处理能力,动态调整发送速率。
核心观点:流控不是「能发多快发多快」,而是「接收方说多少,我就发多少」。没有流控,网络就像没有红绿灯的十字路口——早晚撞车。
1.1 为什么需要流控?三个真实场景
我习惯把流控的必要性拆成三个层面来看:
- 接收方处理不过来:你发 100 个包,接收方应用层一次只能读 10 个。剩下的 90 个堆在缓冲区里,堆满了就只能丢包。丢包意味着重传,重传意味着延迟飙升。
- 内存资源有限:嵌入式设备、手机、机顶盒,接收缓冲区可能只有几十 KB。你一股脑发几 MB 数据过去,人家直接 OOM 给你看。
- 避免网络拥塞恶化:流控虽然主要管端到端,但发送方无节制地发数据,也会加剧中间路由器的队列堆积。你想想看,一个路由器队列满了,丢的可是所有人的包。
我的经验:有一次做 P2P 直播,发送端没做流控,接收端缓冲区设得又小。结果接收方频繁丢包,发送方误以为是网络拥塞,触发拥塞控制降速——两个控制逻辑打架,吞吐直接腰斩。后来加了流控,问题立刻解决。
1.2 TCP 滑动窗口原理
TCP 的流控机制,核心就是滑动窗口。它像一个可移动的「发送许可」窗口,窗口大小决定了发送方能连续发多少数据而不必等待确认。
我画了一张图,帮你理解滑动窗口的工作方式:
滑动窗口的核心逻辑其实就三点:
- 窗口左边界:已经发送并收到确认的数据,左边界向右滑动。
- 窗口右边界:由接收方通告的窗口大小决定,左边界 + 窗口大小 = 右边界。
- 窗口内的数据:可以连续发送,不需要等每个包的 ACK。
注意:我曾经遇到过一个坑——接收方窗口通告为 0,发送方还在不断重传。后来查 RFC 才知道,窗口为 0 时发送方必须停止发送,只发窗口探测(Window Probe)来询问接收方是否恢复了。不遵守这个规则,会导致大量无意义重传。
1.3 接收窗口与发送窗口
这两个概念经常被混淆。我习惯这样区分:
| 概念 | 位置 | 含义 | 谁决定 |
|---|---|---|---|
| 接收窗口(rwnd) | 接收端 | 接收缓冲区剩余可用空间 | 接收端应用层读取速度 |
| 发送窗口(swnd) | 发送端 | 发送端实际能发送的数据量上限 | min(rwnd, 拥塞窗口) |
接收窗口(rwnd)是接收端通过 TCP 头部的 Window Size 字段告诉发送端的。发送端收到后,把自己的发送窗口(swnd)设为 min(rwnd, cwnd)。cwnd 是拥塞窗口,我们后面章节会细讲。
举个例子:
- 接收端说「我还有 64KB 空间」(rwnd = 64KB)
- 拥塞控制说「现在网络状况一般,最多发 32KB」(cwnd = 32KB)
- 发送窗口 = min(64KB, 32KB) = 32KB
说白了,接收窗口管的是「你能不能收」,拥塞窗口管的是「网络能不能传」。两个窗口取最小值,才是真正的发送上限。
避坑指南:我曾经调试过一个流媒体服务器,发现发送窗口一直很小,但接收端明明有足够缓冲区。后来抓包一看,接收端通告的窗口大小是 0。为什么?因为接收端应用层读得太慢,缓冲区被占满了。解决方案不是调大缓冲区,而是优化应用层的读取逻辑——把数据尽快从内核缓冲区搬到用户空间。
1.4 窗口更新与零窗口处理
接收窗口不是一成不变的。接收端每次发送 ACK 时,都会带上当前的窗口大小。如果应用层读得快,窗口变大;读得慢,窗口变小。
这里有个经典问题:零窗口死锁。
场景是这样的:
- 接收端通告 rwnd = 0,发送端停止发送。
- 接收端应用层读取数据,缓冲区有空闲,发送窗口更新(rwnd > 0)。
- 但这个窗口更新 ACK 在网络上丢了。
- 发送端一直等窗口更新,接收端一直等数据——死锁。
TCP 的解决方案是:发送端在收到零窗口后,定期发送 窗口探测(Window Probe) 报文。接收端收到探测后,即使窗口还是 0,也必须回复当前窗口大小。这样就能打破死锁。
关键参数:窗口探测的间隔是递增的——第一次 1.5 秒,第二次 3 秒,第三次 6 秒……最大不超过 60 秒。这个指数退避策略,和拥塞控制的退避思路如出一辙。
1.5 流控在流媒体中的特殊挑战
做流媒体和做文件传输不一样。文件传输可以等,流媒体不行——你等 5 秒再发下一帧,用户早就换台了。
我总结了几点流控在流媒体场景下的特殊性:
- 延迟敏感:流控不能只考虑吞吐,还要考虑延迟。窗口太大,缓冲区堆积,延迟飙升。
- 码率波动:视频编码器的输出码率是波动的(I 帧大,P/B 帧小)。流控需要配合编码器做码率平滑。
- 接收端多样性:手机、电视、机顶盒的缓冲区大小差异巨大。一套流控参数打天下?不现实。
我的教训:有一次做低延迟直播,我把接收窗口设得很大,想着「缓冲区大一点更安全」。结果延迟从 2 秒飙到 15 秒。为什么?因为发送端看到窗口大,一口气发了大量数据,接收端缓冲区被填满,后续数据只能排队。延迟就是这么堆出来的。后来我把窗口控制在 2-3 个 GOP 的大小,延迟才降下来。
好了,流控的基础就回顾到这里。滑动窗口、接收窗口、发送窗口——这三个概念是后续所有流控调试的基石。你想想看,如果连「谁在控制谁」都搞不清楚,后面调拥塞控制、自适应码率,只会越调越乱。