1. TCP协议基础回顾:从握手到挥手,一个老工程师的视角

说实话,每次带新人排查网络问题,我第一件事就是让他们把TCP协议栈模型画出来。不是为难人,而是——你连数据从应用层到物理层怎么封装都搞不清楚,怎么定位重传?

这一章咱们不搞虚的。我把TCP协议栈、报文结构、三次握手、四次挥手、状态转换图串起来讲一遍。重点不是背概念,而是理解每个环节在真实网络中会出什么幺蛾子。

1.1 TCP/IP协议栈模型:数据到底怎么走的?

先看这张图。我习惯把协议栈比作一个快递包裹的封装过程——你写的信是应用层,套上信封是传输层,写上地址是网络层,最后贴上快递单是链路层。

核心要点:数据从应用层往下走,每层加一个头部(Header),这叫封装。接收端从下往上走,每层剥一个头部,这叫解封装。

TCP/IP协议栈模型与数据封装流程 发送端 应用层 HTTP / FTP / SMTP 数据 传输层 TCP头部 + 应用数据 → 报文段 网络层 IP头部 + TCP报文段 → 数据报 链路层 帧头部 + IP数据报 → 帧 物理层 接收端 物理层 链路层 去掉帧头部 → IP数据报 网络层 去掉IP头部 → TCP报文段 传输层 去掉TCP头部 → 应用数据 应用层 HTTP / FTP / SMTP 数据 封装与解封装过程 发送端:数据 → TCP头 → IP头 → 帧头 → 比特流 | 接收端:比特流 → 帧头 → IP头 → TCP头 → 数据

我个人习惯把TCP/IP模型简化为四层:应用层、传输层、网络层、网络接口层。OSI七层模型太理论化了,实战中我们更关注这四层。

实战小贴士:抓包时你看到的每一帧,其实就是链路层封装好的数据。Wireshark里展开的Ethernet II、IP、TCP,就是一层层剥开的过程。我曾经见过新人盯着十六进制流发呆,其实就是没理解这个封装模型。

1.2 TCP报文段结构:头部里藏着什么秘密?

TCP报文段头部最少20字节,最多60字节。为什么有弹性?因为选项字段(Options)可以扩展。嗯,这里要注意——重传定位最常看的几个字段,我列在下面了。

字段 长度 作用 重传排查关注点
源端口 / 目的端口 各2字节 标识通信进程 确认五元组是否正确
序列号 (SEQ) 4字节 标识字节流位置 重传时SEQ是否重复
确认号 (ACK) 4字节 期望收到的下一个SEQ ACK是否在合理范围内
标志位 (Flags) 9位 SYN/ACK/FIN/RST等控制 RST标志出现说明连接异常
窗口大小 (Window) 2字节 接收端可用缓冲区大小 窗口为0说明接收端满负荷
校验和 (Checksum) 2字节 检测报文段是否损坏 校验和错误会导致丢包

你想想看,重传的本质是什么?就是发送端没收到预期的ACK,或者收到了重复的ACK。所以SEQ和ACK这两个字段,是咱们排查重传问题的第一道关卡。

避坑指南:我曾经遇到一个案例,应用层一直报超时,抓包发现TCP重传率高达30%。查了半天,结果是中间防火墙把TCP选项字段里的时间戳(Timestamp)给改了,导致PAWS(Protection Against Wrapped Sequences)机制误判,疯狂重传。所以别小看选项字段,它有时候能坑死人。

1.3 三次握手:连接是怎么建立的?

三次握手的过程,说白了就是双方互相确认“我能发、你能收”的过程。我见过太多人背口诀“SYN→SYN+ACK→ACK”,但真到抓包分析时,连握手失败都看不出来。

  1. 第一次握手(客户端→服务器):客户端发送SYN报文,SEQ=x(随机初始序列号)。客户端进入SYN_SENT状态。
  2. 第二次握手(服务器→客户端):服务器回复SYN+ACK,SEQ=y,ACK=x+1。服务器进入SYN_RCVD状态。
  3. 第三次握手(客户端→服务器):客户端发送ACK,SEQ=x+1,ACK=y+1。双方进入ESTABLISHED状态。

关键点:三次握手完成后,双方都知道了对方的初始序列号。这个序列号是后续数据传输和重传判断的基础。如果握手阶段SEQ就乱套了,后面肯定出问题。

我记得有一次帮客户排查一个跨地域的数据库同步问题,三次握手总是卡在第二次。抓包一看,服务器发了SYN+ACK,但客户端没收到——中间有个老旧路由器把SYN+ACK当成攻击流量给丢了。这种问题,不懂握手流程根本想不到。

1.4 四次挥手:连接是怎么关闭的?

四次挥手比握手复杂一点,因为TCP连接是全双工的——两边都要独立关闭自己的数据流。

步骤 发送方 报文 状态变化
1 主动关闭方 FIN → ESTABLISHED → FIN_WAIT_1
2 被动关闭方 ← ACK ESTABLISHED → CLOSE_WAIT
3 被动关闭方 ← FIN CLOSE_WAIT → LAST_ACK
4 主动关闭方 ACK → FIN_WAIT_2 → TIME_WAIT

这里有个坑——TIME_WAIT状态会持续2MSL(Maximum Segment Lifetime,通常2分钟)。为什么?因为要确保最后一个ACK能被对方收到。如果ACK丢了,对方会重发FIN,TIME_WAIT状态还能处理这个重发的FIN。

实战经验:高并发短连接场景下,服务器上TIME_WAIT连接可能堆积到几万个。我曾经调过一个Nginx代理,TIME_WAIT太多导致端口耗尽,新连接连不上。解决方案?要么开启tcp_tw_reuse(Linux 4.12之前的内核),要么调整应用层连接池策略。

1.5 TCP状态转换图:连接的一生

这张图我建议你贴在工位上。每次排查连接异常,对照着状态图走一遍,基本能定位问题。

TCP状态转换图(核心路径) CLOSED LISTEN SYN_SENT SYN_RCVD ESTABLISHED FIN_WAIT_1 CLOSE_WAIT FIN_WAIT_2 LAST_ACK TIME_WAIT 被动打开 主动打开 收到SYN 收到SYN+ACK 收到ACK 主动关闭 收到FIN 收到ACK 收到FIN+ACK 关闭 收到FIN 收到ACK 2MSL超时

这张图里我标了最常用的几个状态。实际工作中,你看到最多的异常状态是:

  • SYN_RCVD 堆积:服务器收到了SYN但没完成握手,可能是半连接攻击,也可能是服务器 backlog 太小。
  • CLOSE_WAIT 堆积:应用层没调用 close(),导致连接一直挂着。我见过一个Java应用,CLOSE_WAIT 堆到几千个,最后 OOM 了。
  • TIME_WAIT 堆积:前面说过了,高并发短连接的典型问题。

避坑指南:我曾经在排查一个金融交易系统的延迟问题时,发现大量连接卡在 FIN_WAIT_2 状态。原因是客户端发了FIN后,服务端一直不回复FIN,也不关闭连接。查代码发现服务端有个线程池满了,close() 调用被阻塞了。这种问题,不懂状态转换图根本无从下手。

1.6 本章小结:这些基础够用了吗?

说实话,TCP协议栈、报文结构、三次握手、四次挥手、状态转换图——这些是基本功。但基本功不等于简单。我见过十年经验的工程师,遇到TIME_WAIT问题照样抓瞎。

这一章的内容,我希望你记住三句话:

  1. 封装与解封装是理解抓包的基础。
  2. SEQ和ACK是定位重传的核心线索。
  3. 状态转换图是排查连接异常的导航地图。