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,高速网络下根本不够用。
窗口机制的工作流程
- 发送方发送数据,占用窗口空间
- 接收方处理数据,释放窗口空间
- 接收方在ACK中通告新的窗口大小
- 发送方根据窗口大小决定发送多少数据
实战经验:我在分析一个跨国专线的性能问题时,发现窗口经常降到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中告诉发送方:「我收到了哪些数据块,哪些没收到」。发送方只重传真正丢失的部分。
| 特性 | 传统ACK | SACK |
|---|---|---|
| 确认方式 | 只确认连续收到的数据 | 可确认多个不连续的数据块 |
| 重传效率 | 低,可能重传已收到的数据 | 高,只重传丢失的数据 |
| 带宽利用率 | 低 | 高 |
| 是否默认开启 | 是 | 需双方协商 |
在Wireshark里,你可以在SYN报文的TCP选项中看到SACK是否被支持。如果双方都支持,握手时就会协商启用。我建议你在所有服务器上都开启SACK,尤其是在高延迟、高丢包率的网络环境下,效果立竿见影。
验证方法:用过滤器 tcp.options.sack 查看哪些报文携带了SACK选项。如果看到多个SACK块,说明网络确实存在丢包,而且SACK正在发挥作用。
核心流程全景图
下面这张图把TCP的核心流程串起来了。从三次握手建立连接,到数据传输中的窗口控制和重传机制,再到四次挥手断开连接。我建议你把它保存下来,分析问题时对照着看。
这张图把TCP的三大阶段放在一起对比。你会发现,每个阶段的核心都是「确认」——握手要确认,数据要确认,挥手也要确认。TCP的可靠性,说白了就是靠这些确认机制堆出来的。
总结一下:TCP协议栈的这几个机制是环环相扣的。窗口机制决定了发送速率,重传机制保证了数据不丢,SACK提升了重传效率。你在Wireshark里分析问题时,不要孤立地看某个字段,要把它们串起来理解。比如看到大量重传,先查窗口是不是被压到0了,再查SACK有没有生效,最后看RTO是不是设置得太短。