TCP 协议状态解析与问题示例解析

TCP 协议状态解析与问题示例解析 TCP 协议核心内容第一节TCP 协议基础TCP 解决了什么问题类比IP 像“快递公司”只负责把包裹送到目的地但不保证送达、不保证顺序、不保证完整。TCP 是在快递公司基础上加了签收回执 破损赔付 按序排列的高级服务。互联网底层IP 层是“尽力而为”Best-effort投递数据包可能丢失、乱序、重复。TCP 在此之上提供可靠传输数据不丢失有序交付按发送顺序到达错误检测数据不被篡改流量 / 拥塞控制不压垮接收方或网络TCP vs UDP 核心区别特性TCPUDP连接有连接握手建立无连接可靠性可靠确认 重传不可靠顺序保证有序不保证速度较慢开销大极快适用场景HTTP、SSH、数据库DNS、视频流、游戏TCP 报文头关键字段字段作用序列号Seq标记当前数据段的第一个字节在全局字节流中的编号确认号Ack期望收到的下一个字节的序列号即累积确认已成功接收到的所有数据SYN建立连接请求标志ACK确认标志FIN请求断开连接标志RST强制重置连接标志PSH推送标志提示接收端尽快将数据交付应用层URG紧急指针有效标志窗口大小接收方通告的接收窗口rwnd表示当前还能接收的最大数据量接收缓冲区剩余空间会随应用读取动态变化校验和数据完整性验证ℹ️ TCP 头部还包含“选项”字段用于协商 MSS最大报文段大小、窗口缩放因子、时间戳等扩展功能其中 MSS 协商直接影响传输效率与粘包问题。可靠传输的四大机制① 确认应答ACK发送方每发一段数据接收方回复 ACK告知“我已收到期待下一个字节编号”。若发送方迟迟未收到 ACK就触发重传。② 超时重传RTO发送方维护一个定时器超时未收到 ACK 则重传该数据段。RTO 根据网络 RTT 动态调整通过 SRTT 和 RTTVAR 计算避免过早或过晚重传。发生超时重传时通常认为是网络严重拥塞会将拥塞窗口cwnd重置为 1并触发慢启动。③ 流量控制滑动窗口类比收发双方就像两个通过传送带连接的车间。发送方可以连续投放多件产品接收方则根据自己的**加工速度应用读取速率**不断喊出“我还能再收 X 件”这个数字就是窗口大小。发送方只能投放不超过“接收方喊出的余量”和“车间内部预估的安全投放量”中的较小值。接收方在 ACK 中通告窗口大小发送方据此调整发送速率防止接收方被淹没。发送窗口的计算方式为发送窗口 swnd min(接收窗口 rwnd, 拥塞窗口 cwnd)窗口内的数据可连续发出无需逐一等待 ACK每收到一个 ACK左边界右移释放已确认的槽位若接收方通告rwnd0发送方停止发送并启动**零窗口探测ZWP**定时器周期性发送探测报文询问窗口是否打开④ 拥塞控制拥塞控制防止过多数据注入网络导致路由器过载。它由四个核心机制协同工作注意并非线性顺序慢启动连接建立时拥塞窗口cwnd从很小值通常为 110 个 MSS开始每收到一个 ACK 将 cwnd 加倍呈指数增长快速探测网络可用带宽。拥塞避免当 cwnd 达到慢启动阈值ssthresh后进入拥塞避免阶段每个 RTT 将 cwnd 线性增加 1 个 MSS谨慎逼近网络容量。快重传发送方在超时定时器到期前一旦收到3 个重复 ACK即对同一序列号的重复确认就认为该序列号对应的报文丢失立即重传丢失报文而不必等待超时。快恢复在触发快重传后执行。算法会将 ssthresh 设为max(cwnd / 2, 2 * MSS)然后将 cwnd 设为 ssthresh 3 * MSS或减半后直接设定为 ssthresh直接进入拥塞避免阶段而不是跌回慢启动。若触发的是超时重传则 ssthresh cwnd / 2cwnd 重置为 1重新进入慢启动。阶段流转路径慢启动 ⇄ 拥塞避免 ↑ ↓ (收到 3 个重复 ACK) │ 快重传 → 快恢复 │ ↓ └──────超时重传── 回到拥塞避免补充Nagle 算法与 TCP_NODELAYNagle 算法为了减少网络中小包tinygram的数量TCP 规定在已发送的数据未收到 ACK 之前不允许再发送小段数据小于 MSS 的未满包而是将其累积直到 ACK 返回或数据积累到一个 MSS 大小。这适合 Telnet 等交互性不强的场景。TCP_NODELAY禁用 Nagle 算法。开启后数据一旦到达就立刻发送不再等待 ACK 或累积适用于低延迟场景如在线游戏、SSH 终端。设置方法Linuxsetsockopt(fd, IPPROTO_TCP, TCP_NODELAY, on, sizeof(on))。补充TCP 粘包与拆包问题粘包接收方一次读到多个应用层数据包消息边界丢失。拆包一个完整的应用层包被分成多次 read() 返回。产生原因TCP 是面向字节流的协议不保留消息边界。发送方可能将多个小包合并在同一 TCP 段中发送Nagle 算法接收方读取数据的时机与发送时机不等同。解决方案定长报文每个消息固定长度分隔符如\r\nFTP 控制连接即采用此方式长度前缀在消息前添加固定字节表示消息体长度第二节TCP 状态机与流转过程TCP 的 11 种状态总览与状态机图┌─────────┐ │ CLOSED │ └────┬────┘ │ 主动打开 / SYN ▼ ┌─────────┐ 被动打开 │SYN_SENT │◄───────────────────┐ └────┬────┘ │ │ 收到 SYNACK, 回复 ACK │ ▼ │ ┌────────────┐ │ │ESTABLISHED │ │ └──┬──┬──┬───┘ │ │ │ │ 被动方关闭 / FIN │ 主动方关闭/FIN│ │ └─────────┐ │ │ │ ▼ │ ┌───┘ │ ┌──────────┐ │ ▼ │ │CLOSE_WAIT│ │ ┌──────────┐ │ └────┬─────┘ │ │FIN_WAIT_1│ │ │ 应用关闭 │ └──┬───────┘ │ ▼ │ │ 收到ACK │ ┌────────┐ │ ▼ │ │LAST_ACK│ │ ┌──────────┐ │ └───┬────┘ │ │FIN_WAIT_2│ │ │ 收到ACK │ └──┬───────┘ │ ▼ │ │ 收到FIN │ ┌────────┐ │ ▼ │ │ CLOSED │ │ ┌──────────┐ │ └────────┘ │ │TIME_WAIT ├───2MSL──►CLOSED │ └──────────┘ │ │ │ │ 双方同时关闭 │ FIN_WAIT_1 ──收到FIN──► CLOSING ──收到ACK──► TIME_WAIT │ └─收到ACK──► CLOSED一、三次握手连接建立类比三次握手就像打电话确认双方能听到——“喂你听得到吗”→ “听得到你呢”→ “我也听得到开始说吧。”客户端 服务端 │ │ │ [客户端] 主动发起 │ [服务端] 提前进入 LISTEN │ │ CLOSED LISTEN │ │ │──── ① SYN (seqx) ────────────────────────│ │ │ SYN_SENT SYN_RCVD │ │ │─── ② SYN-ACK (seqy, ackx1) ───────────│ │ │ │──── ③ ACK (acky1) ──────────────────────│ │ │ ESTABLISHED ESTABLISHED每次握手的目的第①次客户端 → 服务端验证客户端的发送能力告知初始序列号 x第②次服务端 → 客户端验证服务端的收发能力告知初始序列号 y并确认收到 x第③次客户端 → 服务端验证服务端的发送能力被客户端接收双方完成双向验证初始序列号ISN的随机性ISN 并非固定为 0 或 1而是基于时钟驱动的伪随机数生成目的是防止网络中的延迟旧报文被误认为新连接的有效数据防序列号攻击和旧数据污染。MSS 协商在 SYN 报文中双方通过 TCP 选项字段声明自己期望的最大报文段大小MSS MTU - IP头 - TCP头。连接双方取较小的那个 MSS 作为实际使用的 MSS从而避免 IP 分片。为什么不能两次握手两次握手只能验证“客户端 → 服务端”单向通道无法验证回路。更关键的缺陷若客户端某个延迟的旧 SYN 包到达服务端服务端会误以为是新连接请求并分配资源等待数据而客户端早已放弃——造成服务端资源白白占用。三次握手中服务端必须等到客户端的 ACK 才认为连接建立规避了此问题。二、数据传输阶段ESTABLISHED 下的滑动窗口类比收发双方就像两个通过传送带连接的车间。发送方可以连续投放多件产品接收方则根据自己的**加工速度应用读取速率**不断喊出“我还能再收 X 件”发送方同时还要观察道路拥堵情况网络拥塞取二者的较小值作为实际投放量。发送缓冲区视图发送方视角 ┌──────────────┬──────────────┬──────────────┬──────────────────┐ │ 已发已确认 │ 已发未确认 │ 可立即发送 │ 不可发送 │ │ 可回收 │ 等待 ACK │ 窗口内 │ 超出接收窗口│ └──────────────┴──────────────┴──────────────┴──────────────────┘ ◄──────────── 发送窗口 swnd ──────────────► │ 收到 ACK → 窗口整体右滑核心规则发送窗口swnd min(接收窗口 rwnd, 拥塞窗口 cwnd)窗口内的数据可连续发出无需逐一等待 ACK每收到一个 ACK左边界右移释放已确认的槽位若接收方通告rwnd0发送方停止发送启动零窗口探测ZWP定时器周期性发送窗口探测报文补充RST 报文触发场景RST标志用于强制、异常地终止一个连接。收到 RST 后无需回复 ACK连接立即进入CLOSED状态。常见场景端口未监听客户端向服务器某端口发 SYN该端口无进程监听服务器内核直接回复 RST-ACK。半开连接一方进程崩溃或突然重启再收到对方数据时因找不到连接状态而回复 RST。SO_LINGER 设置为 0调用close()时若设置了SO_LINGER且等待时间为 0内核直接发 RST 而非 FIN丢弃所有待发数据。防火墙阻断中间安全设备直接注入 RST 来断开连接。补充TCP Keepalive 机制TCP 本身没有心跳报文但提供了 Keepalive 扩展默认关闭用于探测连接对端是否存活。开启后如果一条连接在tcp_keepalive_time内无任何数据交互内核将发送探测包。若tcp_keepalive_probes次探测均无响应则关闭连接。相关参数net.ipv4.tcp_keepalive_time空闲多久后开始探测默认 7200 秒net.ipv4.tcp_keepalive_intvl探测间隔默认 75 秒net.ipv4.tcp_keepalive_probes探测次数默认 9 次更推荐的做法是应用层心跳能直接确认业务进程是否存活且不受系统全局参数限制。三、四次挥手连接断开类比挂电话——A 说“我说完了”→ B 说“好的我还有几句”→ B 说“我也说完了”→ A 说“好挂了”。因为 TCP 是全双工的A 不发不代表 B 也不发所以必须各自独立关闭共需四步。主动关闭方 被动关闭方 │ │ ESTABLISHED ESTABLISHED │ │ │ [主动方] 调用 close() │ │──── ① FIN (sequ) ────────────────────────│ │ │ FIN_WAIT_1 CLOSE_WAIT │ │ │─── ② ACK (acku1) ───────────────────────│ [被动方] 内核自动回复 │ │ 应用层仍可发送剩余数据 FIN_WAIT_2 │ │ │ [被动方] 应用层调用 close() │─── ③ FIN (seqv) ─────────────────────────│ │ LAST_ACK │──── ④ ACK (ackv1) ──────────────────────│ │ │ TIME_WAIT CLOSED │ │ 等待 2MSL约 2 分钟 │ CLOSED为什么需要四次而非三次TCP 是全双工的关闭 A→B 的通道不等于关闭 B→A 的通道。当 A 发 FIN 表示“我不再发数据了”B 还可能有数据要继续发给 A。所以 B 先 ACK“我知道了但我还没发完”等发完剩余数据后再单独发 FIN这两步无法合并故必然是四步。TIME_WAIT 存在的意义为什么等 2MSLMSLMaximum Segment Lifetime最大报文段生存时间通常为 60 秒2MSL 2 分钟。原因有两个防止最后 ACK 丢失若 ④ACK 在网络中丢失被动方会重发 FIN。主动方必须在 2MSL 内仍处于 TIME_WAIT 状态才能再次回复 ACK否则被动方无法正常关闭。防止旧连接报文污染新连接确保本次连接所有在途报文在网络中彻底消亡最多存活 1 个 MSL新连接复用相同端口时不会收到上一条连接的“幽灵数据包”。第三节各状态常见问题与解决措施① SYN_RCVD 积压过多 → SYN Flood 攻击维度内容现象SYN_RCVD状态连接数爆炸式增长半连接队列被打满合法新连接无法建立服务不响应根本原因攻击者发送海量伪造源 IP 的 SYN 包服务端为每个 SYN 分配半连接队列资源并等待 ACK队列耗尽后拒绝合法连接排查命令ss -sss -ant state syn-recvnetstat -an | grep SYN_RCVD | wc -l抓取 SYN 包tcpdump -i eth0 tcp[tcpflags] tcp-syn ! 0 -c 100解决方案开启 SYN Cookie无需存储半连接状态sysctl -w net.ipv4.tcp_syncookies1扩大半连接队列sysctl -w net.ipv4.tcp_max_syn_backlog4096根本防护在上游防火墙 / CDN 层过滤攻击流量② CLOSE_WAIT 大量堆积 → 服务端未正确关闭连接维度内容现象CLOSE_WAIT连接数持续增长文件描述符耗尽服务逐渐假死根本原因客户端发送了 FIN内核自动回复 ACK 进入CLOSE_WAIT但应用程序代码没有调用close()导致连接永远停在此状态永不发送 FIN排查命令ss -ant state close-waitnetstat -an | grep CLOSE_WAIT | awk {print $5} | cut -d: -f1 | sort | uniq -c | sort -rnlsof -i | grep CLOSE_WAIT解决方案根本修复代码层确保所有代码路径含异常/超时分支都正确调用了close()或shutdown()检查点连接池是否正确归还try-finally中有无关闭操作HTTP 客户端 Keep-Alive 是否配置了超时临时缓解sysctl -w net.ipv4.tcp_keepalive_time60加速检测死连接抓包示例观察对端不关闭的连接tcpdump -i eth0 tcp[tcpflags] tcp-fin ! 0 and host 对端IP若只能看到对方的 FIN而本机始终无 FIN 发出即证实 CLOSE_WAIT 泄漏。③ TIME_WAIT 大量堆积 → 短连接高并发场景维度内容现象TIME_WAIT数量达数万临时端口耗尽新连接报Cannot assign requested address根本原因主动关闭方进入 2MSL 的TIME_WAIT约 2 分钟。高并发短连接场景下每秒大量连接关闭TIME_WAIT来不及自然消亡就把端口池填满排查命令ss -s | grep TIME-WAITnetstat -n | awk /^tcp/ {print $NF} | sort | uniq -csysctl net.ipv4.ip_local_port_range解决方案方案一推荐改用长连接HTTP Keep-Alive、连接池减少连接创建/销毁频率方案二开启 TIME_WAIT 复用sysctl -w net.ipv4.tcp_tw_reuse1仅对出向连接有效需同时开启net.ipv4.tcp_timestamps1方案三扩大端口范围sysctl -w net.ipv4.ip_local_port_range1024 65535⚠️tcp_tw_recycle在 Linux 4.12 已移除NAT 环境下会导致连接问题禁止使用。tcp_tw_reuse在 NAT 环境下也可能引起混淆生产环境需充分测试抓包示例观察 TIME_WAIT 状态堆积时端口复用情况tcpdump -i eth0 tcp[tcpflags] tcp-fin ! 0 and port 你的端口④ FIN_WAIT_2 长时间挂起 → 对端未发送 FIN维度内容现象FIN_WAIT_2状态连接长期存在主动关闭方已发 FIN 并收到 ACK但对端迟迟不发 FIN根本原因被动关闭方收到 FIN 后进入CLOSE_WAIT但应用程序卡死/阻塞未调用close()永远不发 FIN或对端网络故障但未发 RST排查命令ss -ant state fin-wait-2netstat -an | grep FIN_WAIT2sysctl net.ipv4.tcp_fin_timeouttcpdump -i eth0 host 对端IP -w /tmp/cap.pcap解决方案系统侧缩短 FIN_WAIT_2 超时默认 60ssysctl -w net.ipv4.tcp_fin_timeout30应用侧排查对端是否存在CLOSE_WAIT积压参考问题②修复对端代码调试抓包后用 Wireshark 确认对端是否发出了 FIN⑤ ESTABLISHED 连接假死 → 网络中断但未收到 RST维度内容现象连接显示ESTABLISHED但实际链路已断开读写操作永久阻塞应用进程挂死根本原因中间网络设备防火墙、NAT静默丢弃数据包但不发 RST或对端机器崩溃断电来不及发 RST。TCP 本身没有心跳机制双端不知道链路已断排查命令ss -ant state established | grep portsysctl net.ipv4.tcp_keepalive_timetcpdump -i eth0 tcp[tcpflags] tcp-ack ! 0解决方案方案一开启 TCP Keepalive内核级心跳sysctl -w net.ipv4.tcp_keepalive_time6060s 无数据后发探测包sysctl -w net.ipv4.tcp_keepalive_intvl10探测间隔 10ssysctl -w net.ipv4.tcp_keepalive_probes33 次无响应则关闭连接方案二更推荐应用层心跳协议层面定期发 ping/pong不依赖系统参数方案三设置 Socket 读写超时SO_TIMEOUT防止永久阻塞抓包验证 Keepalive 探测包tcpdump -i eth0 tcp[tcpflags] tcp-ack ! 0 and port 你的端口 and length 1Keepalive 探测包是仅有一个字节序列号为期望的序列号减1的空 ACK可据此过滤。