2. TCP协议基础回顾:TCP面向字节流的特点、TCP缓冲区机制、MSS与MTU的概念
聊粘包拆包之前,我觉得有必要先带大家回顾一下TCP的几个核心机制。说实话,很多人搞不懂粘包,根源就在于对TCP的"脾气"没摸透。我自己带团队时,发现新人最容易踩的坑,就是拿UDP的思维去理解TCP。
2.1 TCP是面向字节流的,不是消息流
这个点太关键了。我再说一遍——TCP是面向字节流的协议。
什么意思呢?
你调用 send() 发送了 "Hello" 和 "World" 两条消息,TCP 底层并不会帮你保留这个"消息边界"。它只管把这两段数据当成一连串的字节,一股脑塞进发送缓冲区。接收方调用 recv() 时,可能一次读到 "HelloWorld",也可能分两次读到 "Hel" 和 "loWorld"。
说白了,TCP 不关心你发的是"一条消息"还是"两条消息"。它只认字节,不认消息。
核心结论:TCP 保证的是字节的有序性和不丢失,但不保证消息边界。
我记得刚入行那会儿,写了一个简单的聊天程序。客户端发了两条消息,服务端 recv 一次全收走了。我当时还以为是bug,查了半天文档才明白——这就是TCP的"尿性"。
2.2 TCP缓冲区机制
TCP 为什么会有粘包?缓冲区是罪魁祸首之一。
每个 TCP socket 都有两个内核缓冲区:
- 发送缓冲区:你调用 send() 时,数据先拷贝到这里。内核再择机通过网卡发出去。
- 接收缓冲区:对端发来的数据,先存到这里。你调用 recv() 时,从这里取。
这两个缓冲区的大小,默认一般是 64KB 左右(不同系统有差异)。你可以通过 setsockopt() 调整。
为什么会粘包?我画个图你就明白了。
嗯,这里要注意:粘包不是TCP的bug,是特性。TCP为了提高传输效率,会把多个小包合并成一个大的TCP段发送。你想想看,如果每发一个字节都走一次网络往返,那效率得多低?
避坑指南:我曾经在一个物联网项目里,设备每隔100ms上报一次状态数据,每条才几十字节。结果服务端收到的数据全粘在一起了。后来加了长度头,才把问题解决。后面章节我会详细讲这个方案。
2.3 MSS 与 MTU 的概念
这两个概念经常被搞混。我简单说一下。
MTU(最大传输单元)
MTU 是网络层(IP层)的概念。它指的是一个网络接口能够传输的最大数据包大小。以太网的 MTU 通常是 1500 字节。
如果 IP 层要发送的数据包超过 MTU,就会发生分片。分片会增加接收端重组开销,也容易丢包。所以 TCP 会尽量避免 IP 分片。
MSS(最大报文段长度)
MSS 是传输层(TCP层)的概念。它指的是 TCP 数据段中数据部分的最大长度。
计算公式很简单:
MSS = MTU - IP头部(20字节) - TCP头部(20字节)
= 1500 - 20 - 20
= 1460 字节
也就是说,TCP 每次发送的数据段,数据部分最多 1460 字节。超过这个值,就要拆分成多个段发送。
这跟粘包有什么关系?关系大了。
| 概念 | 层级 | 典型值 | 与粘包的关系 |
|---|---|---|---|
| MTU | 网络层(IP) | 1500 字节(以太网) | 超过 MTU 会 IP 分片,增加丢包风险 |
| MSS | 传输层(TCP) | 1460 字节(MTU=1500时) | 超过 MSS 会拆包,接收方可能读到不完整的消息 |
我个人的习惯是,在设计应用层协议时,尽量让单条消息小于 MSS。这样能避免 TCP 层自动拆包,减少接收方拼包的复杂度。
2.4 粘包与拆包的本质
讲到这里,我们可以总结一下了。
粘包:发送方发了多条小消息,接收方一次 recv 读到了多条。原因可能是:
- Nagle 算法合并了小包
- 接收缓冲区积压了多条数据
- 发送方连续 send 速度快于网络传输
拆包:发送方发了一条大消息,接收方分多次 recv 才读完。原因可能是:
- 消息长度超过 MSS,TCP 自动拆分
- 接收缓冲区太小,一次装不下
- 应用层 recv 缓冲区小于消息长度
警告:千万不要依赖 TCP 帮你维护消息边界。我见过有人写代码时,发一条消息就 sleep(0.1) 秒,以为这样就不会粘包。这种写法在局域网可能凑效,一上公网就崩。记住——TCP 不保证消息边界,这是铁律。
好了,TCP 的基础就回顾到这里。下一节我们会正式进入粘包拆包的解决方案,我会从最经典的长度头方案讲起,再到更高级的定长法和分隔符法。每种方案我都会给出完整的 Python 代码示例。