目录
[2.1 理解丢包的本质](#2.1 理解丢包的本质)
[2.2 超时时间如何确定](#2.2 超时时间如何确定)
[3.1 深度理解 send/write 和发送缓冲区](#3.1 深度理解 send/write 和发送缓冲区)
[3.2 理解滑动窗口](#3.2 理解滑动窗口)
[3.3 理解结合滑动窗口发送报文的流程](#3.3 理解结合滑动窗口发送报文的流程)
[3.4 滑动窗口机制的进一步理解------发送缓冲区的空间复用](#3.4 滑动窗口机制的进一步理解——发送缓冲区的空间复用)
[3.5 滑动窗口机制的进一步优化------快速重传机制](#3.5 滑动窗口机制的进一步优化——快速重传机制)
[3.6 滑动窗口的大小如何确定](#3.6 滑动窗口的大小如何确定)
[4.1 零窗口探测](#4.1 零窗口探测)
[4.2 16 位窗口大小的限制与窗口扩大因子](#4.2 16 位窗口大小的限制与窗口扩大因子)
[5.1 拥塞窗口](#5.1 拥塞窗口)
[5.2 TCP 如何判断网络是否拥塞](#5.2 TCP 如何判断网络是否拥塞)
[5.3 慢启动](#5.3 慢启动)
[5.4 拥塞避免](#5.4 拥塞避免)
[5.5 发生网络拥塞后怎么办](#5.5 发生网络拥塞后怎么办)
前言
在上一篇文章中,我们介绍了 TCP 协议的基本概念以及 TCP 报头格式,对 TCP 报头中的各个字段进行了详细介绍。
我们知道,TCP 报头中存在许多非常重要的字段,例如:
- 序号:用于标识 TCP 字节流中的数据位置;
- 确认序号:用于表示接收方已经成功接收的数据范围;
- 窗口大小:用于告知发送方当前能够接收的数据量;
- 标志位:用于表示当前 TCP 报文的作用和连接状态。
但是,如果仅仅知道这些字段分别表示什么,实际上还没有真正理解 TCP 是如何工作的。
例如,我们会产生许多问题:
客户端发送的数据如果在网络中丢失了,TCP 是如何发现数据丢失的?
如果客户端没有收到服务器的确认,是不是就一定意味着数据没有到达服务器?
客户端连续发送多个 TCP 报文后,服务器是如何确认已经收到哪些数据的?
如果发送方的发送速度远远大于接收方的处理速度,TCP 又是如何避免接收缓冲区被大量数据占满的?
如果网络本身发生拥塞,TCP 又是如何判断当前网络状况,并调整自己的发送速度的?
这些问题都不是某一个 TCP 报头字段单独完成的,而是由 TCP 中的一系列机制共同完成的。
因此,在上一篇文章认识了 TCP 报头之后,本文将进一步从TCP 的工作机制出发,深入理解 TCP 是如何实现可靠、高效的网络通信的。
本文主要介绍以下几个重要机制:
- 确认应答机制
- 超时重传机制
- 滑动窗口机制
- 流量控制机制
- 拥塞控制机制
- 延迟应答机制
- 捎带应答机制
这些机制并不是彼此完全独立的,而是相互配合,共同构成了 TCP 的核心工作方式。
例如,上一篇文章中介绍的序号和确认序号 ,在确认应答、超时重传以及滑动窗口等机制中都会发挥重要作用;而上一篇文章介绍的窗口大小字段,则是 TCP 实现流量控制的重要基础。
所以,从这一篇开始,我们不再只是关注:
"TCP 报头中的这个字段是什么?"
而是进一步研究:
"TCP 为什么需要这个字段,以及这些字段是如何协同工作的?"
接下来,我们首先从 TCP 最基础的可靠性机制------确认应答机制开始。
一、确认应答机制
在上篇文章中,我们介绍了确认应答的过程 ,即发送端发送一个 TCP 报文,客户端收到它时,会给发送端发送一个 ACK 报文。(ACK 报文:它本质就是一个 TCP 报文,只不过 TCP 报头中 ACK 标志位置为 1,告诉对端该报文是一个应答报文,表明确认序号有效,即说明发送 ACK 报文的一端,告诉对端 "我收到了你发送过来的数据 ")。
在 TCP 协议中,TCP 将发送缓冲区中每个字节的数据都进行了编号,即序列号。


对于发送出去的 TCP 报文,它的序号为要发送数据的起始序号。(例如上图中,主机 A 发送的第一个 TCP 报文,它的序号为 1)
当主机 B 接收到主机 A 发送的 TCP 报文,主机 B 会给主机 A 发送一个 ACK 报文。该 ACK 报文的确认序号为接收的 TCP 报文的序号 + 接收的 TCP 报文的数据长度。
再次明确并加深确认序号的定义:接收方已经收到指定序号之前的所有数据,下一次希望接收的数据序号。
TCP 最基础且最重要的机制 ------ 理解确认应答机制的过程和本质
主机 A 向主机 B 发送 TCP 报文时,如果主机 A 收到该报文的确认应答,那么主机 A 就一定能保证发送的数据被主机 B 已经接收。如果主机 A 没有收到该报文的确认应答,那么对于主机 A 来讲,主机 B 可能收到,也可能没有收到。对于主机 A 来讲,为了保证数据传输的可靠性,就必须通过某种机制来重新发送。
总结一句话:单独的确认应答机制不能保证数据最终一定到达,但可以保证确认应答报文中确认序号前的数据一定到达。
二、超时重传机制
在确认应答机制中,我们分析到:对于发送方,如果没有收到应答报文,发送方无法保证发送的 TCP 报文是否被接收方接收,所以发送方需要重新发送该 TCP 报文,而发送方重新发送该 TCP 报文的机制被称为超时重传机制。
2.1 理解丢包的本质
对于发送方来讲,如果收到应答报文,就一定保证接收方收到数据。如果没有收到应答报文,存在两种丢包情况:
1. 发送方发送的 TCP 报文在网络传输过程中丢失

2. 接收方接收到 TCP 报文,接收方发送的 ACK 报文在网络传输过程中丢失

这两种情况都导致一种结果,发送方无法一定保证接收方接收到 TCP 报文。而 TCP 是可靠的,所以对于这两种情况,发送方都需要重新发送该 TCP 报文。
确认序号功能之一 ------ 去重
对于情况 2,细想的人肯定能发现问题:对于接收方,它难道要接收已经接收过的报文吗?如果它不接收,是根据什么来判断报文应不应该接收?
对于接收方,它是不会接收已经接收过的报文,它是通过确认序号来判断的。以上图为例,发送方第一次发送,接收方收到报文后,接收方会将确认序号维护起来,它的值为 1001,对于发送方第二次发送,发送方发送的 TCP 报文序号为 1,接收方接收到该报文,会根据确认序号来判断该报文是否已经收到,如果已经收到就直接丢弃。
2.2 超时时间如何确定
那么TCP 中的超时时间是如何确定的呢?
理想情况下,我们希望找到一个合适的超时时间,使得发送方能够在该时间内正常收到接收方的 ACK。但网络环境是不断变化的,TCP 报文从发送方到接收方,再从接收方返回发送方所需要的时间并不是固定的,因此超时时间也不能简单地设置成一个固定值。
如果超时时间设置得太长,那么即使 TCP 报文已经丢失,发送方也需要等待较长时间才能进行重传,降低通信效率;如果设置得太短,又可能导致 ACK 只是正常传输时间较长,却被发送方误认为报文丢失,从而进行不必要的重传。
因此,TCP 会根据网络通信过程中测量得到的**往返时间(RTT)**动态计算合理的超时时间。
当发生超时重传后,TCP 还会逐渐增加后续的等待时间。例如,可以简单理解为:
cpp
第一次超时:等待一段时间
↓
第二次超时:等待更长时间
↓
第三次超时:继续延长等待时间
↓
...
这种随着超时重传不断增加等待时间的方式,可以避免在网络出现异常或拥塞时频繁进行重传,从而减少对网络资源的进一步占用,但超时时间并不会一直延长,当重传累计到依次次数时,TCP 认为网络或者对端主机出现异常,不再进行重传,而强制关闭连接。
具体的超时时间计算以及重传次数限制由 TCP 实现决定,这里不再深入展开。
三、滑动窗口机制
在前面的确认应答机制中,我们知道,接收方会通过 ACK 对发送方发送的数据进行确认;在超时重传机制中,我们又知道,如果发送方在一定时间内没有收到 ACK,就需要重新发送数据。
但是,如果 TCP 每发送一个 TCP 报文,都必须等待接收方的 ACK 到达之后,才能继续发送下一个 TCP 报文,那么通信效率会非常低。
例如:

发送方会消耗大量的时间在等待 ACK 上。
尤其在网络延迟较高的情况下,即使发送方和接收方本身都具有很强的处理能力,也无法充分利用网络带宽,那么 TCP 能不能在等待 ACK 的过程中,继续发送后面的数据呢?
答案是可以,TCP 不需要每发送一个 TCP 报文就需要等待对应的 ACK,而是允许发送方在没有收到 ACK 的情况下,连续发送多个 TCP 报文。即发送方可以同时发送一批数据,并通过后续收到的 ACK 确认哪些数据已经被接收。
TCP 用于管理这部分"已经发送但没有收到确认的数据"的机制,被称为滑动窗口机制。
3.1 深度理解 send/write 和发送缓冲区

对于每个 TCP Socket 都有一个发送缓冲区,用来将应用层要发送的数据发送到对端主机,send/write 的本质是将应用层数据拷贝到内核级的发送缓冲区中,对于发送缓冲区中的数据,什么时候发送到网络,一次性发送多少数据,数据丢失怎么办,全由操作系统基于 TCP 协议决定。而send/write 的作用只是将应用层数据拷贝到内核级发送缓冲区,而不是将数据直接发送到网络。
为了更容易理解滑动窗口机制,我们目前将发送缓冲区想象为一个 char bufferN ,所以对于发送缓冲区,我们可以明确将数组划分为三个部分:

3.2 理解滑动窗口
理解滑动窗口之前,我们首先需要理解什么是窗口。对于学过滑动窗口算法的朋友,TCP 中的滑动窗口机制的滑动窗口思想与算法中滑动窗口的思想一致。达到某种条件进行出窗口或者入窗口。
对于窗口的理解,我们可以把它理解为一端连续的子数组,例如:
发送方需要向接收方发送一段数据:1 2 3 4 5 6 7 8 9 10
假设当前发送窗口大小为:4,那么发送方一次最多可以发送:1 2 3 4
即发送的数据量 <= 发送窗口的大小,窗口的本质就是要发送总数据中,一段连续的数据。
在 TCP 协议中,窗口就可以理解为:发送缓冲区的中间部分,可以直接发送,暂时不需要确认的数据。
那么它为什么被称作滑动窗口呢?因为随着发送方不断收到 ACK,已发送未确认的数据会不断变为已发送已确认的数据,那么对应的窗口左边界就会不断向右移动,且右边界也会向右移动。所以对于这种不断滑动的窗口,被称为滑动窗口。
在 TCP 协议中,滑动窗口的本质就是发送缓冲区的一部分,这部分数据的特点是:可以直接发送,暂时不需要确认的数据。
对于滑动窗口的进一步理解:允许 TCP 在等待 ACK 的过程中继续发送一定范围内的数据,进而减少等待时间,提高网络通信效率。
3.3 理解结合滑动窗口发送报文的流程

根据 TCP 中滑动窗口的定义,很容易理解知道滑动窗口在上述通信过程中最大值为 4000 字节,对于滑动窗口内的数据,可以直接发送,暂时不需要确认,所以呈现出发送方发送 1 ~ 1000、1001 ~ 2000、2001 ~ 3000,3001 ~ 4000 的数据时,不需要任何 ACK,而直接发送,对于收到第一个 ACK 后,滑动窗口向右滑动,继续发送第 4001 ~ 5000 的数据,依次类推。
对于上述发送过程如果丢包了应该怎么解决,滑动窗口会不会跳过未接收到的报文进行滑动?
对于丢失情况,我们首先可以分为三类:最左侧丢失 中间丢失 最右侧丢失
(1)最左侧丢失
在超时重传机制中,我们讲到发送方没有收到应答,有两种情况:数据报文丢失和 ACK 丢失
- 数据报文丢失

当滑动窗口的最左侧数据报文丢失时,主机 A 滑动窗口内发送的其他数据报文如果被主机 B 收到,那么主机 B 会给主机 A 发送 ACK 1 号报文,告诉主机 A ,1 号之前的报文我已经收到,你需要从 1 号开始发送,主机 A 通过超时重传机制把 1 号继续发送给主机 B ,从而保证报文的可靠性。
此时主机 A 的滑动窗口的左边界不会发送改变,它需要收到最左侧报文的确认应答才能向右滑动。
而对于主机 B ,它根据确认序号来判断其他报文是已经收到的报文,还是未收到的报文,如果是已经收到的报文,主机 B 会进行丢弃,如果是未收到的报文,主机 B 会将其他报文缓存起来,没有先交付给上层,待最左侧报文到来时,按序交付给上层,从而完成报文有序到达的可靠性。
- ACK 丢失

当最左侧应答报文丢失时,对于主机 B 来讲,主机 B 会对接收到的所有报文做应答,只要主机 B 对报文做了应答,无论主机 A 是否收到应答,这都不重要,因为主机 B 已经收到了数据,主机 B 只希望收到新的数据。即主机 B 对主机 A 应答时,它的确认序号会一直增大,代表主机 B 已经收到了哪些数据。对于主机 A 来讲,它未收到最左侧的应答报文,但收到了后续报文的应答报文,主机 A 通过 ACK 报文的确认序号也可以知道,主机 A 应该从哪个数据开始发送。即当最左侧应答报文丢失,其他报文的应答报文收到,主机 A 明确知道最左侧报文被主机 B 收到,即会更新将要发送的序号更新为最大的确认序号。所以对于滑动窗口来讲,滑动窗口的左边界会向右滑动,来不断发送新的数据。
对于(2)中间报文丢失,(3)最右侧报文丢失这两种情况,我们都可以将其转化为(1)最左侧报文丢失,因为当中间报文丢失时,滑动窗口的左边界会移动到丢失报文的位置,进而转化为最左侧报文丢失,对于最右侧报文丢失,亦是如此。
3.4 滑动窗口机制的进一步理解------发送缓冲区的空间复用
我们之前把发送缓冲区想象成一个 char bufferN ,那么随着发送报文不断增多,滑动窗口可以一直向右滑动吗?
当时我们把发送缓冲区分成了三部分:

其实对于已经发送并且得到确认的数据,发送方已经不再需要这些数据,因此对应的缓冲区空间就可以被释放并重新利用,用来存放后续需要发送的新数据。
所以,我们可以进一步借助环形队列来理解发送缓冲区的空间复用。可以将发送缓冲区抽象成一个环形空间,而滑动窗口只是其中当前允许发送的一部分区域。

3.5 滑动窗口机制的进一步优化------快速重传机制
在 3.3 小节中我们讲到,发送方可以根据滑动窗口连续发送多个 TCP 报文,不再需要每发送一个报文都等待 ACK,从而大幅提高了网络通信效率。
但是,滑动窗口又带来了一个新的问题:

如果发送方发送的一个报文丢失,而其他报文到达接收方主机,接收方发送的 ACK 报文的确认序号只能是最小的那个丢失报文,所以发送方滑动窗口的左边界就不能向后滑动,那么滑动窗口要想滑动就需要通过超时重传机制重新发送丢失的报文,收到应答后,才能向后发送数据,如果只采用此方式,可靠性没有问题,但是效率有所损失。
所以 TCP 又引入了一个新的机制:快速重传机制

在上图中,发送方发送的 1001 ~ 2000 数据丢失,而其他数据到达,接收方接收到丢失数据序号后面序号的数据时,会不断向发送方发送:ACK = 1001
这些 ACK 被称为重复确认。
对于发送方来说,如果连续收到多个相同的 ACK,就说明:接收方一直在等待某一个数据,而后面的数据已经到达。因此,发送方可以推断:这个数据很可能已经在网络传输过程中丢失。
那么,发送方就不需要等待超时再重发报文,而是根据重复 ACK 的数据直接发送报文。
即TCP 可以在收到一定数量的重复 ACK 后,直接重传可能丢失的数据,以致于进一步提升网络通信效率,被称为快速重传机制。
注:快速重传机制属于对超时重传机制的进一步优化,是"锦上添花",而不是"雪中送炭"。快速重传可以在检测到数据丢失后提前进行重传,减少等待超时的时间,但它并不能覆盖所有丢包场景。例如,当数据报文丢失后,后续没有新的数据到达,接收方无法产生足够的重复 ACK,此时仍然需要依靠超时重传机制。因此,超时重传机制是 TCP 实现可靠传输不可或缺的基础机制,而快速重传是在此基础上的性能优化。
3.6 滑动窗口的大小如何确定
前面我们已经知道,滑动窗口允许发送方在没有收到 ACK 的情况下,连续发送一定范围的数据。那么问题来了:滑动窗口的大小应该为多大?
让我们思考一下:窗口太小会有什么后果?窗口太大会有什么后果?
窗口如果太小,发送方能够连续发送的数据就比较少,发送方仍然需要频繁等待 ACK,网络带宽无法得到充分利用,影响效率。
窗口如果太大,发送方能够连续发送的数据量就比较多,可能导致接收方来不及处理数据,使数据大量堆积在接收方的接收缓冲区中,甚至当缓冲区满时,出现接收方丢弃,发送方重传的现象,造成效率降低和网络资源的浪费。
对于窗口如果太大的情况,我们在上一篇文章中知道TCP 报头中存在一个字段:16 位窗口大小
它表示接收方当前允许发送方发送的数据量,即 TCP 接收缓冲区中剩余空间的大小。
我们目前就可以得出一个结论:
TCP滑动窗口的大小取决于对方的接收能力,即滑动窗口的大小 = 16 位窗口大小
因此,TCP 滑动窗口的大小是需要根据通信双方的实际情况动态变化的。
但 TCP 协议不仅仅考虑了双方通信的问题,还考虑了网络环境的因素:
即接收方能够接收多少数据(rwnd,接收窗口)和网络当前能够承受多少数据(cwnd,拥塞窗口)
发送方实际能够发送的数据量,即滑动窗口的大小不能超过这两个窗口所允许的范围,因此:
滑动窗口的大小 = min(rwnd, cwnd)
rwnd(Receive Window):接收窗口,接收方将自己的接收缓冲区剩余空间的大小告诉发送方,用于进行流量控制。
cwnd(Congestion Window):拥塞窗口,由发送方根据网络拥塞情况进行调整,用于进行拥塞控制。
四、流量控制机制
对于流量控制机制的理解,在上一篇文章中:深入理解TCP协议(一):TCP报文格式详解 讲述 TCP 报头字段中的 16 位窗口大小 时,已经详细说明了:流量控制是什么、为什么需要流量控制,以及 TCP 如何通过窗口大小实现流量控制。
因此,本章节不再重复介绍这些基础概念,而是进一步理解 TCP 流量控制是如何工作的。
前面我们知道, TCP 报头中的 16 位窗口大小实际上表示的是:
接收方接收缓冲区剩余空间的大小
接收方会将自己的接收能力通过 TCP 报文中的窗口大小字段告知发送方,发送方根据这个窗口大小控制自己的发送量。
对于双方的接收能力的交互,在三次握手的过程中就已经明确了,而后续双发的接收能力会不断通过 ACK 报文告知对方,进而动态调整窗口大小,进行流量控制。
但很多人就会想到一个问题:如果接收方接收缓冲区满了,就会将 16 位窗口大小设置为 0;这时发送方收到 ACK 后,会将滑动窗口大小设置为 0,不再发送数据。那么,当接收方将接收缓冲区中的数据交付给应用层处理后,接收缓冲区重新出现了空闲空间,发送方又是如何知道接收方已经有能力继续接收数据的呢?
4.1 零窗口探测

当发送方发现接收方通告的窗口大小为 0 时,并不会永远处于等待状态,而是会启动一个持续计时器,经过一段时间后,发送方会主动发送一个的 TCP 报文(不携带数据),询问接收方当前的窗口大小。接收方收到这个报文后,可以通过 ACK 报文告知当前的窗口大小。
注:零窗口检测并不是为了传输正常的数据,而是避免发送方因为接收方通知零窗口而永久陷入等待状态。
4.2 16 位窗口大小的限制与窗口扩大因子
那么问题来了,TCP 报头中的窗口大小字段只有 16 位,最大只能表示 65535,那么 TCP 的窗口最大是不是只有 65535 字节呢?
实际上并不是。在 TCP 报头的选项字段 中存在一个窗口扩大因子(Window Scale),用于扩大 16 位窗口大小字段所能表示的范围。
实际窗口大小可以简单理解为:实际窗口大小 = 窗口字段值 × 2^M
其中 M 就是窗口扩大因子。
例如:
窗口字段 = 10000
M = 2
实际窗口大小 = 10000 × 2² = 40000 字节
通过窗口扩大因子,TCP 就可以突破 16 位窗口字段 65535 字节,从而支持更大的接收窗口。
窗口扩大因子会在TCP 建立连接的过程中进行协商,连接建立后双方按照协商的扩大因子解释窗口大小。
所以,TCP 报头中的 16 位窗口大小并不代表 TCP 实际窗口的最大值只有 65535 字节,窗口扩大因子可以进一步扩大实际窗口。
五、拥塞控制机制
在前面的流量控制机制中,TCP 解决了一个问题:如果发送方发送数据的速度太快,而接收方处理数据的速度较慢,该怎么办?
但是,流量控制只能解决接收方的处理能力问题。假设现在接收方拥有足够大的接收缓冲区,应用程序处理数据的速度也足够快,那么是不是发送方就可以不断提高发送速度呢?
答案显然是否定的。因为发送方和接收方之间还存在着一个非常重要的因素:网络本身的承载能力

我们首先要建立一个正确的认知:对于当今的网络,在同一个网络环境中,少量发送方一般不会轻易造成严重的网络拥塞。但是,当大量主机同时进行网络通信时,共享的网络带宽和网络设备处理能力就可能成为瓶颈。
现实生活中也存在类似的情况。例如,当我们外出旅游时,在旅游景点的网速往往会明显下降。其本质就是在同一网络环境中,同时进行网络通信的用户数量增加,网络资源被大量占用,从而导致网络拥塞。
对于网络拥塞这种情况,发送方发送的 TCP 报文可能会发生大量丢失,并且无法在超时时间内收到应答。如果发送方对此置之不理,仍然按照原来的速度不断发送,甚至在发生丢包后继续进行大量重传,就会进一步增加网络压力,使网络中的数据包不断堆积,从而进一步降低网络通信效率。
因此,TCP 必须考虑网络拥塞的问题:
如何根据网络当前的状况,动态调整发送方的发送速度?
这就是 TCP 中的拥塞控制机制。
5.1 拥塞窗口
那么问题来了:
TCP 是如何根据网络当前的状况,动态调整发送方的发送速度的呢?
TCP 中存在一个非常重要的概念:拥塞窗口(Congestion Window,cwnd)。
前面在讲流量控制时,我们知道,TCP 通过接收方通告的窗口大小 来限制发送方的发送量。这个窗口主要考虑的是:接收方还能接收多少数据。
而拥塞窗口考虑的是:当前网络能够承受多少数据。
因此,发送方实际能够发送的数据量,同时受到这两个因素的限制:
cpp
接收窗口 rwnd
↓
接收方能接收多少数据
拥塞窗口 cwnd
↓
网络能承受多少数据
实际发送滑动窗口可以简单理解为:
实际发送滑动窗口 = min(rwnd, cwnd)
例如:
rwnd = 10000
cwnd = 4000
虽然接收方还能够接收
10000字节的数据,但是 TCP 判断当前网络只能承受4000字节,因此发送方最多只能发送4000字节。反过来,如果:
rwnd = 4000
cwnd = 10000
那么发送方同样只能发送
4000字节,因为此时限制发送速度的是接收方。
所以可以简单理解为:
流量控制关注的是接收方,拥塞控制关注的是网络。
5.2 TCP 如何判断网络是否拥塞
那么问题又来了:
发送方又无法直接看到整个网络的状态,它是如何知道网络是否发生拥塞的呢?
实际上,TCP 并不能直接知道网络内部发生了什么,而是通过数据传输过程中表现出来的现象来判断网络状况。
最典型的情况就是:数据包发生丢失。
此时,TCP 会认为:当前网络可能已经无法承受之前的发送速度。
因此需要降低拥塞窗口 cwnd,降低发送速度。反过来,如果发送方持续发送数据,并且数据都能够正常得到确认,那么 TCP 就可以认为当前网络状况较好,逐渐增大 cwnd,尝试提高发送速度。于是,TCP 就形成了一个不断 试探网络承载能力的过程:
cpp
网络状况良好
↓
增大 cwnd
↓
提高发送速度
↓
继续观察网络
↓
发生大量丢包
↓
减小 cwnd
↓
降低发送速度
↓
重新尝试增加 cwnd
这就是 TCP 拥塞控制的核心思想。
5.3 慢启动
既然 TCP 需要不断试探网络的承载能力,那么在 TCP 刚开始发送数据的时候,应该设置多大的cwnd 呢?
如果一开始就设置一个非常大的拥塞窗口:cwnd = 很大
然后立即向网络发送大量数据,那么很可能一开始就造成网络拥塞。
因此,TCP 在刚开始进行数据传输时,会从一个较小的拥塞窗口开始,逐渐增加拥塞窗口。
这个过程被称为:慢启动(Slow Start)
这里的"慢"并不是说 TCP 的传输速度很慢,而是指:TCP 不会一开始就以非常大的发送量冲击网络,而是逐渐增加发送量,探索当前网络能够承受的范围。
在慢启动阶段,拥塞窗口会快速增长,可以简单理解为:
cpp
cwnd:1 -> 2 -> 4 -> 8 -> 16 -> 32
也就是说,随着数据不断得到确认, cwnd 会快速增加,整体上呈现出指数增长的趋势。
5.4 拥塞避免
但是,如果一直按照这种速度增长:
1 → 2 → 4 → 8 → 16 → 32 → 64 → 128 → ...
那么 cwnd 很快就会变得非常大,最终可能超过网络的承载能力。
因此,TCP 不会让慢启动无限进行。
当 cwnd 增长到一定程度后,TCP 会进入:拥塞避免(Congestion Avoidance)
在拥塞避免阶段,TCP 会更加谨慎地增加 cwnd,不再像慢启动阶段一样快速增长。
简单理解就是:
慢启动:
1 → 2 → 4 → 8 → 16 → 32 快速增长
拥塞避免:
32 → 33 → 34 → 35 → 36 → ... 缓慢增长
所以可以把这两个阶段理解为:慢启动负责快速探索网络的承载能力,拥塞避免负责在接近网络承载能力后更加谨慎地提高发送速度。
5.5 发生网络拥塞后怎么办
如果 TCP 在不断增加 cwnd 的过程中发现了丢包,那么说明当前的发送速度可能已经超过了网络的承载能力。此时 TCP 就需要降低 cwnd。
而 TCP 前面已经学习过两种发现丢包的方式:超时重传和快速重传
如果发送方长时间没有收到 ACK,最终发生超时,说明网络可能出现了比较严重的问题,此时 TCP 会大幅降低发送速度,并重新进入慢启动。
而如果发送方通过重复 ACK发现数据丢失,那么可以通过快速重传提前进行重传,不需要等待超时。
这种情况下,TCP 对拥塞窗口的调整不会像超时那样激进,而是进行相对温和的调整。
这里我们不需要深入具体的窗口调整公式,只需要理解:
发生丢包 → TCP 判断网络可能拥塞 → 减小拥塞窗口 → 降低发送速度。
之后,如果网络逐渐恢复正常,TCP 又会重新增加 cwnd,继续探测网络的承载能力。
因此,TCP 的拥塞控制实际上是一个不断循环的过程:
cpp
增加发送速度
↓
探测网络能力
↓
网络是否正常?
↙ ↘
正常 拥塞
↓ ↓
继续增加 cwnd 减小 cwnd
↓ ↓
重新探测
六、延迟应答机制
在前面的确认应答机制中,我们讲过:**接收方每收到一个 TCP 数据报文,就会向发送方返回一个 ACK。**这种方式虽然能够保证数据可靠传输,但是如果每收到一个 TCP 报文就立即发送一个 ACK,那么网络中就会产生大量 ACK 报文,增加网络通信的额外开销。
因此,TCP 对确认应答进行了进一步优化:延迟应答机制。
延迟应答并不是收到数据后立即发送 ACK,而是暂时等待一小段时间。
在等待期间,如果又收到了新的数据,那么接收方可以利用一个 ACK 对多个数据进行确认,从而减少 ACK 报文的数量。

延迟应答与流量控制
除此之外,延迟应答还可能对流量控制 产生一定的帮助。接收方收到 TCP 数据后,数据会被放入 TCP 接收缓冲区。如果接收方稍微等待一段时间,在这段时间内应用层可能读取了接收缓冲区中的数据,那么接收缓冲区就会出现更多空闲空间。此时,接收方再发送 ACK,就可以在 ACK 中携带更新后的窗口大小 ,告诉发送方:"我现在还能接收更多数据。"
因此,在一定情况下,延迟应答不仅可以减少 ACK 报文数量,还可能让接收方在发送 ACK 时通告一个更大的接收窗口,从而提高数据传输效率,
不过需要注意:延迟应答的核心目的仍然是减少 ACK 的数量,而不是专门为了扩大窗口大小。
七、捎带应答机制
捎带应答的核心思想,就是在发送 ACK 的同时,如果本机恰好也有数据需要发送,那么可以将 ACK 和数据放在同一个 TCP 报文中发送,从而减少单独 ACK 报文带来的网络开销。
对于捎带应答机制的过程理解,在上一篇文章:深入理解TCP协议(一):TCP报文格式详解 中,我们已经结合 TCP 报头中的序号和确认序号进行了详细介绍,这里不再重复说明。
本章节主要介绍 TCP 为了提高网络通信效率而采用的相关机制,因此,这里将捎带应答机制作为补充,保证本章节内容的完整性。
八、总结
到这里,TCP 的数据传输相关机制就介绍完了。
从上一篇文章对 TCP 报头的介绍开始,我们了解了 TCP 的基本结构以及各个字段的作用;在本篇文章中,我们进一步从数据传输的角度,深入理解 TCP 是如何保证数据可靠传输,并尽可能提高网络通信效率的。
TCP 并不是依靠某一个单独的机制来实现可靠通信,而是通过多个机制相互配合:
cpp
确认应答机制 -> 确认数据是否接收
超时重传机制 -> 数据丢失后重新发送
滑动窗口 -> 允许连续发送多个数据,提高传输效率
快速重传 ->尽快发现丢失的数据
流量控制 -> 防止发送方发送速度超过接收方的处理能力
拥塞控制 -> 防止发送速度超过网络的承载能力
延迟应答 + 捎带应答 -> 进一步减少网络通信开销
通过这些机制,TCP 在可靠性、传输效率、接收方处理能力以及网络承载能力之间进行协调,使数据能够更加可靠、高效地在网络中传输。
但是,到目前为止,我们讨论的主要都是:TCP 建立连接之后,如何进行数据传输。
一个新的问题随之而来:TCP 的连接究竟是如何建立的?
TCP 是面向连接的协议,在真正进行数据传输之前,通信双方需要先建立连接;当数据传输完成之后,也需要按照一定的规则关闭连接。
那么:
- TCP 为什么需要建立连接?
- 为什么建立 TCP 连接需要三次握手?
- 三次握手分别做了什么?
- TCP 如何维护连接状态?
- 为什么 TCP 关闭连接需要四次挥手?
- 为什么关闭连接后还需要经历
TIME_WAIT? - TCP 连接建立和关闭的过程中,双方的状态又是如何变化的?
这些问题属于 TCP 的连接管理机制。
因此,下一篇文章将从 TCP 的连接管理入手,详细介绍:
三次握手、四次挥手、TCP 状态转换以及连接建立与关闭过程中的相关机制。
同时,也会对 TCP 中一些其他重要但本篇尚未展开的话题进行介绍。
至此,TCP 数据传输相关机制告一段落,下一篇我们继续深入理解 TCP 的连接管理机制。