1. TCP协议基础回顾:从握手到挥手,一个老工程师的视角
说实话,每次带新人排查网络问题,我第一件事就是让他们把TCP协议栈模型画出来。不是为难人,而是——你连数据从应用层到物理层怎么封装都搞不清楚,怎么定位重传?
这一章咱们不搞虚的。我把TCP协议栈、报文结构、三次握手、四次挥手、状态转换图串起来讲一遍。重点不是背概念,而是理解每个环节在真实网络中会出什么幺蛾子。
1.1 TCP/IP协议栈模型:数据到底怎么走的?
先看这张图。我习惯把协议栈比作一个快递包裹的封装过程——你写的信是应用层,套上信封是传输层,写上地址是网络层,最后贴上快递单是链路层。
核心要点:数据从应用层往下走,每层加一个头部(Header),这叫封装。接收端从下往上走,每层剥一个头部,这叫解封装。
我个人习惯把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”,但真到抓包分析时,连握手失败都看不出来。
- 第一次握手(客户端→服务器):客户端发送SYN报文,SEQ=x(随机初始序列号)。客户端进入SYN_SENT状态。
- 第二次握手(服务器→客户端):服务器回复SYN+ACK,SEQ=y,ACK=x+1。服务器进入SYN_RCVD状态。
- 第三次握手(客户端→服务器):客户端发送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状态转换图:连接的一生
这张图我建议你贴在工位上。每次排查连接异常,对照着状态图走一遍,基本能定位问题。
这张图里我标了最常用的几个状态。实际工作中,你看到最多的异常状态是:
- SYN_RCVD 堆积:服务器收到了SYN但没完成握手,可能是半连接攻击,也可能是服务器 backlog 太小。
- CLOSE_WAIT 堆积:应用层没调用 close(),导致连接一直挂着。我见过一个Java应用,CLOSE_WAIT 堆到几千个,最后 OOM 了。
- TIME_WAIT 堆积:前面说过了,高并发短连接的典型问题。
避坑指南:我曾经在排查一个金融交易系统的延迟问题时,发现大量连接卡在 FIN_WAIT_2 状态。原因是客户端发了FIN后,服务端一直不回复FIN,也不关闭连接。查代码发现服务端有个线程池满了,close() 调用被阻塞了。这种问题,不懂状态转换图根本无从下手。
1.6 本章小结:这些基础够用了吗?
说实话,TCP协议栈、报文结构、三次握手、四次挥手、状态转换图——这些是基本功。但基本功不等于简单。我见过十年经验的工程师,遇到TIME_WAIT问题照样抓瞎。
这一章的内容,我希望你记住三句话:
- 封装与解封装是理解抓包的基础。
- SEQ和ACK是定位重传的核心线索。
- 状态转换图是排查连接异常的导航地图。