TCP

TCP 是计算机网络中重点,需要熟练掌握 TCP 的可靠传输机制,包含连接建立和断开、流量控制、拥塞控制,常常在选择题和大题中出现。

TCP 特点

  • 面向连接:发送数据前后需要分别通过三次握手和四次挥手进行连接的建立和断开。
  • 可靠交付:保证数据传输的无差错、不丢失、不重复、有序。
  • 面向字节流:以滑动窗口的形式对字节按照顺序进行发送和接收。
  • 全双工:通信双方在一个 TCP 连接中都可以发送和接收数据。

TCP 首部

Source Port (16)
Source Port (16)
Sequence Number (32)
Sequence Number (32)
Options
Options
Data
Data
Destination Port (16)
Destination Port (16)
Acknowledgement Number (32)
Acknowledgement Number (32)
Checksum (16)
Checksum (16)
Urgent (16)
Urgent (16)
Window (16)
Window (16)
Header
Length (4)
Header...
Reserved (6)
Reserved (6)
Flags (6)
Flags (6)
TCP Header
TCP Header
Bit 0
Bit 0
Bit 15
Bit 15
Bit 16
Bit 16
Bit 31
Bit 31
20 Bytes
20 Bytes
Text is not SVG - cannot display
  1. 源端口号(Source Port):16 位字段,指示发送端的端口号。
  2. 目标端口号(Destination Port):16 位字段,指示接收端的端口号。
  3. 序列号(Sequence Number):32 位字段,用于标识 TCP 报文段中第一个数据字节的序列号。这个字段用于实现 TCP 的可靠性机制,如数据的按序传递和重传。
  4. 确认号(Acknowledgment Number):32 位字段,如果设置了 ACK 标志位,该字段包含了期望接收的下一个数据字节的序列号。这个字段用于确认已经成功接收的数据。
  5. 数据偏移(Data Offset):4 位字段,指示 TCP 首部的长度,以 32 位字为单位。这个字段用于指示首部的长度,因为 TCP 首部长度可以变化,根据选项的存在而变化。
  6. 保留(Reserved):6 位字段,保留供未来使用,目前必须设置为 0。
  7. 控制标志位(Flags):TCP 报文段的控制标志,共有 6 个标志位,它们分别是:
    • URG(紧急指针有效位):用于指示紧急数据。
    • ACK(确认位):用于指示确认号字段有效。
    • PSH(推送位):用于指示接收端应立即交付数据给应用层,而不需要等待缓冲区满。
    • RST(复位位):用于强制释放连接,重置连接状态。
    • SYN(同步位):用于建立连接,用于初始化序列号。
    • FIN(终止位):用于关闭连接。
  8. 窗口大小(Window Size):16 位字段,指示发送端的可用接收窗口大小。接收端可以根据这个字段的值来告诉发送端可以发送多少数据而不会导致拥塞。
  9. 校验和(Checksum):16 位字段,用于检测 TCP 首部和数据部分的传输中的错误。
  10. 紧急指针(Urgent Pointer):16 位字段,仅当 URG 标志位设置时才有效。用于指示紧急数据的末尾位置。
  11. 选项(Options):可选字段,用于包含一些额外的控制信息,如最大报文段长度、时间戳等。长度可变,最长可达 40 字节。
  12. 填充(Padding):根据选项字段的长度而变化,用于确保 TCP 首部的总长度是 32 位的倍数。

三次握手

ESTABLISHMENT
SYN-SENT
CLOSED
发送 SYN
接收到 SYN + ACK,
发送 ACK
ESTABLISHMENT
SYN-RECEIVED
CLOSED
接收到 SYN,
发送 SYN + ACK
接收到 ACK
# 1
# 2
# 3
Client
Server
client state
server state
SYN = 1
seq = x
SYN = ACK = 1
seq = y
ack = x+1
ACK = 1
ack = y+1
LISTEN
调用 listen

TCP 通过 三次握手 建立连接,其具体过程如上图所示,每次握手的状态变化和字段解释如下:

第一次握手(客户端 → 服务端)

  • 客户端状态变化: CLOSEDSYN-SENT
  • 客户端发送: 一个带有 SYN=1 的包,请求建立连接,并选择一个初始 序列号 seq = x
  • 服务端状态: LISTEN,等待连接

第二次握手(服务端 → 客户端)

  • 服务端状态变化: LISTENSYN-RECEIVED
  • 服务端收到 SYN 包后,回应一个 SYN + ACK 包:
    • SYN = 1:表示服务端也同意建立连接
    • ACK = 1:确认客户端的 SYN
    • seq = y:服务端自己的初始 序列号
    • ack = x+1:确认号是客户端 序列号 + 1

第三次握手(客户端 → 服务端)

  • 客户端状态变化: SYN-SENTESTABLISHMENT
  • 客户端收到服务端的 SYN + ACK 后,回应一个 ACK 包:
    • ACK = 1:确认服务端的 SYN
    • ack = y+1:确认号是服务端 序列号 + 1

这三次握手的 主要目的 是:

  1. 客户端确认服务端可达(第一次握手)
  2. 服务端确认客户端可达,并携带自己的 序列号(第二次握手)
  3. 客户端再次确认服务端的响应,确保双向通信建立(第三次握手)
ISN

ISN(Initial Sequence Number,初始序列号) 是连接建立时双方 各自选取的第一个序列号,用于标识数据字节流的起始位置。

ISN 一般设置是随机的,这样可以区分不同的连接和旧连接的残留包,也能防止攻击。

补充

第三次握手可以携带应用层数据么?

当然可以,你可以想一下,如果发送方还需要收到第三次握手的确认才可以发送数据的话,不就变成四次握手了么,这是不合理的。

不同的 TCP 实现对此有不同的处理方式,有的 TCP 实现是不会将数据放到第三次握手的报文中,有的则会,正确的协议实现对于这两种情况应该都能够正确处理。

四次挥手

ESTABLISHMENT
FIN-WAIT-1
FIN-WAIT-2
TIME-WAIT
CLOSED
应用层调用 close 接口,发送 FIN
接收到 ACK
接收到 FIN,
发送 ACK
ESTABLISHMENT
CLOSE-WAIT
LAST-ACK
CLOSED
接收到 FIN,
发送 ACK,
通知应用关闭
应用准备关闭,
发送 FIN
接收到 ACK
# 1
# 2
# 3
# 4
Client
Server
client state
server state
FIN = 1
seq = u
ACK = 1
seq = v
ack = u+1
FIN = ACK = 1
seq = w
ack = u+1
ACK = 1
seq = u+1
ack = w+1

第一次挥手:客户端发起关闭请求

  • 客户端状态变化: ESTABLISHMENTFIN-WAIT-1
  • 客户端动作: 应用层调用 close(),客户端发送 FIN=1, seq=u,表示不再发送数据了,但仍可以接收数据

第二次挥手:服务端确认关闭请求

  • 服务端状态变化: ESTABLISHMENTCLOSE-WAIT
  • 服务端动作:
    • 收到 FIN 后,发送 ACK=1, seq=v, ack=u+1 表示确认
    • 通知上层应用准备关闭
  • 客户端状态变化: FIN-WAIT-1FIN-WAIT-2

第三次挥手:服务端发起关闭请求

  • 服务端动作:
    • 应用层处理完毕,发送 FIN=1, ACK=1, seq=w, ack=u+1
  • 服务端状态变化: CLOSE-WAITLAST-ACK
  • 客户端动作: 收到该 FIN 报文后,进入 TIME-WAIT 状态

第四次挥手:客户端确认服务端关闭

  • 客户端动作: 发送 ACK=1, seq=u+1, ack=w+1
  • 客户端状态变化: TIME-WAIT等待 2 个最大报文生存时间2×MSL)后 → CLOSED
  • 服务端状态变化: 收到 ACK 后 → CLOSED
MSL

最大报文生存时间(MSL, Maximum Segment Length)是报文在网络中可能存在的最长时间,TCP 为了安全地关闭连接,需要等待 2×MSL 时间,以确保网络中的旧报文不会对新连接产生干扰。

那么为什么要等 2×MSL?主要在于两点:

  1. 确保对方收到了 ACK
    如果 ACK 丢了,对方会重发 FIN,此时还在 TIME-WAIT 的一方可以再次发送 ACK。
  2. 清除网络中的旧报文
    等待 2×MSL,可以确保本连接中的所有残留数据段在网络中都已过期,不会影响后续新的连接。
补充

第一次挥手一定是客户端发起么?

第一次挥手是由发起连接关闭的一方发送的,通常情况下是客户端发送。但在某些特殊情况下,服务器端也可以主动发起连接关闭,不过这种情况相对较少见。

补充

为什么握手是三次,挥手是四次?

三次握手
第一次和第二次握手用于让客户端确认其发送的数据能够到达服务端,从而建立起 客户端到服务端 的半双工通信;
第二次和第三次握手则用于让服务端确认其发送的数据能够到达客户端,从而建立起 服务端到客户端 的半双工通信。
至此,TCP 的全双工通信连接正式建立。

四次挥手
第一次和第二次挥手用于关闭 客户端到服务端 的半双工通信。此时,服务端仍可能有数据尚未发送完毕,因此不能立即关闭;
在服务端数据发送完毕后,通过第三次和第四次挥手来关闭 服务端到客户端 的半双工通信,至此连接完全断开。

滑动窗口机制

0
1
...
74
75
76
77
78
79
80
81
82
...
1660
1671
1672
1673
...
4194304
0
1
...
74
75
76
77
78
79
80
81
82
...
1660
1671
1672
1673
...
4194304
发送窗口
接收窗口
已被确认
已发送
未发送
已接收
未接收
所有的应用层数据
使用 seqno 封装下标进行发送

上图展示了 滑动窗口 的基本结构。滑动窗口其实是一个“移动的视口”,它同时描述了 发送方 正在等待确认的已发送数据,以及 接收方 已经接收并准备好继续接收的空闲空间。换句话说,窗口中的每一个字节都是当前正在传输的字节,它们在整个数据流中占据一个连续的区段。

  1. 发送方 根据窗口大小,将窗口范围内的数据一次性发送出去,而不必等到每个报文段的确认返回。
  2. 接收方 在收到这些报文段后,会向 发送方 发送 ACK(确认)报文。该 ACK 包含两个重要信息:
    • 已成功接收的序列号(用来让发送方确认哪些数据已经安全到达)。
    • 接收窗口大小(即接收方当前还能接收的字节数),该值写在 ACK 报文的窗口字段中。
  3. 发送方 在收到 ACK 后,滑动窗口向前移动,窗口左端排除已确认的数据,右端随即腾出新的空间。此时,发送方就可以继续发送后续的数据。

发送和接收窗口

13
13
14
14
15
15
16
16
17
17
18
18
19
19
20
20
21
21
22
22
23
23
24
24
25
25
26
26
27
27
28
28
29
29
30
30
Usable
Window
Usable...
TCP
Send Window
TCP...
Sent and Acked
Sent and Acked
Sent and  not acked
Sent and  not acked
Not sent
Recipient ready to recv
Not sent...
Not Sent
Recipient not ready to recv
Not Sent...
* * *
* * *
* * *
* * *
Text is not SVG - cannot display

TCP 的 发送窗口 可以按照逻辑划分为四个部分:

  1. 已经发送并且被确认的数据(字节流)
  2. 已经发送但还没有被确认
  3. 尚且还没有发送
  4. 暂时不可以发送

其中第 2、3 个部分构成 TCP 的发送窗口,当发送方收到 ackno 在第 2 个部分内的确认报文时,调整滑动窗口的大小后向前移动滑动窗口,并且发送接下来可以发送的数据。

13
13
14
14
15
15
16
16
17
17
18
18
19
19
20
20
21
21
22
22
23
23
24
24
25
25
26
26
27
27
28
28
29
29
30
30
Available
Window
Available...
TCP
Recv Window
TCP...
Received and Accepted
by application
Received and Accepted...
Received but not accepted
Received but not accepted
Not Received
Not Received
Not ready to be received
Not ready to be received
* * *
* * *
* * *
* * *
Text is not SVG - cannot display

TCP 的 接收窗口 可以按照逻辑划分为四个部分:

  1. 已经被应用层接收的数据
  2. 已经被 TCP 接收,但是还没有被应用层接收的数据
  3. 还没有接收到的数据
  4. 还不可以接收到的数据

窗口大小

在 TCP 传输过程中,窗口的概念决定了发送方能够“在等待确认之前”一次性发送多少数据。常见的窗口有三类:

  • 发送窗口(swnd,send window)
  • 接收窗口(rwnd,receive window)
  • 拥塞窗口(cwnd,congestion window)

下面分别说明这三种窗口的含义、它们是如何相互影响的,以及在 TCP 运行期间会受到哪些因素的调节。

拥塞窗口大小

拥塞窗口(cwnd) 是 发送方 内部维护的一个控制变量,用来限制未确认数据的数量,以避免在网络中引入过多分组而造成拥塞。

cwnd 是 拥塞控制 的核心参数,TCP 的拥塞控制算法会根据 ACK 返回情况丢包信号 动态调整 cwmd。

发送窗口大小

发送窗口大小 是发送方实际能够使用的窗口,它等于拥塞窗口与接收窗口中的较小者:

$$\text{swnd}= \min(\text{cwnd},\text{rwnd})$$

  • 当发送方收到接收方的 ACK 报文时,报文中会携带 window 字段,这个字段的数值反映了接收方当前公开的接收窗口(rwnd)。发送方据此更新 rwnd
  • 同时,发送方根据 ACK 的到达情况(是否成功、是否出现重复 ACK、是否发生超时)来调整 cwnd,这就是拥塞控制算法(如慢启动、拥塞避免、快速恢复等)的工作机制。
  • 只要 cwndrwnd 任意一个发生变化,发送窗口(swnd)也会随之重新计算,从而影响后续能够发送的数据量。
接收窗口大小

接收窗口大小 由接收方根据自身的处理能力和缓冲区使用情况动态决定:

  1. 缓冲区余量:接收方为每个 TCP 连接分配了一块接收缓冲区(receive buffer)。当缓冲区中已有的数据量接近上限时,接收方会把 rwnd 调小,以防止发送方继续发送导致缓冲区溢出。
  2. 应用层消费速率:如果上层应用程序(如文件写入、播放器)处理数据的速度慢于网络到达速度,接收方同样会降低 rwnd,让发送方放慢发送节奏。
  3. 系统资源限制:操作系统可以通过套接字选项(如 SO_RCVBUF)限制每个连接的最大接收缓存,这也是 rwnd 的上限。

当接收方的缓冲区被消费掉一定量后,它会在随后的 ACK 中把 rwnd 调大,通知发送方可以恢复更高的发送速率。这样形成了发送方与接收方之间的“流量控制”循环。

初始发送窗口大小

在一个 TCP 连接刚建立时,双方都需要一个起始窗口值,以便开始数据的交换。初始窗口大小 受以下因素影响:

  • 初始拥塞窗口(ICW,Initial Congestion Window):按照 RFC 6928 的推荐,新建连接的 cwnd 默认是 10 个 MSS(Maximum Segment Size),但实际值会受到操作系统实现和网络路径的 MTU 限制。
  • 接收方的接收缓冲区:在三次握手的 SYN 包中,接收方会把自己的 rwnd(即接收窗口)放入 Window Size 字段,发送方据此确定初始的 swnd。如果接收方的缓冲区较大,则初始 rwnd 也会相应较大。
  • 系统默认配置:不同的操作系统或协议栈会在内核参数中预设一个默认的 rwnd(比如 Linux 的 net.ipv4.tcp_rmem),这些参数会在未显式设置时使用。

综上,初始窗口大小 实际上是 cwndrwnd 两者的交叉结果:

$$\text{初始 swnd}= \min(\text{ICW},\text{初始 rwnd})$$

在连接建立后,随着数据的发送、确认以及接收方缓冲区的消耗,这三个窗口会不断被重新评估和调节,从而实现 TCP 的可靠传输、拥塞控制和流量控制三大核心功能。

流量控制

流量控制(TCP Transmission Control Protocol)是一种端到端的机制,旨在协调发送方和接收方之间的数据传输速率,防止接收方因处理能力或缓冲区限制而被压垮,从而避免数据包的丢失和重传。流量控制的核心目标是保证数据的可靠交付,同时尽可能高效地利用可用的网络带宽。

在 TCP 中,流量控制通过 滑动窗口机制 实现,具体过程如下:

  1. 接收方通告窗口大小
    接收方根据自身的处理速度、可用缓存空间以及当前的负载,计算出一个 窗口大小(Receiver Window),并在每个 ACK 报文中将该值告知发送方。窗口大小代表接收方在收到下一个 ACK 之前,能够接受的最大未确认字节数。

  2. 发送方依据窗口限制发送数据
    发送方在发送数据时,必须确保已发送且尚未被确认的字节总量不超过接收方通告的窗口大小。若窗口为零,发送方会暂停发送新数据,只等待接收方释放缓冲区并更新窗口;此时若仍需要保持连接活跃,发送方会周期性发送 Zero‑Window Probe(零窗口探测)报文,以探测对方是否已恢复接收能力。

  3. 窗口的动态调整
    随着接收方处理数据并释放缓存,窗口大小会在后续的 ACK 中被重新通告,发送方随即可以利用增加的窗口发送更多数据。这个过程在整个连接期间持续循环,使得发送速率始终与接收方的实际承载能力保持同步。

需要注意的是,流量控制 关注的是单个连接内部的发送/接收平衡,而拥塞控制 则针对网络整体的拥塞状态(例如链路容量、路径上的竞争)进行调节,两者相互独立但常常协同工作,以实现端到端的可靠与高效传输。

补充

零窗口控制:如果接收方的缓冲区已满,它可以将窗口大小设置为零,表示不接受任何数据。发送方会注意到这一点并暂停数据的发送,直到接收方准备好接收数据。

可靠传输机制

TCP 的 可靠传输 通过多种机制共同实现,下文将对三个关键机制进行介绍。

序列号和确认号

序列号(seqno, sequence number)和 确认号(ackno, acknowledge number)是 TCP 首部的两个字段,TCP 协议通过 序列号 来记录目前已经发送了哪些数据,通过 确认号 记录哪些数据已经被接收方所接收。

发送方发送 序列号 和 接收方返回 确认号 的交互可能存在以下几种情况:

  • 如果发送的数据段丢失了,接收方不会发送更新的 确认号,这会最终导致发送方超时并重传丢失的数据段。
  • 如果数据段到达了接收方,但是是乱序的,接收方将持续发送最后一个正确 序列号确认号,提示发送方其中有数据段需要重新传输。
  • 如果数据段到达了接收方,并且是按顺序的,接收方发送一个新的 确认号,提示发送方到目前为止的所有数据都已正确接收。
补充

绝对下标和序列号的概念区别

绝对下标(Absolute Number)指的是在整个 TCP 会话期间,数据字节在传输流中的位置,其大小无上限。可以把它想象成一个 TCP 连接中从第一个字节开始的整体计数器。例如,如果一个 TCP 连接传输了 5000 字节的数据,那么这些数据的绝对下标范围是从 1 到 5000。

序列号(Seqno,Sequence Number)是一个 32 位的非负数(unsigned),是 TCP 报文段中实际使用的一个字段,从一个随机的 初始序列号(ISN)开始计数。

在 TCP 发送和接收数据时,其需要将 序列号绝对下标 进行转换,假设第一次握手使用的 序列号ISN,那么绝对下标 index 对应的 序列号(index + 1 + ISN) & UINT32_MAX

如果接收方接收到的 序列号seqno,那么对应的绝对下标为 (seqno - ISN - 1 + UINT32_MAX) % UINT32_MAX + n * UINT32_MAX,其中这里 n 为一个整数,取决于滑动窗口在 [0, UINT32_MAX] 的区间内移动了几个来回。

超时重传

TCP 是一种 可靠传输协议,它要确保发送的数据 被对方接收并确认。但网络中可能发生:

  • 包丢失(网络拥堵)
  • ACK 丢失(确认丢了)
  • 延迟很大(RTT 不稳定)

为了应对这些情况,TCP 协议在发送一个 数据段(segment)时,会为每一个发送的 segment 设置一个 重传定时器

如果某个 segment 的定时器超时了,就说明发送方在 超时时间阈值 内没有接收到该 segment 的确认,发送方就会触发 超时重传(Retransmission Timeout),重新发送超时的 segment。

RTO

RTO 是 TCP 为等待 ACK 设置的超时时间,如果超时没收到 ACK,就会重传数据包。

那么 超时重传的时间时如何确定的?(了解即可)

超时重传的时间 Timeout 可以通过如下公式计算得到:

$$ \begin{align*} \text{\small RTO} &= \text{\small EstimatedRTT} + 4*\text{\small DevRTT} \\ \text{\small EstimatedRTT} &= (1-\alpha) * \text{\small EstimatedRTT} + \alpha * \text{\small SampleRTT} \\ \text{\small DevRTT} &= (1-\beta) * \text{\small DevRTT} + \beta * \left|\text{\small SampleRTT} - \text{\small EstimatedRTT}\right| \end{align*} $$

SampleRTT 是每一次报文往返时间的样本,EstimatedRTT 是加权平均的往返时间,DevRTT 是往返时间的偏差,而 $\alpha$ 和 $\beta$ 是权重,通常取值为 $\alpha=0.125, \beta=0.25$。

换句简单的话说,RTO 是在“当前平均往返时间”的基础上,加上“4 倍的波动范围”,这样可以容忍一定的网络抖动,减少误判重传。

校验和

TCP 首部包含一个 校验和(Checksum)字段,用于检测数据在传输过程中的任何变化。如果 接收方 检测到校验和错误,该数据段会被 丢弃,然后接收方会要求发送方 重传 该数据段。

TCP 的校验和计算方法和 IP 校验和计算方法 一致,不过两者校验的范围和目的有所不同。

其 IP 校验和只针对 IP 头部进行校验,主要用于检测数据在传输过程中由于网络故障等原因造成的错误。
而 TCP 校验和不仅要校验 TCP 头部,还要校验 TCP 载荷(即数据部分)。因此,TCP 校验和能提供 更全面的错误检测

拥塞控制

拥塞控制(tcp)指的是 tcp 限制传输数据的速率,进而防止注入过多的数据到网络中,进而造成网络链路过载。

大家需要了解,tcp 不是一个 “自私” 的算法,一段链路上可以同时包含很多 tcp 连接,tcp 的拥塞控制的目的是尽量去实现一个 总体的最优,而不是个体的最优。当 tcp 检测到数据传输出现拥塞之时,即一段时间内没有接收到一些确认,它就会降低自己传输数据的速率。

慢开始

慢开始 是 TCP 连接开始时的一个阶段,相较于直接以较高的速率发送数据,慢开始会以一个较低的速率开始,然后 逐步试探 当前网络传输的能力,以指数的速率增加发送速率。慢开始的流程如下:

  • 初始化:当一个 TCP 连接开始时,拥塞窗口 cwnd 设置为一个很小的值,通常是 1MSS (最大段大小)。
  • 指数增长:对于每个收到的 ACKcwnd 会增加一个 MSS。这意味着每个 RTT(往返时间)cwnd 都会翻倍,导致了指数增长。
  • 转换阈值:当 cwnd 达到 ssthresh(慢开始阈值)时,TCP 会从慢开始模式转换到拥塞避免模式。
Sender
Receiver
Data
ACK
RTT
每收到一个 ACK,
cwnd += 1
cwnd 在到达 ssthresh 之前指数型增长
0
1
2
3
4
5
2
4
6
8
10
12
14
16
RTT
cwnd
ssthresh
注意

拥塞窗口指数增长 和 接收确认 的关系

发送方每收到一个发送段的确认,拥塞窗口大小 +1(cwmd += 1)。因为在慢开始期间,发送方发送的 TCP 段的数量呈现指数增长趋势,所以在理想情况下,正确接收所有发送段的确认之后,拥塞窗口大小呈现指数增长趋势。

这并不是说在慢开始阶段,接收到一个确认,拥塞窗口直接指数增长,这是很多很多容易理解错误的一个点。

补充

什么是 MSS?

MSS 是 Maxium Segment Size 的简称,即最大段大小。MSS 通常是根据网络路径的 MTU(最大传输单元)来确定的,MTU 是网络层上一种数据包最大尺寸的限制,常见的 MSS 值为 1460 字节(MTU 1500 字节减去 IP 头部和 TCP 头部的大小)。

拥塞避免

当 TCP 进入拥塞避免(Congestion Avoidance)阶段后,拥塞窗口(cwnd)的增长方式从慢启动的指数增长改为 线性增长

具体来说:每收到一个 ACK,cwnd 增加 1/cwnd 个 MSS(最大报文段长度)。由于在一个往返时间(RTT)内会收到大约 cwnd 个 ACK,这等价于在每个 RTT 结束时 cwnd 只增长约 1 MSS。这种“每 RTT 加 1 MSS”的线性增长可以避免窗口迅速膨胀,引起网络拥塞。

补充
  • 慢开始指数增长:收到一个 ACK 后 cwnd += 1。
  • 拥塞避免线性增长:收到一个 ACK 后 cwnd += 1/cwnd。

TCP 通过两种方式 检测网络是否出现拥塞

  1. 超时(Timeout):发送的数据在预期的时间内没有收到相应的 ACK,说明该段可能已经丢失。
  2. 三个冗余 ACK(Triple Duplicate ACK):连续收到三个冗余的 ACK,表明网络中有数据段在传输过程中被丢弃,但后续的 ACK 仍然能够到达。
三个冗余的 ACK 的准确理解

TCP 规范里说的 “收到 3 个冗余 ACK” 的意思是:

  • 假设某个包序号 N 丢失了,接收方 已经收到 N+1、N+2 等包,它会连续发送确认号为 N 的 ACK(重复 ACK)。
  • 第一次收到 ACK=N 时,它是 正常 ACK(不是冗余的)。
  • 接下来连续收到 3 个相同的 ACK=N,这 3 个就是所谓的 冗余 ACK
  • 当发送方收到这 3 个冗余 ACK 后,就触发快速重传,重发包 N。

所以 总共收到 4 个 ACK=N 才会触发快速重传:

  1. 第 1 个 ACK=N → 正常 ACK
  2. 第 2 个 ACK=N → 冗余 ACK 1
  3. 第 3 个 ACK=N → 冗余 ACK 2
  4. 第 4 个 ACK=N → 冗余 ACK 3 → 触发快速重传

一旦检测到上述任意一种情况,TCP 认为网络已经发生拥塞,并执行如下处理:

  • 将慢启动阈值(ssthresh)设置为当前 cwnd一半ssthresh = cwnd / 2),以限制后续的窗口增长速度。
  • 将拥塞窗口 cwnd 重新设为 1 MSS,相当于回到最保守的状态。
  • 随后进入 慢启动(Slow Start) 阶段,重新探测网络的可用带宽。

通过上述线性增长与灵活的拥塞检测/恢复机制,TCP 能够在保持高吞吐量的同时,尽可能避免网络过载,从而实现可靠且高效的端到端传输。

快速重传

在传统的 TCP 实现中,重传计时器负责触发超时重传:只要计时器到期而仍未收到对应的数据段的 ACK,发送方就会重新发送该段。在高带宽‑低时延的网络环境下,这种“等计时器”的方式往往效率低下,因为计时器的超时时间往往远大于实际的往返时延(RTT),导致数据恢复延迟。

快速重传(Fast Retransmission),当发送方 连续收到三个冗余的 ACK 时,说明网络中已经有一个 数据段丢失,而后续的 ACK 只能确认已经成功到达的后续段。此时,TCP 就会 立即重传 那个被认定为丢失的段,而不必等到重传计时器超时,这一过程被称为快速重传。

sequenceDiagram
    participant S as 发送方 (Sender)
    participant R as 接收方 (Receiver)

    Note over S,R: 假设包 N 丢失

    S->>R: 发送包 N-1
    S->>R: 发送包 N (丢失)
    S->>R: 发送包 N+1
    S->>R: 发送包 N+2

    Note over R: 包 N 丢失,但已收到 N+1, N+2

    R->>S: ACK N  (第一次收到 ACK N)
    R->>S: ACK N  (冗余 ACK 1)
    R->>S: ACK N  (冗余 ACK 2)
    R->>S: ACK N  (冗余 ACK 3 -> 触发快速重传)

    Note over S: 收到第 3 个冗余 ACK -> 快速重传包 N
    S->>R: 重传包 N

触发快速重传后,TCP 会切换到 快速恢复 (Fast Recovery)阶段。该阶段的核心目标是:

  1. 防止拥塞窗口(cwnd)骤降到 1 MSS,从而避免吞吐量剧烈下降;
  2. 利用已经收到的重复 ACK 来“填补”因丢包而空出的窗口,保持管道尽可能饱和。

在快速重传和快速恢复中,cwnd 的变化过程 如下:

  1. 快速重传触发后
    • ssthresh ← cwnd / 2
    • cwnd ← ssthresh + 3·MSS
  2. 快速恢复期间(每收到一个重复 ACK)
    • cwnd ← cwnd + MSS
    • 这种线性增长相当于“用已经确认的 ACK 为窗口“买票”,使得发送方能够继续发送新数据,而不会因为单个丢包而立刻停滞。
  3. 退出快速恢复(收到一个新的、非重复的 ACK)
    • 该 ACK 表明丢失的数据段已经被成功重传并得到确认。
    • cwnd ← ssthresh(即把窗口恢复到慢启动阈值)
    • 随后进入 拥塞避免(Congestion Avoidance)阶段,cwnd 以后将以 每 RTT 增加 1 MSS 的速度线性增长。
0
2
4
6
8
10
12
14
16
18
20
22
24
4
8
12
16
20
24
28
32
36
40
ssthresh
cwnd(new)
ssthresh (new)
0
2
4
6
8
10
12
14
4
8
12
16
20
24
28
32
36
40
ssthresh
cwnd(new)
Congestion
avoidance
ssthresh (new)
16
18
20
22
24
TCP 慢开始和拥塞避免
TCP 快速重传和快速恢复
拥塞窗口 (cwnd)
拥塞窗口 (cwnd)
慢开始
拥塞避免
RTO
超时
拥塞避免
慢开始
拥塞避免
拥塞避免
TCP 快速重传和
快速恢复
往返时间(RTT)
往返时间(RTT)
三个重复的 ACK

总结

  • cwnd < ssthresh 时,使用 慢开始 算法,cwnd 以指数增长
  • cwnd >= ssthresh 时,使用 拥塞避免 算法,cwnd 线性增长
  • 当在 RTO 内没有收到发送的某个分组的确认时
    • 如果启动了快速恢复,则设置 cwnd = ssthresh = cwnd / 2,开始使用拥塞控制算法
    • 如果没有启动快速恢复,则设置 ssthresh = cwnd / 2, cwnd = 1,开始使用 慢开始 算法
  • 如果启用了快速重传,并且收到 3 个与先前重复的 ACK(总共收到 4 个相同的 ACK),则不用等待超时器 RTO 结束,可以马上重传该数据包,cwnd 可能会减少(这里不考察)