TCP拥塞控制核心算法与窗口变化详解
2026/9/24 23:00:29 网站建设 项目流程

TCP拥塞控制这块,我在复习计算机网络时总觉得它比三次握手难啃。三次握手好歹是固定流程,背下来就能应付考试,拥塞控制却是一堆算法、窗口、阈值的博弈,而且教材里讲的版本还不一样,看谢希仁、看自顶向下、看王道,细节各有出入,越看越晕。后来反复推演了几遍,又抓包看了实际数据,才慢慢理顺。

这篇就按我的理解,把拥塞控制的来龙去脉、四种算法、窗口变化过程,以及复习时容易踩的坑,重新梳理一遍。不管是期末复习、考研408,还是面试前突击,都可以参考这个思路。我尽量用大白话把原理讲透,再贴一些实际排查经验。

1. 为什么TCP非要做拥塞控制

1.1 拥塞的根源:链路资源不是无限大的

先说一个被我以前忽略的本质问题:拥塞不是某一台主机出了问题,而是整个网络路径上的某个环节过载了。路由器或者交换机的处理能力是有限的,交换机背板带宽有限,链路带宽有限,当多条TCP连接同时往同一个出口挤数据时,路由器里的缓存队列会堆满,之后到达的数据包就只能被丢弃。这就是丢包的根源之一。

丢包带来的连锁反应非常可怕。如果TCP没有拥塞控制机制,发送方一股脑往网络里灌数据,丢包之后只能靠超时重传,重传的数据又继续加剧拥塞,造成更多丢包,最终形成“拥塞崩溃”。这不是理论推演,早期互联网还真遇到过类似问题,后来才引入了拥塞控制相关的机制。所以拥塞控制的本质任务是:发送方主动探测网络的可用容量,限制自己往网络里发送的速率,让整条链路保持在一个“能吞得下”的水平

1.2 流量控制和拥塞控制,别搞混了

这俩名字太像,考试和面试都爱混着问。我用最简单的方式区分:

  • 流量控制(Flow Control):管的是发送方和接收方之间的事。接收方处理不过来,就在TCP首部的窗口字段里告诉发送方“你慢点发”,窗口大小由接收方的缓存空闲程度决定。这属于点对点的“上下游协调”,解决的是“接收方吃不吃得下”的问题。
  • 拥塞控制(Congestion Control):管的是发送方和整个网络之间的事。网络中间的路由器拥塞了,TCP无法直接感知,只能通过丢包或延迟变化来推断网络状态,从而主动调整发送速率。这属于“全网协作”,解决的是“中间链路受不受得了”的问题。

可以把流量控制理解成:你家楼下的快递柜满了,快递小哥不再塞件;而拥塞控制是:整条送货路线上的分拣中心爆仓了,快递公司主动限制发货数量。两种场景的应对完全不在一个层级。

1.3 拥塞控制的核心设计目标:效率与公平

拥塞控制背后有两个设计目标,很多人复习时没意识到,但理解了这两个目标,后面算法就好懂了。

第一个是效率。网络链路空闲时,要把利用率拉上去;链路拥塞时,要快速降速;拥塞缓解后,再慢慢提速。整个过程其实就是一条“增大——探测——降速——再增大”的曲线。

第二个是公平性。如果同一条瓶颈链路上有多条TCP连接,不能因为某条连接先启动就把带宽全占满。TCP用“加性增”的机制,让各连接在稳态时近似公平地分享带宽。这个“公平”不是绝对的数学公平,而是大致按比例分配,实际效果非常符合直觉。

2. 拥塞控制的四种算法拆解

TCP拥塞控制的完整方案由四个算法组合而成,分别是慢启动、拥塞避免、快重传、快恢复。这一套组合拳从1988年提出后不断演进,目前主流版本都基于RFC 5681的框架。先记住两个核心参数:

  • cwnd:拥塞窗口,发送方自己维护的状态变量,单位是MSS(最大报文段长度)。发送窗口大概等于min(cwnd, rwnd)
  • ssthresh:慢启动阈值,它是慢启动和拥塞避免之间的“分界线”。

2.1 慢启动:先小步试探,再指数爬坡

慢启动这个名字特别容易误导初学者。第一个误区是觉得“慢启动”就是慢慢发,其实不然,它的增长方式非常激进。

连接刚建立时,cwnd初始值很小。早期TCP实现把初始拥塞窗口设为1个MSS,最新RFC 6928建议把初始窗口提高到10个MSS(大约14KB左右),实际Linux系统也按10个MSS左右设置。因为TCP不知道网络能承受多大流量,只能从一个较小的值开始探测,这叫“小步试探”。

启动之后,每收到一个ACK,cwnd就增加1个MSS。不要小看这个“每ACK加1”,它是等比增长的:第一次发送1段,收到1个ACK后cwnd变成2;下一轮发2段,收到2个ACK后cwnd变成4;再下一轮发4段,收到4个ACK后cwnd变成8。所以虽然名字叫“慢启动”,实际窗口大小是指数增长的。如果网络通畅,几个RTT内就能把窗口顶上来。

为什么叫“慢”呢?它是相对于TCP早期那种一上来就灌满整个接收窗口的方案而言的。慢在“初始”,不在“过程”。

慢启动什么时候结束?有三个退出条件:

  1. 发生超时重传:说明网络扛不住了,ssthresh降到当前cwnd的一半,cwnd重置为初始值,重新开始慢启动。
  2. cwnd达到ssthresh:说明按指数增长的方式已经不太合适了,转入拥塞避免阶段。
  3. 收到3个重复ACK:触发快重传机制,进入快速恢复流程。

2.2 拥塞避免:从指数改成线性

拥塞避免阶段做的事情很简单:把窗口的增长方式从“每收到ACK加1MSS”改成“每个RTT加1MSS”。

需要特别说明的是,拥塞避免的“避免”不是说不发生拥塞,而是在接近网络容量上限后,不能再莽撞地加倍增长,要用线性增长探路。因为指数增长在链路还很空闲时效率很高,但一旦逼近拥塞点,加倍试探代价太大,极容易直接把网络打爆。线性增长每次只增加1个MSS,相当于缓慢探头,稳扎稳打。

理想情况下,cwnd会沿着一条线性斜坡持续上升,直到碰到拥塞点。拥塞信号有两个来源:

  • 超时重传:这个信号很严重,说明不仅是丢包,可能连ACK都丢了,网络状态比较糟糕。处理方式是:ssthresh = cwnd / 2cwnd = 初始值,重新进入慢启动。
  • 收到3个重复ACK:这个信号相对轻一些,说明网络还能把数据送回来,只是某个段丢了,但后续的数据都到达了接收方。处理方式是:进入快重传和快恢复。

2.3 快重传与快恢复:把丢包的损失控制到最小

先理解什么是重复ACK。假设发送方发出去1、2、3、4、5五个报文段,其中段3丢了。接收方收到段2之后,按序期待段3,但收到的却是段4,TCP不能乱序交付,只能再次确认“我等的还是段3”。于是段4到达后,接收方会再发一次ACK=3。之后段5到达,接收方又会继续发ACK=3。连续收到3个重复ACK(算上第一次发出的那个,一共4个ACK都是确认序号3),发送方就能断定段3丢了。

为什么需要快重传?因为如果傻等超时,往往要等一个RTO(重传超时时间),这个时间可能比正常RTT长很多。而重复ACK已经明确指认了哪个段丢了,立刻重传它,效率更高。快重传的触发条件就是连续收到3个重复ACK,收到后立即重传丢失的报文段,不必等超时。

为什么是3个重复ACK而不是2个?这是TCP设计里的一个很巧妙的折中。因为乱序也会导致重复ACK。比如段2先到了,段3和段4还在路上,接收方收到段2后发了一个ACK=3,接着段4先于段3到达,接收方又会发一个ACK=3。这种情况下,收到少量重复ACK说明可能是乱序而不是丢包。如果收到3个重复ACK,大概率是真的丢了。这个阈值是经验和工程权衡的结果,不是严格的数学推导。

快恢复是配合快重传用的。收到3个重复ACK后,发送方会把cwnd减半,ssthresh也设为减半后的值,然后cwnd从减半后的值开始,执行拥塞避免的线性增长。注意:这个版本和早期版本不一样,早期Tahoe版本遇到重复ACK也会把cwnd降到1重新慢启动;后来Reno版本引入了快恢复,才改为减半而非归零。现在考试和实际系统里,一般以Reno和之后版本为准。

2.4 四种算法的关系与触发条件汇总

学到这里,很多人会混淆四种算法之间的关系。其实它们的组合规则很清楚:

算法触发条件窗口变化对ssthresh的影响
慢启动连接刚建立 / 超时重传后每收到ACK,cwnd加1MSS(指数增长)超时时,ssthresh = cwnd / 2
拥塞避免cwnd >= ssthresh每个RTT,cwnd加1MSS(线性增长)不变
快重传收到3个重复ACK立即重传丢失段,cwnd临时减半再恢复不直接改ssthresh
快恢复快重传之后cwnd = ssthresh后的值,继续线性增长ssthresh = cwnd / 2

这张表对期末简答题特别管用,考试基本就是考这些触发条件和窗口变化。

3. 拥塞窗口变化过程推演

3.1 一次完整连接的窗口演变全过程

光记算法不推演,考场上一遇复杂题还是容易卡住。我平时复习时最喜欢画图,但这里只能用文字描述。假设这样一组参数:cwnd初始为1MSS,ssthresh初始为16MSS,不考虑接收窗口限制。

阶段一,慢启动:

  • 第1个RTT:发送1个MSS,收到ACK后cwnd=2
  • 第2个RTT:发送2个MSS,收到全部ACK后cwnd=4
  • 第3个RTT:发送4个MSS,收到全部ACK后cwnd=8
  • 第4个RTT:发送8个MSS,收到全部ACK后cwnd=16

这时候cwnd已经从1涨到16,正好碰到ssthresh,进入拥塞避免。

阶段二,拥塞避免:

  • 第5个RTT:发送16个MSS,收到全部ACK后cwnd=17
  • 第6个RTT:发送17个MSS,收到全部ACK后cwnd=18
  • 第7个RTT:发送18个MSS,收到全部ACK后cwnd=19
  • 第8个RTT:发送19个MSS,收到全部ACK后cwnd=20

到这里网络一直很畅通,cwnd线性爬坡。

阶段三,拥塞发生了:

假设第9个RTT发送20个MSS,中间某些报文段丢失,发送方收到3个重复ACK,触发快重传和快恢复。

处理过程是:ssthresh更新为20 / 2 = 10cwnd调整为10,然后进入拥塞避免阶段继续线性增长。如果后续没有再次拥塞,cwnd会从10开始继续每个RTT加1MSS。

但假设在第10个RTT之后发生的是超时重传,处理就完全不同了:ssthresh仍然减半为10,但cwnd直接重置为1,重新执行慢启动到新的ssthresh,再转拥塞避免。

这种推演的熟练度直接决定期末大题能不能得分。我建议用纸笔亲自画一遍,比看任何网课都有效。

3.2 教材里没细说的“约等于”细节

复习时如果只看谢希仁教材,有几个细节容易引发困惑,实际工程实现又有差异:

第一,初始窗口已经从1MSS变成了10MSS。考试题里如果明确说“初始为1MSS”,按1算;如果不提,最新的Linux实现初始窗口约10段(RFC 6928)。408考试一般会给出参数,不会让你猜。

第二,cwnd的单位是MSS,不是字节。虽然它代表拥塞窗口,但在实际发送时,发送窗口 =min(cwnd, rwnd),二者取小。考试计算题如果给了接收窗口,一定别忘了比较。

第三,ACK的累积确认会让cwnd增长比理论值更快一些。比如慢启动阶段发送了2个MSS,收到一个累积ACK(确认了第2个段),cwnd加1变成3;但如果这两个MSS的ACK分别到达,那么cwnd会加2变成4。教材通常按后一种理想情况计算,实际情况可能略慢或略快。这个细节考试一般不追究,但做实验抓包时会发现数字对不上,不必太焦虑。

第四,快恢复过程中还有“暂缓增长”的细节。Reno版本进入快恢复后,收到重复ACK时cwnd要临时增加1MSS,收到新数据的ACK后再退出恢复状态。如果题目出得很细,会把临时增加的部分也算进去。大多数期末题不会考到这一步,但408真题偶尔会玩。

4. 期末复习和实战排查中的高频问题

4.1 期末/考研考点速记清单

这节写给马上考试的朋友。挑了几个最常见的考点,也是我以前反复记混的地方:

  • 慢启动和拥塞避免的分界线是什么?cwndssthresh的大小关系,cwnd < ssthresh时慢启动,cwnd >= ssthresh时拥塞避免。
  • 发生超时重传后怎么处理?ssthresh = cwnd / 2cwnd重置为初始值(1MSS或按题目给定值),重新慢启动。
  • 收到3个重复ACK后怎么处理?快重传,进入快恢复:ssthresh = cwnd / 2cwnd = ssthresh,进入拥塞避免。
  • 为什么慢启动是“慢”的?相对于最初未加限制直接灌满窗口的方案,它初始值小,需要逐渐探测。实际增长速率并不慢。

另外还有一些高频面试追问,比如“TCP怎么知道网络拥塞了?” TCP本身不能直接感知路由器的拥塞状态,它只能靠间接信号:超时重传和重复ACK。超时往往意味着网络严重拥塞或链路中断,重复ACK说明局部丢包但后续报文还在走,程度相对较轻。

还有一类比较新的考点:公平性与TCP的Reno/BBR等算法对比。考试一般只要求传统Reno,面试如果聊得深,可以提一句“BBR通过测量瓶颈带宽和往返时延来规避丢包信号,和传统基于丢包的算法思路不同”。作为了解即可,别把BBR和Reno混在一起答题。

4.2 实际抓包怎么看拥塞控制现象

学习阶段光看书容易“纸上弹道”。如果环境允许,可以用Wireshark或tcpdump抓包观察。做法很简单:下载一个大文件,同时抓包,然后看TCP流的时序图。

具体步骤:

# 抓取某个端口上的TCP流量 sudo tcpdump -i eth0 -w tcp_congestion.pcap tcp port 80

下载完成后,用Wireshark打开pcap文件,在Statistics菜单里选“TCP Stream Graph” → “Time-Sequence Graph(Stevens)”,就能看到窗口随时间变化的曲线。正常情况下能看到明显的“慢启动指数抬升——线性爬坡——下跌”的锯齿状曲线。

我实测过的现象是:在丢包率比较低的内网环境,曲线几乎是一条平滑的线性上升,很难看到指数段,因为初始窗口10MSS起步,几十个RTT就到了ssthresh,整个慢启动过程转瞬即逝。在弱网环境(比如加了丢包模拟的网络)里,锯齿会非常明显,每到一个尖峰就下跌,循环往复。这个观察特别有助于理解“加性增、乘性减”的曲线形态。

4.3 常见APP层TCP报错和拥塞控制的关系

热搜词里出现了很多TCP相关的报错,比如“curl: (35) TCP connection reset by peer”、“bind: address already in use”之类。这些大多数和拥塞控制没有直接关系,但作为TCP学习者,区分“链路拥塞”和“应用层错误”很重要。

  • TCP connection reset by peer:一般是对端应用程序把连接关掉了,发送方收到RST。可能是对端进程崩溃、对端主动拒绝服务,或者防火墙发送了RST,不是拥塞控制触发的现象。
  • bind: address already in use:本地端口被占用,TCP服务端起不来。这是因为TIME_WAIT或LISTEN状态占用了端口。解决办法是换端口,或者设置SO_REUSEADDR选项。这属于Socket编程细节,跟拥塞控制无关,但非常常见,顺手记录一下。
  • 网络下载慢、网页卡顿:才可能和拥塞控制有关,但也可能是应用层并发连接数不够、DNS解析慢、服务端处理慢等因素。排障时要先从端到端分层排查,别一言不合就怪TCP拥塞控制。

还有一个在Socket编程里常被问到的“TCP粘包问题”,这里也顺带说清楚:粘包是应用层消息边界问题,TCP是字节流协议,本身没有消息边界概念。拥塞控制影响的是“什么时候把数据发出去”,粘包影响的是“收到数据后怎么切分业务消息”,两者根本不在一个层面。如果你处理粘包只能想到sleep或强制flush,那说明还没理解TCP的流式传输本质。正确做法是在应用层定义消息长度字段,或者使用分隔符协议。这一块经常在简历项目中被追问,值得单独深入。

4.4 学习顺序推荐

最后聊聊怎么学这块效率最高。我的个人建议:先看谢希仁《计算机网络》中运输层的前半部分,把三次握手、四次挥手、TCP报文段结构弄清楚;然后把拥塞控制章节从头到尾读一遍,不需要立刻背,先理解来龙去脉;再看一遍自顶向下那本教材里对应的章节,重点看它的时序图;之后开始画图,从初始cwnd=1画到自己设定的ssthresh,推到拥塞发生,画完整条线;最后抓包验证一次,这一步不进实验室也能做,只要在电脑上抓一下普通网页访问就能看到大致的窗口走势。

画图这一步比看网课管用。网课听了觉得会了,一做题还是不会推;自己手写一遍数字,坑才会露出来。比如我第一次画的时候,压根没意识到快恢复后cwnd应该等于ssthresh而保留了慢启动时的增长状态,结果把后面的线性增长起点算错了。这种细节只有动笔才会发现。

结尾

在大学期间,计算机网络这门课我很遗憾没有学得很深入,直到后来做后端开发,天天和TCP打交道,才回过头来把运输层重新啃了一遍。我的体会是:拥塞控制这块,理解了“发送方通过丢包/延迟间接感知网络状态”这一条主线,其他细节都能推导出来;反过来死记硬背,没几天就忘了。

如果你正在复习期末或准备面试,可以试着自己画一遍慢启动到拥塞避免的窗口变化图,再把快重传和快恢复的流程完整描述一遍,看能不能一气呵成。如果中间卡壳,回看第3节的内容,保证比单纯刷题效率高。TCP协议栈还有很多有趣的内容,比如TIME_WAIT、Nagle算法、延迟确认、BBR拥塞控制算法,多的不敢说,后续可以慢慢写。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询