2、TCP协议详解:TCP报文格式、三次握手、四次挥手、状态转换图

聊到网络编程,TCP 是绕不开的核心。我刚开始做网络开发时,总觉得 TCP 就是个可靠传输协议,没什么好深究的。直到有一次线上服务出现大量连接超时,排查了整整两天,最后发现是三次握手阶段出了问题。嗯,从那以后,我再也不敢小看 TCP 的细节了。

说白了,TCP 协议就是两台机器之间的一条「虚拟管道」。它保证数据按顺序到达,不丢包,不重复。但这条管道是怎么建立的?怎么关闭的?中间经历了哪些状态?咱们今天一次性说清楚。

核心要点:TCP 是面向连接的、可靠的、基于字节流的传输层协议。它的可靠性建立在三次握手、四次挥手、滑动窗口、拥塞控制等机制之上。

2.1 TCP 报文格式

先看报文格式。我习惯把 TCP 报文头想象成一个「快递面单」——它不装货物,但告诉接收方怎么处理货物。

TCP 报文头最少 20 字节,最多 60 字节。关键字段如下:

字段 长度 说明
源端口 / 目的端口 各 2 字节 标识通信的应用程序
序列号 (SEQ) 4 字节 本报文段数据的第一个字节序号
确认号 (ACK) 4 字节 期望收到对方下一个报文段的序号
数据偏移 4 位 TCP 头部的长度(单位 4 字节)
标志位 6 位 URG、ACK、PSH、RST、SYN、FIN
窗口大小 2 字节 接收方的可用缓冲区大小
校验和 2 字节 校验头部和数据部分
紧急指针 2 字节 紧急数据的末尾位置

这里我特别想强调一下标志位。6 个标志位里,SYN、ACK、FIN 是握手和挥手的主角。你想想看,三次握手其实就是 SYN 和 ACK 的「你来我往」。RST 标志位也很重要——我在项目中遇到过客户端突然崩溃,服务端收到 RST 后直接重置连接,省去了等待超时的开销。

个人经验:抓包分析时,我习惯先看 SYN 和 ACK 标志位。如果 SYN 发出去没收到 SYN+ACK,大概率是防火墙拦截了,或者对端端口没开。

2.2 三次握手

三次握手,说白了就是建立连接前的「互相确认」过程。为什么需要三次?两次行不行?我当年面试时也被问过这个问题。

先看流程:

  1. 第一次握手:客户端发送 SYN=1,SEQ=x。客户端进入 SYN_SENT 状态。
  2. 第二次握手:服务端回复 SYN=1,ACK=1,SEQ=y,ACK_NUM=x+1。服务端进入 SYN_RCVD 状态。
  3. 第三次握手:客户端发送 ACK=1,SEQ=x+1,ACK_NUM=y+1。客户端进入 ESTABLISHED 状态,服务端收到后也进入 ESTABLISHED。

为什么是三次?我举个例子你就明白了。假设只有两次握手:客户端发 SYN,服务端回 SYN+ACK,然后服务端就认为连接建立了。但如果这个 SYN 是网络延迟重发的旧包呢?服务端会白白浪费资源维护一个无效连接。三次握手的关键在于——客户端收到服务端的 SYN+ACK 后,再发一次 ACK,告诉服务端「我确实在跟你通信,不是旧包乱入」。

避坑指南:我曾经遇到过 SYN 洪水攻击。攻击者只发 SYN,不回复第三次握手。服务端维护的半连接队列很快被填满,正常用户无法建立连接。解决方案是开启 SYN Cookie 或者调整 tcp_syncookies 内核参数。

2.3 四次挥手

断开连接比建立连接复杂一些。因为 TCP 是双工通信,双方都需要关闭自己的发送通道。

流程如下:

  1. 第一次挥手:客户端发送 FIN=1,SEQ=u。客户端进入 FIN_WAIT_1 状态。
  2. 第二次挥手:服务端回复 ACK=1,SEQ=v,ACK_NUM=u+1。服务端进入 CLOSE_WAIT 状态。客户端收到后进入 FIN_WAIT_2。
  3. 第三次挥手:服务端发送 FIN=1,ACK=1,SEQ=w,ACK_NUM=u+1。服务端进入 LAST_ACK 状态。
  4. 第四次挥手:客户端发送 ACK=1,SEQ=u+1,ACK_NUM=w+1。客户端进入 TIME_WAIT 状态,等待 2MSL 后关闭。

这里有个关键点:第二次和第三次挥手之间,服务端可能还有数据要发送。所以挥手是四次,不是三次。你想想看,如果服务端在收到 FIN 时立刻也发 FIN,那就变成三次了——但这种情况很少见,因为服务端通常需要时间处理剩余数据。

注意:TIME_WAIT 状态持续 2MSL(约 2 分钟)。我见过很多高并发服务因为 TIME_WAIT 过多导致端口耗尽。解决方案是开启 tcp_tw_reuse 和 tcp_tw_recycle(但要注意 NAT 环境下的问题)。

2.4 状态转换图

下面这张图是我自己整理的 TCP 状态转换流程。它把三次握手和四次挥手串在一起,看起来更直观。

TCP 状态转换图 客户端 服务端 CLOSED SYN_SENT ESTABLISHED FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT CLOSED LISTEN SYN_RCVD ESTABLISHED CLOSE_WAIT LAST_ACK ① SYN=1, SEQ=x ② SYN=1, ACK=1, SEQ=y ③ ACK=1, SEQ=x+1 ① FIN=1, SEQ=u ② ACK=1, SEQ=v ③ FIN=1, ACK=1, SEQ=w ④ ACK=1, SEQ=u+1 三次握手 四次挥手

这张图里,蓝色箭头是三次握手,紫色箭头是四次挥手。你注意看服务端多了一个 LISTEN 状态——这是服务端特有的,表示它在监听端口等待连接。客户端没有 LISTEN,它直接从 CLOSED 跳到 SYN_SENT。

还有一个容易被忽略的状态:CLOSE_WAIT。我排查过一个问题,服务端大量连接处于 CLOSE_WAIT,导致文件描述符耗尽。原因很简单——服务端收到客户端的 FIN 后,没有调用 close() 关闭 socket。嗯,写代码时一定要记得在收到 FIN 后及时关闭连接。

调试技巧:用 netstat -anp | grep TIME_WAIT 可以查看 TIME_WAIT 数量。如果超过几万个,建议优化内核参数。用 ss -s 可以快速查看所有 TCP 状态的统计信息。

好了,TCP 协议的核心内容就这些。报文格式是基础,三次握手和四次挥手是灵魂,状态转换图是调试利器。我个人建议你把这张图记在脑子里——排查网络问题时,90% 的问题都能通过状态转换找到线索。

可可精品资料库