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,左边界右移,释放已确认的槽位
  • 若接收方通告 rwnd=0,发送方停止发送,并启动**零窗口探测(ZWP)**定时器,周期性发送探测报文询问窗口是否打开

④ 拥塞控制

拥塞控制防止过多数据注入网络,导致路由器过载。它由四个核心机制协同工作(注意:并非线性顺序):

  • 慢启动:连接建立时,拥塞窗口(cwnd)从很小值(通常为 1~10 个 MSS)开始,每收到一个 ACK 将 cwnd 加倍,呈指数增长,快速探测网络可用带宽。
  • 拥塞避免:当 cwnd 达到慢启动阈值(ssthresh)后,进入拥塞避免阶段,每个 RTT 将 cwnd 线性增加 1 个 MSS,谨慎逼近网络容量。
  • 快重传:发送方在超时定时器到期前,一旦收到 3 个重复 ACK(即对同一序列号的重复确认),就认为该序列号对应的报文丢失,立即重传丢失报文,而不必等待超时。
  • 快恢复:在触发快重传后执行。算法会将 ssthresh 设为 max(cwnd / 2, 2 * MSS),然后将 cwnd 设为 ssthresh + 3 * MSS(或减半后直接设定为 ssthresh),直接进入拥塞避免阶段,而不是跌回慢启动。若触发的是超时重传,则 ssthresh = cwnd / 2,cwnd 重置为 1,重新进入慢启动。

阶段流转路径

  慢启动 ⇄ 拥塞避免
     ↑         ↓ (收到 3 个重复 ACK)
     │       快重传 → 快恢复
     │                    ↓
     └──────(超时重传)── 回到拥塞避免

补充:Nagle 算法与 TCP_NODELAY

Nagle 算法:为了减少网络中小包(tinygram)的数量,TCP 规定,在已发送的数据未收到 ACK 之前,不允许再发送小段数据(小于 MSS 的未满包),而是将其累积,直到 ACK 返回或数据积累到一个 MSS 大小。这适合 Telnet 等交互性不强的场景。

TCP_NODELAY:禁用 Nagle 算法。开启后,数据一旦到达就立刻发送,不再等待 ACK 或累积,适用于低延迟场景(如在线游戏、SSH 终端)。设置方法(Linux):setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on))


补充:TCP 粘包与拆包问题

粘包:接收方一次读到多个应用层数据包(消息边界丢失)。拆包:一个完整的应用层包被分成多次 read() 返回。

产生原因:TCP 是面向字节流的协议,不保留消息边界。发送方可能将多个小包合并在同一 TCP 段中发送(Nagle 算法),接收方读取数据的时机与发送时机不等同。

解决方案

  • 定长报文:每个消息固定长度
  • 分隔符:如 \r\n,FTP 控制连接即采用此方式
  • 长度前缀:在消息前添加固定字节表示消息体长度

第二节:TCP 状态机与流转过程

TCP 的 11 种状态总览与状态机图

                      ┌─────────┐
                      │ CLOSED  │
                      └────┬────┘
                           │ 主动打开 / SYN
                           ▼
                      ┌─────────┐        被动打开
                      │SYN_SENT │◄───────────────────┐
                      └────┬────┘                    │
                           │ 收到 SYN+ACK, 回复 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 (seq=x) ────────────────────────>│
  │                                             │
SYN_SENT                                    SYN_RCVD
  │                                             │
  │<─── ② SYN-ACK (seq=y, ack=x+1) ───────────│
  │                                             │
  │──── ③ ACK (ack=y+1) ──────────────────────>│
  │                                             │
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,左边界右移,释放已确认的槽位
  • 若接收方通告 rwnd=0,发送方停止发送,启动零窗口探测(ZWP)定时器,周期性发送窗口探测报文

补充:RST 报文触发场景

RST 标志用于强制、异常地终止一个连接。收到 RST 后无需回复 ACK,连接立即进入 CLOSED 状态。常见场景:

  1. 端口未监听:客户端向服务器某端口发 SYN,该端口无进程监听,服务器内核直接回复 RST-ACK。
  2. 半开连接:一方进程崩溃或突然重启,再收到对方数据时,因找不到连接状态而回复 RST。
  3. SO_LINGER 设置为 0:调用 close() 时若设置了 SO_LINGER 且等待时间为 0,内核直接发 RST 而非 FIN,丢弃所有待发数据。
  4. 防火墙阻断:中间安全设备直接注入 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 (seq=u) ────────────────────────>│
  │                                             │
FIN_WAIT_1                                CLOSE_WAIT
  │                                             │
  │<─── ② ACK (ack=u+1) ───────────────────────│  [被动方] 内核自动回复
  │                                             │  应用层仍可发送剩余数据
FIN_WAIT_2                                      │
  │                                             │  [被动方] 应用层调用 close()
  │<─── ③ FIN (seq=v) ─────────────────────────│
  │                                          LAST_ACK
  │──── ④ ACK (ack=v+1) ──────────────────────>│
  │                                             │
TIME_WAIT                                    CLOSED
  │
  │  等待 2MSL(约 2 分钟)
  │
CLOSED

为什么需要四次,而非三次?

TCP 是全双工的,关闭 A→B 的通道不等于关闭 B→A 的通道。当 A 发 FIN 表示“我不再发数据了”,B 还可能有数据要继续发给 A。所以 B 先 ACK(“我知道了,但我还没发完”),等发完剩余数据后再单独发 FIN,这两步无法合并,故必然是四步。

TIME_WAIT 存在的意义(为什么等 2MSL)?

MSL(Maximum Segment Lifetime,最大报文段生存时间)通常为 60 秒,2MSL = 2 分钟。原因有两个:

  1. 防止最后 ACK 丢失:若 ④ACK 在网络中丢失,被动方会重发 FIN。主动方必须在 2MSL 内仍处于 TIME_WAIT 状态,才能再次回复 ACK,否则被动方无法正常关闭。
  2. 防止旧连接报文污染新连接:确保本次连接所有在途报文在网络中彻底消亡(最多存活 1 个 MSL),新连接复用相同端口时不会收到上一条连接的“幽灵数据包”。

第三节:各状态常见问题与解决措施

① SYN_RCVD 积压过多 → SYN Flood 攻击

维度内容
现象SYN_RCVD 状态连接数爆炸式增长,半连接队列被打满,合法新连接无法建立,服务不响应
根本原因攻击者发送海量伪造源 IP 的 SYN 包,服务端为每个 SYN 分配半连接队列资源并等待 ACK,队列耗尽后拒绝合法连接
排查命令ss -s
ss -ant state syn-recv
netstat -an | grep SYN_RCVD | wc -l
抓取 SYN 包:tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0' -c 100
解决方案开启 SYN Cookie(无需存储半连接状态):sysctl -w net.ipv4.tcp_syncookies=1
扩大半连接队列:sysctl -w net.ipv4.tcp_max_syn_backlog=4096
根本防护:在上游防火墙 / CDN 层过滤攻击流量

② CLOSE_WAIT 大量堆积 → 服务端未正确关闭连接

维度内容
现象CLOSE_WAIT 连接数持续增长,文件描述符耗尽,服务逐渐假死
根本原因客户端发送了 FIN,内核自动回复 ACK 进入 CLOSE_WAIT,但应用程序代码没有调用 close(),导致连接永远停在此状态,永不发送 FIN
排查命令ss -ant state close-wait
netstat -an | grep CLOSE_WAIT | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
lsof -i | grep CLOSE_WAIT
解决方案根本修复(代码层):确保所有代码路径(含异常/超时分支)都正确调用了 close()shutdown()
检查点:连接池是否正确归还;try-finally 中有无关闭操作;HTTP 客户端 Keep-Alive 是否配置了超时
临时缓解sysctl -w net.ipv4.tcp_keepalive_time=60(加速检测死连接)

抓包示例(观察对端不关闭的连接):

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-WAIT
netstat -n | awk '/^tcp/ {print $NF}' | sort | uniq -c
sysctl net.ipv4.ip_local_port_range
解决方案方案一(推荐):改用长连接(HTTP Keep-Alive、连接池),减少连接创建/销毁频率
方案二:开启 TIME_WAIT 复用:sysctl -w net.ipv4.tcp_tw_reuse=1(仅对出向连接有效,需同时开启 net.ipv4.tcp_timestamps=1
方案三:扩大端口范围:sysctl -w net.ipv4.ip_local_port_range="1024 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-2
netstat -an | grep FIN_WAIT2
sysctl net.ipv4.tcp_fin_timeout
tcpdump -i eth0 host <对端IP> -w /tmp/cap.pcap
解决方案系统侧:缩短 FIN_WAIT_2 超时(默认 60s):sysctl -w net.ipv4.tcp_fin_timeout=30
应用侧:排查对端是否存在 CLOSE_WAIT 积压(参考问题②),修复对端代码
调试:抓包后用 Wireshark 确认对端是否发出了 FIN

⑤ ESTABLISHED 连接假死 → 网络中断但未收到 RST

维度内容
现象连接显示 ESTABLISHED,但实际链路已断开,读写操作永久阻塞,应用进程挂死
根本原因中间网络设备(防火墙、NAT)静默丢弃数据包但不发 RST;或对端机器崩溃(断电)来不及发 RST。TCP 本身没有心跳机制,双端不知道链路已断
排查命令ss -ant state established | grep <port>
sysctl net.ipv4.tcp_keepalive_time
tcpdump -i eth0 'tcp[tcpflags] & tcp-ack != 0'
解决方案方案一:开启 TCP Keepalive(内核级心跳)
sysctl -w net.ipv4.tcp_keepalive_time=60(60s 无数据后发探测包)
sysctl -w net.ipv4.tcp_keepalive_intvl=10(探测间隔 10s)
sysctl -w net.ipv4.tcp_keepalive_probes=3(3 次无响应则关闭连接)
方案二(更推荐):应用层心跳,协议层面定期发 ping/pong,不依赖系统参数
方案三:设置 Socket 读写超时(SO_TIMEOUT),防止永久阻塞

抓包验证 Keepalive 探测包

tcpdump -i eth0 'tcp[tcpflags] & tcp-ack != 0 and port <你的端口> and length <= 1'

Keepalive 探测包是仅有一个字节(序列号为期望的序列号减1)的空 ACK,可据此过滤。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值