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. 首部长度(Header Length):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 一般设置是随机的,这样可以区分不同的连接和旧连接的残留包,也能防止攻击。

补充

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

第三次握手发送的是一个 ACK 报文段,TCP 标准允许它同时携带数据(俗称 ACK + Data)。

不过实际中:

  • 大多数客户端都是发送一个纯 ACK完成握手;
  • 然后紧接着再发送第一个数据报文。
  • 一些协议(TCP Fast Open 等)会利用第三次握手携带数据,以减少一次 RTT。

假设客户端第一次握手的 SYN 的 seqno 为 x,那么客户端发送的 第一个应用层字节的序列号(x + 1) mod 2³²。 如果第三次握手携带数据,那么这个字节就在第三次握手报文中;否则就在握手完成后的第一个数据报文中。

四次挥手

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 后,滑动窗口向前移动,窗口左端排除已确认的数据,右端随即腾出新的空间。此时,发送方就可以继续发送后续的数据。

绝对下标和序列号

可以把 TCP 发送的数据想象成一条无限长的字节流,每个字节在这条流中都有一个唯一的位置,这个位置就是绝对下标(absolute index)。它从连接开始不断递增,本质上是一个“不会回绕的理想编号”。

但 TCP 报文头部的 Sequence Number(seqno) 只有 32 位,因此它不可能直接表示这个无限增长的绝对位置,而是采用了一个取模映射的方式。

0
1
2
3
4
5
N-2
N-1
seq0
seq1
seq2
seq3
seq4
seq5
seq(N-2)
seq(N-1)
转换为序列号
send window
receive window
seq0
seq1
seq2
seq3
seq4
seq5
seq(N-2)
seq(N-1)
0
1
2
3
4
5
N-2
N-1
转换为数据字节下标
发送数据
待发送的字节流数据,下标从 0 开始
seq0 = (ISN + 1) % 232
接收到的字节流数据

1. 从“字节流”到“窗口”

发送方并不是一次发送所有字节,而是维护一个滑动窗口

  • 窗口左边界:最早未被确认的字节(absolute index)
  • 窗口右边界:允许发送的最大字节位置(由接收窗口决定)

也就是说:窗口内的每一个字节都有一个绝对下标,同时也必须映射成一个 seqno 才能发出去


2. 绝对下标 → 序列号(发送时)

TCP 在连接建立时会选择一个初始序列号(ISN)。 之后发送第一个数据字节时,并不是用 ISN,而是:

  • ISN:用于 SYN
  • ISN + 1:对应第一个数据字节

因此映射关系是:

直觉理解:

  • absolute index = 0 → seqno = ISN + 1
  • absolute index = 1 → seqno = ISN + 2
  • ……

seqno 本质就是 absolute index + 一个偏移量,再取 2³² 模

3. 序列号 → 绝对下标(接收时)

接收方拿到一个 seqno 时,会遇到一个问题:

seqno 是 32 位的,会“回绕”(wrap around),那它到底对应哪一个真实位置?

例如:

  • seqno = 100,可能是第 100 字节
  • 也可能是第 字节

所以必须结合当前滑动窗口的位置来判断。

核心思路是:

👉 在所有可能的绝对下标中,选一个最接近当前接收窗口的那个

可以写成:

其中:

  • 前半部分:得到“模 2³² 的位置”
  • (k):选择合适的轮次,使其落在窗口附近

4. 为什么滑动窗口能解决歧义?

关键点在这里:

窗口大小远小于 (通常最大也就几十 KB 到几 MB)

因此在任意时刻:

  • 合法的数据只会落在一个很小的区间内
  • 不会跨越多个“2³²周期”

这就保证了:

每个 seqno 在当前窗口中只可能对应一个合理的 absolute index


可以用一句话总结整个机制:

TCP 用 absolute index 管理真实的数据流顺序,用 seqno(32位) 在网络上传输,通过 滑动窗口的位置 消除回绕带来的歧义。

更具体地:

  • 发送方
    • 用 absolute index 组织数据
    • 映射成 seqno 发出去
    • 维护发送窗口(哪些还没被 ACK)
  • 接收方
    • 收到 seqno
    • 根据窗口位置还原 absolute index
    • 决定是否接收、缓存、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)
发送方 Sender拥塞窗口 cwnd发送方内部维护由拥塞控制算法驱动慢启动 / 拥塞避免 / 快恢复↑ ACK到达 · 丢包信号发送窗口 swnd= min(cwnd, rwnd)实际可发未确认数据量cwnd 或 rwnd 变化即更新发送缓冲区已确认已发未确认可发← swnd →cwnd 调整触发✓ 新ACK → cwnd 增大✗ 3重复ACK → cwnd 减半✗ 超时 → cwnd 重置为1接收方 Receiver接收窗口 rwnd接收方公告给发送方= 接收缓冲区剩余空间影响因素:缓冲区余量 · 应用消费速率SO_RCVBUF 系统上限接收缓冲区已占用(待应用读取)空闲← rwnd →应用层消费读取数据 → 缓冲区释放→ rwnd 增大 → 通知发送方流量控制循环缓冲区满 → rwnd → 0→ 发送方暂停应用读取 → rwnd 恢复→ 发送方继续网络数据包拥塞信号丢包 / 重复ACK→ 反馈给发送方ACK + rwnd数据ACK 携带 rwndrwnd ↓swnd = min(cwnd, rwnd)

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

拥塞窗口大小

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

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

发送窗口大小

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

  • 当发送方收到接收方的 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 两者的交叉结果:

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

窗口移动过程

TCP 可以看成在一条连续的字节流上维护一个“滑动窗口”,窗口表示当前允许发送且尚未确认的数据范围

  • 初始状态: 发送方根据接收方通告的窗口大小,确定一个发送窗口 [左边界, 右边界),并一次性发送窗口内的数据(按序列号对应的字节序列)。
  • 收到 ACK: 接收方确认已正确接收的数据(ACK = 下一个期望字节的序列号)。
  • 窗口右移(滑动): 发送方收到 ACK 后:
    • 将窗口左边界移动到 ACK 指示的位置(已确认的数据被“移出窗口”)
    • 窗口整体向前滑动
    • 右边界随之扩展(如果接收窗口允许)
  • 继续发送: 新进入窗口的字节可以继续发送,实现“流水线式”传输,而无需逐个等待确认。

本质就是一句话:

ACK 推动窗口向前滑动,释放发送空间,从而实现连续高效的数据传输。

101
901
size = 800
发送窗口被设置
901
size = 800
发送了 200B
101
301
901
size = 600
发送的数据被 ACK,发送窗口向前移动
301
901
size = 600
发送了 300B
301
601
1001
size = 400
窗口向前移动,调整窗口大小
601
1201
size = 600
调整窗口大小
601
101
901
rwnd = 800
接收窗口被设置
901
rwnd = 600
接收到 200B,接收窗口向前移动
101
301
1001
rwnd = 400
接收到 300B,应用层读取 100B,rwnd 调整为 400
201
601
1201
rwnd = 600
应用层读取了 200B,rwnd 增大
401
601
seqNo: 100
seqNo: 101
Data: 200B
ackNo: 1001
rwnd: 2000
seqNo: 1000
ackNo: 101
rwnd: 800
seqNo: 301
Data: 300B
ackNo: 301
rwnd: 600
ackNo: 601
rwnd: 400
ackNo: 601
rwnd: 600
Client
Server
SYN
SYN + ACK
ACK
Data
ACK
Data
ACK
ACK

流量控制

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

TCP 流量控制示意图展示发送方如何根据接收方通告的 rwnd 动态调节发送窗口 swnd = min(cwnd, rwnd)TCP 流量控制:发送窗口由 rwnd 约束发送方 Sender发送缓冲区已确认已发未确认可发送未发送不可发送swnd(发送窗口)SND.UNASND.NXTswnd = min(cwnd, rwnd)拥塞窗口 与 接收窗口 取较小值cwnd拥塞控制决定rwnd接收方缓存决定接收方 Receiver接收缓冲区已消费(释放)已接收待处理空闲rwndrwnd 计算缓冲区空闲字节数rwnd = 0发送方暂停,发探测报文数据段(seq, data)ACK + rwnd(剩余缓存)发送方更新 swnd根据最新 rwnd 重新计算可发送量接收方处理数据消费后释放缓存↻ 窗口持续动态调整,发送速率始终与接收处理能力同步rwnd = 0 时发送方周期性发送 Zero-Window Probe,探测接收方是否恢复

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

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

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

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

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

补充

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

可靠传输机制

TCP 运行在不可靠的 IP 网络之上,链路中可能面临 丢包(数据或确认报文在传输途中丢失)、乱序(后发送的数据先到达)、重复(同一数据被多次发送导致重复接收)以及 数据损坏(传输过程中发生比特错误)等问题。为了屏蔽这些不可靠因素,TCP 通过多种机制共同实现 可靠传输,下文将对三个关键机制进行介绍。

序列号和确认号

序列号(seqno, sequence number)和 确认号(ackno, acknowledgment number)是 TCP 首部中的两个重要字段。

TCP 通过 序列号 标识当前报文段中 数据部分的第一个字节 对应的序列号,结合 数据长度 即可确定该报文段携带的是 哪一段连续的字节流

TCP 通过 确认号 表示 截至哪个字节之前的数据已经被成功接收,其值始终等于接收方下一步期望收到的数据的第一个字节的序列号,从而实现对已接收数据的累计确认。

TCP 序列号与确认号示意图展示 TCP 字节流中序列号标识数据位置、确认号表示期望下一字节的机制TCP 序列号与确认号… 已收到0 ~ 99报文段 1字节 100 ~ 199(长度 100)报文段 2字节 200 ~ 299(长度 100)未发送数据300 ~0100200300报文段 1 首部字段序列号(seqno)= 100数据部分第一个字节的编号数据长度 = 100 字节携带字节 100 ~ 199涵盖字节 100 ~ 199接收方回复的 ACK(对报文段 1 的确认)确认号(ackno)= 200期望下一步收到字节 200,即 0~199 均已收到核心规律seqno 标识"本段数据从哪个字节开始"ackno = seqno + 数据长度,表示"截至此前全部收到,期望下一个字节"ackno = seqno + len(数据长度)例:seqno=100,len=100 ⟹ ackno=200

TCP 正是通过 序列号确认号 的配合来检测数据是否被正确接收。由于网络传输过程中可能出现 丢包乱序 等情况,因此发送方发送 序列号 和接收方返回 确认号 的交互通常有以下几种情况:

  • 数据段丢失:发送方发送的数据段在传输过程中丢失,接收方无法收到该数据段,因此也无法更新 确认号。发送方在超时时间内未收到新的确认后,会认为该数据段已经丢失,并重新发送该数据段。
  • 数据段乱序到达:接收方收到了后续数据段,但前面的某个数据段尚未到达。由于 TCP 默认采用 累计确认 机制,接收方会反复发送相同的 确认号,表示自己仍然缺少某一段数据,并继续等待该数据段到达。当发送方连续收到多个相同的确认号时,会认为发生了丢包,并触发快速重传
  • 数据段按序到达:接收方按顺序收到了当前数据段,并且此前的数据也都已经收到,此时会返回一个新的 确认号,表示截至该确认号之前的所有数据都已成功接收,同时指出下一步期望接收的数据的第一个字节对应的序列号。
TCP 可靠传输三种场景并排展示数据段丢失(超时重传)、乱序到达(快速重传)、按序到达(正常推进)三种情况下 seqno 与 ackno 的交互过程数据段丢失 → 超时重传乱序到达 → 快速重传按序到达 → 正常推进发送方接收方发送方接收方发送方接收方seq=100seq=200ack=100超时等待seq=100重传ack=300超时重传seq=100seq=200ack=100 ×1seq=300ack=100 ×2seq=400ack=100 ×3seq=100快速重传ack=500快速重传seq=100ack=200seq=200ack=300seq=300ack=400ACK递增正常发送数据段(seqno)ACK(ackno)丢失

这样三个 case 的逻辑分别对应:

  1. 没收到 → ACK 不前进 → 超时重传
  2. 收到了后面的,但前面的没收到 → ACK 不前进(重复 ACK)→ 快速重传
  3. 按顺序收到 → ACK 前进 → 正常继续发送

超时重传

TCP 超时重传(RTO)时序图展示发送方发送 segment、等待 ACK、RTO 超时后触发重传的完整过程,包含正常确认、ACK 丢失和超时重传三条场景线。TCP 超时重传(RTO)时序图发送方 Sender接收方 Receiver时间 ↓时间 ↓① 正常:segment 到达,ACK 返回,定时器取消Seq=1(数据 segment)RTOACK=1(确认)✓ 定时器取消② ACK 丢失:接收方已收到,但 ACK 在网络中丢失Seq=2(数据 segment)RTO已接收 ✓✕ ACK 丢失超时!重传 Seq=2(超时重传)ACK=2 ✓③ 包丢失:segment 在网络中丢失,接收方未收到✕ 丢包RTO无响应超时!重传 Seq=3(超时重传)数据 segmentACK 确认超时重传RTO 定时器

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

  • 包丢失(网络拥堵)
  • ACK 丢失(确认报文丢失)
  • 网络时延增大(RTT 波动)

为了应对这些情况,TCP 会维护 重传定时器。发送方在发送数据后,如果在 超时时间阈值(RTO,Retransmission Timeout)内仍未收到对应数据的确认,就认为该数据可能已经丢失,从而触发 超时重传,重新发送相应的数据段(segment)。

补充

TCP 的超时重传机制介于 回退 N 帧选择性重传 之间。它保留了 GBN 的 滑动窗口累计确认机制,同时借鉴了 SR 的 乱序缓存选择性重传 思想,并通过快速重传、快速恢复、SACK 等机制进一步提高了传输效率。

因此现代 TCP 通常只重传丢失的数据段,而不会像 GBN 那样重传之后所有未确认的数据,并且只为发送窗口最左边那个还没有确认的数据段维护一个超时器。

TCP 重传机制与 GBN / SR 对比展示三种场景下包2丢失时的重传行为:GBN全部重传、TCP快速重传仅重传包2、SACK精确重传GBN超时后全部重传TCP(快速重传)3 个重复 ACK → 只重传 2TCP + SACK精确告知已收到哪些包发送方接收方12345发送窗口✓ 1ACK=2 × 4超时重传:2345全部重传 ✗12345✓ 1ACK=23 重复 ACK只重传:2仅重传 2 ✓缓存 3-5 直接确认→ ACK=612345✓ 1ACK=2SACK={3,4,5}发送方确知:3、4、5 已收到2← 精确只补 2GBNTCP(无 SACK)TCP + SACK接收窗口= 1(丢弃乱序)> 1(缓存乱序)> 1(缓存乱序)ACK 类型累计确认累计确认累计 + SACK丢包后重传全部重传只重传丢失包只重传丢失包触发机制超时计时器3 重复 ACK / 超时3 重复 ACK / 超时效率低(重传多余包)高(快速重传)最高(精确感知)类似GBNGBN 累计 ACKSR 行为+ SR 重传策略
RTO

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

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

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

SampleRTT 是每一次报文往返时间的样本,EstimatedRTT 是加权平均的往返时间,DevRTT 是往返时间的偏差,而 是权重,通常取值为

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

校验和

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

TCP 校验和工作原理展示 TCP 校验和的计算范围、伪首部结构以及发送方与接收方的处理流程TCP 校验和覆盖范围伪首部(Pseudo-header,不传输,仅用于校验)源 IP · 目的 IP · 协议号 (0x06) · TCP 段长度TCP 首部源端口 · 目的端口 · 序号 · 确认号 · 窗口 · 校验和 · …TCP 数据(Payload)应用层数据…校验范围校验和字段(16 bit)写入此处发送方① 将校验和字段置 0② 对「伪首部 + 首部 + 数据」按 16 bit 字求反码和③ 结果填入校验和字段接收方① 对「伪首部 + 首部 + 数据」(含校验和字段)再次求反码和结果 = 0xFFFF✓ 无误,接收结果 ≠ 0xFFFF✗ 丢弃,等待重传对比 IP 校验和IP 校验和:仅覆盖 IP 首部,不含 IP 数据TCP 校验和:覆盖伪首部 + TCP 首部 + TCP 数据,错误检测范围更全面

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

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

拥塞控制

在计算机网络中,拥塞 指的是网络中传输的数据量超过了链路或节点的处理能力,导致数据包延迟增大、丢失或网络性能下降的状态。

拥塞控制(congestion control)指的是 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(cwnd += 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”的线性增长可以避免窗口迅速膨胀,引起网络拥塞。

TCP 拥塞避免阶段示意图左侧为发送方与接收方的数据/ACK 时序图,右侧为 cwnd 随 RTT 线性增长的曲线图,并展示超时拥塞事件后的处理流程。发送方接收方RTTDataACK超时每 RTTcwnd += 1RTTcwnd123456780123456ssthresh新 ssthresh慢启动重新开始线性增长+1 MSS/RTT超时重置进入CA 阶段
补充
  • 慢开始指数增长:收到一个 ACK 后 cwnd += 1。
  • 拥塞避免线性增长:收到一个 ACK 后 cwnd += 1/cwnd。

TCP 通过 超时检测 检测网络是否出现拥塞。一旦检测到超时,TCP 认为网络已经发生拥塞,并执行如下处理:

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

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

快速重传

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

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

TCP 快速重传时序图(N+1 至 N+3)发送方连续发包 N-1、N、N+1、N+2、N+3,包 N 丢失,接收方缓存后三个包并连续发送冗余 ACK,触发快速重传。发送方接收方包 N-1包 N(丢失)包 N+1包 N+2包 N+3包 N 丢失,缓存 N+1/N+2/N+3ACK N(第 1 次,正常 ACK)ACK N(冗余 ACK 1)ACK N(冗余 ACK 2)ACK N(冗余 ACK 3 → 触发)收到第 3 个冗余 ACK → 快速重传重传包 N正常数据包冗余 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 实现(如 Reno)中,触发快速重传后,TCP 会切换到 快速恢复(Fast Recovery)阶段。该阶段的核心目标是:

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

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

  1. 触发快速重传后
    • ssthresh ← cwnd / 2
    • cwnd ← ssthresh
  2. 退出快速恢复
    • 随后进入 拥塞避免(Congestion Avoidance)阶段,拥塞窗口从 ssthresh 开始,以 每 RTT 增加约 1 MSS 的速度线性增长。
补充

408 考试中快速恢复是怎么设置 cwnd 和 ssthread 的,是以下哪种?

  1. ssthresh = cwnd / 2, cwnd = ssthresh
  2. ssthresh = cwnd / 2, cwnd = ssthresh + 3

这是一个有争议的细节,需要区分教材版本RFC 标准

408 考研统考口径(谢希仁《计算机网络》第8版)

采用的是第 1 种:

收到三次重复 ACK → ssthresh = cwnd / 2cwnd = ssthresh,进入拥塞避免。

谢希仁教材并未引入"cwnd = ssthresh + 3"这一步。图中 cwnd 直接从折半后的 ssthresh 出发做线性增长,没有额外的 +3 膨胀阶段。

RFC 5681(Reno 标准行为)

采用第 2 种:

ssthresh = cwnd / 2cwnd = ssthresh + 3

+3 的含义是:三个重复 ACK 说明有 3 个报文段已经离开网络(被接收方缓存),因此可以把窗口临时膨胀 3 个 MSS,以维持报文段在途数量不变。快速恢复期间每多收一个重复 ACK 再 +1,直到收到新 ACK 后 cwnd 回落到 ssthresh。

结论

场景用哪种
408 考研答题第 1 种(cwnd = ssthresh = cwnd/2)
计算机网络原理/RFC第 2 种(cwnd = ssthresh + 3)

备考时按第 1 种写即可,不会丢分;若题目明确说 “Reno TCP” 或 “RFC 行为” 则用第 2 种。当然这种细节基本也不会考了。

注意

快速恢复和快速重传的关系

  • 快速重传和快速恢复是两个独立的机制,但现代 TCP(如 Reno)通常会把它们配合使用。
  • 在支持快速恢复的系统中,快速恢复由快速重传触发
TCP 实现收到 3 个重复 ACK后续行为
TCP Tahoe快速重传进入慢启动(没有快速恢复)
TCP Reno快速重传进入快速恢复

TCP 拥塞方案对比

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 可能会减少(这里不考察)