使用sockets与pickle遇远程客户端数据截断问题求助
这个问题我之前踩过类似的坑,核心原因其实是TCP协议的流式特性在远程网络环境下的表现和本地完全不同,哪怕你确认消息总大小没超过recv的缓冲区大小,也会出现这个问题。
为什么本地没问题,远程会报错?
本地局域网延迟极低、网络链路稳定,服务器发送的pickle数据几乎会一次性到达客户端的网络缓冲区,所以你调用sock.recv(16384)能直接拿到完整的序列化数据,pickle.loads自然不会报错。
但远程网络(比如跨机房、公网)存在延迟、路由转发甚至偶尔的微丢包,TCP会把完整的消息拆分成多个数据段分批发送。这时候客户端一次recv可能只拿到了部分数据——哪怕总大小远小于16384字节,剩下的还在传输路上或者客户端的缓冲区里没被读取。用不完整的字节数据去做pickle.loads,必然会触发_pickle.UnpicklingError: pickle data was truncated。
解决办法:保证接收完整的序列化数据
TCP是流式协议,本身不关心“消息边界”,所以我们需要自己在应用层定义消息的边界,确保客户端能拿到完整的pickle数据再解包。最可靠的方案是先发送数据长度,再发送实际数据,具体实现如下:
服务器端代码修改
先把序列化后的字节数据长度用固定格式打包发送,再发送实际数据:
import struct import pickle # 序列化数据 tick_dict_bytes = pickle.dumps(tick_dict) # 用struct打包数据长度(!I表示大端序的无符号4字节整数,支持最大2^32-1字节的数据) length_packed = struct.pack('!I', len(tick_dict_bytes)) # 先发送长度,再发送实际数据(用sendall保证所有数据都发出去) sock.sendall(length_packed) sock.sendall(tick_dict_bytes)
客户端代码修改
先接收固定长度的长度信息,再循环接收直到拿到完整的序列化数据:
import struct import pickle # 第一步:接收数据长度 length_buffer = b'' # 循环接收直到拿到完整的4字节长度数据 while len(length_buffer) < 4: chunk = sock.recv(4 - len(length_buffer)) if not chunk: # 如果chunk为空,说明连接已断开 raise ConnectionError("与服务器的连接已意外关闭") length_buffer += chunk # 解析出实际数据的长度 data_length = struct.unpack('!I', length_buffer)[0] # 第二步:接收完整的序列化数据 data_buffer = b'' while len(data_buffer) < data_length: # 每次接收的字节数不超过剩余需要的长度,避免浪费缓冲区 chunk_size = min(16384, data_length - len(data_buffer)) chunk = sock.recv(chunk_size) if not chunk: raise ConnectionError("与服务器的连接已意外关闭") data_buffer += chunk # 现在可以安全地解包了 tick_dict = pickle.loads(data_buffer) data_handler.update_dataframes(tick_dict)
其他备选方案(不推荐)
如果你不想用长度前缀,也可以用特殊分隔符标记消息结束(比如b'---END---'),但这个方案有个致命问题:如果你的序列化数据本身包含这个分隔符,会导致客户端提前终止接收,同样触发错误。所以除非你能100%保证数据里不会出现分隔符,否则不建议用。
总结一下:本地的“偶然成功”是因为网络环境太理想,远程网络才是TCP流式特性的真实体现——必须在应用层处理消息边界,才能避免pickle数据截断的问题。
内容的提问来源于stack exchange,提问作者jatorna

