TCP协议详解------三次握手、四次挥手与连接状态
本文基于华为云 ECS 实战环境,在 server-3(192.168.0.23)上通过 tcpdump 抓包、ss 连接状态查看、sysctl 内核参数读取等手段,深入剖析 TCP 协议的核心机制。所有数据均来自真实生产环境抓包,完整还原 TCP 连接的建立、数据传输与断开全过程。
一、TCP协议概述
TCP(Transmission Control Protocol,传输控制协议)是互联网基石协议之一,位于 OSI 模型第四层------传输层。与 UDP 不同,TCP 具有以下三大核心特性:
| 特性 | 说明 | 对比 UDP |
|---|---|---|
| 面向连接 | 通信前必须通过三次握手建立连接 | UDP 无连接,直接发送 |
| 可靠传输 | 通过序号、确认、重传保证数据可达 | UDP 尽力而为,不保证可靠 |
| 字节流 | 面向字节流,无消息边界 | UDP 面向报文,保留消息边界 |
此外,TCP 还提供流量控制(滑动窗口)、拥塞控制(cubic/reno 算法)、连接管理(状态机)等高级机制,确保在各种网络条件下高效、公平地传输数据。
二、TCP报文头结构
TCP 报文头标准长度为 20 字节(不含选项),其结构如下:
scss
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口 (16bit) | 目的端口 (16bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号 (32bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号 (32bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据偏移 | 保留 |U|A|P|R|S|F| 窗口 (16bit) |
| (4bit) | (3b) |R|C|S|S|Y|I| |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 (16bit) | 紧急指针 (16bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项 (可变长度) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
各关键字段含义:
| 字段 | 长度 | 说明 |
|---|---|---|
| 源端口 | 16 bit | 发送方端口 |
| 目的端口 | 16 bit | 接收方端口 |
| 序列号 (seq) | 32 bit | 标识本报文段数据第一个字节的序号 |
| 确认号 (ack) | 32 bit | 期望收到对方下一个报文段的第一个字节序号 |
| 标志位 | 6 bit | URG/ACK/PSH/RST/SYN/FIN |
| 窗口 (win) | 16 bit | 接收窗口大小,用于流量控制 |
| 校验和 | 16 bit | 检验报文头和数据的完整性 |
tcpdump 输出中常见的标志位缩写:[S]=SYN,[.]=ACK,[P.]=PSH+ACK,[F.]=FIN+ACK,[S.]=SYN+ACK,[R.]=RST+ACK。
三、TCP三次握手
3.1 抓包实战
在 server-3(192.168.0.23)上对 server-1(192.168.0.201:80)发起 HTTP 请求,同时用 tcpdump 抓取完整握手过程:
bash
# 在 server-3 上抓包
tcpdump -i eth0 -nn -v -S 'host 192.168.0.201 and tcp port 80' -c 20
抓包结果(真实输出):
ini
12:05:10.465060 IP (tos 0x0, ttl 64, id 39911, offset 0, flags [DF], proto TCP (6), length 60)
192.168.0.23.42778 > 192.168.0.201.80: Flags [S], cksum 0x825f (incorrect -> 0x6492),
seq 619644684, win 64240, options [mss 1460,sackOK,TS val 18321871 ecr 0,nop,wscale 7], length 0
12:05:10.465230 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
192.168.0.201.80 > 192.168.0.23.42778: Flags [S.], cksum 0xbd35 (correct),
seq 991996670, ack 619644685, win 65160,
options [mss 1460,sackOK,TS val 4061121412 ecr 18321871,nop,wscale 7], length 0
12:05:10.465250 IP (tos 0x0, ttl 64, id 39912, offset 0, flags [DF], proto TCP (6), length 52)
192.168.0.23.42778 > 192.168.0.201.80: Flags [.], cksum 0x8257 (incorrect -> 0xe894),
ack 991996671, win 502, options [nop,nop,TS val 18321871 ecr 4061121412], length 0
3.2 逐包解析
第一个包:SYN(客户端 → 服务端)
Flags [S]:SYN 标志位置 1,表示发起连接请求seq 619644684:客户端初始序列号(ISN),由内核随机生成win 64240:客户端宣告的接收窗口大小options [mss 1460,sackOK,TS val 18321871 ecr 0,nop,wscale 7]:mss 1460:最大段大小,表示客户端能接收的每个 TCP 段最大数据长度sackOK:支持选择性确认(Selective ACK)TS val 18321871 ecr 0:时间戳选项,用于计算 RTTwscale 7:窗口缩放因子为 7,实际窗口 = 64240 × 2^7 = 8,222,720 字节
第二个包:SYN-ACK(服务端 → 客户端)
Flags [S.]:SYN 和 ACK 同时置 1seq 991996670:服务端初始序列号ack 619644685:确认号 = 客户端 ISN + 1 = 619644684 + 1win 65160:服务端宣告的接收窗口- 同样携带
mss 1460和wscale 7选项
第三个包:ACK(客户端 → 服务端)
Flags [.]:仅 ACK 标志位置 1ack 991996671:确认号 = 服务端 ISN + 1 = 991996670 + 1win 502:窗口缩放后的通告值(502 × 2^7 = 64,256 字节)- 连接建立完成,进入 ESTABLISHED 状态
3.3 时序图
ini
client (192.168.0.23:42778) server (192.168.0.201:80)
| |
|------- SYN, seq=619644684 ------------>|
| win=64240, mss=1460, wscale=7 |
| (客户端进入 SYN_SENT) |
| |
|<------ SYN+ACK, seq=991996670 ---------|
| ack=619644685, win=65160 |
| (服务端进入 SYN_RECV) |
| |
|------- ACK, ack=991996671 ------------>|
| win=502 |
| (双方进入 ESTABLISHED) |
| |
|======= 连接建立,可传输数据 =============|
| |
|------- PSH+ACK, GET / HTTP/1.1 ------->|
| seq=619644685, length=76 |
| |
三次握手的本质是双方互相确认对方的接收能力正常,并协商初始序列号。之所以是"三次"而非"两次",是因为第二次握手时服务端的 SYN 和 ACK 合并为一个包发送,减少了通信开销。
四、TCP四次挥手
4.1 抓包实战
同一抓包会话中,连接断开时的四次挥手(以 server-1 到 server-3 的代理连接为例):
ini
12:05:10.465761 IP (tos 0x0, ttl 64, id 24000, offset 0, flags [DF], proto TCP (6), length 52)
192.168.0.23.80 > 192.168.0.201.55960: Flags [F.], cksum 0x8257 (incorrect -> 0xab1c),
seq 3618244973, ack 2627313244, win 508, options [nop,nop,TS val 18321872 ecr 4061121412], length 0
12:05:10.465830 IP (tos 0x0, ttl 64, id 20550, offset 0, flags [DF], proto TCP (6), length 52)
192.168.0.201.55960 > 192.168.0.23.80: Flags [.], cksum 0xab23 (correct),
ack 3618244973, win 501, options [nop,nop,TS val 4061121413 ecr 18321872], length 0
12:05:10.465856 IP (tos 0x0, ttl 64, id 20551, offset 0, flags [DF], proto TCP (6), length 52)
192.168.0.201.55960 > 192.168.0.23.80: Flags [F.], cksum 0xab21 (correct),
seq 2627313244, ack 3618244974, win 501, options [nop,nop,TS val 4061121413 ecr 18321872], length 0
12:05:10.465860 IP (tos 0x0, ttl 64, id 24001, offset 0, flags [DF], proto TCP (6), length 52)
192.168.0.23.80 > 192.168.0.201.55960: Flags [.], cksum 0x8257 (incorrect -> 0xab1a),
ack 2627313245, win 508, options [nop,nop,TS val 18321872 ecr 4061121413], length 0
4.2 时序图
ini
主动关闭方 (192.168.0.23:80) 被动关闭方 (192.168.0.201:55960)
| |
|------- FIN+ACK, seq=3618244973 ------->|
| ack=2627313244 |
| (进入 FIN_WAIT_1) |
| |
|<------ ACK, ack=3618244973 ------------|
| (进入 FIN_WAIT_2) |
| (对端进入 CLOSE_WAIT) |
| |
|<------ FIN+ACK, seq=2627313244 --------|
| ack=3618244974 |
| (对端进入 LAST_ACK) |
| |
|------- ACK, ack=3627313245 ------------>|
| (进入 TIME_WAIT, 等待 2MSL) |
| (对端进入 CLOSED) |
| |
|======== 连接完全关闭 ===================|
四次挥手之所以是四次而非三次,是因为 TCP 是全双工通信------关闭连接时,每个方向都需要独立关闭。FIN 只表示"我没有数据要发了",但仍然可以接收数据。因此两个方向的 FIN 需要分别发送和确认。
4.3 3包挥手现象
值得注意的是,在原始客户端连接(192.168.0.23.42778 → 192.168.0.201:80)的断开过程中,抓包显示的是3包挥手:
ini
12:05:10.465931 - 192.168.0.23.42778 > 192.168.0.201.80: Flags [F.], seq 619644761
12:05:10.466011 - 192.168.0.201.80 > 192.168.0.23.42778: Flags [F.], seq 991997009, ack 619644762
12:05:10.466013 - 192.168.0.23.42778 > 192.168.0.201.80: Flags [.], ack 991997010
服务端将 ACK 和 FIN 合并为一个包返回,从而将四次挥手压缩为三次。这在服务端没有待发送数据时很常见。
五、TCP连接状态
5.1 状态机总览
| 状态 | 说明 | 触发条件 |
|---|---|---|
| LISTEN | 监听等待连接 | 服务端调用 listen() |
| SYN_SENT | 已发送SYN | 客户端调用 connect() |
| SYN_RECV | 已收到SYN并发送SYN+ACK | 服务端收到SYN |
| ESTABLISHED | 连接已建立 | 三次握手完成 |
| FIN_WAIT_1 | 已发送FIN | 主动关闭方调用 close() |
| FIN_WAIT_2 | 收到对端ACK | 半关闭状态 |
| TIME_WAIT | 等待2MSL | 主动关闭方收到最后FIN的ACK |
| CLOSE_WAIT | 收到对端FIN | 被动关闭方等待应用调用 close() |
| LAST_ACK | 已发送FIN | 被动关闭方等待最后ACK |
| CLOSED | 连接完全关闭 | 最终状态 |
5.2 实战状态查看
在 server-3 上执行 ss -tan 查看当前所有 TCP 连接状态:
bash
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
真实输出:
css
9 TIME-WAIT
7 LISTEN
2 ESTAB
1 State
可以看到,系统中有 9 个 TIME-WAIT 连接(之前的短连接关闭后遗留)、7 个 LISTEN 端口和 2 个 ESTABLISHED 连接。
查看 TIME-WAIT 状态的具体连接:
css
Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0 0 192.168.0.23:39826 192.168.0.201:80
0 0 192.168.0.23:39862 192.168.0.201:80
0 0 192.168.0.23:39846 192.168.0.201:80
0 0 192.168.0.23:80 192.168.0.201:61610
这些 TIME-WAIT 连接都是与 192.168.0.201 的 HTTP 通信结束后遗留的。大量 TIME-WAIT 是短连接场景的典型现象。
查看 ESTABLISHED 状态的连接:
css
Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0 0 192.168.0.23:46292 100.125.12.110:33554
0 0 192.168.0.23:22 114.116.247.146:55650
当前有两条活跃连接:一条 SSH 会话(端口22)和一条内网管理通道。
5.3 TIME_WAIT的意义
TIME_WAIT 状态持续 2MSL(最大报文段生存时间),有两个核心作用:
- 确保最后一个 ACK 到达对端:如果最后发出的 ACK 丢失,对端会重发 FIN,本端可以再次发送 ACK
- 防止旧连接的报文干扰新连接:等待 2MSL 后,网络中残留的旧报文必然过期消失
六、TCP窗口与MSS
6.1 窗口缩放
从 SYN 抓包可以看到,客户端和服务端都协商了窗口缩放因子:
ini
192.168.0.23.42778 > 192.168.0.201.80: Flags [S],
seq 619644684, win 64240, options [mss 1460,sackOK,TS val 18321871 ecr 0,nop,wscale 7]
| 参数 | 值 | 说明 |
|---|---|---|
| win | 64240 | SYN 包中的通告窗口 |
| wscale | 7 | 窗口缩放因子 |
| 实际窗口 | 64240 × 2^7 = 8,222,720 字节 | 约 7.8 MB |
| mss | 1460 | 最大段大小 |
没有窗口缩放选项时,TCP 窗口最大只有 65535 字节(16 bit),在高带宽延迟网络中会成为瓶颈。窗口缩放选项将窗口扩展为 32 bit,最大可达 1GB。
6.2 MSS协商
MSS(Maximum Segment Size)在 SYN 阶段协商,双方各通告自己的 MSS。本例中双方 MSS 均为 1460 字节,这是以太网 MTU 1500 减去 IP 头 20 字节和 TCP 头 20 字节后的标准值。
6.3 内核TCP统计
通过 /proc/net/tcp 和 nstat 可以查看内核 TCP 统计:
bash
nstat -az | grep -i 'Tcp*' | head -15
真实输出:
TcpActiveOpens 226 0.0
TcpPassiveOpens 117 0.0
TcpAttemptFails 9 0.0
TcpEstabResets 72 0.0
TcpInSegs 14259 0.0
TcpOutSegs 11275 0.0
TcpRetransSegs 56 0.0
TcpInErrs 0 0.0
TcpOutRsts 994 0.0
TcpInCsumErrors 0 0.0
TcpExtSyncookiesSent 0 0.0
TcpExtSyncookiesRecv 0 0.0
TcpExtSyncookiesFailed 0 0.0
TcpExtEmbryonicRsts 5 0.0
| 统计项 | 值 | 含义 |
|---|---|---|
| TcpActiveOpens | 226 | 主动发起的连接数(客户端角色) |
| TcpPassiveOpens | 117 | 被动接受的连接数(服务端角色) |
| TcpInSegs | 14259 | 接收的 TCP 段总数 |
| TcpOutSegs | 11275 | 发送的 TCP 段总数 |
| TcpRetransSegs | 56 | 重传的 TCP 段数 |
| TcpOutRsts | 994 | 发送的 RST 段数 |
七、TCP保活机制
TCP Keepalive 用于检测长时间空闲的连接是否仍然存活。通过 sysctl 查看内核保活参数:
bash
sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl \
net.ipv4.tcp_keepalive_probes net.ipv4.tcp_fin_timeout \
net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_tw_reuse
真实输出:
ini
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_fin_timeout = 60
net.ipv4.tcp_max_syn_backlog = 1024
net.ipv4.tcp_tw_reuse = 2
| 参数 | 默认值 | 说明 |
|---|---|---|
| tcp_keepalive_time | 7200s (2小时) | 连接空闲多久后开始发送保活探测 |
| tcp_keepalive_intvl | 75s | 两次探测之间的间隔 |
| tcp_keepalive_probes | 9 | 探测失败多少次后判定连接死亡 |
| tcp_fin_timeout | 60s | FIN_WAIT_2 状态保持时间 |
| tcp_max_syn_backlog | 1024 | SYN 队列最大长度 |
| tcp_tw_reuse | 2 | TIME_WAIT 端口复用(2=仅环回口) |
保活总超时 = 7200 + 75 × 9 = 7875 秒(约 2 小时 11 分钟)。在生产环境中,对于长连接服务(如数据库连接池),通常需要调小 tcp_keepalive_time 以更快发现死连接。
八、TCP拥塞控制
8.1 拥塞控制算法
bash
sysctl net.ipv4.tcp_congestion_control net.ipv4.tcp_available_congestion_control \
net.core.somaxconn net.ipv4.tcp_max_syn_backlog
真实输出:
ini
net.ipv4.tcp_congestion_control = cubic
net.ipv4.tcp_available_congestion_control = reno cubic
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 1024
当前系统使用 cubic 算法(Linux 2.6.19 起默认),可用的算法包括 reno 和 cubic。TCP Fast Open 已启用(值为 1)。
8.2 连接内部状态
通过 ss -ti 可以查看已建立连接的拥塞控制内部参数:
perl
ESTAB 0 0 192.168.0.23:46292 100.125.12.110:33554
cubic wscale:7,7 rto:205 rtt:4.967/0.958 ato:40 mss:1452 pmtu:1500
rcvmss:1460 advmss:1460 cwnd:10 bytes_sent:5814 bytes_acked:5815
bytes_received:8301 segs_out:58 segs_in:31 data_segs_out:27
data_segs_in:29 send 23.4Mbps lastsnd:34325 lastrcv:34319
lastack:34319 pacing_rate 46.8Mbps delivery_rate 2.88Mbps
delivered:28 app_limited busy:131ms rcv_space:14600
rcv_ssthresh:66660 minrtt:4.028 snd_wnd:50688
| 参数 | 值 | 含义 |
|---|---|---|
| cubic | - | 拥塞控制算法 |
| wscale | 7,7 | 发送/接收窗口缩放因子 |
| rto | 205ms | 重传超时时间 |
| rtt | 4.967ms | 往返时间 |
| rttvar | 0.958ms | RTT 方差 |
| cwnd | 10 | 拥塞窗口(以段为单位) |
| mss | 1452 | 实际最大段大小 |
| send | 23.4Mbps | 当前发送速率 |
| pacing_rate | 46.8Mbps | 发送节奏速率 |
| snd_wnd | 50688 | 发送窗口大小 |
九、TCP重传机制
9.1 重传统计
bash
cat /proc/sys/net/ipv4/tcp_retries2
nstat -az | grep -i retrans
真实输出:
15
TcpRetransSegs 56 0.0
TcpExtTCPLostRetransmit 37 0.0
TcpExtTCPFastRetrans 0 0.0
TcpExtTCPSlowStartRetrans 0 0.0
TcpExtTCPRetransFail 0 0.0
TcpExtTCPSynRetrans 33 0.0
| 统计项 | 值 | 含义 |
|---|---|---|
| tcp_retries2 | 15 | 数据段最大重传次数(超过后放弃连接) |
| TcpRetransSegs | 56 | 重传段总数 |
| TcpExtTCPLostRetransmit | 37 | 丢失的重传段 |
| TcpExtTCPFastRetrans | 0 | 快速重传次数 |
| TcpExtTCPSynRetrans | 33 | SYN 重传次数 |
tcp_retries2 = 15 意味着数据段最多重传 15 次,按指数退避算法,总超时约 924 秒后才放弃连接。SYN 重传次数由 tcp_syn_retries 控制(默认 6 次)。
9.2 并发连接测试
发起 5 个并发 HTTP 请求,观察连接建立情况:
bash
for i in $(seq 1 5); do curl -s -o /dev/null http://192.168.0.201/ & done
sleep 2
ss -tn dst 192.168.0.201 | head -10
ss -tn dst 192.168.0.201 | wc -l
输出结果显示,由于请求处理速度极快,在 sleep 2 后所有连接已关闭并进入 TIME-WAIT 状态,ss -tn 仅返回 1 行(表头),说明没有活跃的 ESTABLISHED 连接残留。
十、TCP定时器
通过 ss -to 查看连接的定时器信息:
bash
ss -to dst 192.168.0.201 2>/dev/null | head -10
ss -tn state established | head -10
真实输出:
yaml
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
---
Recv-Q Send-Q Local Address:Port Peer Address:Port Process
0 0 192.168.0.23:46292 100.125.12.110:33554
0 64 192.168.0.23:22 114.116.247.146:55650
与 192.168.0.201 的连接已全部关闭(无定时器信息显示),当前 ESTABLISHED 连接为内网管理通道和 SSH 会话。ss -to 的 -o 选项会显示定时器信息(如 timer:(keepalive,30sec,0)),此处无输出表示连接处于空闲状态,未激活任何定时器。
TCP 内部维护四类核心定时器:
| 定时器 | 触发场景 | 默认参数 |
|---|---|---|
| 重传定时器 | 发出数据后启动 | 初始 RTO 由 RTT 动态计算 |
| 保活定时器 | 连接空闲超过 keepalive_time | 7200s |
| TIME_WAIT 定时器 | 主动关闭方进入 TIME_WAIT | 2 × MSL(60s) |
| 持续定时器 | 对端窗口为 0 时启动 | 由探测周期决定 |
十一、总结
本文通过真实抓包数据,完整剖析了 TCP 协议的核心机制:
- 三次握手:SYN → SYN-ACK → ACK,双方协商初始序列号和窗口参数
- 四次挥手:FIN → ACK → FIN → ACK,全双工连接需要双向独立关闭
- 连接状态:11 种状态构成完整状态机,TIME_WAIT 是短连接场景的典型状态
- 窗口与MSS:窗口缩放(wscale 7)使窗口可达 8MB,MSS 1460 适配以太网 MTU
- 保活机制:默认 2 小时开始探测,9 次失败后判定连接死亡
- 拥塞控制:cubic 算法,cwnd=10 段,发送速率 23.4Mbps
- 重传机制:tcp_retries2=15,SYN 重传 33 次,数据重传 56 次
理解 TCP 这些底层机制,对于排查网络延迟、连接超时、吞吐量瓶颈等生产问题具有重要指导意义。在实际运维中,建议根据业务场景合理调整 tcp_keepalive_time、tcp_tw_reuse、tcp_max_syn_backlog 等参数,以优化连接管理效率。