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

Python socket+Tkinter程序中Tkinter类启动失败、端口占用报错问题

问题背景

我正尝试使用socket与tkinter开发一款Python在线游戏,项目包含两个代码文件:server.py与client.py。
代码量较大,文末附详细问题说明,请耐心阅读。


现有代码

server.py

import socket
import multiprocessing
from tkinter import *
import pickle

def foo(l):
    return list(map(int, l))

IP = #<my_IPv4>
PORT = 1234

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((IP, PORT))

msg = "[800, 400]"
s.listen(1)
conn, addr = s.accept()

receivedPos = []
changePosCount = 0
print("Line 22")
def ReceivePosition():
    while True:
        receivedPos = pickle.loads(conn.recv(20))
        if not receivedPos:
            break
        else:
            changePosCount = changePosCount +1
print("Line 30")
changingPos = multiprocessing.Process(target=ReceivePosition)
changingPos.start()
print("Line 33")
while changePosCount == 0:
    continue

class root(Tk):
    def __init__(self):
        super(root, self).__init__()

        self.title("1- Tkinter Changing Position")
        self.minsize(500,400)
        print("Line 43")
        self.createLabel()
        print("Line 45")
    def createLabel(self):
        self.label = Label(self, text = str(receivedPos).replace('[', '').replace(']', '') , bg='black', fg='blue', font='Calibri 40 bold')
        print("Line 48")
        self.label.place(x=receivedPos[0], y=receivedPos[1])


print("Line52")
root = root()
root.mainloop()
print("Line 55")

client.py

import socket
import pickle

def foo(l):
    return list(map(int, l))

IP = #<my_IPv4>
PORT = 1234

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))

while True:
    enteredPos = foo(str(input("New Position (Separate x and y with a \",\", eg: 40,60): ")).split(','))
    enteredPos_bytes = pickle.dumps(enteredPos)
    
    s.sendall(enteredPos_bytes)
    print(f"You placed the label at x={enteredPos[0]}, y={enteredPos[1]}")

问题说明:
初始阶段我定义了两个空变量:receivedPos和changePosCount。ReceivePosition函数负责接收客户端传入的位置数据,将其赋值给receivedPos后将changePosCount计数加1,该函数会在客户端输入位置后与程序其余逻辑并行运行。
当客户端输入位置完成、receivedPos被正确赋值且changePosCount大于0后,程序将并行执行两段逻辑:ReceivePosition接收函数、tkinter GUI搭建逻辑。
我在代码中每隔数行添加了行号打印语句,经排查程序在应当创建GUI的位置(对应代码第36行)抛出错误,具体错误信息如下:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\multiprocessing\spawn.py", line 116, in spawn_main
    exitcode = _main(fd, parent_sentinel)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\multiprocessing\spawn.py", line 125, in _main
    prepare(preparation_data)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\multiprocessing\spawn.py", line 236, in prepare
    _fixup_main_from_path(data['init_main_from_path'])
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\multiprocessing\spawn.py", line 287, in _fixup_main_from_path
    main_content = runpy.run_path(main_path,
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\runpy.py", line 288, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\runpy.py", line 97, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.3568.0_x64__qbz5n2kfra8p0\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "d:\Users\Jean Paul\OneDrive\Programming\JP\Socket\Exercises\1- Tkinter Changing Position\server.py", line 13, in <module>
    s.bind((IP, PORT))
OSError: [WinError 10048] Only one usage of each socket address (protocol/network address/port) is normally permitted

我检索相关资料后了解到,该错误通常在目标端口不可用/被占用时触发,但无论我更换为哪个端口(无论端口是否可用),都会抛出相同错误。


问题根因

这个报错和端口是否被外部程序占用没有关系,是Windows下Python多进程的默认运行机制导致的。
Windows系统中multiprocessing默认用spawn模式创建子进程:子进程启动时会从头导入、执行主模块(也就是你的server.py)的所有顶层代码。你把socket创建、绑定端口、监听、接受连接的逻辑全写在了模块顶层,子进程启动时会重复执行绑定端口的操作,相当于同一个端口被绑定两次,自然触发10048端口占用错误。

除了这个直接触发报错的问题,你的代码还有几个逻辑问题,就算绕过端口报错也跑不起来:

  • 多进程之间内存完全隔离,子进程里修改的receivedPos、changePosCount和主进程的同名变量互不影响,主进程里while changePosCount == 0会永远死循环,GUI永远拿不到收到的坐标
  • conn.recv(20)固定读取20字节,pickle序列化后的数据长度不固定,要么读不全数据要么粘包,会直接触发反序列化报错
  • Tkinter的所有UI控件只能在主线程操作,跨进程/跨线程直接更新UI会导致程序卡死、崩溃

修复方案

按以下几点调整即可正常运行:

  • 所有多进程启动、socket初始化、GUI启动的逻辑放到if __name__ == '__main__':代码块下,避免子进程重复执行主模块顶层逻辑
  • 放弃多进程,改用threading实现socket消息接收:线程和主进程共享内存,不需要额外处理跨进程通信,更适配Tkinter的运行模型
  • 修正socket接收逻辑:先发送4字节的消息长度标识,再按长度读取完整消息,避免粘包、断包问题
  • 不要在接收线程里直接操作Tkinter控件,用Tkinter自带的after方法定时刷新UI,保证所有UI操作都在主线程执行
  • 给socket开启地址复用选项,避免程序退出后端口处于TIME_WAIT状态,导致短时间内无法重启服务

修复后的server.py参考

import socket
import threading
from tkinter import *
import pickle

def foo(l):
    return list(map(int, l))

IP = "127.0.0.1" # 替换为你自己的IPv4地址
PORT = 1234

receivedPos = []
conn = None

def receive_position():
    global receivedPos
    while True:
        try:
            # 先读4字节消息长度,再读对应长度的完整数据,避免粘包/断包
            data_len = int.from_bytes(conn.recv(4), byteorder='little')
            data = b''
            while len(data) < data_len:
                chunk = conn.recv(data_len - len(data))
                if not chunk:
                    break
                data += chunk
            if data:
                receivedPos = pickle.loads(data)
        except Exception as e:
            print("连接断开:", e)
            break

class GameRoot(Tk):
    def __init__(self):
        super().__init__()
        self.title("1- Tkinter Changing Position")
        self.minsize(500,400)
        self.label = Label(self, text="等待坐标传入...", bg='black', fg='blue', font='Calibri 40 bold')
        self.label.place(x=0, y=0)
        self.update_pos()

    def update_pos(self):
        if receivedPos and len(receivedPos) ==2:
            self.label.config(text=f"{receivedPos[0]}, {receivedPos[1]}")
            self.label.place(x=receivedPos[0], y=receivedPos[1])
        # 每100毫秒刷新一次UI
        self.after(100, self.update_pos)

if __name__ == '__main__':
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 开启地址复用
    s.bind((IP, PORT))
    s.listen(1)
    print("等待客户端连接...")
    conn, addr = s.accept()
    print(f"客户端已连接:{addr}")
    # 启动守护接收线程
    recv_thread = threading.Thread(target=receive_position, daemon=True)
    recv_thread.start()
    # 启动GUI
    app = GameRoot()
    app.mainloop()
    conn.close()
    s.close()

修复后的client.py参考

import socket
import pickle

def foo(l):
    return list(map(int, l))

IP = "127.0.0.1" # 和服务端IP保持一致
PORT = 1234

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))

while True:
    enteredPos = foo(str(input("New Position (Separate x and y with a \",\", eg: 40,60): ")).split(','))
    enteredPos_bytes = pickle.dumps(enteredPos)
    # 先发4字节消息长度,再发实际内容
    s.sendall(len(enteredPos_bytes).to_bytes(4, byteorder='little'))
    s.sendall(enteredPos_bytes)
    print(f"You placed the label at x={enteredPos[0]}, y={enteredPos[1]}")

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 21:06:27