4. 模拟粘包现象:连续发送小数据包
粘包,说白了就是接收端一次读到了多个本该分开的数据包。我刚开始调网络协议栈时,第一次遇到这现象还以为是网卡坏了——明明发送端分了三次 send,接收端却只收到一大块数据。嗯,今天我们就亲手复现这个场景,看看它到底怎么发生的。
4.1 为什么小数据包会粘在一起?
TCP 是流式协议,它不关心你的应用层边界。你调用 send() 发送的数据,会被 TCP 协议栈放进发送缓冲区,然后按自己的节奏打包成报文段发出去。接收端同样是从缓冲区里读数据,它读到的可能是一次 send 的内容,也可能是多次 send 的合并。
我个人的理解是:TCP 只保证字节顺序和可靠性,不保证消息边界。你想想看,如果发送端连续发了三个 10 字节的小包,而接收端一次 recv 了 30 字节,这就是典型的粘包。
- Nagle 算法:把小包合并成大包再发送,提高网络利用率
- 接收端缓冲区积累:应用层来不及读取,数据在缓冲区里堆在一起
- TCP 报文段大小限制:MSS 限制了单次传输的最大长度,但小包会被合并
4.2 代码演示:发送端连续发送小数据包
我们先写一个发送端,连续发送 5 个小数据包,每个包只有 10 字节。注意,我这里故意不添加任何边界标记,就是为了让你看到粘包的效果。
# sender.py — 连续发送小数据包
import socket
import time
def main():
host = '127.0.0.1'
port = 8888
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
messages = [
b'Hello_0001',
b'Hello_0002',
b'Hello_0003',
b'Hello_0004',
b'Hello_0005'
]
print('[发送端] 开始发送 5 个小数据包...')
for i, msg in enumerate(messages, 1):
sock.send(msg)
print(f'[发送端] 已发送第 {i} 个包: {msg}')
time.sleep(0.01) # 极短间隔,模拟连续发送
print('[发送端] 发送完毕')
sock.close()
if __name__ == '__main__':
main()
这里我用了 10 毫秒的间隔。在实际项目中,如果间隔更短甚至没有间隔,粘包的概率会更高。我曾经在一个 IoT 项目里见过,传感器每 50 毫秒上报一次状态,结果接收端经常一次读到 3-4 条数据,就是因为发送间隔太短了。
4.3 代码演示:接收端观察数据合并
接收端我们故意只做一次 recv,看看能读到多少数据。
# receiver.py — 单次接收,观察粘包
import socket
def main():
host = '127.0.0.1'
port = 8888
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
sock.listen(1)
print('[接收端] 等待连接...')
conn, addr = sock.accept()
print(f'[接收端] 客户端已连接: {addr}')
# 只做一次 recv,看看能读到多少
data = conn.recv(1024)
print(f'[接收端] 一次 recv 读到的数据: {data}')
print(f'[接收端] 数据长度: {len(data)} 字节')
conn.close()
sock.close()
if __name__ == '__main__':
main()
4.4 运行结果与日志分析
先启动接收端,再启动发送端。你猜结果会怎样?
发送端日志:
[发送端] 开始发送 5 个小数据包...
[发送端] 已发送第 1 个包: b'Hello_0001'
[发送端] 已发送第 2 个包: b'Hello_0002'
[发送端] 已发送第 3 个包: b'Hello_0003'
[发送端] 已发送第 4 个包: b'Hello_0004'
[发送端] 已发送第 5 个包: b'Hello_0005'
[发送端] 发送完毕
接收端日志:
[接收端] 等待连接...
[接收端] 客户端已连接: ('127.0.0.1', 54321)
[接收端] 一次 recv 读到的数据: b'Hello_0001Hello_0002Hello_0003Hello_0004Hello_0005'
[接收端] 数据长度: 50 字节
看到了吗?接收端一次 recv 读到了 50 字节,正好是 5 个 10 字节包的合并。这就是粘包!
4.5 粘包发生的完整流程
为了让你更直观地理解整个过程,我画了一张流程图。它展示了从发送端 send 到接收端 recv 之间,数据是怎么被合并的。
4.6 粘包与拆包的关系
粘包和拆包其实是同一个问题的两面。我简单列个对比表,帮你理清思路。
| 现象 | 定义 | 典型场景 | 影响 |
|---|---|---|---|
| 粘包 | 多个数据包合并成一个 | 连续发送小包,间隔短 | 接收端无法区分消息边界 |
| 拆包 | 一个数据包被拆成多次接收 | 数据包大于 MSS 或缓冲区 | 接收端需要拼接完整消息 |
4.7 如何判断是否发生了粘包?
在实际调试中,我一般通过以下几个特征来判断:
- 接收端一次 recv 的数据长度大于预期 — 比如你期望每次收到 10 字节,结果收到了 30 字节
- 数据内容包含多个消息头 — 如果你有自定义协议头,解析时发现连续出现多个头
- 日志时间戳异常 — 接收端两次 recv 的时间间隔远大于发送端两次 send 的间隔
嗯,这里要注意:不要一看到数据合并就认为是粘包。有时候是应用层逻辑写错了,比如 recv 循环没处理好。先确认发送端确实分了多次 send,再往粘包方向排查。
4.8 小结
今天我们亲手复现了粘包现象。核心就一句话:TCP 不保边界,小包连续发,接收端大概率会合并。代码很简单,但背后的原理很重要。你想想看,如果接收端依赖「一次 recv 就是一个完整消息」来写业务逻辑,那粘包就会导致解析错乱。