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 输出的每一个状态。
这张图怎么看?我教你一个窍门:先找 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 的流量控制和拥塞控制——这才是性能优化的主战场。