← 返回文章列表

TCP 核心机制详解:三次握手、四次挥手、滑动窗口与拥塞控制

深入理解 TCP 协议的核心机制,包括三次握手四次挥手的原理、序列号的本质、超时重传与快速重传、SACK 机制、滑动窗口与流量控制、拥塞控制四大算法等。

15 分钟阅读
字号

TCP(Transmission Control Protocol)是互联网最重要的协议之一,作为传输层的核心协议,它为上层应用提供可靠、有序、面向连接的字节流服务。本文系统讲解 TCP 的核心机制。

一、TCP 三次握手

1.1 为什么需要三次握手

三次握手的核心目的是确认双方的发送能力和接收能力都正常,同时协商初始序列号。

1.2 详细过程

次数方向标志位序列号确认号状态变化
第一次客户端 → 服务器SYN=1Seq=x (ISN)0客户端: CLOSED → SYN_SENT
第二次服务器 → 客户端SYN=1, ACK=1Seq=y (ISN)Ack=x+1服务器: LISTEN → SYN_RCVD
第三次客户端 → 服务器ACK=1Seq=x+1Ack=y+1双方: SYN_RCVD → ESTABLISHED

1.3 为什么不是两次

假设用两次握手:

  • 客户端发SYN包,网络延迟导致超时,客户端重发SYN包
  • 服务器收到第一个SYN包后回复,进入 ESTABLISHED
  • 但延迟的旧SYN包到达服务器,服务器再次回复
  • 服务器建立了无效连接,浪费资源

三次握手能避免这个问题:客户端需要收到服务器的确认才能真正建立连接。

1.4 ISN 为什么是随机的

ISN(Initial Sequence Number)必须随机,是为了防止旧连接的延迟数据包被新连接错误接收。

旧连接: 客户端用 ISN=1000 发送的数据被网络延迟
新连接: 客户端用 ISN=5000 重新建立
服务器: 收到旧数据,但序列号5000不匹配当前期望,丢弃
 
如果ISN固定为0:
  旧连接数据可能被新连接错误接收!

二、TCP 四次挥手

2.1 为什么需要四次挥手

TCP是全双工通信,两个方向可以独立关闭,所以需要四次。

建立连接: 只需要同步一个方向 → 三次握手
关闭连接: 需要关闭两个方向 → 四次挥手

2.2 详细过程

客户端                                              服务器
  |                                                |
  |  FIN (Seq=u)                    →              |  第一次挥手:主动关闭
  |  ESTABLISHED → FIN_WAIT_1                       |  ESTABLISHED → CLOSE_WAIT
  |                                                |
  |            ACK (Ack=u+1)        ←              |  第二次挥手:确认关闭请求
  |  FIN_WAIT_1 → FIN_WAIT_2                       |
  |                                                |
  |                                    ... 传输完剩余数据 ...
  |                                                |
  |            FIN (Seq=w)           ←              |  第三次挥手:被动关闭
  |  FIN_WAIT_2 → LAST_ACK                          |  CLOSE_WAIT → LAST_ACK
  |                                                |
  |  ACK (Ack=w+1)                  →              |
  |  LAST_ACK → TIME_WAIT                           |  LAST_ACK → CLOSED
  |                                                |
  |  等待 2MSL                                      |
  |  TIME_WAIT → CLOSED                             |

2.3 TIME_WAIT 状态

为什么等待 2MSL?

MSL(Maximum Segment Lifetime)是数据包在网络中的最大存活时间,通常为60秒。

  1. 确保最后的ACK能到达服务器:如果服务器没收到ACK,会重发FIN,客户端需要在这个时间内能接收重发的FIN
  2. 让旧数据包在网络中自然消散:防止延迟的旧数据包被新连接错误接收

2.4 三次挥手的情况

正常情况下挥手必须是四次,但有一种特殊情况:同时关闭(Simultaneous Close)

客户端                          服务器
  |                              |
  |  FIN + ACK                   |  ──────────────────────→  |  第一次
  |         FIN + ACK            |  ←──────────────────────  |  第二次:合并了ACK和FIN
  |         ACK                  |  ──────────────────────→  |  第三次

这种情况下看起来是三次,但本质是双方同时发起关闭,刚好合并了ACK和FIN。


三、TCP 报文段结构

3.1 报文段头部

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |  源端口 / 目标端口
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |  序列号
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Acknowledgment Number                     |  确认号
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Offset |  Flags  |                Window                      |  数据偏移 / 标志位 / 窗口
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Checksum           |         Urgent Pointer        |  校验和 / 紧急指针
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options (variable length)                 |  可选字段
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

3.2 六大标志位

标志位名字含义
SYNSynchronize请求同步序列号,建立连接时使用
ACKAcknowledgment确认号字段有效,确认收到了数据
FINFinish请求关闭连接
RSTReset强制重置连接
PSHPush催促数据交付上层应用
URGUrgent紧急指针有效,优先处理

3.3 序列号的本质

序列号 = 字节偏移

TCP发送的是字节流,不是"第几个包"。

发送方要发送: "Hello" (5个字节)
假设初始序列号 ISN = 1000
 
  'H' -> 序号1000
  'e' -> 序号1001
  'l' -> 序号1002
  'l' -> 序号1003
  'o' -> 序号1004

确认号 = 期望收到的下一个字节序号

接收方收到后说:"我收到了字节1000-1004,下一个期望是1005"。

3.4 SYN 包与 ACK 包示例

第一次握手(SYN包)

Source Port: 54321(客户端随机端口)
Destination Port: 443
Sequence Number: 0xA8B3C4D2(随机ISN)
Acknowledgment: 0
Flags: SYN=1, ACK=0

第二次握手(SYN+ACK包)

Source Port: 443
Destination Port: 54321
Sequence Number: 0x7F2E1D9A(服务端随机ISN)
Acknowledgment: 0xA8B3C4D3(客户端ISN+1)
Flags: SYN=1, ACK=1
Options: MSS=1460

第三次握手(ACK包)

Sequence Number: 0xA8B3C4D3
Acknowledgment: 0x7F2E1D9B(服务端ISN+1)
Flags: ACK=1

四、超时重传与快速重传

4.1 超时重传(RTO)

RTO = Retransmission Timeout

TCP为每个发送的包设置一个定时器,如果在RTO时间内没收到ACK,就认为丢包了,重传。

RTO 计算

RTT = Round Trip Time,往返时间
RTO = SRTT + 4 × RTTVAR
 
SRTT = 平滑RTT(多次测量的平均值)
RTTVAR = RTT偏差(波动程度)

4.2 快速重传

问题:超时重传要等RTO到了才发现丢包,太慢。

解决方案:如果接收方收到失序的数据包,立即重复发送对最后一个按序包的ACK。

发送方发送: 1, 2, 3, 4, 5
接收方收到: 1, 2, 4, 5(3丢了)
 
收到4时: 发现期望3但收到4 → 立即发送 ACK=2
收到5时: 还是期望3但收到5 → 再次发送 ACK=2
发送方连续收到3个重复ACK=2 → 立即重传包3(不等超时)

4.3 快速重传的局限

快速重传只知道"有一个包丢了",但不知道丢了几个包。后面用 SACK 机制优化。


五、SACK 与 DSACK

5.1 SACK(Selective Acknowledgment)

解决的问题:允许接收方告诉发送方所有已成功接收的不连续数据块。

发送: 1, 2, 3, 4, 5(但3丢了)
接收方收到: 1, 2, 4, 5
 
没有SACK:
  → 只能发送 ACK=2
  → 发送方不知道4和5有没有收到
  → 只能重传 3, 4, 5(浪费!)
 
有SACK:
  → 发送 ACK=2, SACK=(4,5)
  → 发送方只重传3,完美!

5.2 DSACK(Duplicate SACK)

核心作用:告诉发送方收到了重复数据,帮助诊断网络问题。

ACK丢失导致发送方以为丢包:
  发送: 1, 2, 3
  接收方收到: 1, 2, 3,发送 ACK=3
  ACK丢了!发送方超时,重传 1, 2, 3
 
没有DSACK:
  → 发送方不知道是"ACK丢了"还是"数据丢了"
  → 可能一直盲目重传
 
有DSACK:
  → 接收方发送 ACK=3, DSACK=(1,3)
  → 发送方恍然大悟:"接收方已收到,是ACK丢了!"

六、滑动窗口

6.1 为什么需要滑动窗口

问题1:每发一个包都要等ACK,效率太低。

没滑动窗口: 发包1 → 等ACK → 发包2 → 等ACK → ...
滑动窗口:   发包1,2,3,4,5 → 等ACK → 发包6,7,8,9,10

问题2:接收方处理能力有限,发送方不知道。

6.2 发送窗口

发送方窗口(窗口大小=4):
 
已发送已确认: | 1 | 2 | 3 |     ← 已发且收到ACK
已发送未确认: | 4 | 5 | 6 | 7 |  ← 已发但还没ACK
可以立即发送: | 8 | 9 | 10| 11|  ← 窗口内数据,可立即发
窗口外:       |12 | 13|...       ← 不能发,等ACK来了滑动

6.3 流量控制

接收方通过窗口大小告诉发送方"我还剩多少空间"。

实际发送上限 = min(cwnd, rwnd)
 
cwnd = 拥塞窗口(发送方根据网络状况自己算)
rwnd = 接收窗口(接收方告诉发送方)

七、拥塞控制

7.1 四种算法

算法什么时候用特点
慢启动连接建立之初指数增长,快速探明网络容量
拥塞避免达到ssthresh后线性增长,谨慎增大
快速重传收到3个重复ACK不等超时,提前重传
快速恢复快速重传后减半恢复,不从零开始

7.2 慢启动

初始: cwnd = 1 MSS
 
每收到一个ACK: cwnd += 1
每轮RTT: cwnd × 2
 
RTT1: cwnd = 1
RTT2: cwnd = 2
RTT3: cwnd = 4
RTT4: cwnd = 8
...

为什么叫"慢"启动?

"慢"是相对于当时"一上来就猛发"的蛮力策略而言的。起点低(从1开始),用指数增长快速探明网络容量,实际上一点都不慢。

7.3 拥塞避免

达到ssthresh后进入,线性增长:

每轮RTT: cwnd += 1 MSS
 
每收到一个ACK: cwnd += MSS²/cwnd

7.4 丢包后的处理

丢包类型处理方式
超时丢包重新慢启动,cwnd=1
3个重复ACK快速恢复,cwnd减半+ssthresh

7.5 完整流程图

建立连接

ssthresh = 65535

慢启动(指数增长)

达到ssthresh

拥塞避免(线性增长)

正常传输中...

丢包了

超时?                    3个重复ACK?
    ↓                      ↓
重新慢启动            快速重传 + 快速恢复
ssthresh = cwnd/2    ssthresh = cwnd/2
cwnd = 1             cwnd = ssthresh + 3

八、补充知识点

8.1 窗口扩大因子(Window Scaling)

窗口字段只有2字节,最大只能表示65535字节(约64KB),对高速网络太小。

三次握手时协商 Window Scale = 3
 
实际窗口 = 窗口字段值 × 2³
 
原来最大64KB → 现在最大 512KB

8.2 糊涂窗口综合征(SWS)

接收方窗口只剩1字节,发送方为这1字节发了个小包,效率极低。

解决:接收方窗口小于MSS一半时不更新窗口;发送方数据积累到MSS才发。

8.3 零窗口与窗口探测

接收方窗口=0时,发送方停止发送。但接收方处理完后,发送方怎么知道?

解决:发送方定期发探测包,接收方回复当前窗口大小

8.4 Nagle 算法与 CORK

算法作用场景
Nagle合并小包,减少网络往返适用于ssh等交互式
CORK禁止发送小包,必须凑满MSS适用于文件传输

九、总结

┌─────────────────────────────────────────────────────────────────┐
│                      TCP 核心机制总结                            │
├─────────────────────────────────────────────────────────────────┤
│  三次握手    │ 确认双方收发能力正常,协商ISN                      │
│  四次挥手    │ 关闭两个方向,必须四次                             │
│  序列号      │ 字节偏移,确认号=期望下一个字节                     │
│  RTO        │ 动态计算的超时时间                                 │
│  快速重传    │ 3个重复ACK时提前重传                              │
│  SACK       │ 选择性确认,告知不连续的数据块                      │
│  DSACK      │ 告知重复接收,诊断网络问题                          │
│  滑动窗口    │ 批量发送 + 流量控制                               │
│  拥塞控制    │ 慢启动→拥塞避免→快速重传→快速恢复                 │
└─────────────────────────────────────────────────────────────────┘

理解这些核心机制,是掌握网络编程和排查网络问题的基础。

分享

// RELATED_POSTS

0%