1、TCP协议基础:TCP/IP协议栈回顾、TCP报文段结构、TCP三次握手与四次挥手、TCP的可靠传输机制

1.1 TCP/IP协议栈回顾

聊TCP粘包拆包之前,得先把地基打牢。TCP/IP协议栈,说白了就是一套让计算机能互相通信的规则集。我刚开始接触网络编程时,总觉得这东西太抽象,直到亲手抓了几次包,才真正理解每一层在干什么。

协议栈分四层,从上到下依次是:

  • 应用层:HTTP、FTP、SMTP 这些,咱们写的业务代码基本都在这一层。
  • 传输层:TCP 和 UDP 的老家。TCP 提供可靠连接,UDP 只管发不管到。
  • 网络层:IP 协议在这层干活,负责寻址和路由。
  • 网络接口层:把数据变成电信号或光信号,扔到网线上。

你想想看,数据从应用层一路往下,每经过一层都会被加上一个头部。这个过程叫封装。到了接收端,再一层层剥开。TCP 粘包拆包的问题,就出在传输层和应用层的交界处。

核心观点:TCP 是流式协议,没有消息边界。应用层发出去的多条消息,可能在 TCP 层被合并或拆分。这就是粘包和拆包的根源。

我在项目中遇到过好几次,客户端发了一串 JSON,服务端读出来却是半截的。一开始以为是代码写错了,后来才意识到是 TCP 的流特性在作怪。

1.2 TCP报文段结构

TCP 报文段长什么样?我习惯把它分成两部分:头部和数据。头部固定 20 字节,后面可以跟选项,最长 60 字节。

字段 长度(位) 说明
源端口 16 发送方端口号
目的端口 16 接收方端口号
序列号 32 本报文段第一个字节的序号
确认号 32 期望收到的下一个字节序号
数据偏移 4 头部长度,单位是 4 字节
标志位 9 SYN、ACK、FIN 等控制位
窗口大小 16 接收方还能收多少数据
校验和 16 校验头部和数据
紧急指针 16 紧急数据结束位置

这里我特别想提一下序列号和确认号。这两个字段是 TCP 可靠传输的基石。序列号标记了每个字节的位置,确认号告诉对方“我收到哪了”。

小技巧:抓包时盯着序列号和确认号看,能快速判断 TCP 连接的状态。我曾经靠这个定位过一个诡异的丢包问题。

1.3 TCP三次握手与四次挥手

三次握手,就是建立连接的过程。我习惯用“打电话”来类比:

  1. 第一次握手(SYN):客户端说“喂,能听到吗?”
  2. 第二次握手(SYN+ACK):服务端说“能听到,你呢?”
  3. 第三次握手(ACK):客户端说“我也能听到,开始聊吧。”

为什么是三次?不是两次也不是四次?嗯,这里要注意。两次握手的话,服务端无法确认客户端收到了自己的 SYN+ACK。万一这个 SYN+ACK 丢了,客户端会重发 SYN,服务端就得开两个连接,浪费资源。

四次挥手是断开连接的过程:

  1. 第一次挥手(FIN):客户端说“我说完了,准备关。”
  2. 第二次挥手(ACK):服务端说“知道了,但我还有话要说。”
  3. 第三次挥手(FIN):服务端说“我也说完了,关吧。”
  4. 第四次挥手(ACK):客户端说“好,关了。”

这里有个坑:客户端发送第四次挥手后,不会立刻关闭,而是进入 TIME_WAIT 状态,等 2MSL(最大报文段生存时间)后才真正关闭。为什么?因为要确保服务端收到了 ACK。如果 ACK 丢了,服务端会重发 FIN,客户端还能响应。

避坑指南:我曾经在生产环境遇到过大量 TIME_WAIT 连接,导致端口耗尽。后来调整了内核参数,缩短了 TIME_WAIT 时间,问题才解决。

下面这张图展示了三次握手和四次挥手的完整流程:

TCP 三次握手与四次挥手 客户端 服务端 CLOSED LISTEN ① SYN=1, seq=x SYN_SENT SYN_RCVD ② SYN=1, ACK=1, seq=y, ack=x+1 ③ ACK=1, seq=x+1, ack=y+1 ESTABLISHED ESTABLISHED 数据传输阶段 ④ FIN=1, seq=u FIN_WAIT_1 CLOSE_WAIT ⑤ ACK=1, seq=v, ack=u+1 FIN_WAIT_2 ⑥ FIN=1, ACK=1, seq=w, ack=u+1 LAST_ACK ⑦ ACK=1, seq=u+1, ack=w+1 TIME_WAIT (2MSL) CLOSED 说明:红色箭头表示 FIN 标志位,灰色箭头表示 ACK 标志位

1.4 TCP的可靠传输机制

TCP 怎么保证数据不丢、不乱、不重复?说白了就是三板斧:确认应答、超时重传、流量控制。

确认应答:每收到一个报文段,接收方都要回一个 ACK。发送方收到 ACK,才知道数据到了。如果 ACK 没回来,发送方就知道可能丢了。

超时重传:发送方启动一个定时器,如果超时还没收到 ACK,就重传。这个超时时间不是固定的,TCP 会根据网络状况动态调整。我见过不少新手把超时时间设成固定值,结果网络一波动就出问题。

流量控制:接收方通过窗口大小告诉发送方“我还能收多少”。发送方不能一股脑全发出去,得看对方的接收能力。这个机制叫滑动窗口协议。

关键点:滑动窗口的大小决定了发送方能连续发多少数据而不等 ACK。窗口越大,吞吐量越高,但风险也越大。粘包拆包问题在窗口较大时更容易出现。

还有一个机制叫拥塞控制,它和流量控制不一样。流量控制是端到端的,拥塞控制是全局的。TCP 通过慢启动、拥塞避免、快重传、快恢复四个阶段来应对网络拥塞。

我记得有一次线上服务响应变慢,排查后发现是拥塞控制算法在作祟。网络偶尔抖动,TCP 就自动降低了发送速率,导致整体吞吐量下降。后来调整了内核参数,把拥塞控制算法从 cubic 换成了 bbr,问题才缓解。

经验之谈:理解 TCP 的可靠传输机制,对解决粘包拆包问题至关重要。因为粘包拆包的本质,就是 TCP 为了保证可靠性而做的缓冲和重组。