简介:高级计算机网络课程第7章《拥塞控制与流量控制》配套PPT,面向网络工程、通信技术方向学习者,系统讲解网络拥塞的成因、开环/闭环控制原理以及TCP慢启动、拥塞避免、快速重传、快速恢复等核心算法。课件共80页,单文件PPTX格式,压缩包约1.02MB,便于直接下载后按章节精读或课堂演示。内容从缓冲区预分配、分组丢弃、定额控制到随机早期检测(RED),并结合直接死锁、重装死锁等典型实例,详细剖析流量控制与拥塞控制的区别和联系。同时涵盖TCP友好拥塞控制算法等进阶内容,帮助读者理解如何维持网络吞吐量、避免拥塞崩溃。已有96人学习,适合作为计算机网络课程教学辅助、期末复习或考研备考材料。

1. 拥塞控制与流量控制:80页课件只讲两件事,为什么总有人把它们混成一件事

我接触过不少准备计算机网络期末复习和考研 408 的读者,拿着这份 80 页的《高级计算机网络课程》第 7 章课件来问同一个问题:拥塞控制和流量控制到底差在哪。这两个名词长得太像了,一个管“发太快”,一个管“路太堵”,但实际作用链路上完全不是一个位置。流量控制解决的是收发两端速率不匹配,接收方吃不下就去通告窗口缩水;拥塞控制解决的是中间网络承载不了,路由器缓冲区溢出导致丢包。这份 80 页的 PPT 拆开看,约 20 页在讲滑动窗口与流量控制,40 页在讲拥塞控制的四个算法和曲线,剩下的是对比、例题和习题。把这两条线分清楚,课件里一半的内容你已经吃透了,真正的难点反而是 TCP 发送窗口同时受两个窗口约束这件事。适合读这篇文章的人有三类:正在复习谢希仁《计算机网络》第八版的期末考生、备考 408 的选择题选手、以及工作中要用抓包工具排查 TCP 慢问题的工程师。

2. 流量控制:滑动窗口不是越大越快,rwnd 才是端到端的硬约束

2.1 流量控制在管什么:从发送缓冲到接收缓冲的一段路

流量控制是 TCP 端到端的机制,目的只有一个:防止发送方把接收方的缓冲区打爆。回想 TCP 连接建立后,收发双方各有一块内存作为接收缓冲区,应用进程从缓冲区取数据的速度往往跟不上网络到达速度。如果发送方不管不顾地连续发,接收缓冲区最终会被写满,后续到达的报文只能丢弃。TCP 解决这个问题的办法是在报文段头部携带一个 16 位的窗口字段,接收方用它通告自己当前还能接收多少字节。发送方看到这个值后,把未确认的数据总量限制在这个范围内,这就是流量控制的全部逻辑。

很多课程在讲这一章时都会画那张经典的状态图,把发送窗口、接收窗口、可用窗口三者画在一起。发送窗口的上限是接收方通告的 rwnd,但实际发送窗口还要再取一个最小值,即 min(cwnd, rwnd),其中 cwnd 是拥塞窗口。聊天软件和网页这种短连接场景里,瓶颈往往是 rwnd;而长时间大文件传输时,瓶颈经常是 cwnd。这个 min 关系一定要记牢,后面做抓包分析时,窗口算错一步就会得出完全相反的结论。

可用窗口的计算公式是:可用窗口 = min(cwnd, rwnd) - 已发送未确认的数据量。TCP 发送方真正能继续发多少新数据,取决于这个差值,而不是简单地看 rwnd 还剩多少。课件里如果画了发送窗口和可用窗口两张状态图,对照着看会很清楚。

2.2 用 Wireshark 的命令行工具看 rwnd:一个可以直接跑的抓包命令

图形界面的 Wireshark 人人都能打开,但命令行工具 tshark 更适合在服务器上快速分析一份 pcap 文件。tshark 和 Wireshark 装的是同一个解析引擎,字段名完全一致。我一般这样提取一条 TCP 流的窗口相关字段:

# 从 tcp.pcap 中提取第一条 TCP 流的窗口通告变化
# -Y 是显示过滤器,-T fields 指定输出哪些列
tshark -r tcp.pcap -Y "tcp.stream eq 0" -T fields \
  -e frame.number \
  -e tcp.srcport \
  -e tcp.dstport \
  -e tcp.window_size_value \
  -e tcp.window_size_scalefactor \
  -e tcp.len \
  -e tcp.flags.ack

这段命令里, tcp.stream eq 0 把范围限制在第一条 TCP 流,避免多个连接混在一起; tcp.window_size_value 是报文里 16 位窗口字段的原始数值,单位是字节; tcp.window_size_scalefactor 是三次握手时协商的窗口缩放因子,只有握手阶段会出现这个字段; tcp.len 表示当前报文承载的 payload 长度,不是整包长度。输出结果里每一行代表一个报文,把 window_size_value 和 scalefactor 乘起来,才是接收方真实的可用窗口字节数。

一个很容易看错的地方是:TCP 报文里的窗口字段只在 ACK 报文中才有意义,纯数据报文里的窗口通告是重复上次的值,甚至可能因为延迟 ACK 而暂时不更新。如果你想确认接收方是不是快满了,重点看窗口缩小的那几行 ACK,以及缩小前接收方接受了多少数据。如果抓到的 pcap 里窗口字段从 65535 一下跌到 3000 多,说明接收方应用层消费太慢,接收缓冲区已经快被写满了。遇到这种场景,先去排查应用进程为什么读不出去,而不是怀疑网络丢包。

2.3 糊涂窗口综合症:小包堆积的典型坑

流量控制里最反直觉的一个问题叫糊涂窗口综合症。现象是:接收方的缓冲区只剩一点点空间,它通告了一个很小的窗口,比如几个字节;发送方看到窗口非零就立刻发一个小报文填满它;接收方读完一个字节后又只能再通告一个小窗口。一来一回,整个链路上全是几十字节的小包,吞吐量急剧下降。

产生这个现象有两条路径,一条在发送端,一条在接收端。发送端的问题是没有攒够数据就急着发,解决算法是 Nagle 算法:当已发送数据还没被确认,且当前要发的数据小于一个 MSS 时,先把数据攒在发送缓冲区里,等前面数据的 ACK 回来或者积够一个 MSS 再发。接收端的问题则是通告了过小的窗口,解决方法是延迟 ACK 和窗口扩大策略。延迟 ACK 的意思是接收方不每收到一个报文就立刻回 ACK,而是等最多 500 毫秒,把多个 ACK 合并成一个;接收方通告窗口时,如果剩余空间不足一个 MSS 或不足缓冲区总大小的一半,就不扩大通告窗口。

实际工程里,Nagle 算法和延迟 ACK 各自工作都正常,两者叠加却会出问题。发送端等 ACK,接收端等数据,双方各等各的,交互式应用会明显感觉到延迟。做嵌入式 TCP 开发时经常碰到这类问题,比如 STM32 上跑一个小型 TCP/IP 协议栈,触摸屏交互指令总是感觉慢半拍。排查思路是先确认是不是 Nagle 在攒包,再确认对端是不是做了延迟 ACK。通用解法是在 socket 上设置 TCP_NODELAY 直接禁用 Nagle 算法,代价是可能增加小包数量,适合交互频繁但单包数据量小的场景。

3. 拥塞控制:cwnd 的四个阶段,背口诀不如把曲线跑一遍

3.1 慢开始与拥塞避免:ssthresh 是那条分割线

流量控制管的是两端,拥塞控制管的是整条路径。网络发生拥塞时,路由器队列开始堆积、缓冲区溢出、报文被丢弃,TCP 只能靠丢包和超时感知拥塞。拥塞控制的全部逻辑就是回答两个问题:什么时候该试探性地多发包,什么时候该果断降速。

TCP 引入了一个发送方维护的变量 cwnd,叫拥塞窗口。发送窗口的实际上限是 min(cwnd, rwnd),所以 cwnd 直接限制了单位时间能注入网络的数据量。慢开始的“慢”指的是从一个 MSS 开始,而不是从零开始,更不是慢吞吞地线性增长。每收到一个确认 ACK,cwnd 增加一个 MSS,因此每个 RTT 内 cwnd 翻一倍:1、2、4、8、16。这就是指数增长。课件上那张经典曲线,一开始陡峭上升就是在跑慢开始。

但指数增长不能一直持续,否则很快撞上网络容量上限。TCP 设置了一个阈值 ssthresh,当 cwnd 达到这个值时就切换到拥塞避免阶段,cwnd 改为每个 RTT 只增加一个 MSS,线性增长。这个阈值不是固定的,发生超时时,TCP 把 ssthresh 更新为当前 cwnd 的一半,同时把 cwnd 重置为 1 个 MSS,重新慢开始。注意这里的因果顺序:不是因为拥塞所以 ssthresh 减半,而是因为检测到超时,TCP 认定网络已经过载,把“安全水位线”下调一半。

一个容易混淆的点是:很多人以为慢开始阶段只发生在连接刚建立时。实际上,每次超时重传之后,TCP 都会重新回到慢开始。也就是说,一条长期运行的 TCP 连接可能在传输过程中多次经历慢开始,而不是只在握手之后跑一次。

3.2 快重传与快恢复:丢了包不一定要一切重来

早期 TCP 只有超时这一种丢包检测手段,超时时间通常要等一个 RTT 甚至更久,一旦超时就把 cwnd 打到 1 个 MSS,代价很大。快重传的改进点是:接收方收到乱序报文时,会立刻重复发送对最后一个有序字节的 ACK。发送方连续收到 3 个重复 ACK,就能相当有把握地认为某个报文丢了,不必等超时,立刻重传。这 3 个重复 ACK 的阈值叫 dupthresh。

与快重传配套的是快恢复。收到 3 个重复 ACK 说明路径还能把报文送回来,拥塞程度没有超时那么严重,所以不需要把 cwnd 打到 1。TCP Reno 的做法是:ssthresh = cwnd / 2,cwnd = ssthresh,然后进入拥塞避免阶段线性增长。这个“降一半再接续”的策略,比超时后从头开始温和得多。

很多人把快重传和快恢复当成两个独立机制,其实它们是一条完整链路:快速感知丢包,快速重传缺失数据,再快速恢复到合理速率。TCP Tahoe 只有快重传没有快恢复,丢包后一律回 1 重新慢开始,所以吞吐曲线是锯齿状,每个齿都从底部爬起;TCP Reno 加上快恢复后,锯齿变成了“减半-爬升”的形态。期末选择题如果拿 Reno 和 Tahoe 做对比,考的就是丢包后 cwnd 是回到 1 还是减半。

3.3 把 cwnd 的锯齿画出来:一段可运行的 Python 模拟代码

四个算法的文字总是绕口,我建议直接跑一段模拟代码,把 cwnd 随 RTT 的变化打印出来。下面是一个简化版的 TCP Reno 模拟器,事件驱动逻辑和真实协议栈一致,只是省去了 ACK 粒度细节:

# 模拟 TCP Reno 的拥塞窗口变化,以 RTT 为最小时间单位
def tcp_reno(initial_cwnd=1, ssthresh_init=16, loss_rtts=(25, 32), max_rtt=40):
    cwnd = initial_cwnd          # 当前拥塞窗口,单位:MSS
    ssthresh = ssthresh_init     # 慢开始阈值
    print("RTT\tcwnd\tssthresh\t事件")
    for rtt in range(1, max_rtt + 1):
        # 慢开始阶段:每个 RTT 翻倍;拥塞避免阶段:每个 RTT +1
        if cwnd < ssthresh:
            cwnd *= 2
        else:
            cwnd += 1

        # 模拟收到 3 个重复 ACK,触发快重传 + 快恢复
        if rtt in loss_rtts:
            ssthresh = max(cwnd // 2, 2)   # 阈值减半,下限 2 个 MSS
            cwnd = ssthresh                # 快恢复:从减半后的值开始线性增长
            print(f"{rtt}\t{cwnd}\t{ssthresh}\t3 个重复 ACK,快恢复")
        else:
            print(f"{rtt}\t{cwnd}\t{ssthresh}")

if __name__ == "__main__":
    tcp_reno()

这段代码的关键参数有三个。 initial_cwnd 对应 RFC 5681 里建议的初始拥塞窗口,算法上是 1 个 MSS,实际上 Linux 内核在较新版本把初始窗口提升到了 10 个 MSS,这就是为什么新连接前几轮传输就能跑得很快; ssthresh_init 是慢开始阈值初值,真实协议栈里初始值通常很大,Linux 里常常超过几万个 MSS,意味着绝大多数连接从建立到首次丢包都在慢开始阶段; loss_rtts 是预设的丢包时刻,用来模拟特定 RTT 收到 3 个重复 ACK 后 cwnd 的锯齿状变化。

运行后你会看到:第 25 个 RTT 丢包时 cwnd 瞬间减半,第 32 个 RTT 再次减半。两次减半之间 cwnd 保持线性增长而不是指数增长,这正是 Reno 快恢复和 Tahoe 的最大区别。真实网络里 cwnd 不是按 RTT 整数跳变,而是每收到一个 ACK 更新一次,但趋势和这个模拟完全一致。想画图的话,把输出结果收集到列表里,用 matplotlib 的 plot 画出来即可,这里刻意不引入绘图依赖,保证任何一台机器都能直接跑出结果。

4. 从教材到实网:Reno、CUBIC、BBR 与 408 考点的对照

4.1 教材章节与考点映射:谢希仁第八版、自顶向下、王道分别怎么讲

不同教材对拥塞控制和流量控制的组织方式差别很大,复习时如果同时看好几本,容易越看越乱。谢希仁《计算机网络》第八版把流量控制和拥塞控制都放在第 5 章运输层,先讲 TCP 报文段格式和滑动窗口,再讲可靠传输,最后单独开一节讲拥塞控制,四个算法按时间线串起来。这本书的特点是文字推导多,适合理解“为什么要这样设计”。

《计算机网络:自顶向下方法》的中文译本在传输层一章里用很大的篇幅讲 TCP 拥塞控制,但它把流量控制弱化了,重点是 AIMD 行为、吞吐量公式和公平性分析。如果你的目标是应对期末简答题,谢希仁的书更贴课本;如果老师用的是自顶向下教材,那要多关注 TCP 吞吐量的近似公式:吞吐量约等于 1.22 倍 MSS 除以 RTT 乘丢包率的平方根。王道考研系列则是完全按 408 大纲组织,拥塞控制这部分常考的是 cwnd 曲线形态判断、ssthresh 更新值计算、以及快重传和快恢复的触发条件,选择题居多。

我建议的复习路径是:以谢希仁第八版为主线读一遍,再用湖科大教书匠的动画视频把四个算法过一遍,最后用王道习题检验。动画形式对理解窗口爬升过程帮助很大,比盯着静态曲线图直观得多。如果你不考 408,只想通过期末,那重点是记住:超时后 cwnd 回到 1,3 个重复 ACK 后 cwnd 减半,这两条结论占比极高。

4.2 从 RFC 5681 到 CUBIC:为什么真实网络不按课本曲线跑

课本里讲的 Reno 算法是 1990 年代的设计,但今天 Linux 和 Windows 默认的拥塞控制算法早就不再是 Reno 了。Linux 长期默认的是 CUBIC,它的特点是丢包后 cwnd 恢复曲线呈三次函数形态,前半段快速追赶丢包前的速率,接近原速率后变得平缓,特别适合高带宽长时延链路。Windows 也早就把默认算法换成了 CUBIC 的变体。这就是为什么你在真实服务器上用 ss -ti 查看连接的拥塞控制算法,几乎看不到 Reno。

CUBIC 仍然以丢包作为拥塞信号,但 Google 的 BBR 走的是完全不同的路线。BBR 不再把丢包看成拥塞的唯一信号,而是实时测量瓶颈带宽和最小 RTT,把发送速率控制在两者的乘积附近,也就是带宽延迟积 BDP 附近。BBR 在长肥网络上能把吞吐量拉得很高,代价是和传统丢包判拥塞的算法共存时公平性一般。408 和期末基本不考 BBR,但工作中排查跨国链路问题时,你可能会发现默认 CUBIC 跑不满带宽,换 BBR 反而能稳定跑满,这个属于实网调优话题,课件里不会讲。

4.3 408 和期末复习的取舍清单:哪些必背,哪些了解即可

如果你的目标是考试,拥塞控制与流量控制这块的投入产出比很清晰。必背的是:滑动窗口与 rwnd 的含义、可用窗口计算公式、慢开始与拥塞避免的 cwnd 变化规则、ssthresh 在超时和快重传两种场景下的不同更新方式、快重传触发条件是 3 个重复 ACK。这些内容在选择题和简答题里反复出现,分值稳定。

理解即可的是:Tahoe 与 Reno 的对比、CUBIC 的三次函数增长特性、BBR 的带宽延迟积模型、ECN 显式拥塞通知的工作过程。ECN 在教材里只是一小节,408 基本不考,但真实网络里遇到路由器显式标记 ECN 的报文,抓包时能认出来就够了。复习时不要钻牛角尖去推 CUBIC 的数学公式,考试不要求,弄懂 Reno 的曲线形态就已经覆盖了绝大多数考点。

5. 避坑指南:拥塞控制与流量控制最常见的 5 个误用

5.1 现象:抓包看到 Window 字段很小,就认定发生了网络拥塞

有人在 Wireshark 里看到对端通告的 Window 只有几千字节,立刻断定是网络拥塞,换 CDN、加带宽,问题依旧。原因是把流量控制当成了拥塞控制。窗口字段是接收方通告的 rwnd,它反映的是接收方应用消费速度和缓冲区剩余空间,不是网络状态。接收方进程卡住、读数据慢,窗口就会缩小,与链路质量完全无关。解决:确认发送方实际发送窗口是取 cwnd 与 rwnd 的最小值,先用 ss -ti 看 cwnd,再在 tshark 里对比 tcp.window_size_value 与缩放因子。如果 cwnd 很大而窗口通告很小,问题在接收端应用,不在链路。

5.2 现象:把接收缓冲区调到 1MB 就觉得流量控制不再是瓶颈

接收缓冲区加大后 rwnd 确实变大,但发送窗口的上限是 min(cwnd, rwnd),在长肥网络里真正限制吞吐的往往是 cwnd 和丢包率。带宽 100Mbps、RTT 200ms 的链路,带宽延迟积约 2.5MB,如果接收缓冲区只有 256KB,即使链路再空,吞吐也上不去。反过来,接收缓冲区调到很大,但路径丢包率 1%,Reno/CUBIC 的拥塞窗口会被压得很低,吞吐照样上不去。解决:用带宽延迟积公式估算合理缓冲区大小,再结合抓包确认瓶颈。判断原则是:rtt 大、带宽高,优先检查接收窗口和 cwnd;丢包高,优先检查链路质量和中间设备队列。

5.3 现象:交互式应用每条指令都慢 20 到 40 毫秒,怀疑是拥塞控制

很多小包交互型应用卡顿,背锅的经常是“网络不好”,实际上是被 Nagle 算法和延迟 ACK 的组合拳打死的。Nagle 算法攒小包等 ACK,延迟 ACK 等小包凑数据,双方进入死锁般的等待。整个链路没有任何拥塞,也没有丢包,纯粹是算法配合问题。解决:对交互式 socket 设置 TCP_NODELAY 禁用 Nagle,或者在对端关闭延迟 ACK。注意如果数据流本身是大块传输,禁用 Nagle 反而造成小包泛滥,所以要分场景。判断方法很简单:抓包看时间戳,如果小包间隔总在 20ms 到 40ms 量级,基本可以锁定这个问题。

5.4 现象:在本地模拟环境里调参有效,部署到公网全部失效

本地用网络模拟器做实验,把 ssthresh、初始窗口等参数调得很漂亮,一到公网吞吐就打回原形。原因是模拟器网络拓扑固定、队列长度可控、丢包率恒定,而公网链路上的路由、队列、竞争流量千变万化。拥塞控制本身就是针对不可预知网络设计的自适应机制,参数调优的空间远没有想象中大。解决:把模拟器当作理解机制的工具,不要在模拟器里追求最优参数;真实调优时,先用 ping 和 iperf3 测出基础 RTT、丢包率、可用带宽,再决定是换算法、调缓冲区还是改应用发送模式。

5.5 现象:网络乱序被误判为丢包,快重传频繁触发,吞吐被无故砍半

快重传依赖 3 个重复 ACK,但重复 ACK 也可能是路径乱序造成的。比如两个报文走了不同路由,后发的先到,接收方就会重复 ACK 请求缺失的字节。真实网络尤其是多出口链路,乱序并不罕见。TCP 无法区分“真丢包”和“乱序先行”,只能按丢包处理,结果就是 cwnd 被无故减半,吞吐曲线频繁塌陷。解决:优先确认链路是否有负载均衡导致的多路径乱序;高版本 Linux 默认启用 DSACK 和 RACK,能在一定程度上减少误判,但根因还是要收敛多路径转发。排查手法是在抓包里统计乱序报文的比例, tshark 的 tcp.analysis.duplicate_ack 过滤可以快速数出重复 ACK 数量。

6. 用 tc 和 iperf3 复现拥塞场景:验证理论最快的一条路

纸上谈兵的算法曲线看了十遍,不如亲手制造一次拥塞来得记忆深刻。我用一个很轻量的方案:在一台 Linux 机器上用 tc 命令模拟丢包和延迟,再用 iperf3 打流量,最后用 ss -ti 观察 cwnd 的变化。这套实验不需要任何专用网络设备,普通双网卡 Linux 主机就可以做,实测效果非常直观。

# 第一步:在网卡上增加 5% 丢包率,模拟弱网环境
sudo tc qdisc add dev ens33 root netem loss 5%

# 第二步:开 30 秒测试,每 1 秒打一条吞吐日志
iperf3 -c 192.168.1.10 -t 30 -i 1

# 第三步:观察当前连接的拥塞窗口和算法
ss -ti | grep -E "cwnd|cubic|bbr"

# 第四步:实验结束务必删除排队规则,恢复原状
sudo tc qdisc del dev ens33 root

第一次跑这个实验的人会看到,iperf3 的吞吐量不再是一条平稳的直线,而是每隔几秒就出现一次明显下跌,这正是 cwnd 因为丢包减半、再重新爬升的过程。 ss -ti 输出里的 cwnd 会周期性变化,拥塞控制算法默认是 cubic 。把丢包率从 5% 改成 0.1% 再跑一次,吞吐曲线会明显平稳,这是理解拥塞控制最直接的身体记忆。

我的习惯是把删除排队规则的命令写在实验脚本的第一行,也就是先执行一点点删除动作,再加根规则。运维老手应该都经历过实验做完了,tc 规则忘记删除,导致整条链路一直模拟丢包,业务方找上门来的翻车时刻。这套实验做完,你就能把课件里的曲线图和真实网络的锯齿吞吐对上号了,下次再有人争论拥塞算法优劣,直接拉数据说话,比背公式有效得多。希望帮到你。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

更多推荐