4. 传输层基石:TCP头部结构、端口号、三次握手与四次挥手的状态机

聊到连接稳定性,咱们得先搞清楚TCP这个老大哥到底长什么样。我做了这么多年网络协议分析,见过太多因为对TCP理解不透彻导致的诡异问题。说白了,TCP就是两台机器之间的一条「虚拟管道」,而它的头部结构就是这条管道的控制面板。

4.1 TCP头部结构:每一比特都有故事

TCP头部最少20字节,最多60字节。嗯,这里要注意,别小看这20字节,每一个字段都可能成为你排查问题的突破口。

字段 长度(bit) 作用
源端口号 16 标识发送方的应用进程
目的端口号 16 标识接收方的应用进程
序列号(SEQ) 32 保证数据按序到达,防乱序
确认号(ACK) 32 告诉对方我期望收到的下一个字节
数据偏移 4 头部长度,单位是4字节
标志位 9 控制连接状态(SYN、ACK、FIN等)
窗口大小 16 流量控制,告诉对方我能收多少
校验和 16 检测头部和数据是否损坏
紧急指针 16 配合URG标志位使用

我个人习惯在抓包时先看标志位和序列号。为什么?因为80%的连接问题都出在这两个地方。有一次我排查一个视频流卡顿的问题,抓包一看,序列号乱跳,原来是中间有个NAT设备把TCP包给改了。

核心要点:TCP头部中的「序列号」和「确认号」是实现可靠传输的基石。它们让接收方能够检测丢包、去重、排序。没有这两个字段,TCP就不是TCP了。

4.2 端口号:应用层的门牌号

端口号是16位的,范围0-65535。你想想看,一台服务器上跑着Web服务、数据库、邮件服务,怎么区分数据包该交给谁?就是靠端口号。

  • 知名端口(0-1023):HTTP用80,HTTPS用443,SSH用22。这些是约定俗成的,别乱改。
  • 注册端口(1024-49151):比如MySQL用3306,Redis用6379。
  • 动态/私有端口(49152-65535):客户端临时使用,用完就释放。

避坑指南:我曾经遇到过一个问题,客户端连不上服务器,排查了半天发现是防火墙只开放了80端口,但应用层用了8080。端口号不匹配,三次握手根本发不出去。所以,做网络调试时,第一件事就是确认端口号对不对。

4.3 三次握手:建立连接的「双向确认」

三次握手说白了就是双方互相确认「你准备好了吗?」「我准备好了!」的过程。我刚开始学的时候觉得这很简单,后来才发现里面门道很多。

  1. 第一次握手(SYN):客户端发送SYN包,序列号设为x。客户端进入SYN_SENT状态。
  2. 第二次握手(SYN+ACK):服务器收到后,回复SYN+ACK,序列号设为y,确认号设为x+1。服务器进入SYN_RCVD状态。
  3. 第三次握手(ACK):客户端发送ACK包,确认号设为y+1。双方进入ESTABLISHED状态。

为什么非要三次?两次不行吗?你想想看,如果只有两次握手,服务器无法确认客户端是否收到了自己的SYN+ACK。万一这个包丢了,服务器以为连接建立了,客户端却不知道,就会造成资源浪费。

注意:三次握手中的序列号是随机生成的,不是从0开始。这是为了防止「历史连接」的干扰。我在项目中遇到过因为序列号预测攻击导致的安全问题,所以现在很多系统都启用了TCP时间戳选项来增强随机性。

TCP三次握手状态机 客户端 服务器 CLOSED SYN_SENT SYN_RCVD ESTABLISHED ① SYN (seq=x) ② SYN+ACK (seq=y, ack=x+1) ③ ACK (ack=y+1)

4.4 四次挥手:优雅地告别

断开连接比建立连接复杂。为什么?因为双方可能还有数据没发完。四次挥手就是给双方一个「把话说完」的机会。

  1. 第一次挥手(FIN):主动关闭方发送FIN包,序列号为u。进入FIN_WAIT_1状态。
  2. 第二次挥手(ACK):被动关闭方回复ACK,确认号为u+1。进入CLOSE_WAIT状态。主动方收到后进入FIN_WAIT_2。
  3. 第三次挥手(FIN):被动关闭方发送FIN包,序列号为v。进入LAST_ACK状态。
  4. 第四次挥手(ACK):主动关闭方回复ACK,确认号为v+1。进入TIME_WAIT状态,等待2MSL后关闭。

关键点:TIME_WAIT状态要持续2MSL(最大报文生存时间),通常是1-4分钟。为什么?一是为了让迟到的包消失,二是为了保证最后一个ACK能被对方收到。我在生产环境见过因为TIME_WAIT太多导致端口耗尽的问题,后来通过调整内核参数和启用SO_REUSEADDR解决了。

TCP四次挥手状态机 主动关闭方 被动关闭方 ESTABLISHED FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT ESTABLISHED CLOSE_WAIT LAST_ACK ① FIN (seq=u) ② ACK (ack=u+1) ③ FIN (seq=v) ④ ACK (ack=v+1)

4.5 状态机全景:从生到死的完整路径

TCP的状态机其实就是一个有限状态自动机。我建议你把下面这张表打印出来贴在工位上,排查问题时对照着看,效率会高很多。

状态 含义 常见问题
CLOSED 初始状态,没有连接 —
LISTEN 服务器等待客户端连接 端口未监听
SYN_SENT 客户端已发SYN,等待回复 连接超时
SYN_RCVD 服务器收到SYN,已回复SYN+ACK 半连接攻击
ESTABLISHED 连接已建立,可以传输数据 —
FIN_WAIT_1 主动方已发FIN,等待ACK 对方不响应
FIN_WAIT_2 主动方收到ACK,等待对方FIN 对方不关闭
CLOSE_WAIT 被动方收到FIN,等待应用关闭 应用未关闭socket
LAST_ACK 被动方已发FIN,等待最后ACK 最后一个ACK丢失
TIME_WAIT 主动方等待2MSL后关闭 端口耗尽

实战经验:我曾经排查过一个HTTP服务间歇性不可用的问题。用netstat一看,CLOSE_WAIT状态堆积了几千个。原因是服务端代码没有正确关闭socket,导致被动方一直卡在CLOSE_WAIT。修复方法很简单:确保每次读取完数据后调用close()。但就是这个小问题,让整个服务瘫痪了半小时。

TCP的状态机看起来复杂,但核心逻辑就一句话:每个状态变化都是由特定事件触发的。你只要记住SYN和FIN是「开关」,ACK是「确认」,就能把整个流程串起来。嗯,今天就先聊到这里,这些基础打牢了,后面讲拥塞控制和流量控制时你会觉得轻松很多。