3. TCP协议基础回顾:报文段结构、序列号与确认号、滑动窗口机制、三次握手与四次挥手

聊粘包拆包之前,我习惯先带大家把TCP的老底翻一翻。你想想看,如果连TCP报文长什么样、序列号怎么用都不清楚,那抓包回来也是一堆乱码。这一节咱们就踏踏实实把TCP的几个核心概念过一遍。

3.1 TCP报文段结构——数据在线上长什么样

TCP报文段说白了就是TCP层给应用层数据套的“信封”。这个信封有固定20字节的头部,后面跟着选项和数据。我当年第一次用Wireshark看TCP包时,就被这堆字段搞晕了。后来自己画了个图,才真正记住。

TCP报文段头部结构(20字节固定部分):

偏移(字节) 字段 长度(位) 说明
0-1 源端口 16 发送方端口号
2-3 目的端口 16 接收方端口号
4-7 序列号 32 本报文段数据的第一个字节序号
8-11 确认号 32 期望收到的下一个字节序号
12-13 数据偏移+保留+标志位 4+6+6 头部长度、控制标志
14-15 窗口大小 16 接收方剩余缓冲区大小
16-17 校验和 16 覆盖头部+数据的校验
18-19 紧急指针 16 URG标志置位时有效

这里我特别想强调两个字段:序列号和确认号。它们俩是TCP可靠传输的命根子。后面讲粘包拆包时,你抓包主要盯的就是这两个值。

3.2 序列号与确认号——TCP的“对账本”

序列号不是简单的包序号。它标记的是数据流中每个字节的位置。比如你发100字节数据,第一个字节的序列号是1000,那这个TCP段的序列号就是1000,下一个段的序列号就是1100。

确认号呢?它表示“我已经收到你发来的所有数据,下一个我期望收到的字节序号是XXX”。说白了就是告诉对方:你发到XXX-1我都收到了,从XXX开始发吧。

我的抓包小技巧:在Wireshark里看TCP流时,我习惯把“相对序列号”打开(Preferences → TCP → Relative sequence numbers)。这样序列号从0开始,一眼就能看出数据偏移量,不用去算绝对序号。这个设置我用了十年,强烈推荐。

举个例子你就明白了。假设客户端发了一个包,序列号=0,数据长度=10。服务端回复的确认包,确认号=10。意思就是:0-9这10个字节我收到了,你下一个从10开始发。

我曾经在排查一个粘包问题时,发现客户端连续发了两个包,序列号分别是100和200。但服务端只收到了一个确认号=150的ACK。这说明第二个包丢了,或者被合并了。嗯,这就是粘包拆包问题的根源之一。

3.3 滑动窗口机制——TCP的流量阀门

滑动窗口是TCP做流量控制的核心。接收方告诉发送方:“我缓冲区还剩多少空间,你最多发这么多,别撑着我。”这个值就是报文头里的窗口大小。

我画了一张图,帮你理解窗口是怎么“滑”的。

TCP滑动窗口机制示意图 发送缓冲区数据流 已确认 已发送未确认 可发送 不可发送 ← 窗口大小(接收方通告) → 接收方缓冲区 已接收未读取 + 空闲空间 ACK中携带窗口大小更新 窗口左边界随ACK右移,右边界由接收方通告窗口决定

窗口左边界随着收到ACK不断右移,右边界由接收方通告的窗口大小决定。中间这段就是“已发送但还没确认”的数据。如果窗口满了,发送方就得停下来等ACK。

我曾经踩过的坑:有一次做高并发推送服务,发现客户端收不到数据。抓包一看,接收方窗口大小变成了0。原来客户端应用层处理太慢,缓冲区满了,TCP直接关闭了窗口。发送方只能不断发零窗口探测包。这个案例让我深刻理解了——滑动窗口不只是协议概念,它直接决定了你的应用吞吐量。

3.4 三次握手——建立连接的三步舞

三次握手是TCP建立连接的过程。说白了就是双方互相确认:“我在,你也在,咱们可以聊了。”

流程是这样的:

  1. 客户端发SYN:序列号=x,标志位SYN=1。意思是“我想跟你建立连接,我的初始序号是x”。
  2. 服务端回SYN+ACK:序列号=y,确认号=x+1,标志位SYN=1, ACK=1。意思是“收到你的请求,我的初始序号是y,我期待你下一个发x+1”。
  3. 客户端发ACK:序列号=x+1,确认号=y+1,标志位ACK=1。意思是“收到你的确认,咱们开始传数据吧”。

这里有个细节:SYN包本身会消耗一个序列号。所以客户端发完SYN后,下一个数据包的序列号是x+1,而不是x。很多人抓包时看到序列号跳了1,以为丢包了,其实不是。

我个人的习惯:排查连接问题时,我第一件事就是看三次握手是否完整。如果只有SYN没有SYN+ACK,那可能是防火墙拦截了。如果只有前两次没有第三次ACK,那就是半连接攻击或者客户端挂了。这些在Wireshark里一眼就能看出来。

3.5 四次挥手——优雅地分手

四次挥手比握手复杂一点,因为TCP连接是双向的,每一方向都要单独关闭。

标准流程:

  1. 主动方发FIN:序列号=u,标志位FIN=1。意思是“我这边数据发完了,我要关闭发送通道”。
  2. 被动方回ACK:确认号=u+1。意思是“收到你的FIN,但我还有数据要发”。
  3. 被动方发FIN:序列号=v,标志位FIN=1。意思是“我这边数据也发完了,可以关了”。
  4. 主动方回ACK:确认号=v+1。意思是“收到,连接关闭”。

注意第二步和第三步之间,被动方可能还在发数据。这就是为什么挥手需要四次,而不是三次。

粘包拆包与挥手的关系:你可能会问,挥手跟粘包有什么关系?关系大了。如果服务端在发送完响应后立刻调用close(),而客户端还没读完数据,那客户端就会收到RST包,导致数据丢失。这就是所谓的“TCP RST导致数据截断”,是粘包问题的一个变种。

我记得有一次排查一个HTTP长连接的问题,客户端发完请求后直接发了FIN,服务端还在处理中。结果服务端回响应时发现连接已经半关闭了,数据发不出去。嗯,这就是典型的“过早关闭”问题。

3.6 本章小结

TCP的这些基础机制,每一个都跟粘包拆包有直接关系:

  • 报文段结构决定了你能从抓包中看到什么信息
  • 序列号和确认号帮你追踪数据是否完整到达
  • 滑动窗口决定了数据发送的节奏,窗口变化会导致数据合并或拆分
  • 三次握手和四次挥手决定了连接的建立和关闭时机,时机不对就会丢数据