3、TCP协议深度解析:三次握手与四次挥手,状态转换图,TCP头结构

TCP 协议,说白了就是网络通信的「定海神针」。我做了这么多年网络优化,见过太多问题归根结底都是对 TCP 理解不够深。你想想看,HTTP 跑在它上面,数据库连接跑在它上面,连视频流也得靠它。要是 TCP 这层没搞透,性能优化就是空中楼阁。

这一章,咱们就把 TCP 的骨头拆开来看。三次握手、四次挥手、状态机、报文头,每一个细节都跟性能息息相关。

3.1 TCP 头结构:每一比特都有用

先看最基础的东西——TCP 头。我习惯把它分成两部分:固定部分(20 字节)和选项部分(最多 40 字节)。

字段 长度(位) 说明
源端口 / 目的端口 16 + 16 标识应用进程,范围 0-65535
序列号(SEQ) 32 本报文段第一个字节的序号
确认号(ACK) 32 期望收到的下一个字节序号
数据偏移 4 头部长度,单位 4 字节
标志位 9 URG/ACK/PSH/RST/SYN/FIN 等
窗口大小 16 接收窗口,流量控制核心
校验和 16 覆盖头部+数据+伪头部
紧急指针 16 URG=1 时有效

性能关键点:窗口大小字段直接决定了发送方能一口气发多少数据。我在调优视频直播时,发现默认窗口太小导致带宽利用率不到 30%。后来启用了窗口缩放选项(Window Scale),吞吐量直接翻了三倍。

选项部分里,有几个对性能影响巨大的家伙:

  • MSS(最大报文段长度):协商单次能发多少数据,避免 IP 分片。
  • Window Scale:把 16 位窗口扩展到 30 位,高延迟链路必备。
  • Timestamp:计算 RTT,也是防序列号回绕的关键。
  • SACK(选择性确认):丢了哪个包就重传哪个,不用全重传。

我个人习惯在抓包时先看 SYN 包里的 MSS 和 Window Scale 选项。如果 MSS 小于 1460,说明中间有隧道或 VPN 在捣乱。曾经有一次跨机房延迟 80ms,吞吐死活上不去,一查发现 MSS 被中间设备改成了 512——嗯,这就是典型的 MTU 问题。

3.2 三次握手:不只是打招呼

三次握手的过程,教科书上都写过。但我想说的是,它背后藏着很多性能玄机。

客户端                         服务器
  |                              |
  |-------- SYN (SEQ=x) -------->|  第一次握手
  |                              |
  |<---- SYN+ACK (SEQ=y,ACK=x+1)-|  第二次握手
  |                              |
  |-------- ACK (SEQ=x+1) ------>|  第三次握手
  |                              |

每次握手都在干什么?

  • 第一次:客户端说「我要连你」,顺便告诉服务器自己的初始序列号。
  • 第二次:服务器说「收到,我也准备好了」,带上自己的序列号,同时确认客户端的。
  • 第三次:客户端说「好,开始传数据吧」。注意,这次 ACK 可以捎带数据。

避坑指南:我曾经遇到一个诡异问题——客户端连服务器总是卡 3 秒才成功。抓包发现第三次握手的 ACK 丢了,服务器没收到确认,就不断重传 SYN+ACK。默认重传超时是 3 秒、6 秒、12 秒……这就是典型的「三次握手丢包」场景。解决方案?要么优化网络减少丢包,要么调整 tcp_synack_retries 参数。

从性能角度看,三次握手有几个关键点:

  • RTT 影响:一次握手至少需要一个 RTT 才能开始传数据。如果 RTT 是 200ms,那建连就要 200ms。对于高频短连接场景,这个开销非常可观。
  • SYN Flood:攻击者只发 SYN 不发 ACK,服务器半连接队列很快被填满。我建议生产环境一定要开 tcp_syncookies。
  • TCP Fast Open:允许在 SYN 里捎带数据,省掉一个 RTT。我在内部 RPC 调用中开启后,延迟降低了 15%。

3.3 四次挥手:优雅地告别

断开连接比建立连接复杂,因为它是双向的。每一方都要单独关闭自己的通道。

主动关闭方                   被动关闭方
  |                              |
  |-------- FIN (SEQ=u) -------->|  第一次挥手
  |                              |
  |<------- ACK (SEQ=v,ACK=u+1)--|  第二次挥手
  |                              |
  |<------- FIN (SEQ=w) -------->|  第三次挥手
  |                              |
  |-------- ACK (SEQ=w+1) ------>|  第四次挥手
  |                              |

这里有个关键状态——TIME_WAIT。主动关闭方发送最后一个 ACK 后,会进入这个状态,等待 2MSL(通常 60 秒)。

为什么要有 TIME_WAIT?两个原因:一是怕最后一个 ACK 丢了,被动方重传 FIN 时还能回应;二是让旧连接的所有报文在网络中消失,避免干扰新连接。我在高并发短连接场景下,经常看到服务器上有几万个 TIME_WAIT 连接。这时候就要考虑开启 tcp_tw_reuse 或者调整 MSL 值了。

3.4 TCP 状态转换图

下面这张图,是我认为 TCP 协议里最重要的知识图谱。理解了它,你就能看懂 netstat 输出的每一个状态。

TCP 状态转换图 TCP 状态转换图 CLOSED LISTEN SYN_SENT SYN_RCVD ESTABLISHED FIN_WAIT_1 CLOSE_WAIT FIN_WAIT_2 LAST_ACK TIME_WAIT CLOSING 被动打开 主动打开 收到 SYN 收到 SYN+ACK 同时打开 收到 ACK 主动关闭 收到 FIN 收到 ACK 收到 FIN 收到 ACK 收到 FIN 关闭 收到 ACK 2MSL 超时 说明: 实线表示客户端主动关闭路径,虚线表示同时关闭路径。TIME_WAIT 是主动关闭方必须经历的状态,持续 2MSL(约 60 秒)。

这张图怎么看?我教你一个窍门:先找 ESTABLISHED。它是所有状态的「心脏」。往上看是建连过程,往下看是断连过程。左边是主动方,右边是被动方。

我在排查连接问题时,经常用 netstat -tan 看状态分布。如果发现大量 SYN_RCVD,说明有人在搞 SYN Flood 或者服务器处理不过来。如果大量 CLOSE_WAIT,说明应用程序忘了关 socket——这是最常见的 bug 之一。大量 TIME_WAIT 则说明短连接太多,可以考虑连接池或者开启 tcp_tw_reuse。

3.5 性能视角下的 TCP 状态

每个状态都可能成为性能瓶颈。我总结了几种常见场景:

状态 常见原因 性能影响 优化建议
SYN_RCVD 半连接队列满、SYN Flood 新连接无法建立 开启 tcp_syncookies,增大 tcp_max_syn_backlog
CLOSE_WAIT 应用层未调用 close() 文件描述符泄漏 检查代码,确保 socket 正确关闭
TIME_WAIT 主动关闭短连接过多 端口耗尽、内存占用 开启 tcp_tw_reuse,使用连接池
FIN_WAIT_2 对端不发送 FIN 连接挂起 设置 tcp_fin_timeout 超时

避坑指南:我曾经接手过一个电商后端,高峰期经常报「Too many open files」。一查,CLOSE_WAIT 连接堆了上万个。原因是某个微服务在读取响应后没有关闭 socket,导致连接一直挂在 CLOSE_WAIT。修复方法很简单——加了一个 try-finally 确保 close() 一定会执行。但排查过程花了两天,因为日志里根本看不出哪个连接没关。

3.6 小结

TCP 协议看起来复杂,但核心就三件事:可靠传输、流量控制、拥塞控制。三次握手和四次挥手是可靠传输的基础,状态机是连接生命周期的地图,报文头是控制信令的载体。

你想想看,理解了这些,再看 netstat 的输出、tcpdump 的抓包、Wireshark 的时序图,是不是就清晰多了?下一层,咱们要聊的是 TCP 的流量控制和拥塞控制——这才是性能优化的主战场。