目录
[1. 阶段一:慢启动(指数级增长)](#1. 阶段一:慢启动(指数级增长))
[2. 阶段二:拥塞避免(加法增大)](#2. 阶段二:拥塞避免(加法增大))
[3. 阶段三:网络拥塞(乘法减小)](#3. 阶段三:网络拥塞(乘法减小))
在上一篇文章中,我们学习了 TCP 的确认应答机制,超时重传机制,连接管理机制(三次握手四次挥手),流量控制,滑动窗口等内容,对 TCP 传输层协议已经有了一定的认识,本篇文章在上一篇文章的基础上继续讲解 TCP 的剩余内容。
接下来我们先学习 TCP 另一大核心机制 ------ 拥塞控制。
一、拥塞控制

在此之前我们学习的超时重传、连接管理、滑动窗口流量控制等 TCP 策略,都只围绕发送端与接收端两端自身 的状态来工作,但 TCP 整条通信还受中间网络的影响,和网络传输相关的异常问题,都由拥塞控制来处理。同时也要明确,拥塞控制只能解决 TCP 传输相关的网络异常,无法处理全网所有类型的网络故障。
因为有了流量控制,所以发送方给接收方发送数据时就不会超量发送,接收方也就不会超量接收,所以如果数据丢包了,和双方的关系一般都不大,那再丢包就是网络问题了。
举个例子 : 比如发送了 10000 个数据,只丢了 1 或 2 个数据包,这属于网络正常丢包,正常走重传流程即可;但假设如果丢了 9888 个包 (这种情况基本不会发生,这里我们只是举例),就代表网络出现了严重异常,TCP 就会判定为网络拥塞。网络拥塞可以理解为网络拥堵了,网络中的报文树太多了,导致所有报文都在各自的路由器中排队等,导致网络延迟,进而再导致收不到数据,也就是丢包了。
那如果此时是网络拥塞,可不可以重传呢?
不能重传了,因为已经拥塞了,再重传的话会导致更加拥塞,只会再次加重网络的拥堵问题。
这里需要注意的是对于 TCP 而言,如果是网络出现了问题导致大量丢包,TCP 只能判定是网络拥塞了,因为 TCP 只是传输层的协议,只能在自身协议能力范围内调控发送速率,缓解网络压力,无法干预处理自身以外的其他网络故障。

那这里可能有人有疑问了,网络中有无数个主机,无数个报文,我的主机的报文丢了,我重传一下影响应该不大吧,万一重传一下就能正常送达呢?
这种想法是错误的,正因为网络中有多个主机,这些主机都遵守 TCP 协议,报文都要通过网络传输,而网络对于这些主机和报文来说是共享的,如果你重传一下,他重传一下,本质上还是会加重网络的拥堵情况,当然这里我们只考虑网络的拥塞情况,先不考虑其他的网络异常问题。
那此时这些主机应该做什么?
那面对网络拥塞,TCP 主机正确的处理方式就是执行拥塞控制。
慢启动机制
如何进行拥塞控制?
如果出现了网络拥塞,发送方主机A出现了丢包,那主机A还是要重传,这是只要重传一个报文,相当于去探路,如果这一个报文还是没有收到应答,就说明网络堵死了,这个通信过程就行不通了,主机A就断开连接了。
那如果这一个报文发了之后收到了应答,就说明最起码能通信,此时就会接着再发两个报文,如果再发的这两个报文也收到了应答,就会接着再发四个报文,收到应答再发八个......就像这样如果每次都收到应答了,下一次就会指数级增长。这个机制也叫慢启动机制。
TCP 拥塞控制的具体执行逻辑是这样的:当判定出现网络拥塞、发生报文丢包时,主机不会大批量疯狂的重传数据,而是会先发送 1 个探测报文前去探路 。如果这个报文依旧收不到 ACK 应答,就说明网络已经彻底堵塞了,当前无法正常通信,主机就会断开本次连接。如果这 1 个探测报文成功收到应答,就证明网络最起码是通畅的,接下来就依次发送 2 个正常的业务数据报文、再收到应答就发送 4 个、接着发送 8 个...... 以此类推,每次应答正常,发送数量就按指数倍数增长。这套循序渐进探测网络承载能力的机制,就是 TCP 的慢启动机制。
拥塞窗口
下面我们就再引入一个窗口进而解释拥塞机制。我们现在已经知道了两个窗口了,一个是表示接受能力的 16 位窗口大小 (win窗口) ,一个是滑动窗口,第三个窗口就是下来要讲的拥塞窗口。
拥塞窗口代表整条网络的承载能力,网络最多能一次性扛住多少数据,由拥塞控制决定
我们先来谈一谈这三种窗口的关系
滑动窗口 = min (win窗口,拥塞窗口,发送方真实数据量),也就是说滑动窗口大小是 16 位窗口大小,拥塞窗口大小,以及发送方要发送的数据量三者中的最小值。也就说先看发送方自己要发送的数据量,再看接收方能不能收下 (16 位窗口大小),最后看网络能不能扛住,取三者的最小值。
怎么做到第一次发一个,收到应答后再发两个,再发四个,八个......换句话说是怎么做到的灵活控制发送报文的数目的呢?
一开始把拥塞窗口设置成较小值,比如 1000 字节 (对应 1 个报文),所以第一轮只能发 1 个数据包。每收到一轮完整的 ACK 确认应答,就把拥塞窗口大小翻倍 1000→2000→4000→ 8000...窗口翻倍,允许一次性发送的报文数量就跟着翻倍,自然就实现了发 1 个、收到应答发 2 个、再收到发 4 个、接着发 8 个的指数增长逻辑。同时这个指数增长不会无限持续下去,一方面接收端接收能力有上限,实际滑动窗口会被接收能力限制住;另一方面拥塞窗口涨到阈值后,也会切换成缓慢线性增长,不会一直疯狂暴涨导致网络再次堵塞。
慢启动阶段发的第一个报文,是不带数据的探测报文吗?这个报文会占字节流位置?
慢启动阶段发送的第一个报文,并不是空的 "纯探测包",而是一段带有的业务数据的正常报文。所谓的 "探路" 只是它的逻辑作用,用来试探网络是否通畅,但报文本身包含了真实的业务数据,比如1001~2000 这 1000 字节,因此它必然会占用字节流中的位置,也会被纳入滑动窗口的统计范围。如果发送不带数据的空包,接收端无法根据字节序号回复对应的 ACK 确认,也就无法完成网络状态的验证,所以慢启动的 "探路" 功能,必须通过发送真实数据报文来实现。
拥塞窗口指数翻倍增长,但滑动窗口取三者的最小值,会不会出现冲突?
这两者并不冲突,滑动窗口的最终大小,会被接收窗口、拥塞窗口和本地待发数据量三者中的最小值约束。哪怕拥塞窗口按照慢启动机制指数翻倍增长,只要接收端的接收窗口或本地待发数据量更小,滑动窗口就会被更小的值 "卡住",不会无限制跟随拥塞窗口扩大。比如接收端接收窗口只有 2000 字节,那么即使拥塞窗口涨到 4000 字节,实际滑动窗口也只会是 2000 字节。同时,慢启动的指数增长也不会一直持续,当拥塞窗口达到慢启动阈值后,就会切换为线性缓慢增长,进一步避免窗口失控,与滑动窗口的取值规则完全兼容。
怎么理解这个慢启动?
慢启动的核心逻辑,就是以 "先慢后快" 的指数级增长方式,在传输可靠性和效率之间找到平衡点。它之所以叫 "慢启动",并不是全程都保持低速,而是前期起步阶段增长幅度很小,仅发送少量报文试探网络状态 ,用这种 "探路" 的方式确认网络是否恢复通畅,避免一开始就发送大量数据再次加剧网络拥塞;一**旦收到正常应答、验证网络状态良好,它就会以指数级的速度快速提升发送速率,也就是我们常说的 "恢复快",在短时间内把发送速率拉升到接近网络承载上限的水平。**整个过程既通过前期的慢速试探保障了网络的稳定性,又通过后期的指数级增长兼顾了传输效率,也是慢启动机制设计的根本目的。
拥塞窗口的算法
下面我们就看一下完整的拥塞控制的算法设计,是如何在对方接收能力大小和自身报文发送速率之间找到平衡的。

1. 阶段一:慢启动(指数级增长)
TCP 在网络拥塞开始时,会先从一个很小的拥塞窗口(比如 1 个 MSS)开始发送数据。每收到一轮完整的 ACK,拥塞窗口就翻倍增长:1→2→4→8→16......这个阶段叫 "慢启动",但它的增长速度其实非常快,是指数级的。它的目的,就是用最快的速度,试探出当前网络的承载能力上限,同时又不会一上来就把网络堵死。
2. 阶段二:拥塞避免(加法增大)
当拥塞窗口涨到慢启动阈值(ssthresh) 时,指数增长就会停止,切换为线性增长 :每收到一轮 ACK,拥塞窗口只增加 1 个 MSS。这就是 "加法增大",它的作用是进入 "稳扎稳打" 的阶段,慢慢逼近网络的真实上限,避免指数增长冲太快,直接把网络压垮。
3. 阶段三:网络拥塞(乘法减小)
如果这个时候又出现了网络拥塞。TCP 会立刻做两件事:1. 把新的慢启动阈值 ssthresh设置为当前拥塞窗口的一半(这就是 "乘法减小")。2. 把拥塞窗口重新重置为 1 个 MSS,重新进入慢启动阶段。这样做的目的,是立刻大幅降低发送速率,给网络 "松绑",避免拥塞进一步恶化。
这里的 **MSS(Maximum Segment Size) 指的是最大分段大小,就是 TCP 单次发往网络的 "最大数据块" 大小,**不包含 IP 和 TCP 头,只算要传输的 "纯数据部分"。之前我们一直用的 1000字节 就是一个 MSS:1 个 MSS = 1000 字节数据 → 对应一次发送 1 个报文,拥塞窗口的大小,本质就是 "一次能发多少个 MSS"。
TCP 的拥塞控制,就是一个 "慢启动 加法增大 乘法减小" 的过程:先用慢启动的指数增长快速探路,再用加法增大稳步逼近上限,一旦发现网络拥堵,就立刻用乘法减小把窗口砍半,重新从慢启动开始,始终在 "尽快传数据" 和 "不把网络堵死" 之间找平衡。
慢启动的本质
慢启动本质是探索当前网络的接收能力,拥塞窗口本质上是被探索出来的,所以在某一个时间范围内是一个较准的值
问题 1:拥塞窗口应不应该变化?
拥塞窗口必须动态变化,因为网络状态本身是时刻波动的,网络的承载能力也会随之改变,固定不变的窗口无法适配这种变化。如果窗口一直过大,会在网络拥堵时加剧丢包;如果窗口一直过小,又会在网络通畅时浪费带宽,所以拥塞窗口必须根据当前网络的实时状态,灵活调整大小,才能既不加剧拥塞,又充分利用传输能力。
问题 2:TCP 是怎么知道拥塞窗口的大小应该是多少?
TCP 本身无法直接获知网络的真实承载能力,它只能通过不断试探和试错来确定合适的拥塞窗口大小。TCP 没有上帝视角,看不到整条链路的带宽和队列状态,它唯一能感知的信号,就是报文是否能按时收到 ACK 应答。当 ACK 正常返回时,它会认为网络通畅,逐步增大窗口;当出现超时重传或大量丢包时,它会判定网络拥堵,立刻减小窗口。整个过程就像 "摸着石头过河",靠一次次反馈调整,找到当前网络能承受的最优窗口值。
问题 3:拥塞控制属于保障可靠性的一种吗?
拥塞控制不是直接实现可靠性的核心机制,却是保障可靠性正常工作的重要辅助。真正直接保障数据可靠传输的,是确认应答、超时重传、序号校验这些机制,而拥塞控制的作用,是通过控制发送速率,从源头减少网络拥堵和大规模丢包的发生,避免因网络瘫痪导致重传机制失效,为可靠性传输创造稳定的网络环境。
二、延迟应答
延迟应答是 TCP 中一种优化应答效率的机制,它的核心逻辑是把多次确认的应答报文合并成一次应答发送。

如上图,发送方连续发送了 1~1000 和 1001~2000 两段数据,按照常规流程,接收方需要分别回复 1001 和 2001 两个确认应答;但延迟应答会把这两次确认合并,只回复一次 2001,而这个确认序号本身就代表 "序号 2001 之前的所有数据都已收到",既完成了确认,又减少了应答报文的数量,降低了网络开销。
延迟应答还有一个额外的好处,就是能更高效地利用接收方的接收缓冲区空间大小。接收方的上层应用层会不断从接收缓冲区读取数据,如果收到报文后立刻发送应答,此时缓冲区里还堆积着未被读取的数据,应答报文中携带的接收窗口大小会偏小;但如果延迟一小段时间再回复,应用层很可能已经把缓冲区里的数据取走,剩余可用空间变大,此时回复的应答报文就能向发送方通告一个更大的接收窗口,让发送方可以一次性发送更多数据,从而提升整体网络吞吐量。
举个例子,接收端缓冲区总大小为 1M,一次收到了 500K 数据,如果立刻应答,返回的窗口只有 500K;但如果应用层在 10ms 内就处理完了这 500K 数据,此时再回复 ACK,就能通告 1M 的完整窗口,传输效率会显著提升。
延迟应答的本质是 : 通过延时,一定概率可以给发送方通告一个更大的接收窗口。
不过延迟应答也不是无限推迟,它有严格的限制规则,避免出现 ACK 超时导致发送方误判丢包的情况。通常有两种控制方式:
- 一种是数量限制,一般每收到 2 个报文就必须应答一次;
- 一种是时间限制,最大延迟时间通常不超过 200ms,超过这个时间无论是否收到足够数量的报文,都必须回复 ACK 应答。
不同操作系统的具体参数会有差异,但核心逻辑都是在减少 ACK 报文数量、通告更大窗口和避免误判丢包之间找到平衡,在不影响可靠性的前提下,最大化传输效率。
三、捎带应答
捎带应答机制是 TCP 协议里一种很经典的效率优化机制,它的核心逻辑就是把 "确认应答" 和 "业务数据" 合并在同一个报文中发送,以此减少网络中的报文数量,降低传输开销。

比如图里的场景,主机 A 发送了数据 hello,主机 B 不仅需要回复 ACK应答报文确认收到,还要向主机 A 发送自己的数据 world,捎带应答就会把这两个动作合并:在发送 world数据的同时,把对 hello的确认序号也塞进同一个报文里,一次发送就同时完成了数据传输和应答确认,让交互效率更高。
丰富场景:

捎带应答的逻辑在三次握手的场景里也能体现出来。三次握手的前两次报文 (SYN 和 SYN+ACK)是不允许携带业务数据的,因为此时双方还没有完成连接建立,无法对数据进行可靠确认和重传;
但第三次握手的 ACK 报文就不同了,对于发送这个 ACK 的主机来说,当它发出第三次握手的报文时,自身视角下三次握手流程已经完成,连接已经处于建立状态,此时就可以在这个 ACK 应答报文中携带数据,把 "连接确认" 和 "首次数据发送" 合并成一次传输,这也是捎带应答的一种典型应用,既完成了握手流程,又直接开始了数据传输,进一步提升了交互效率。
四、面向字节流

TCP 的 "面向字节流",是指 TCP 将应用层的数据视作一串连续、无边界的字节序列来处理,而不是一个个独立的消息单元。
当我们调用 write 接口发送数据时,这些数据并不会立刻被打包发送,而是先被写入内核的发送缓冲区中;TCP 协议本身会根据 MSS、拥塞窗口、Nagle 算法等规则,自动决定何时、以多大的块将这些数据拆分成报文发送出去。同样,接收方的数据也先到达内核的接收缓冲区,再由应用层通过 read 接口按需读取。这种机制带来了一个核心特点:发送方的 write 调用次数和大小,与接收方的 read 调用次数和大小,完全不需要一一对应。比如发送方可以一次性写入 100 个字节,也可以分 100 次每次写入 1 个字节;接收方则可以一次性读取这 100 个字节,也可以分多次每次读取部分字节,只要最终收到的字节序列完整有序,数据内容就不会有任何变化。
支撑这种 "面向字节流" 特性的关键,就是内核为每个 TCP 连接创建的发送和接收缓冲区。发送缓冲区的存在,让 TCP 可以灵活地 "攒数据",避免了每次小数据都单独发送带来的巨大网络开销;而接收缓冲区则允许应用层按照自己的节奏处理数据,不必担心数据到达速度和处理速度不匹配的问题。这两个缓冲区的存在,让 TCP 的数据传输过程和应用层的读写操作完全解耦,应用层只需要关心要发送的字节序列,而不用操心底层的传输细节。
TCP 的发送缓冲区与接收缓冲区,本质是内核维护的环形队列。从存储结构上来看,缓冲区就是一段连续内存开辟出来的队列空间,严格遵循先进先出的存取规则,完美契合队列的核心特性。发送数据时,应用层把字节数据从队列尾部存入发送缓冲区,内核网络协议栈再从队列头部取出数据进行封装发送;接收数据时,网卡收到的字节数据先存入接收缓冲区队列尾部,上层应用程序再从队列头部依次读取数据进行解析处理。
正因为是队列结构,数据才会保持原有顺序不乱,也才能实现数据暂存、分批存取的效果,这也是面向字节流能够脱离应用层读写次数限制自由拆分合并数据的底层原因。队列可以暂时积压数据,既能够让发送端积攒小数据合并发送减少开销,也能让接收端从容处理 arrived 的数据,不会因为应用读取速度慢直接丢失数据,和缓冲区起到的作用一模一样,所以说这两个缓冲区内核层面就是队列结构。
和 TCP 不同,UDP 是面向数据报的协议,它将每一次 write 发送的数据都视为一个独立的报文单元。UDP 不会对数据进行拆分或合并,发送方 write 多少,接收方就必须一次性 read 多少,否则剩下的数据就会被丢弃。同时,UDP 报文头部包含明确的长度字段,用来标识每个报文的边界,而 TCP 的报头中没有这样的字段,它所有的数据都被视作连续的字节流,这也是两者最本质的区别之一。这种差异也导致了 TCP 会出现粘包、半包问题,而 UDP 则不存在这样的问题,但 UDP 也因此无法提供可靠的传输保障。
五、粘包问题

粘包问题是 TCP 面向字节流特性带来的典型应用层问题,它指的是两个或多个应用层数据包在传输过程中被粘连在一起,导致接收方无法正确区分每个独立的报文边界,要么将多个报文解析成一个,要么将一个完整报文拆分成多个片段,最终造成数据解析错误。从本质上看,TCP 协议头中没有像 UDP 那样专门的报文总长度字段,只有序号字段,传输层只是将收到的报文数据按序号排好序放入内核缓冲区,而应用层看到的只是一串连续无边界的字节流,无法自行判断一个应用层数据包的起始和结束位置,这就为粘包问题的出现埋下了隐患。
要避免粘包问题,核心思路就是在应用层为字节流添加明确的消息边界。对于长度固定的数据包,可以约定每次按固定大小读取,直接从缓冲区中按固定长度依次取出完整报文;对于长度可变的数据包,最常用的方式是在每个消息的头部附加一个存储消息体长度的字段,接收方先读取头部的长度信息,再根据这个长度读取后续的消息体数据,精准定位每个报文的边界;也可以在不同报文之间约定一个特殊的分隔符,只要确保分隔符不会出现在消息体内容中,接收方就可以通过识别分隔符来拆分出完整的报文。
和 TCP 不同,UDP 是面向报文的协议,每个 UDP 报文都携带明确的长度信息,内核会将收到的每个完整报文一次性交付给应用层,应用层要么收到完整的报文,要么收不到,不会出现收到 "半个报文" 的情况,因此 UDP 不存在粘包问题。这种差异的根源就在于两者的设计理念不同:TCP 只保证字节流的有序和完整传输,不负责消息边界的区分,而 UDP 则以独立报文为单位进行传输,天然保留了消息边界,也因此避免了粘包问题的出现。
六、TCP异常退出情况
问题 1:双方正常通信时,一方进程突然退出,连接会怎么处理?
一方进程正常退出或被终止时,操作系统会自动释放相关文件描述符,并触发 TCP 发送 FIN 报文,双方会按照标准的四次挥手流程完成连接关闭,和主动调用 close () 关闭连接的行为没有本质区别。
问题 2:双方正常通信时,一方机器突然重启,连接会怎么处理?
机器重启前会先关闭所有进程,最终依然会触发正常的四次挥手流程断开连接,和进程退出的处理逻辑相同。
问题 3:客户端突然拔掉网线或机器断电,连接会怎么处理?
这种情况下,客户端没有机会发送 FIN 报文,服务器端会误以为连接依然存在,形成 "半开连接"。TCP 内置的保活定时器会定期向长时间不活跃的连接发送探测报文,如果多次未收到应答,就会判定连接失效并主动关闭;如果客户端后续恢复网络并发送数据,服务器会直接回复 RST 报文,客户端收到后会重置连接,需要重新发起请求。
问题 4:服务端先断网,客户端仍认为连接存在并发送数据,会发生什么?
当客户端发送数据时,服务器会因找不到对应连接而回复 RST 报文,客户端收到后会立即重置连接,后续需要重新发起连接请求才能继续通信。
除了 TCP 自带机制,还有什么方式可以检测异常连接?
很多应用层协议会实现独立的心跳检测机制,比如 HTTP 长连接、QQ 等应用,会通过定期发送心跳包主动确认对方在线状态,进一步提升连接的可靠性,避免无效连接长期占用资源。
七、TCP小结
至此关于 TCP 的所有内容我们就讲完了。

TCP 协议之所以设计得如此复杂,核心目的就是要在保证数据传输绝对可靠的同时,尽可能地提升传输性能,让两者达到精妙的平衡。我们之前学习的所有机制,都可以归入这两大目标之下。
首先,为了实现可靠性,TCP 设计了一整套机制:
- 校验和:通过对数据进行校验,确保传输过程中数据没有发生损坏。
- 序列号与确认应答:这是可靠性的基石。序列号保证数据按序到达,同时实现去重;确认应答则让发送方知道哪些数据已经被对方成功接收。
- 超时重传:当发送方发送的数据在指定时间内没有收到应答,就会认为数据丢失并重传,保证数据最终一定能到达。
- 连接管理:通过三次握手建立连接、四次挥手关闭连接,确保通信双方在开始和结束时状态完全同步,避免数据混乱。
- 流量控制:接收方通过告知发送方自己的接收窗口大小,防止发送方发送过快,超出接收方的处理能力,导致数据溢出丢失。
- 拥塞控制:通过慢启动、拥塞避免、快重传、快恢复等算法,动态调整发送速率,避免网络发生拥塞,从根本上减少因网络拥堵导致的丢包和重传。
其次,为了在保证可靠的基础上提升性能,TCP 也做了大量优化:
- 滑动窗口:允许发送方连续发送多个报文,而不用每发一个就等待一个确认,极大提高了传输效率,是实现流水线传输的核心。
- 快速重传:当发送方连续收到三次重复的确认应答,就提前判断数据丢失,无需等待超时,立刻重传,减少了不必要的等待时间。
- 延迟应答:接收方不必每收到一个报文就立刻回复确认,可以稍作延迟,把多次确认合并成一次发送,减少了 ACK 报文的数量,降低了网络开销。
- 捎带应答:当接收方需要回复数据时,会把对之前数据的确认应答 "捎带" 在数据报文中一起发送,同样减少了单独发送 ACK 的次数,提升了效率。
除此之外,TCP 还依赖各种定时器来维护整个协议的运转,比如超时重传定时器、保活定时器、TIME_WAIT 定时器等,它们在后台默默处理着连接的状态维护和异常清理工作,这些工作都由操作系统在内核中完成,用户态的应用程序通常感知不到,但它们是 TCP 稳定运行不可或缺的部分。
总的来说,我们学习的所有 TCP 机制,最终都服务于 "可靠" 和 "高效" 这两个目标。从面向字节流的特性,到由此引出的粘包问题,再到各种复杂的控制算法,它们共同构成了 TCP 协议的完整体系,也正是这些设计,让 TCP 成为了互联网中最可靠、应用最广泛的传输层协议。
谢谢大家的观看!