Skip to content

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)。
  • 字节流:不保留应用消息边界。
  • 全双工:两个方向独立传输、独立关闭。
  • 单播:一条连接由本地/远端地址与端口识别。

连接通常由四元组唯一标识:

text
(源 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/CWRTCP 状态机驱动状态与功能tcp.flags异常 RST、SYN Flood
Window16 位从 ACK 起愿意接收的字节数接收端限制发送窗口tcp.window_size_value零窗口、缩放误读
Checksum16 位伪首部 + TCP 头 + 数据发送端错误则丢弃tcp.checksum本机卸载产生假错误(04)
Urgent Pointer16 位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)
TimestampsRTT 测量与 PAWS 防旧序列号双方支持后在后续段持续携带(RFC 7323)

MSS 不等于 MTU:以太网 MTU 1500、无 IP/TCP 选项时,IPv4 常见 MSS 1460,IPv6 常见 MSS 1440。隧道会进一步降低路径 MTU,需要 PMTUD 或 MSS Clamping(09)。

三、三次握手 ​

设客户端初始序列号为 x,服务端为 y:

SYN 和 FIN 各消耗一个序列号,即便它们不携带普通应用数据。

为什么不是两次 ​

三次握手同时解决:

  1. 客户端确认自己的发送与接收路径可用。
  2. 服务端确认自己的发送与接收路径可用。
  3. 双方同步本次连接的初始序列号,避免旧重复报文被误认为新连接数据。
  4. 双方确认 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),主要为了:

  1. 最终 ACK 丢失时,对端重发 FIN,主动关闭方仍能重发 ACK。
  2. 让旧连接的延迟重复段从网络消失,避免污染相同四元组的新连接。

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。

应用常见错误:

text
Connection reset by peer
ECONNRESET

排查必须确定 RST 由谁发出、在哪个抓包点首次出现、发出前发生了什么。中间负载均衡、防火墙也可能生成 RST,不能只看客户端报错就归因服务端进程。

攻击者大量发送 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 到达但无响应服务端抓包、规则/内核计数
监听 socketSYN 队列压力、监听地址不匹配丢弃、延迟或 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 先于缺失数据到达:

  1. 接收端可暂存该 FIN;
  2. ACK 仍指向下一个缺失字节;
  3. 缺失数据补齐、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、四次挥手为什么可能只看到三个报文 ​

“四次”描述两个方向分别关闭:

text
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 只交付有序字节:

text
发送端: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)。
  • 应用心跳:业务层在可控时间内验证进程级健康与语义。

连接池应设置:

  • 连接/读/写/空闲超时。
  • 最大连接数、最大空闲数、最大存活时间。
  • 取用前健康校验或幂等重试。
  • 与 NAT/LB 空闲超时协调(18、19)。

十三、最小可复现实验 ​

1、字节流与半关闭 ​

本机 macOS 26.6.2、Python 3,回环网络:

bash
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

真实输出:

text
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。本机真实输出(端口为当次动态值):

text
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、抓握手(需抓包权限,当前未执行) ​

bash
# 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 8080

Wireshark 过滤:

text
tcp.port == 8080
tcp.flags.syn == 1
tcp.analysis.retransmission
tcp.analysis.zero_window

当前 macOS 没有免密抓包权限,因此未生成或伪造 PCAP 输出。

十四、常见故障与排查 ​

现象优先检查
connect timeoutSYN 是否发出、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 观测:

bash
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)。

十七、关联章节 ​

检索关键词 ​

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

权威来源 ​

TCP 字段、状态与握手/关闭语义已于 2026-09-07 对照 RFC 9293 核验。字节流、半关闭和状态迁移实验已在 macOS 26.6.2 本机真实执行;抓包和 Linux 队列计数因当前权限/平台限制明确标记为未执行。

ICP 备案号待补充 · Gitee 主页