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

