TCP 协议深入 — 可靠传输的艺术
TCP 是互联网最重要的传输协议之一。它就像一位严谨的快递员——不仅确保包裹准确送达,还要按正确的顺序摆好,丢了的重新补发,速度太快还会主动放慢脚步以免造成拥堵。
📋 开篇自测:你已经知道多少?
- TCP 三次握手的每一步分别在做什么?为什么是三次而不是两次?
- TCP 的滑动窗口机制如何实现流量控制?
- TCP 连接断开时的四次挥手为什么需要四步?TIME_WAIT 状态存在的意义是什么?
一、TCP 概览:面向连接的可靠传输
1.1 TCP 要解决的核心问题
IP 层提供的是”尽力而为”的服务——数据包可能丢失、乱序、重复,甚至被损坏。TCP 在这样不可靠的基础上,构建了一套可靠传输机制:
| 问题 | TCP 的解决方案 |
|---|---|
| 数据丢失 | 超时重传、快速重传 |
| 数据乱序 | 序列号排序 |
| 数据重复 | 序列号去重 |
| 数据损坏 | 校验和验证 |
| 接收方处理不过来 | 流量控制(滑动窗口) |
| 网络拥堵 | 拥塞控制算法 |
1.2 TCP 头部结构
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口 (16位) | 目的端口 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号 (32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号 (32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |保留 |C|E|U|A|P|R|S|F| |
| 偏移 | |W|C|R|C|S|S|Y|I| 窗口大小 (16位) |
| | |R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 (16位) | 紧急指针 (16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项(可变长度) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
关键字段说明:
- 源端口 / 目的端口(各 16 位):标识通信的两端进程
- 序列号(32 位):标识本段数据的第一个字节在整个数据流中的位置
- 确认号(32 位):告诉对方”我已收到的数据到第几个字节,下一个期待收到的字节编号”
- 标志位:SYN(建立连接)、ACK(确认)、FIN(关闭连接)、RST(重置)、PSH(推送)、URG(紧急)
- 窗口大小(16 位):告诉对方自己的接收缓冲区还有多少空间
二、三次握手:建立连接
2.1 握手的完整过程
客户端(CLOSED) 服务端(LISTEN)
| |
| 1. SYN=1, seq=x |
| 客户端进入 SYN_SENT 状态 |
| ---------------------------------------------> |
| |
| 2. SYN=1, ACK=1, seq=y, ack=x+1 |
| 服务端进入 SYN_RCVD |
| <--------------------------------------------- |
| |
| 3. ACK=1, seq=x+1, ack=y+1 |
| 客户端进入 ESTABLISHED |
| ---------------------------------------------> |
| 服务端进入 ESTABLISHED |
| |
| ========== 连接建立,可以传输数据 ========== |
2.2 每一步在做什么
第一次握手:客户端发送 SYN 包,声明自己的初始序列号为 x。
- 目的:客户端通知服务端”我想建立连接”,并告知自己的初始序列号。
第二次握手:服务端回复 SYN+ACK,声明自己的初始序列号为 y,并确认收到了客户端的 SYN(ack=x+1)。
- 目的:服务端确认客户端的发送能力正常,并告知自己的初始序列号。
第三次握手:客户端发送 ACK,确认收到服务端的 SYN(ack=y+1)。
- 目的:客户端确认服务端的发送能力正常。
2.3 为什么是三次?两次行不行?
两次握手的问题在于:服务端无法确认客户端的接收能力。
两次握手的灾难场景:
客户端 服务端
| SYN (旧的、延迟到达的连接请求) |
| ---- (在网络中漂流了很久) ----------> |
| |
| SYN+ACK |
| <------------------------------------ |
| 服务端认为连接已建立,开始等待数据 |
| 但客户端知道这是过期的请求,不会响应 |
| |
| 服务端白白浪费资源等待... |
三次握手可以防止这种情况:即使服务端收到旧的 SYN 并回复了 SYN+ACK,客户端不会发送第三次 ACK(或发送 RST),服务端就不会进入 ESTABLISHED 状态。
🤔 想一想 TCP 三次握手期间,IP 层和 MAC 层在做什么?它们也只是”三次”吗?
三、四次挥手:断开连接
3.1 挥手的完整过程
客户端(ESTABLISHED) 服务端(ESTABLISHED)
| |
| 1. FIN=1, seq=u |
| 客户端进入 FIN_WAIT_1 |
| ----------------------------------------> |
| |
| 2. ACK=1, ack=u+1 |
| 服务端进入 CLOSE_WAIT |
| <---------------------------------------- |
| 客户端进入 FIN_WAIT_2 |
| |
| (服务端可能还有数据要发送...) |
| |
| 3. FIN=1, seq=w |
| 服务端进入 LAST_ACK |
| <---------------------------------------- |
| |
| 4. ACK=1, ack=w+1 |
| 客户端进入 TIME_WAIT |
| ----------------------------------------> |
| 服务端进入 CLOSED |
| |
| (等待 2MSL 后...) |
| 客户端进入 CLOSED |
3.2 为什么是四次而不是三次?
因为 TCP 是全双工协议,每个方向的关闭是独立的。当客户端发 FIN 时,只表示客户端不再发送数据,但服务端可能还有数据要发。所以服务端先回 ACK 表示”收到了你的关闭请求”,等自己数据发完后再发 FIN。
3.3 TIME_WAIT 的意义
客户端在发完最后一个 ACK 后,不是立即关闭,而是等待一段时间。RFC 793 定义 TIME_WAIT 应持续 2MSL(Maximum Segment Lifetime,最大报文寿命),MSL 建议值为 2 分钟。但 Linux 内核将 TIME_WAIT 硬编码为 60 秒(TCP_TIMEWAIT_LEN = 60*HZ,定义于 include/net/tcp.h),不可通过内核参数调节。
原因有二:
- 确保最后的 ACK 到达:如果服务端没收到 ACK 会重发 FIN,客户端在 TIME_WAIT 期间可以重新回复 ACK。
- 让旧连接的残余数据包消失:防止延迟的旧数据包干扰新连接。
TIME_WAIT 问题:高并发服务器短时间内大量连接关闭,
会产生大量 TIME_WAIT 状态的连接,占用端口和内存。
解决方案:
- 内核参数 tcp_tw_reuse=1 允许复用 TIME_WAIT 的端口
- 使用长连接(Keep-Alive)减少连接创建/关闭次数
- 让客户端主动关闭(TIME_WAIT 在客户端而非服务端)
注意: 早期常见的 tcp_tw_recycle 参数已在 Linux 4.12 内核中被移除,
因为它在 NAT 环境下会导致连接异常丢弃,不应再使用。
四、序列号与确认机制
4.1 序列号的作用
TCP 将数据视为一个连续的字节流,每个字节都有编号。序列号标识的是一段数据的第一个字节在整个流中的位置。
发送方发送 3 个段:
段1: seq=1, 数据="Hello"(5字节) --> 字节 1-5
段2: seq=6, 数据=" World"(6字节) --> 字节 6-11
段3: seq=12, 数据="!"(1字节) --> 字节 12
接收方确认:
收到段1后: ack=6 "我已收到前5字节,期待第6个"
收到段2后: ack=12 "我已收到前11字节,期待第12个"
收到段3后: ack=13 "我已收到前12字节,期待第13个"
4.2 累积确认
TCP 使用累积确认机制:确认号表示”这个编号之前的所有字节都已收到”。
发送: seg1(seq=1), seg2(seq=100), seg3(seq=200)
场景1: 全部按序到达
收到seg1 -> ack=100
收到seg2 -> ack=200
收到seg3 -> ack=300
场景2: seg2 丢失
收到seg1 -> ack=100
收到seg3 -> ack=100 (仍然是100,因为seg2还没收到)
重传seg2到达 -> ack=300 (一次性确认到300)
五、滑动窗口与流量控制
5.1 停等协议的低效
如果每发一个包就停下来等确认,效率极低:
停等协议(Stop-and-Wait):
发送方 网络 接收方
|--seg1--> [延迟] -->|
| |--ack-->|
|<---[延迟]---ack----|
|--seg2--> [延迟] -->|
...
大部分时间在等待!
5.2 滑动窗口机制
TCP 使用滑动窗口允许发送方在收到确认前连续发送多个段:
发送窗口大小 = 4 个段
发送方的视角:
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|已确认|已确认| 已发送 | 已发送 | 已发送 | 已发送 | 待发送 | 待发送 |
^ ^
| |
窗口左边界 窗口右边界
|<--------- 发送窗口(4) ---------->|
收到 ack=3 后,窗口右移:
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|已确认|已确认|已确认| 已发送 | 已发送 | 已发送 | 已发送 | 待发送 |
^ ^
|<--------- 发送窗口(4) ---------->|
5.3 流量控制
接收方通过 TCP 头部的窗口大小字段,告诉发送方自己的接收缓冲区还剩多少空间。发送方据此调整发送速率:
接收方缓冲区变化:
时刻1: 缓冲区空,通告窗口 = 4000 字节
发送方: 可以发送最多 4000 字节
时刻2: 收到 2000 字节,应用还未读取,通告窗口 = 2000
发送方: 最多再发 2000 字节
时刻3: 又收到 2000 字节,缓冲区满,通告窗口 = 0
发送方: 停止发送!(零窗口)
时刻4: 应用读取了 3000 字节,通告窗口 = 3000
发送方: 恢复发送
当窗口变为 0 时,发送方启动持续计时器,定期发送窗口探测报文(Window Probe),以检测窗口是否重新打开。
六、拥塞控制:对网络负责
6.1 拥塞控制 vs 流量控制
- 流量控制:防止发送方淹没接收方(端到端问题)
- 拥塞控制:防止发送方淹没网络(全局问题)
6.2 四大算法
TCP 的拥塞控制包含四个核心算法:慢启动、拥塞避免、快速重传和快速恢复。
拥塞窗口(cwnd)变化示意图:
cwnd
^
| * <- 检测到丢包(超时)
| *
| * 拥塞避免(线性增长)
| *
| * <- ssthresh(慢启动阈值)
| *
| *
| * 慢启动(指数增长)
| *
| *
| *
| *
| *
+*----------------------------------------------> 时间
|
| 丢包后: ssthresh = cwnd/2, cwnd 重置
|
| *
| * 重新慢启动...
| *
| *
慢启动:经典定义中初始 cwnd=1 MSS(最大段大小),但现代系统已大幅提升——RFC 6928(2013)将推荐初始窗口增加到 10 MSS(约 14.6KB),Linux 2.6.39+ 默认即采用此值。每收到一个 ACK,cwnd 翻倍。增长看似”慢”,实际是指数增长。
拥塞避免:当 cwnd 达到慢启动阈值(ssthresh)后,改为每个 RTT 只增加 1 MSS,线性增长。
快速重传:收到 3 个重复 ACK 时立即重传,不等超时定时器。
快速恢复:快速重传后,ssthresh = cwnd/2,cwnd = ssthresh + 3,然后进入拥塞避免阶段。
6.3 现代拥塞控制算法
经典的 TCP Reno 算法在高带宽长延迟网络中效率不佳。现代算法包括:
| 算法 | 特点 | 适用场景 |
|---|---|---|
| CUBIC | Linux 默认,基于三次函数调整窗口 | 通用场景 |
| BBR | Google 开发,基于带宽和 RTT 估算 | 高延迟、有丢包的网络 |
| DCTCP | 数据中心优化,利用 ECN 标记 | 数据中心内部 |
🤔 想一想 为什么 TCP 在移动网络(4G/5G)环境下性能不如预期?丢包和信号波动会如何影响拥塞控制?
七、超时重传与定时器
7.1 超时重传时间(RTO)的计算
RTO 不是固定值,而是根据网络状况动态调整的:
RTT 采样和 RTO 计算(Jacobson 算法):
SRTT = (1-α) * SRTT + α * RTT_sample (平滑RTT, α=1/8)
RTTVAR = (1-β) * RTTVAR + β * |SRTT - RTT_sample| (RTT偏差, β=1/4)
RTO = SRTT + 4 * RTTVAR
示例:
RTT采样: 100ms, 120ms, 90ms, 150ms
SRTT 逐步逼近实际 RTT 的加权平均
RTTVAR 反映 RTT 的波动程度
RTO 通常是 SRTT 的 1-3 倍
7.2 TCP 的重要定时器
| 定时器 | 用途 |
|---|---|
| 重传定时器 | 发送数据后启动,超时则重传 |
| 持续定时器 | 零窗口时启动,定期探测窗口 |
| 保活定时器 | 长时间无数据时探测连接是否存活(Keep-Alive) |
| 2MSL 定时器 | TIME_WAIT 状态下等待 2 倍最大段寿命 |
八、TCP 连接的状态机
8.1 完整的状态转换图
+---------+
| CLOSED |
+----+----+
主动打开 / | \ 被动打开
发送 SYN | 创建 TCB
| | |
v | v
+---------+ | +---------+
|SYN_SENT | | | LISTEN |
+----+----+ | +----+----+
收到SYN+ACK/ | | | 收到SYN/
发送ACK | | | 发送SYN+ACK
v | v
+---------+ | +---------+
| ESTAB- |<--+-->|SYN_RCVD |
| LISHED | +----+----+
+----+----+ |
| 收到ACK |
主动关闭/ | |
发送FIN | v
| +---------+
v | ESTAB- |
+---------+ | LISHED |
|FIN_WAIT1| +---------+
+----+----+
收到ACK/ |
v
+---------+ 收到FIN/发送ACK
|FIN_WAIT2| ---------------+
+----+----+ |
收到FIN/ | v
发送ACK | +-----------+
v | CLOSE_WAIT|
+---------+ +-----+-----+
|TIME_WAIT| | 发送FIN
+----+----+ v
| +-----------+
2MSL超时 | | LAST_ACK |
v +-----+-----+
+---------+ | 收到ACK
| CLOSED | v
+---------+ +---------+
| CLOSED |
+---------+
使用 netstat 或 ss 命令可以查看当前系统中各 TCP 连接的状态:
$ ss -ant
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:80 0.0.0.0:*
ESTAB 0 0 192.168.1.10:52341 93.184.216.34:80
TIME_WAIT 0 0 192.168.1.10:52340 93.184.216.34:80
九、章节小结
- 三次握手建立连接,确保双方的发送和接收能力都正常,并协商初始序列号。
- 四次挥手断开连接,因全双工通信需要独立关闭两个方向。TIME_WAIT 等待 2MSL 确保旧数据包消亡。
- 序列号和确认号是可靠传输的基石,配合累积确认实现数据的有序、不丢、不重。
- 滑动窗口实现流量控制,防止发送方速率超过接收方处理能力。
- 拥塞控制(慢启动、拥塞避免、快速重传、快速恢复)防止网络过载,是 TCP 的全局责任感的体现。
- 超时重传通过动态计算 RTO 来适应不同的网络条件。
📝 结尾自测:检验你的收获
- 请详细描述 TCP 三次握手的过程,包括每一步的序列号和标志位变化。为什么不能是两次?
- TCP 四次挥手中,为什么需要 TIME_WAIT 状态?如果没有这个状态会出什么问题?
- 滑动窗口和拥塞窗口有什么区别?TCP 实际的发送窗口大小由什么决定?
- 解释快速重传的触发条件和工作机制:3 个重复 ACK 为什么意味着丢包?
- 在高并发 Web 服务器上经常看到大量 TIME_WAIT 连接,这是为什么?有哪些解决方案?
下一章预告:TCP 可靠但开销不小,并非所有场景都需要它。下一章我们将认识 TCP 的”轻量级兄弟” UDP——看看它为什么只有 8 字节的头部却能撑起 DNS 查询、视频通话等关键应用,同时深入 DNS、DHCP 等核心应用层协议的工作原理。
购买课程解锁全部内容
网络通信第一课:10 章掌握计算机网络
¥29.90