TCP协议基础回顾:面向字节流、报文段结构与MSS/MTU
在正式进入Wireshark抓包分析粘包之前,我觉得有必要先把TCP的几个核心概念捋清楚。很多朋友抓包抓了半天,看到一堆连续的数据包却不知道怎么分析,说白了就是基础没打牢。我自己带团队时也发现,新人最容易栽跟头的地方,往往不是抓包操作本身,而是对TCP协议的理解有盲区。
TCP面向字节流的特点
TCP是面向字节流的协议。这句话听起来简单,但真正理解它的人不多。
什么叫字节流?我举个例子。你通过TCP发送了两次数据:第一次发"Hello",第二次发"World"。在应用层看来,这是两个独立的消息。但在TCP眼里,它根本不关心你的消息边界——它只看到一串连续的字节:H-e-l-l-o-W-o-r-l-d。
为什么会这样?因为TCP的设计目标就是可靠、有序地传输字节序列,而不是保护应用层的消息边界。它就像一个水管,你往里面倒水,它只管把水送到另一端,至于水是一杯一杯倒的还是连续倒的,它不管。
核心要点:TCP不保留应用层消息的边界。发送方多次write()的数据,接收方可能一次read()就全读走了;反过来,发送方一次write()的数据,接收方也可能分多次read()才能读完。这就是粘包和拆包问题的根源。
我在项目中遇到过这样一个场景:一个实时监控系统,客户端每100ms发送一个状态包。开发同学发现服务端收到的数据经常"粘"在一起,解析出来全是乱码。他以为是网络问题,折腾了两天。我过去一看,其实就是典型的TCP粘包——应用层没有做消息边界处理。
TCP报文段结构
TCP把字节流切分成一个个报文段(Segment)来发送。每个报文段长什么样?我带你过一遍关键字段。
| 字段 | 长度 | 作用 |
|---|---|---|
| 源端口 / 目的端口 | 各16位 | 标识通信的应用程序 |
| 序列号(SEQ) | 32位 | 本报文段数据的第一个字节的序号 |
| 确认号(ACK) | 32位 | 期望收到的下一个字节的序号 |
| 数据偏移 | 4位 | TCP首部长度(单位:32位字) |
| 标志位 | 6位 | SYN、ACK、FIN、RST、PSH、URG |
| 窗口大小 | 16位 | 接收方还能接收多少字节 |
| 校验和 | 16位 | 检测报文段是否损坏 |
| 紧急指针 | 16位 | 紧急数据的结束位置 |
这里我特别想强调序列号和确认号。这两个字段是TCP实现可靠传输的核心。序列号标记了每个字节的位置,接收方用确认号告诉发送方"我已经收到了哪些字节"。你想想看,如果没有这两个字段,TCP怎么保证数据不丢、不乱序?
嗯,这里要注意:序列号不是从0或1开始的。TCP在建立连接时,双方会随机生成一个初始序列号(ISN)。这样做是为了防止旧连接的数据包干扰新连接。我在做安全审计时,就见过有人利用ISN预测来攻击TCP连接——当然,那是另一个话题了。
MSS与MTU的关系
这两个概念经常被混淆,我简单说清楚。
MTU(最大传输单元)是网络层(IP层)能承载的最大数据包大小。以太网的MTU通常是1500字节。这个值是由物理网络决定的,你改不了。
MSS(最大报文段长度)是TCP层能发送的最大数据载荷大小。它等于MTU减去IP首部(20字节)再减去TCP首部(20字节)。所以以太网环境下,MSS = 1500 - 20 - 20 = 1460字节。
我的建议:在抓包分析时,多留意MSS的值。如果发现MSS异常小(比如小于500),可能是中间设备做了限制,或者路径MTU探测出了问题。我曾经排查过一个性能问题,最后发现是防火墙把MSS改成了512,导致TCP吞吐量上不去。
两者的关系可以用一句话概括:MSS是TCP层的数据上限,MTU是IP层的包上限。TCP在发送数据时,会确保每个报文段的数据部分不超过MSS,这样加上IP首部和TCP首部后,整个IP包就不会超过MTU。
避坑指南:我曾经遇到过一个案例,开发同学在代码里设置了TCP_NODELAY选项,然后每次发送1字节的数据。结果每个报文段只有1字节数据,加上40字节的首部,有效载荷率只有2.5%。网络带宽被白白浪费了。这就是典型的"小包问题"——虽然不违反MSS,但效率极低。
下面这张图可以帮你理清TCP报文段、IP包和以太网帧之间的关系:
从这张图可以看得很清楚:应用层数据经过TCP封装后,加上IP首部,再封装到以太网帧里。每一层都有自己的大小限制。TCP通过MSS来约束数据大小,目的就是避免IP层做分片——因为IP分片会降低性能,而且一旦某个分片丢失,整个数据包都要重传。
我个人习惯在抓包时先看一眼SYN包里的MSS选项。如果两端MSS不一致,TCP会取较小值。这个值直接影响后续数据传输的效率和粘包行为。举个例子:如果MSS是1460,你一次发送3000字节的数据,TCP会把它拆成两个报文段发送——第一个1460字节,第二个1540字节(其中1460字节数据+80字节可能留给下一个报文段)。这种拆分本身就是一种"拆包"行为。