Python Socket多人游戏客户端发送start消息后全员断连问题排查
问题描述
代码运行流程:先启动服务端,再运行两个客户端,二者均成功连接服务端,各自向服务端发送连接成功消息并收到服务端回传的相同内容,此时第二个客户端处于等待接收服务端消息的状态。当房主点击开始游戏按钮时,会向服务端发送start消息,服务端收到后将消息回传,此时房主客户端发送并接收start消息后,两个客户端均与服务端失去连接。
相关代码
服务端代码
import _thread import threading import socket server = '' port = 9999 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.bind((server, port)) except socket.error as e: str(e) print("Waiting for a connection, Server Started") s.listen(2) clients = set() clients_lock = threading.Lock() def threaded_client(client, address): print("Accepted connection from: ", address) with clients_lock: clients.add(client) try: while True: data = client.recv(1024) print("Received: ", data.decode()) if not data: break else: print("Sending: ", data.decode()) with clients_lock: for c in clients: c.sendall(data) except: print("Lost Connection") conn.close() players = 0 while players != 2: conn, addr = s.accept() players += 1 _thread.start_new(threaded_client, (conn, addr))
客户端Network类代码
import socket class Network: def __init__(self): self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server = socket.gethostname() self.port = 9999 self.addr = (self.server, self.port) def connect(self): try: self.client.connect(self.addr) self.client.send(str.encode("Connected")) return self.client.recv(2048).decode() except: pass def send(self, data): try: data = data.decode() self.client.send(str.encode(data)) return self.client.recv(2048).decode() except socket.error as e: print(e) def recv(self): return self.client.recv(2048).decode()
房主创建房间逻辑代码
class HostGamePage: def clearFrame(self): # destroy all widgets from frame for widget in self.frame.winfo_children(): widget.destroy() def __init__(self, root, frame): n = Network() n.connect() self.root = root self.frame = frame self.start_game_button = Button(self.frame, padx=60, pady=40, text="Start Game", command=lambda:[HostGamePage.clearFrame(self), QuestionBoard(self.root, self.frame, 0), n.send(str.encode("start"))]) #Define Labels self.host_label = Label(self.frame,padx=40,pady=20,text="Waiting for Player 2") self.lobby_code_label = Label(self.frame,padx=40,pady=20,text="") #Configure Labels self.host_label.config(font = ('Heletica bold', 32)) #Putting it all together self.host_label.grid(row=0,column=0) self.start_game_button.grid(row=2,column=0)
客户端加入房间逻辑代码
# -*- coding: utf-8 -*- """ Created on Fri Nov 26 16:02:51 2021 @author: marquesk, gonzaleza15 """ from tkinter import * from network import Network from recieve1 import Recieve class JoinPage: def __init__(self, root, frame): n = Network() n.connect() self.root = root self.frame = frame self.joinLabel = Label(self.frame, text = "Looking for game to join...") self.joinLabel.grid(row=0, column=0) Recieve(n.recv(), self.root, self.frame)
游戏面板页面代码
# -*- coding: utf-8 -*- """ Created on Wed Nov 24 09:59:38 2021 @author: marquesk, gonzaleza15 """ from question_page import * from finalResults import * from Question_Object_List import * from itertools import groupby from recieve2 import Recieve from network import Network # This is the Question Board Class. Within it, the class creates the entire gameboard that the players will see # for their entire time playing the game. There are numerous functions, labels, and buttons that make up it's # entire screen. See Comments for Details. class QuestionBoard: # This function is used to check if all button widgets on the screen have been destroyed. # It's generic and requires an iterable variable. def all_equal(iterable): g = groupby(iterable) return next(g, True) and not next(g, False) # Every widget in tkinter has a function called winfo_exists(). This function returns 1 if a widget # is on screen and 0 if it's destroyed. This function type casts these values of 1's and 0's to # create a boolean array that can be used to determine if all buttons on the screen have been destroyed. def makeBoolArray(self, lst): self.finalArray = [] for i in range(len(lst)): self.question = lst[i] self.truthValue = bool(self.question.winfo_exists()) self.finalArray.append(self.truthValue) return self.finalArray # This function takes the boolean array of whether a widger or not exists and checks to see if all buttons on screen # have been destroyed. If so, the gameboard GUI is completely cleared and the final results page is displayed. def checkClear(self, questionArray): booleanArray = QuestionBoard.makeBoolArray(self, questionArray) if QuestionBoard.all_equal(booleanArray): QuestionBoard.clearFrame(self) LeaderBoard(self.root, self.frame) # This function clears the entire frame to make room for other GUI pages. def clearFrame(self): for widgets in self.frame.winfo_children(): widgets.destroy() # This is the constructor for the question board class. Within it, everything that the board displays is created. def __init__(self, root, frame, player): n = Network() self.root = root self.frame = frame # Create Labels self.label = Label(self.frame, text="Networks") self.label2 = Label(self.frame, text="Data Protocols") self.label3 = Label(self.frame, text="Commands") self.label4 = Label(self.frame, text="Trivia") # Put labels on the grid self.label.grid(column=0, row=0) self.label2.grid(column=1, row=0) self.label3.grid(column=2, row=0) self.label4.grid(column=3, row=0) # All the statements underneath this comment create and intialize all buttons seen on screen. # Every command with in them opens a Question Page object with the root and a Question Object. # It then destroys the button and checks if it was the final button to be destroyed. # The .grid functions below each button places these buttons on the grid structure of the GUI page. self.network1 = Button(self.frame, text='100', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, networking1), self.network1.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.network1.grid(column=0, row=1, padx=20, pady=10) self.network2 = Button(self.frame, text='200', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, networking2), self.network2.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.network2.grid(column=0, row=2, padx=20, pady=10) self.data1 = Button(self.frame, text='100', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, data1), self.data1.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.data1.grid(column=1, row=1, padx=20, pady=10) self.data2 = Button(self.frame, text='200', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, data2), self.data2.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.data2.grid(column=1, row=2, padx=20, pady=10) self.command1 = Button(self.frame, text='100', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, commands1), self.command1.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.command1.grid(column=2, row=1, padx=20, pady=10) self.command2 = Button(self.frame, text='200', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, commands2), self.command2.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.command2.grid(column=2, row=2, padx=20, pady=10) self.trivia1 = Button(self.frame, text='100', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, trivia1), self.trivia1.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.trivia1.grid(column=3, row=1, padx=20, pady=10) self.trivia2 = Button(self.frame, text='200', width=15, height=5, command=lambda: [QuestionPage.openQuestionWindow(self, root, trivia2), self.trivia2.destroy(), self.checkClear(self.questionList), Recieve(n.send(str.encode(networking1)), root, networking1)]) self.trivia2.grid(column=3, row=2, padx=20, pady=10) # Below this comment is a list variable. It creates a list of all the buttons created above. # It had to be located inside the constructor.so that it may be passed to the "checkClear" function # so that it can create a Boolean array so it can then be evaluated # if all buttons are destroyed, which will display the leaderboard and end the game. self.questionList = [self.network1, self.network2, self.data1, self.data2, self.command1, self.command2, self.trivia1, self.trivia2] if player == 1: n.recv() print(networking2)
消息接收处理代码
from question_board import * class Recieve: def __init__(self, data, root, frame): self.data = data self.root = root self.frame = frame if self.data == "start": for widgets in self.frame.winfo_children(): widgets.destroy() QuestionBoard(self.root, self.frame, 1)
问题原因
- 核心问题:客户端每个页面都会新建
Network实例,也就是新建独立的socket连接,但服务端只允许最多2个连接,且满2个连接后就停止监听新连接,后续新建的连接无法接入。同时旧的连接实例因为是页面构造函数的局部变量,页面跳转后没有引用会被自动回收,导致原有连接被主动关闭。 - 服务端代码错误:线程处理函数异常时关闭的是全局的
conn变量(服务端最新接收的连接),而不是当前线程对应的客户端连接,会误关闭正常连接。同时连接断开后没有从客户端集合clients中移除失效连接,后续广播消息时会触发异常。
修复方案
- 客户端复用同一个socket连接:将
Network实例改为类的实例属性而非局部变量,页面跳转时传递该实例,或者将Network封装为单例类,保证客户端全程只建立一个连接。 - 修复服务端线程处理逻辑:
def threaded_client(client, address): print("Accepted connection from: ", address) with clients_lock: clients.add(client) try: while True: data = client.recv(1024) print("Received: ", data.decode()) if not data: break else: print("Sending: ", data.decode()) with clients_lock: for c in list(clients): # 转成列表避免遍历过程中集合变更 try: c.sendall(data) except: # 单个客户端发送失败直接移除 c.close() clients.remove(c) except: print("Lost Connection from", address) finally: # 最终统一清理连接 with clients_lock: if client in clients: clients.remove(client) client.close()
- 调整页面跳转逻辑,避免局部的
Network实例被提前回收。
内容的提问来源于stack exchange,提问作者Aidan Gonzalez
相关产品推荐
相关产品推荐

