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

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中移除失效连接,后续广播消息时会触发异常。
修复方案
  1. 客户端复用同一个socket连接:将Network实例改为类的实例属性而非局部变量,页面跳转时传递该实例,或者将Network封装为单例类,保证客户端全程只建立一个连接。
  2. 修复服务端线程处理逻辑:
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()
  1. 调整页面跳转逻辑,避免局部的Network实例被提前回收。

内容的提问来源于stack exchange,提问作者Aidan Gonzalez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 21:06:06