TCP/IP协议栈深度解析:三次握手与四次挥手、窗口机制、重传与SACK

说实话,搞网络分析这么多年,我见过太多人把TCP协议背得滚瓜烂熟,但一抓包就懵了。为什么?因为理论是理论,报文是报文,中间差着一条「实践」的河。今天咱们就拿着Wireshark这把刀,把TCP协议栈的核心机制一层层剥开来看。

TCP三次握手:不只是「你好你好你好」

三次握手大家都懂——SYN、SYN-ACK、ACK。但你真的在Wireshark里仔细看过每个字段吗?我建议你下次抓包时,点开第一个SYN报文,看看里面的「Window size」和「MSS」选项。

核心要点:三次握手不仅仅是建立连接,它还在做三件重要的事——协商初始序列号、交换窗口大小、确认MSS(最大报文段大小)。

举个例子。客户端发SYN时,会带上自己的初始序列号(ISN),这个数不是从0开始的,而是随机生成的。为什么?为了防止旧连接的数据包干扰新连接。我在项目中遇到过一个问题:某台服务器频繁出现TCP连接重置,抓包一看,ISN生成算法有bug,导致序列号冲突。

握手过程中的关键参数

报文方向关键字段含义
客户端→服务器SYN, Seq=0 (相对值)请求建立连接,初始序列号随机
服务器→客户端SYN, ACK, Seq=0, Ack=1确认收到SYN,同时发送自己的SYN
客户端→服务器ACK, Seq=1, Ack=1确认收到服务器的SYN,连接建立

小技巧:在Wireshark里,默认显示的是相对序列号(Relative Sequence Number)。如果你想看真实的原始序列号,右键点击TCP字段,选择「Protocol Preferences」→「Relative sequence numbers」取消勾选。我调试序列号相关问题时,一定会切到原始模式。

四次挥手:优雅告别还是强行断开?

四次挥手比握手复杂得多。为什么?因为TCP是全双工的,每一方向都需要单独关闭。说白了,就是「我要关了我的发送通道,你那边随意」。

正常的挥手流程:FIN→ACK→FIN→ACK。但实际抓包中,你经常会看到FIN+ACK合并的情况,或者出现RST(重置)直接断开的场景。

避坑指南:我曾经排查过一个线上问题——客户端频繁报「Connection reset by peer」。抓包后发现,服务器端在收到FIN后,没有走正常的四次挥手,而是直接发了RST。原因是服务器端的应用层代码在关闭socket时调用了SO_LINGER选项,设置了超时时间为0。这会导致TCP直接发送RST,而不是FIN。嗯,这个坑很多新手都会踩。

挥手状态机速查

状态说明常见问题
FIN_WAIT_1主动关闭方发送FIN后如果收不到ACK,会超时重传FIN
CLOSE_WAIT被动关闭方收到FIN后应用层没调用close(),会一直卡在这里
FIN_WAIT_2主动关闭方收到ACK后等待对方的FIN,可能一直等不到
TIME_WAIT主动关闭方收到FIN并回复ACK后等待2MSL,确保旧数据包消失

你想想看,TIME_WAIT为什么要等2MSL?说白了,就是为了防止最后一个ACK丢失,对方重传FIN时,这边还能响应。我见过一台高并发服务器上TIME_WAIT连接堆积到几万个,直接把端口耗尽了。解决方案?调整内核参数,或者开启SO_REUSEADDR。

窗口机制:TCP的流量阀门

窗口机制是TCP的核心智慧。它解决了什么问题?发送方不能一股脑地把数据全扔出去,得看接收方「吃不吃得消」。接收方通过窗口大小告诉发送方:「我现在还能收多少字节,你看着发」。

在Wireshark里,你可以在TCP报文头部的「Window size」字段看到这个值。但要注意,这个值是原始值,实际可用窗口还要乘以「Window scale factor」(窗口缩放因子)。

关键公式:实际窗口大小 = Window size × 2^(Window scale factor)

举个例子,Window size显示65535,Window scale factor是7,那实际窗口就是65535 × 128 = 8,388,480字节,约8MB。这就是为什么现代网络能跑满带宽——没有窗口缩放,TCP窗口最大只有64KB,高速网络下根本不够用。

窗口机制的工作流程

  1. 发送方发送数据,占用窗口空间
  2. 接收方处理数据,释放窗口空间
  3. 接收方在ACK中通告新的窗口大小
  4. 发送方根据窗口大小决定发送多少数据

实战经验:我在分析一个跨国专线的性能问题时,发现窗口经常降到0。这意味着接收方处理不过来了。进一步排查,是接收方的应用程序读取socket的速度太慢,导致内核缓冲区满了。解决方案?优化应用层读取逻辑,或者增大socket缓冲区。

重传机制:丢了怎么办?

网络是不可靠的,丢包是常态。TCP怎么应对?重传。但重传的方式有好几种,每种都有自己的适用场景。

超时重传(RTO)

发送方发完数据后,启动一个定时器。如果超时还没收到ACK,就重传。RTO(Retransmission Timeout)不是固定的,TCP会动态计算——基于RTT(往返时间)和RTT的抖动。

在Wireshark里,你可以通过「Analysis」→「Expert Info」快速定位重传报文。我习惯用过滤器 tcp.analysis.retransmission 直接筛选。

快速重传

如果发送方收到3个重复的ACK,说明对方没收到某个包,但后续的包都收到了。这时候不用等超时,直接重传丢失的包。这就是快速重传,比超时重传快得多。

注意:快速重传的前提是接收方启用了「延迟确认」机制。如果接收方每收到一个包就立即ACK,那重复ACK就不会出现。我曾经调优过一个存储系统的网络性能,关闭了延迟确认后,快速重传反而失效了,性能下降明显。后来改回来才恢复正常。

SACK:精准打击,不浪费带宽

传统的重传机制有个问题:如果一次丢了多个包,发送方只能重传从丢失点开始的所有数据。比如发了1、2、3、4、5,只收到了1和5,那发送方会重传2、3、4、5——即使5已经收到了。这显然浪费带宽。

SACK(Selective Acknowledgment,选择性确认)解决了这个问题。接收方在ACK中告诉发送方:「我收到了哪些数据块,哪些没收到」。发送方只重传真正丢失的部分。

特性传统ACKSACK
确认方式只确认连续收到的数据可确认多个不连续的数据块
重传效率低,可能重传已收到的数据高,只重传丢失的数据
带宽利用率低高
是否默认开启是需双方协商

在Wireshark里,你可以在SYN报文的TCP选项中看到SACK是否被支持。如果双方都支持,握手时就会协商启用。我建议你在所有服务器上都开启SACK,尤其是在高延迟、高丢包率的网络环境下,效果立竿见影。

验证方法:用过滤器 tcp.options.sack 查看哪些报文携带了SACK选项。如果看到多个SACK块,说明网络确实存在丢包,而且SACK正在发挥作用。

核心流程全景图

下面这张图把TCP的核心流程串起来了。从三次握手建立连接,到数据传输中的窗口控制和重传机制,再到四次挥手断开连接。我建议你把它保存下来,分析问题时对照着看。

TCP核心流程全景图 阶段一:三次握手(建立连接) 客户端 → SYN 服务器 → SYN+ACK 客户端 → ACK 协商ISN、窗口大小、MSS、SACK等参数 阶段二:数据传输(窗口控制 + 重传 + SACK) 发送方发送数据 接收方回复ACK 窗口更新 超时重传 / 快速重传 SACK选择性确认 只重传丢失 窗口大小动态调整,接收方通过ACK通告剩余缓冲区 阶段三:四次挥手(断开连接) 主动方 → FIN 被动方 → ACK 被动方 → FIN 主动方 → ACK 主动方进入TIME_WAIT状态,等待2MSL

这张图把TCP的三大阶段放在一起对比。你会发现,每个阶段的核心都是「确认」——握手要确认,数据要确认,挥手也要确认。TCP的可靠性,说白了就是靠这些确认机制堆出来的。

总结一下:TCP协议栈的这几个机制是环环相扣的。窗口机制决定了发送速率,重传机制保证了数据不丢,SACK提升了重传效率。你在Wireshark里分析问题时,不要孤立地看某个字段,要把它们串起来理解。比如看到大量重传,先查窗口是不是被压到0了,再查SACK有没有生效,最后看RTO是不是设置得太短。