Python Socket服务端pickle.loads(recv())阻塞问题求助
问题排查:TCP粘包导致服务端recv阻塞
问题现象
客户端发起注册请求并输入用户名后,服务端卡在代码行username = pickle.loads(clientSocket.recv(4028)).text处,无任何报错,程序停滞。
原因分析
核心问题是TCP粘包:
- 客户端连续调用两次
clientSocket.sendall(pickle.dumps(...)),TCP作为流式协议会将两次发送的字节流合并成一个数据包发送,不存在天然的数据包边界。 - 服务端
clientHandler里的第一次recv会一次性读取到两个Message对象的序列化字节流,pickle.loads只会解析第一个完整的Message(即"SignUp*******"那条),剩余的第二个Message字节留在socket缓冲区中。 - 进入
signUp函数后,服务端再次调用recv,误以为要接收新的数据,但客户端已经发送完毕,此时服务端会一直等待,导致阻塞。
解决方案
通过先发送数据长度,再发送序列化内容的方式,明确每个数据包的边界,彻底避免粘包问题。具体实现:
- 封装发送函数:先将序列化后的字节长度转为固定长度的字节(用
struct打包为4字节大端整数),发送长度后再发送序列化数据。 - 封装接收函数:先接收固定长度的字节,解析出数据长度,再循环接收对应长度的字节,确保拿到完整的单个序列化对象。
修改后的代码示例
通用工具函数(两端代码都需添加)
import struct import pickle def send_pickle(sock, obj): data = pickle.dumps(obj) # 用大端字节序打包数据长度为4字节整数 length_packed = struct.pack('!I', len(data)) sock.sendall(length_packed) sock.sendall(data) def recv_pickle(sock): # 先接收长度字节 length_data = sock.recv(4) if not length_data: return None # 客户端断开连接 data_length = struct.unpack('!I', length_data)[0] # 循环接收完整数据 received_data = b'' while len(received_data) < data_length: chunk = sock.recv(min(data_length - len(received_data), 4096)) if not chunk: return None received_data += chunk return pickle.loads(received_data)
客户端修改(signUp函数替换原收发逻辑)
def signUp(): while True: global myAccount userName = input("Please enter a user name you like or q to quit\n").strip() messageToSend = message.Message(userName) if userName == "q": break # 替换原sendall为封装的send_pickle send_pickle(clientSocket, message.Message("SignUp*******")) send_pickle(clientSocket, messageToSend) # 替换原recv为封装的recv_pickle created_acc_msg = recv_pickle(clientSocket) if not created_acc_msg: print("Connection lost") break createdAcc = created_acc_msg.account if createdAcc.status == account.Status.OFFLINE: print("That username is taken\n") else: myAccount = createdAcc print(f"Account created your username is:\n{myAccount.accUsername}") break
服务端修改(clientHandler和signUp函数替换原收发逻辑)
def signUp(clientSocket): print("Top") # debug # 替换原recv为封装的recv_pickle username_msg = recv_pickle(clientSocket) if not username_msg: return username = username_msg.text print("Buttom") # debug if not alreadyAUser(username): user = account.Account() user.accUsername = username user.status = account.Status.ONLINE with userBaseLock: userBase.append(user) send_pickle(clientSocket, message.Message().withAccount(user)) else: send_pickle(clientSocket, message.Message().withAccount(account.Account())) def clientHandler(clientSocket): while True: # 替换原recv为封装的recv_pickle messageSent = recv_pickle(clientSocket) if not messageSent: break # 客户端断开连接,退出循环 message = messageSent.text # Sign Up if message == requests[0]: signUp(clientSocket)
额外注意事项
- 两端必须统一使用
struct的字节序(示例用!I大端序,跨平台兼容性更好),否则会出现长度解析错误。 - 接收数据时必须循环读取,避免单次
recv无法获取完整数据(尤其是序列化后的大对象)。 - 若客户端异常断开,
recv_pickle会返回None,服务端需处理该情况避免无效循环。
内容的提问来源于stack exchange,提问作者somkinggamingwolf
相关产品推荐
相关产品推荐

