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
相关产品推荐
相关产品推荐

