2. 粘包拆包问题演示:用 Python socket 编写一个简单的客户端和服务端,演示粘包现象。

理论说再多,不如动手跑一遍。这一章,我们就来亲手制造一个粘包现场。

我会带着你写一个最简的 TCP 服务端和客户端。代码不长,但能让你亲眼看到:明明发送了两次数据,接收端却只收到一次——这就是粘包。

2.1 先搭个服务端骨架

我个人习惯,写网络程序先从服务端开始。因为服务端是“守株待兔”的那一方,逻辑更直观。

下面这个服务端,监听本地的 8888 端口。收到客户端连接后,它什么都不做,只是把收到的数据原样打印出来。

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen(5)

print('服务端已启动,等待连接...')

conn, addr = server_socket.accept()
print(f'客户端已连接:{addr}')

# 接收数据
data = conn.recv(1024)
print(f'服务端收到:{data}')

# 再收一次
data = conn.recv(1024)
print(f'服务端再次收到:{data}')

conn.close()
server_socket.close()

注意看,我调用了两次 recv(1024)。按常理,如果客户端发送了两次数据,服务端就应该收到两次。但结果会让你意外。

2.2 再写个客户端

客户端更简单。连接服务端后,连续发送两条消息。

import socket
import time

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 8888))

# 发送两条消息
client_socket.send(b'Hello, ')
client_socket.send(b'World!')

# 等一会再关闭,确保服务端能收到
time.sleep(1)
client_socket.close()

这里我故意把 "Hello, " 和 "World!" 分两次发送。你想想看,如果 TCP 是“发一次收一次”,服务端应该分别打印出这两段。但事实呢?

2.3 运行结果:粘包出现了

先启动服务端,再启动客户端。控制台输出如下:

服务端已启动,等待连接...
客户端已连接:('127.0.0.1', 54321)
服务端收到:b'Hello, World!'
服务端再次收到:b''

看到了吗?第一次 recv 就把两条消息全收走了。第二次 recv 只拿到了空数据。

为什么会这样?因为 TCP 是流式协议。它不关心你分了几次 send,只关心字节流。两次发送的数据在传输层被合并成了一个包,接收端一次性全读走了。

核心结论:TCP 粘包的本质,是接收端无法区分消息的边界。你分两次发,它可能一次收;你分一次发,它也可能分两次收(拆包)。

2.4 换个角度:拆包又是怎么回事?

刚才演示的是粘包。那拆包呢?其实原理一样,只是方向相反。

我举个例子。你在客户端一次发送 5000 字节的数据。服务端 recv(1024) 一次最多读 1024 字节。那剩下的 3976 字节怎么办?

答案是:它们留在内核缓冲区里,等下一次 recv 再读。这就叫拆包——一次发送,多次接收。

现象 发送端行为 接收端行为 本质原因
粘包 多次 send 一次 recv 收完 Nagle 算法 / 缓冲区合并
拆包 一次 send 多次 recv 才能收完 TCP 缓冲区大小限制

嗯,这里要注意:粘包和拆包不是 bug,是 TCP 的固有特性。你没法“关闭”它,只能“处理”它。

2.5 用一张图看清整个过程

下面这张 SVG 流程图,展示了客户端两次 send 到服务端两次 recv 的完整路径。你可以看到数据在传输层是如何被合并的。

TCP 粘包产生过程 客户端 send("Hello, ") send("World!") TCP 传输层 Nagle 算法 缓冲区合并 "Hello, World!" 服务端 recv(1024) 收到 "Hello, World!" recv(1024) 收到 b'' 两次 send 在传输层被合并,服务端一次 recv 全部收走,第二次 recv 只能读到空数据

一个小技巧:你可以在客户端两次 send 之间加一个 time.sleep(0.5),再运行看看。有时候延迟足够大,粘包就不会发生。但这只是侥幸,不能依赖。

2.6 避坑指南:我曾经踩过的坑

我曾经在一个物联网项目里,用 TCP 传输传感器数据。设备每 100 毫秒发一次数据,服务端用 recv(1024) 接收。结果呢?服务端经常一次收到好几条数据,解析出来全是乱码。

当时我第一反应是“数据丢了”,查了半天网卡、抓包、看日志。最后才发现,不是丢了,是粘了——多条数据粘在一起,解析逻辑没处理好。

从那以后,我养成了一个习惯:只要用 TCP,就必须考虑粘包拆包。不管协议多简单,都得加边界标记。

警告:千万不要在 send 之后立刻 close 连接。TCP 的 close 会触发 FIN 包,可能导致数据还没发完就被截断。我见过不少新手因为这个丢数据。

2.7 小结

这一章,我们用不到 30 行代码,亲手复现了粘包现象。你看到了:

  • 两次 send 被合并成一次 recv 收走
  • 第二次 recv 只能读到空数据
  • 粘包和拆包是 TCP 的固有特性,不是 bug