TCP 核心机制详解:三次握手、四次挥手、滑动窗口与拥塞控制
深入理解 TCP 协议的核心机制,包括三次握手四次挥手的原理、序列号的本质、超时重传与快速重传、SACK 机制、滑动窗口与流量控制、拥塞控制四大算法等。
TCP(Transmission Control Protocol)是互联网最重要的协议之一,作为传输层的核心协议,它为上层应用提供可靠、有序、面向连接的字节流服务。本文系统讲解 TCP 的核心机制。
一、TCP 三次握手
1.1 为什么需要三次握手
三次握手的核心目的是确认双方的发送能力和接收能力都正常,同时协商初始序列号。
1.2 详细过程
| 次数 | 方向 | 标志位 | 序列号 | 确认号 | 状态变化 |
|---|---|---|---|---|---|
| 第一次 | 客户端 → 服务器 | SYN=1 | Seq=x (ISN) | 0 | 客户端: CLOSED → SYN_SENT |
| 第二次 | 服务器 → 客户端 | SYN=1, ACK=1 | Seq=y (ISN) | Ack=x+1 | 服务器: LISTEN → SYN_RCVD |
| 第三次 | 客户端 → 服务器 | ACK=1 | Seq=x+1 | Ack=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秒。
- 确保最后的ACK能到达服务器:如果服务器没收到ACK,会重发FIN,客户端需要在这个时间内能接收重发的FIN
- 让旧数据包在网络中自然消散:防止延迟的旧数据包被新连接错误接收
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 六大标志位
| 标志位 | 名字 | 含义 |
|---|---|---|
| SYN | Synchronize | 请求同步序列号,建立连接时使用 |
| ACK | Acknowledgment | 确认号字段有效,确认收到了数据 |
| FIN | Finish | 请求关闭连接 |
| RST | Reset | 强制重置连接 |
| PSH | Push | 催促数据交付上层应用 |
| URG | Urgent | 紧急指针有效,优先处理 |
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²/cwnd7.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 → 现在最大 512KB8.2 糊涂窗口综合征(SWS)
接收方窗口只剩1字节,发送方为这1字节发了个小包,效率极低。
解决:接收方窗口小于MSS一半时不更新窗口;发送方数据积累到MSS才发。
8.3 零窗口与窗口探测
接收方窗口=0时,发送方停止发送。但接收方处理完后,发送方怎么知道?
解决:发送方定期发探测包,接收方回复当前窗口大小8.4 Nagle 算法与 CORK
| 算法 | 作用 | 场景 |
|---|---|---|
| Nagle | 合并小包,减少网络往返 | 适用于ssh等交互式 |
| CORK | 禁止发送小包,必须凑满MSS | 适用于文件传输 |
九、总结
┌─────────────────────────────────────────────────────────────────┐
│ TCP 核心机制总结 │
├─────────────────────────────────────────────────────────────────┤
│ 三次握手 │ 确认双方收发能力正常,协商ISN │
│ 四次挥手 │ 关闭两个方向,必须四次 │
│ 序列号 │ 字节偏移,确认号=期望下一个字节 │
│ RTO │ 动态计算的超时时间 │
│ 快速重传 │ 3个重复ACK时提前重传 │
│ SACK │ 选择性确认,告知不连续的数据块 │
│ DSACK │ 告知重复接收,诊断网络问题 │
│ 滑动窗口 │ 批量发送 + 流量控制 │
│ 拥塞控制 │ 慢启动→拥塞避免→快速重传→快速恢复 │
└─────────────────────────────────────────────────────────────────┘理解这些核心机制,是掌握网络编程和排查网络问题的基础。