TCP 报文、连接建立与状态机
内容摘要:传输控制协议(Transmission Control Protocol,TCP)为应用提供可靠、有序、双向的字节流。连接建立不只是“三个包”,还要同步初始序列号、验证双向可达并协商 MSS、窗口扩大、SACK 和时间戳等能力。本章从报文头与四元组开始,完整拆解三次握手、四次挥手、半关闭、RST、11 个状态、监听队列、TIME_WAIT/CLOSE_WAIT,以及 SYN Flood 与 SYN Cookie。
难度:高级 | 前置知识:04 套接字与内核网络栈、09 MTU、12 UDP | 所属层次:传输层
一、TCP 的核心语义
RFC 9293 已取代 RFC 793,定义现代 TCP 的基础行为。TCP 向应用提供:
- 面向连接:通信前建立双方状态。
- 可靠、有序:用序列号、确认与重传修复丢失和乱序(14)。
- 字节流:不保留应用消息边界。
- 全双工:两个方向独立传输、独立关闭。
- 单播:一条连接由本地/远端地址与端口识别。
连接通常由四元组唯一标识:
(源 IP, 源端口, 目的 IP, 目的端口)加上协议 TCP 后常称五元组(02)。同一服务端 IP:443 能同时接收大量连接,因为客户端源 IP/源端口不同。
TCP 不自带“应用存活”语义:连接
ESTABLISHED只说明内核 TCP 状态存在,不代表远端进程业务线程健康。快速发现业务假死仍需请求超时或应用心跳(04)。
二、TCP 报文头
TCP 基础首部 20 字节,带选项时最长 60 字节。
| 字段 | 位宽 | 语义 | 生成方 | 接收方处理 | 抓包显示 | 常见异常 |
|---|---|---|---|---|---|---|
| 源/目的端口 | 各 16 位 | 应用复用 | socket 层 | 按连接查找控制块 | tcp.srcport/dstport | 端口未监听、NAT 改写 |
| 序列号 | 32 位 | 本段第一个数据字节序号;SYN 自占一个序号 | 发送 TCP | 排序、去重、确认 | tcp.seq | 抓包工具默认显示相对序号 |
| 确认号 | 32 位 | 下一个期望字节 | 接收 TCP | 累积确认 | tcp.ack | 重复 ACK、ACK 超出窗口 |
| 数据偏移 | 4 位 | TCP 头长度,单位 4 字节 | 发送 TCP | 定位载荷 | tcp.hdr_len | 选项截断 |
| 标志位 | 8 位 | SYN/ACK/FIN/RST/PSH/URG/ECE/CWR | TCP 状态机 | 驱动状态与功能 | tcp.flags | 异常 RST、SYN Flood |
| Window | 16 位 | 从 ACK 起愿意接收的字节数 | 接收端 | 限制发送窗口 | tcp.window_size_value | 零窗口、缩放误读 |
| Checksum | 16 位 | 伪首部 + TCP 头 + 数据 | 发送端 | 错误则丢弃 | tcp.checksum | 本机卸载产生假错误(04) |
| Urgent Pointer | 16 位 | URG 语义,历史兼容 | 应用/TCP | 实现差异较多 | tcp.urgent_pointer | 不宜设计新协议依赖 |
1、常见 TCP 选项
选项多在 SYN/SYN-ACK 中协商:
| 选项 | 作用 | 协商边界 |
|---|---|---|
| MSS(Maximum Segment Size) | 告诉对端自己愿意接收的最大 TCP 载荷 | 只在 SYN 中发送;每个方向可不同 |
| Window Scale | 把 16 位窗口扩展到更大范围 | 双方 SYN 都携带才生效(RFC 7323) |
| SACK Permitted | 允许建立后使用选择性确认 | 只在 SYN 中协商(RFC 2018) |
| Timestamps | RTT 测量与 PAWS 防旧序列号 | 双方支持后在后续段持续携带(RFC 7323) |
MSS 不等于 MTU:以太网 MTU 1500、无 IP/TCP 选项时,IPv4 常见 MSS 1460,IPv6 常见 MSS 1440。隧道会进一步降低路径 MTU,需要 PMTUD 或 MSS Clamping(09)。
三、三次握手
设客户端初始序列号为 x,服务端为 y:
SYN 和 FIN 各消耗一个序列号,即便它们不携带普通应用数据。
为什么不是两次
三次握手同时解决:
- 客户端确认自己的发送与接收路径可用。
- 服务端确认自己的发送与接收路径可用。
- 双方同步本次连接的初始序列号,避免旧重复报文被误认为新连接数据。
- 双方确认 TCP 选项协商结果。
若只有两次,服务端发出 SYN-ACK 后无法确认客户端是否收到,容易为陈旧 SYN 或伪造源地址过早建立完整状态。
初始序列号不是简单从 0 开始
初始序列号(Initial Sequence Number,ISN)应随时间和连接四元组不可预测地变化:
- 防止旧连接的延迟段落入新连接窗口。
- 降低盲注入与连接劫持风险。
- 抓包工具常显示“相对序列号 0/1”,那是展示简化,不是线上真实 ISN。
四、从 socket API 看服务端握手
listen() 返回不代表某客户端已连接;accept() 返回的是新连接 socket,监听 socket 继续接受其他连接。
1、半连接队列与全连接队列
概念上可以区分:
- SYN 队列/半连接队列:收到 SYN、发送 SYN-ACK,尚未收到最终 ACK。
- Accept 队列/全连接队列:三次握手已完成,等待应用
accept()。
Linux 实现细节随内核版本演进,backlog、somaxconn、tcp_max_syn_backlog 分别影响不同阶段,不能简单说“只有一条队列”或“backlog 就是最大连接数”。
| 队列异常 | 典型原因 | 证据 |
|---|---|---|
| SYN 队列压力 | SYN Flood、回包路径失败、服务端资源不足 | SYN_RECV 增长、SYN-ACK 重传 |
| Accept 队列满 | 应用 accept 太慢、线程池阻塞 | 握手重传/丢弃、listen overflow 计数 |
| 已连接 fd 耗尽 | 连接泄漏、上限太低 | EMFILE/ENFILE、fd 使用率 |
五、TCP 状态机
RFC 9293 定义的主要状态:
| 状态 | 含义 | 运维判断 |
|---|---|---|
| LISTEN | 等待新连接 | 服务端正常监听 |
| SYN_SENT | 已发 SYN 等回应 | 持续多通常是对端/路径不可达 |
| SYN_RECEIVED | 已回 SYN-ACK 等最终 ACK | 大量堆积需查攻击或回程 |
| ESTABLISHED | 双向连接建立 | 不等于应用健康 |
| FIN_WAIT_1/2 | 主动关闭方等待 ACK/FIN | 长期 FIN_WAIT_2 可能对端不关闭 |
| CLOSE_WAIT | 已收 FIN,等待本地应用 close | 持续增长通常是应用泄漏 |
| LAST_ACK | 被动关闭方已发 FIN 等 ACK | 长期存在需查路径/对端 |
| TIME_WAIT | 主动关闭方等待旧段消失并可重发最终 ACK | 正常协议状态,不应见到就调内核 |
六、四次挥手与半关闭
两个方向独立关闭,因此常见关闭需要四段:
中间两个包可在服务端立即关闭时合并为 FIN+ACK,所以抓包不一定永远是严格四个包。
半关闭
应用调用 shutdown(fd, SHUT_WR):
- 本方向发送 FIN,表示“我不会再发送数据”。
- 接收方向仍开放,可以继续
recv()对端的剩余响应。 - TCP
recv()返回 0 字节表示对端有序关闭了发送方向。
这与 UDP 的 0 字节数据报完全不同(12)。
七、TIME_WAIT 与 CLOSE_WAIT
1、TIME_WAIT 为什么存在
主动关闭方进入 TIME_WAIT,等待 2 × MSL(Maximum Segment Lifetime),主要为了:
- 最终 ACK 丢失时,对端重发 FIN,主动关闭方仍能重发 ACK。
- 让旧连接的延迟重复段从网络消失,避免污染相同四元组的新连接。
TIME_WAIT 多通常意味着:
- 服务端采用短连接且由本端主动关闭。
- 客户端并发高、频繁连接同一目标。
- 连接复用不足。
优先措施是 HTTP Keep-Alive/连接池、让协议合理决定关闭方、扩展源地址/端口容量。不要把 tcp_tw_reuse 等内核开关当第一反应,更不能推荐已废弃或有 NAT 风险的配置。
2、CLOSE_WAIT 为什么危险
CLOSE_WAIT 表示内核已收到对端 FIN 并通知应用,但应用没有 close()。
持续增长几乎总要回到应用代码:
- 异常分支未执行
close()/finally。 - 线程永久阻塞,未处理 EOF。
- 连接池把已关闭连接误判为可复用。
八、RST:立即复位而非正常关闭
RST(Reset)立即终止连接,不保证发送缓冲区数据交付。常见触发:
- 访问未监听 TCP 端口。
- 对不存在的连接收到非 RST 段。
- 应用 abort/进程崩溃或使用
SO_LINGER触发 abortive close。 - 防火墙主动 reject。
应用常见错误:
Connection reset by peer
ECONNRESET排查必须确定 RST 由谁发出、在哪个抓包点首次出现、发出前发生了什么。中间负载均衡、防火墙也可能生成 RST,不能只看客户端报错就归因服务端进程。
九、SYN Flood 与 SYN Cookie
攻击者大量发送 SYN(常伪造源地址),消耗半连接队列和 CPU:
纵深防御:
- 边界源地址验证、ACL 与速率限制。
- 合理配置 backlog、
tcp_max_syn_backlog和 SYN-ACK 重试。 - 启用 SYN Cookie 作为队列压力下的兜底。
- 监控 SYN_RECV、listen overflow、SYN Cookie 计数和建连成功率。
SYN Cookie 原理:服务端不立即保存半连接状态,而把必要状态编码进 SYN-ACK 的序列号;收到合法 ACK 后重建连接状态。
边界:
- 它保护的是半连接状态,不解决带宽、CPU、全连接队列或应用层慢请求攻击。
- 可编码的 TCP 选项信息有限;现代实现有所改进,但仍应视为压力兜底而非唯一防线。
十、TCP 边界行为与异常推导
1、SYN 为什么会被丢弃
“SYN 没有得到 SYN-ACK”只是一种现象,可能发生在不同位置:
| 位置 | 原因示例 | 客户端常见现象 | 证据 |
|---|---|---|---|
| 客户端 | 无路由、源地址选择失败、本地防火墙 | 立即报错或未发包 | 客户端路由、socket 错误、本机抓包 |
| 中间网络 | ACL、安全组、黑洞路由、反向路径过滤 | SYN 重传后超时 | 双点抓包、策略命中计数、流日志 |
| 服务端主机 | 防火墙 drop、conntrack/资源耗尽 | SYN 到达但无响应 | 服务端抓包、规则/内核计数 |
| 监听 socket | SYN 队列压力、监听地址不匹配 | 丢弃、延迟或 RST,依实现与策略 | ss -lnt、listen overflow、SYN cookie |
| 无监听端口 | 内核通常返回 RST | 快速 Connection refused | 服务端抓到 SYN 后发 RST |
防火墙 drop 常表现为超时,reject 可主动返回 RST 或 ICMP。只凭客户端报错不能断定是哪一层。
2、已建立连接收到 SYN
一个处于 ESTABLISHED 的连接收到不符合当前窗口的 SYN,不应简单“重新握手”。RFC 5961 为部分异常段引入 Challenge ACK,用于确认对端是否真正持有当前连接状态,降低伪造 RST/SYN 注入风险。
实际响应取决于:
- SYN 的序列号是否在可接受窗口;
- 是否同时携带 ACK/RST;
- TCP 实现及安全补丁;
- Challenge ACK 限速;
- 中间设备是否改写或丢弃。
验证时应同时保留连接四元组、相对/绝对序列号和双向抓包。不能仅看到一个 SYN 就认定“连接被重新建立”。
3、乱序 FIN
FIN 在 TCP 序列空间中占用一个序号。若 FIN 先于缺失数据到达:
- 接收端可暂存该 FIN;
- ACK 仍指向下一个缺失字节;
- 缺失数据补齐、FIN 变为连续序列后,才向应用交付 EOF 并迁移状态。
因此抓包出现 FIN 不等于应用立即读到 EOF。必须结合序列号、ACK、重传和状态迁移判断。
4、TIME_WAIT 中收到新 SYN
新 SYN 是否能复用同一四元组,受初始序列号、时间戳、旧报文是否仍可能存在和操作系统实现影响。安全目标是避免旧连接的延迟报文污染新连接。
- 新连接使用不同源端口时,不是同一四元组,通常不受原 TIME_WAIT 直接阻挡。
- 同一四元组复用必须满足实现的安全判定,不能假设“一律拒绝”或“一律成功”。
- Linux
tcp_tw_reuse的语义和默认值随内核版本变化,主要涉及主动打开连接;必须查目标内核文档和源码。 - 优先使用连接复用、合理的主动关闭方和更多源地址/端口,不把修改 TIME_WAIT 参数当首选优化。
5、断电、进程崩溃和拔网线
| 事件 | 本端能否发 FIN/RST | 对端如何发现 | 恢复边界 |
|---|---|---|---|
应用正常 close() | 通常 FIN | 正常关闭时序 | 可立即进入关闭流程 |
| 进程退出/崩溃,OS 仍运行 | 内核关闭 socket;可能 FIN,也可能因未读数据等条件发送 RST | 收到 FIN/RST | 取决于 socket 状态,不能笼统说一定 RST |
| 主机断电/内核崩溃 | 通常没有任何报文 | 发送数据后的重传超时、TCP Keepalive 或应用心跳 | 静默连接可长时间留存 |
| 网线拔出/链路中断 | socket 状态通常仍在 | 写入后重传;接口/路由事件可能使错误更早返回 | 链路在超时前恢复时连接可能继续 |
TCP 没有“实时知道对端活着”的魔法。对故障检测时间有要求时,应设计应用心跳、截止时间和恢复逻辑。
6、没有 listen() 与没有 accept()
- 没有
listen():目标地址可达且端口未监听时,主机通常对 SYN 返回 RST;若被防火墙丢弃,客户端则重传后超时。 - 调用
listen()但应用没有accept():内核仍可完成三次握手并把连接放入 accept 队列,因此客户端可能连接成功;队列填满后,新连接行为取决于内核和配置。 accept()不参与线上三次握手,它从内核已完成连接队列取出 socket。
7、四次挥手为什么可能只看到三个报文
“四次”描述两个方向分别关闭:
A -> B: FIN
B -> A: ACK
B -> A: FIN
A -> B: ACK若 B 在确认 A 的 FIN 时已经准备关闭,可以把 ACK 与自己的 FIN 合并为一个 FIN,ACK 段,于是抓包只看到三个报文。语义仍是两个方向各自关闭,不是另一种 TCP 协议。
8、TCP 可靠不等于业务数据永不丢
send()成功通常只表示数据进入本机内核发送缓冲区。- TCP ACK 表示字节已被对端 TCP 接收,不表示对端应用已处理、写入数据库或持久化。
- 对端内核 ACK 后应用崩溃,业务仍可能没有完成。
- 应用超时后重试,而第一次请求其实已执行,可能产生重复副作用。
需要“业务不丢且不重复”时,必须在应用层设计响应确认、幂等键、事务、去重和持久化语义。
9、TCP 与 UDP 能否使用同一个端口号
可以。TCP 与 UDP 是不同传输协议,操作系统查找 socket 时会考虑协议、地址和端口等信息。例如 TCP 53 与 UDP 53 可以由同一 DNS 服务分别监听。冲突规则还受绑定地址、地址族和 SO_REUSEADDR/SO_REUSEPORT 实现影响。
十一、字节流、“粘包”与 framing
TCP 只交付有序字节:
发送端:send("hello"); send("world")
接收端可能得到:
"helloworld"
或 "hel" + "lo" + "world"
或任意其他不丢字节且顺序不变的切分“粘包”不是 TCP 故障,而是应用错误地假设 send() 与 recv() 一一对应。应用必须定义消息 framing:
| 方法 | 适用 | 风险 |
|---|---|---|
| 固定长度 | 固定记录 | 浪费空间、扩展困难 |
| 分隔符 | 文本协议 | 需转义,防止无限行 |
| 长度前缀 | 二进制/RPC | 校验长度上限,防内存攻击 |
| 自描述编码 | HTTP、Protobuf 等 | 依赖成熟解析器 |
长度前缀读取必须使用“读满 N 字节”循环,单次 recv(N) 可能少于 N。
十二、Keepalive、应用心跳与连接池
三者不可混淆:
- TCP Keepalive:内核在长时间空闲后探测对端;默认通常很慢,参数因平台而异。
- HTTP Keep-Alive:复用一个 TCP 连接处理多个 HTTP 请求(16)。
- 应用心跳:业务层在可控时间内验证进程级健康与语义。
连接池应设置:
十三、最小可复现实验
1、字节流与半关闭
本机 macOS 26.6.2、Python 3,回环网络:
python3 - <<'PY'
import socket, threading
ready = threading.Event()
result = []
server = socket.socket()
server.bind(("127.0.0.1", 0))
server.listen(1)
port = server.getsockname()[1]
def serve():
ready.set()
conn, _ = server.accept()
chunks = []
while True:
part = conn.recv(3)
if not part:
break
chunks.append(part)
result.append(chunks)
conn.sendall(b"ACK-after-FIN")
conn.close()
server.close()
threading.Thread(target=serve).start()
ready.wait()
client = socket.create_connection(("127.0.0.1", port))
client.sendall(b"hello")
client.sendall(b"world")
client.shutdown(socket.SHUT_WR)
reply = client.recv(1024)
client.close()
print("server recv chunks:", result[0])
print("client recv after SHUT_WR:", reply)
PY真实输出:
server recv chunks: [b'hel', b'lo', b'wor', b'ld']
client recv after SHUT_WR: b'ACK-after-FIN'结论:
- 两次发送并没有形成两个接收消息,
recv(3)按字节流拆成四块。 - 客户端
SHUT_WR后仍能接收服务端响应,验证 TCP 全双工与半关闭。
2、状态机实测
测试在连接建立、客户端主动关闭、服务端关闭三个时刻读取 netstat。本机真实输出(端口为当次动态值):
after connect:
127.0.0.1.62035 127.0.0.1.62036 ESTABLISHED
127.0.0.1.62036 127.0.0.1.62035 ESTABLISHED
127.0.0.1.62035 *.* LISTEN
after client active close:
127.0.0.1.62035 127.0.0.1.62036 CLOSE_WAIT
127.0.0.1.62036 127.0.0.1.62035 FIN_WAIT_2
after server close:
127.0.0.1.62036 127.0.0.1.62035 TIME_WAIT这与状态机一致:客户端主动关闭后进入 FIN_WAIT_2,服务端尚未 close() 时为 CLOSE_WAIT;服务端随后发送 FIN,客户端确认后进入 TIME_WAIT。
3、抓握手(需抓包权限,当前未执行)
# Linux
sudo tcpdump -i lo -nn -s 0 -w tcp-handshake.pcap 'tcp port 8080'
# 另一个终端启动服务与客户端
nc -l 127.0.0.1 8080
nc 127.0.0.1 8080Wireshark 过滤:
tcp.port == 8080
tcp.flags.syn == 1
tcp.analysis.retransmission
tcp.analysis.zero_window当前 macOS 没有免密抓包权限,因此未生成或伪造 PCAP 输出。
十四、常见故障与排查
| 现象 | 优先检查 |
|---|---|
connect timeout | SYN 是否发出、SYN-ACK 是否返回、防火墙是否 silent drop、回程路由 |
connection refused | 对端 RST:端口未监听或防火墙 reject |
| SYN_RECV 大量堆积 | SYN Flood、最终 ACK 回不来、队列压力 |
| CLOSE_WAIT 持续增长 | 应用未 close;定位持有 fd 的代码路径 |
| TIME_WAIT 很多 | 是否短连接/主动关闭;先优化复用,不先乱调内核 |
reset by peer | 找到最早 RST 的发送者及前序报文 |
| 偶发首包失败 | backlog、端口耗尽、NAT/LB、SYN 重传 |
| 小消息正常、大响应卡住 | PMTUD 黑洞(09) |
Linux 观测:
ss -lntp # 监听 socket
ss -tan state syn-recv # 半连接
ss -tan state close-wait
ss -tan state time-wait
ss -s # 汇总
nstat -az | grep -E 'Listen|Syncookies|Retrans'十五、安全、性能与容量
- 随机化 ISN 与临时端口,降低盲注入和连接碰撞。
- 对 TCP/监听端口实施最小暴露、控制平面 ACL 和速率限制。
- backlog、fd 上限和线程池要联合容量规划;只调
somaxconn不能修复应用accept()太慢。 - Nagle 算法减少小包,但与延迟 ACK 组合可能增加交互延迟;只有在有测量证据时设置
TCP_NODELAY。 - TLS 不改变 TCP 状态机,但增加握手 RTT、CPU 与连接复用价值(17)。
十六、版本与平台差异
- 现代基础规范是 RFC 9293,而不是继续单独以 RFC 793 为唯一依据。
- Linux 推荐
ss;macOS/BSD 使用netstat;Windows 使用Get-NetTCPConnection/netstat。 - TIME_WAIT 时长、SYN Cookie 行为、backlog 截断与计数器名称均有实现差异。
- 抓包显示的 checksum、分段和相对序列号可能受卸载与工具显示选项影响(04)。
十七、关联章节
- 上一章:12 UDP 与应用层可靠性
- 下一章:14 TCP 可靠传输、流控与拥塞控制
- 相关:04 socket 与连接池、09 PMTUD、17 TLS、网络安全
检索关键词
TCP 报文头 字段 SYN ACK FIN RST TCP 三次握手 为什么 TCP 四次挥手 半关闭 TCP 11 状态 TIME_WAIT 2MSL CLOSE_WAIT 泄漏 SYN 队列 accept 队列 backlog SYN Cookie TCP 字节流 粘包 framing TCP Keepalive HTTP Keep-Alive 心跳 connection refused reset by peer
权威来源
- RFC 9293:Transmission Control Protocol
- RFC 5961:Improving TCP's Robustness to Blind In-Window Attacks
- RFC 7323:TCP Extensions for High Performance
- RFC 2018:TCP Selective Acknowledgment Options
- RFC 4987:TCP SYN Flooding Attacks and Common Mitigations
- POSIX socket API;Linux
tcp(7)、listen(2)、ss(8)man page
TCP 字段、状态与握手/关闭语义已于 2026-09-07 对照 RFC 9293 核验。字节流、半关闭和状态迁移实验已在 macOS 26.6.2 本机真实执行;抓包和 Linux 队列计数因当前权限/平台限制明确标记为未执行。
