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

Python Socket多人CLI游戏聊天系统仅接收首条消息问题求助

客户端Socket接收线程仅能获取第一条消息的问题排查与修复

我正在开发一款多人CLI游戏的Python Socket聊天系统,遇到以下问题:客户端的receive_from_server线程持续运行,但仅能接收服务器的第一条响应,后续消息无法获取。相关服务器与客户端代码如下:

服务器代码

import socket
import json
from _thread import start_new_thread

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

ipaddr = "127.0.0.1"
port = 8888

try:
    server.bind((ipaddr, port))
except socket.error as er:
    print(str(er))
server.listen(100)

def send_message(un, sn, msg, t):
    return json.dumps({
            "status": 200,
            "type": t,
            "data": {
              "username": un,
              "servername": sn,
              "message": msg
            }
          })

def broadcast(message, room, connection, t):
    if t == 0:
        try:
            connection.send(message.encode())
        except:
            connection.close()
            remove(connection)
    elif t == 1:
        for client in list_of_clients.keys():      
            try:
                print("just send", message)
                client.send(message.encode())
            except:
                client.close()
                remove(client)

def clientthread(conn):
    while True:
        try:
            msg = json.loads(conn.recv(4096).decode())
            if msg["type"] == "sendmessage":
                res = send_message(msg["data"]["username"],msg["data"]["servername"],msg["data"]["message"],msg["type"])
                broadcast(res, msg["data"]["servername"], conn, 1)
            else:
                remove(conn)
        except Exception as e:
            conn.close()
            remove(conn)
            return

while True:
    conn, addr = server.accept()
    start_new_thread(clientthread, (conn,))

conn.close()
server.close()

客户端代码

import socket
import json
import threading

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.connect(("127.0.0.1", 8888))

started = 0
members = []
data = {}

def game_lobby_template(sn):
    # 假设此处是游戏大厅模板渲染逻辑
    pass

def send_message(sn, msg):
    server.send(json.dumps({
        "type": "sendmessage",
        "data": {
            "servername": sn,
            "username": "test_user",
            "message": msg
        }
    }).encode())

def leave_game(sn):
    # 假设此处是离开房间逻辑
    pass

def start_game(sn, flag):
    # 假设此处是开始游戏逻辑
    pass

def reload_template(res, flag):
    # 假设此处是模板重载逻辑
    pass

def game_lobby(sn):
    game_lobby_template(sn)
    while started != 1:
        msg = ""
        while msg == "":
            msg = input()
            if msg != "leave":
                send_message(sn, msg)
            else:
                leave_game(sn)
    start_game(sn, 1)

def receive_from_server():
    global server,members,data
    while True:
        res = ""
        res = json.loads((server.recv(4096)).decode())
        if res["type"] == "sendmessage":
            if res["status"] == 200:
                reload_template(res,0)
                else:
                    reload_template(res,0)

receive_thread = threading.Thread(target=receive_from_server)
receive_thread.start()

# 假设调用游戏大厅函数
game_lobby("test_room")

核心问题分析

你的问题主要来自TCP流式传输的特性处理不当,同时存在代码语法错误和线程异常未捕获的问题,具体如下:

  • TCP流式数据的不完整接收:TCP是基于流的协议,recv(4096)不一定能一次性拿到完整的JSON数据包。如果某次recv只拿到部分JSON内容,json.loads会抛出异常,导致接收线程卡住或退出,无法处理后续消息。
  • 客户端代码语法错误:receive_from_server函数中else的缩进错误,会触发语法异常,线程启动后直接崩溃(你可能没注意到控制台的报错)。
  • 服务器端未处理客户端断开的边界情况:list_of_clients和remove函数未定义,广播时可能出现KeyError,导致客户端连接被错误关闭。
  • 接收线程未捕获异常:客户端接收线程没有异常捕获逻辑,一旦出现错误就会直接退出,表面上看起来线程还在运行实际已经终止。

具体修复方案

1. 修复客户端接收线程的语法错误与异常处理

修改receive_from_server函数,修复缩进并添加异常捕获,同时用缓冲区处理不完整的JSON数据:

def receive_from_server():
    global server, members, data
    buffer = ""  # 用缓冲区存储不完整的接收数据
    while True:
        try:
            chunk = server.recv(4096).decode()
            if not chunk:  # 服务器断开连接
                print("与服务器断开连接")
                break
            buffer += chunk
            # 尝试解析JSON,直到拿到完整数据
            while True:
                try:
                    res = json.loads(buffer)
                    # 处理消息
                    if res["type"] == "sendmessage":
                        reload_template(res, 0)
                    buffer = ""  # 清空缓冲区
                    break
                except json.JSONDecodeError:
                    # 缓冲区数据不完整,继续接收下一段
                    break
        except Exception as e:
            print(f"接收消息出错: {e}")
            break

2. 修复服务器端的广播与客户端管理逻辑

首先补充list_of_clients和remove函数的定义,确保客户端连接管理正常:

list_of_clients = {}  # 存储客户端连接,键为conn,值为客户端信息

def remove(connection):
    if connection in list_of_clients:
        del list_of_clients[connection]

然后修改broadcast函数,确保只向目标房间的客户端发送消息(原代码是给所有客户端发,不符合房间逻辑):

def broadcast(message, room, connection, t):
    if t == 0:
        try:
            connection.send(message.encode())
        except:
            connection.close()
            remove(connection)
    elif t == 1:
        # 只向同一房间的客户端广播
        for client, info in list_of_clients.items():
            if info["servername"] == room and client != connection:
                try:
                    print("just send", message)
                    client.send(message.encode())
                except:
                    client.close()
                    remove(client)

同时修改clientthread函数,在客户端连接时记录房间信息,并处理recv的异常情况:

def clientthread(conn):
    try:
        # 先接收客户端的房间信息(假设客户端连接后先发送房间信息)
        init_msg = json.loads(conn.recv(4096).decode())
        if init_msg["type"] == "joinroom":
            list_of_clients[conn] = {
                "servername": init_msg["data"]["servername"],
                "username": init_msg["data"]["username"]
            }
        else:
            remove(conn)
            return
    except Exception as e:
        conn.close()
        remove(conn)
        return

    while True:
        try:
            msg = conn.recv(4096).decode()
            if not msg:  # 客户端断开
                remove(conn)
                break
            msg_data = json.loads(msg)
            if msg_data["type"] == "sendmessage":
                res = send_message(
                    msg_data["data"]["username"],
                    msg_data["data"]["servername"],
                    msg_data["data"]["message"],
                    msg_data["type"]
                )
                broadcast(res, msg_data["data"]["servername"], conn, 1)
            else:
                remove(conn)
                break
        except json.JSONDecodeError:
            # 接收数据不完整,继续等待
            continue
        except Exception as e:
            conn.close()
            remove(conn)
            break

3. 确保线程安全的共享变量访问

客户端的started变量如果是多线程共享的,需要用threading.Lock来保护,避免竞态条件:

import threading

started = 0
started_lock = threading.Lock()

def game_lobby(sn):
    global started
    game_lobby_template(sn)
    while True:
        with started_lock:
            if started == 1:
                break
        msg = input().strip()
        if not msg:
            continue
        if msg == "leave":
            leave_game(sn)
            break
        send_message(sn, msg)
    start_game(sn, 1)

关键注意事项

  • TCP是流式协议,永远不要假设recv能一次性拿到完整的应用层数据包,必须用缓冲区拼接数据,直到能解析出完整的JSON。
  • 所有线程中的异常必须捕获并处理,否则线程会静默退出,难以排查问题。
  • 多线程共享变量必须加锁,避免出现竞态条件导致的逻辑错误。

内容的提问来源于stack exchange,提问作者Tr4pper Rapper

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 22:04:53