You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,误以为要接收新的数据,但客户端已经发送完毕,此时服务端会一直等待,导致阻塞。

解决方案

通过先发送数据长度,再发送序列化内容的方式,明确每个数据包的边界,彻底避免粘包问题。具体实现:

  1. 封装发送函数:先将序列化后的字节长度转为固定长度的字节(用struct打包为4字节大端整数),发送长度后再发送序列化数据。
  2. 封装接收函数:先接收固定长度的字节,解析出数据长度,再循环接收对应长度的字节,确保拿到完整的单个序列化对象。

修改后的代码示例

通用工具函数(两端代码都需添加)

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 16:44:54