
简介基于Python的网络版中国象棋是一份包含服务器端与双客户端的完整项目源码面向需要实战练习Python网络编程和桌面游戏开发的学习者帮助解决在局域网环境下实现双人实时对战的问题。压缩包共31个文件大小6.67MB内含9个py源文件、14个pyc编译文件及各类型素材其中py源码覆盖服务器通信、客户端交互、棋盘与棋子逻辑、红黑双方设置等模块素材则包括jpg/png/bmp图片、ttf字库和txt配置说明。项目采用一个server加两个main的运行模式通过修改config中的IP和端口即可实现跨电脑对弈适合学习套接字编程、并发消息处理、pygame或tkinter界面开发以及中国象棋规则的代码化实现。已有2151人学习下载无论用于课程设计、毕业课题还是个人技术进阶都能从中获得架构设计和排错思路是理论与实践结合紧密的Python实战资料。1. 从单人摆棋到双人联网Python网络版中国象棋到底做了什么之前接手过一个局域网联机象棋的小项目需求很简单——两个人能在一台电脑上玩很久的本地象棋搬到网上让人远程对弈。那时候我第一反应是“走子规则不是早就有人写烂了”结果真拆下来才发现最大的坑根本不在棋盘和车马炮而在于两个客户端之间的状态同步A走的棋怎么稳定地到B那边、谁先谁后、断线了怎么办、B的界面卡住时A要不要等。这套基于Python网络版中国象棋的资源正是把“单机走子”和“网络传输”这两件事完整打通的一套参考实现适合拿去做课程设计、毕业设计、或者自己从零搭一个联机棋类游戏的脚手架。它解决的问题非常具体各端独立、协议清晰、能跑得通、踩过的坑都在代码里标注好。2. 拆解架构通信协议、棋盘表示与服务端/客户端的分工2.1 选型理由C/S结构还是P2P结构做网络象棋第一个要定的不是界面而是网络拓扑。常见做法是服务器中转C/S或者双方直连P2P。直连看着省一台机器但在NAT后面基本没法用——很多家庭和办公网络不允许主动入站连接双方都要公网IP才能连得上。常见做法是选一台云服务器做中转客户端只负责主动连接服务端把一方的消息转给另一方。这样穿透问题被绕开了整个系统只需要服务端有公网地址就行。这份资源里采用的也是C/S中转方案。服务端本身不关心谁赢谁输也不保存棋局状态它只干一件事情维护在线客户端的列表把某条消息从发送方转发给对局方。这么做的好处是可以把“对局管理”“登录校验”“观战广播”这类扩展功能挂到服务端上客户端逻辑简单也容易做断线重连。数据流向是这样的玩家A在界面上走了一步棋客户端A把走棋消息打包发给服务端服务端根据房间号查找到玩家B的连接原样转发给客户端BB收到消息后直接按里面携带的坐标更新棋盘。整个过程不要求两边客户端保持完全一致的实时帧同步因为回合制游戏天然是“事件同步”而不是“状态同步”你传的是“谁从哪走到哪”不传“当时的完整局面”。2.2 棋盘表示二维数组还是坐标字符串棋盘数据结构是整个项目的地基。中国象棋是10行9列最自然的表示就是二维列表。资源里用board[10][9]存局面0表示空正数表示红方棋子负数表示黑方棋子对应的字典把数字映射成“车”“马”“炮”等字符。这个表示法在老项目中很常见打印调试也直观。但走棋消息不能直接把整个board塞进去那样消息体积大、而且没法做校验。资源里的做法是用坐标字符串把行号列号拼成目标字符串比如红车从(0,0)走到(0,1)消息体就携带from“00”to“01”。这样一条走棋消息固定只有几十字节放大到局域网或者弱网环境都非常轻松。还有一套替代方案是用一维索引0到89配合行列转换函数。我一般不建议在协议层用一维索引因为调试的时候从抓包工具里看一条“move 12 34”还得自己心算行号列号太容易翻车。坐标字符串的可读性对排障非常有帮助尤其是联机对弈出问题时谁走的哪一步一目了然。2.3 协议字段JSON消息体的约定与扩展性协议层成败决定整个系统能不能扩展。资源里的消息统一采用JSON文本格式最外层固定包含type字段后续字段随type不同而变化。这样设计的好处是调试方便、跨语言兼容、加字段不需要改底层。下面是核心消息类型type方向关键字段说明join客户端→服务端name, room加入指定房间match服务端→客户端rival_name, color匹配成功告知执红/执黑move客户端→服务端from, to, role本次走棋的起止坐标move_ok服务端→客户端from, to, fen权威走棋结果附带局面哈希move_fail服务端→客户端reason拒绝走棋并返回原因retract客户端→服务端step_id请求悔棋end服务端→客户端winner宣布对局结束move里带一个role字段用来标记当前是谁的回合服务端可以据此拒绝越权走子。move_ok里带fen字段是可选的用来做局面哈希校验防止两端棋盘状态悄悄跑偏。如果只是课程设计这个字段可以省掉想做严谨一点的平台建议保留——它相当于两端对局的校验和每一步都对一下能提前发现同步问题。2.4 走子规则模块合法性与被“吃掉”的棋子规则判断是整个系统里最容易被复制错的部分。资源里把合法性校验抽成了一个独立函数输入是棋盘、起点、终点、轮到哪一方输出是布尔值和错误原因。它内部要依次做五件事第一检查起点是不是当前方的棋子不能拿对方的子走棋。第二检查终点是不是同色棋子中国象棋不允许吃自己人。第三按棋子类型走不同的移动逻辑车走直线、炮隔子吃、马走日蹩马腿、象走田塞象眼、士只能在九宫斜线、将不能出九宫。第四检查落点是否导致老将被直接照面——也就是走后形成将帅对脸的局面要判非法。第五兵过河前只能前进过河后才能横移。这里我提一个很多人踩过的坑马的蹩马腿判定。网上很多版本只判断“目标位置在马的日字对角线上”不检查马腿所在位置的中间点是不是有棋子导致马能“飞过去”。资源里的实现把蹩马腿条件明确写成了中间点的坐标棋盘值是否为0测试用例里也专门覆盖了“马被别在中间时不能跳”的场景。3. 基于socket实现联机模块服务端骨架与帧编解码实现3.1 服务端骨架监听、房间分发与连接列表管理服务端是整个联机系统的心脏。下面这份代码是服务端最核心的连接管理骨架。它要做的事情有三件同时监听多个客户端连接、把新连接分配到指定房间、把消息从一个连接转发到同房间的另一个连接。这里直接用多线程模型每个客户端连接跑一个线程用一把全局锁保护共享的连接字典。import socket import threading import json # 管理连接: room_id - {client_id: socket} rooms {} rooms_lock threading.Lock() def register_client(sock, room_id, client_id): with rooms_lock: if room_id not in rooms: rooms[room_id] {} rooms[room_id][client_id] sock def send_to_room(room_id, exclude_cid, payload: bytes): with rooms_lock: if room_id not in rooms: return for cid, conn in rooms[room_id].items(): if cid exclude_cid: continue try: conn.sendall(payload) except OSError: # 连接已断开交给清理逻辑处理 pass def handle_client(conn, addr): # 实际项目中在这个函数里完成三件事: # 1. 读取并解析消息 # 2. 先查房间和身份再做转发 # 3. 连接断开时从 rooms 中移除 pass def start_server(host, port): server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((host, port)) server.listen(5) while True: conn, addr server.accept() threading.Thread(targethandle_client, args(conn, addr), daemonTrue).start()逻辑说明rooms是一个二级字典第一层key是房间号第二层key是客户端唯一标识。send_to_room遍历某个房间里的所有连接把payload原样转发给每个客户端。handle_client函数在完整资源里承担消息解析和路由上面只保留了结构骨架。start_server中socket.SO_REUSEADDR参数很关键不加它的话服务端重启时经常报“端口被占用”导致开发期反复重启很痛苦。参数说明host建议设置成0.0.0.0而不是127.0.0.1否则远端客户端根本连不进来。port选一个未被占用的端口课程设计环境建议用10000以上的高位端口避免和系统服务冲突。listen(5)表示内核排队等待accept的连接队列长度这个数字不用调太大对象棋这种并发极低的场景够用。3.2 帧编解码提取消息边界的标准姿势TCP是流协议没有“一条消息”的概念。如果收发两端各自调sendall和recv很可能出现一次send的数据被拆成两次recv或者两次send的数据被拼到一次recv里。这就是开发联机程序时最容易翻车的地方。业界常用解决办法是自定义帧格式每条报文前面用固定4个字节表示后面JSON数据的字节长度。接收方先读取定长4字节解析出长度N再循环读取直到攒够N字节才算拿到一条完整消息。下面给出封包和拆包的参考实现import struct def build_frame(payload: bytes) - bytes: # 4字节大端无符号整数 荷载长度 length len(payload) header struct.pack(!I, length) return header payload def recv_exact(conn, n: int) - bytes: data b while len(data) n: chunk conn.recv(n - len(data)) if not chunk: raise ConnectionError(connection closed while reading) data chunk return data def recv_frame(conn): header recv_exact(conn, 4) length struct.unpack(!I, header)[0] if length 1024 * 1024: raise ValueError(frame too large, possible protocol error) payload recv_exact(conn, length) return json.loads(payload.decode(utf-8))逻辑说明build_frame把待发送的JSON字节序列加了一个4字节长度前缀recv_exact是一个循环读满指定字节数的工具函数它解决了recv单次返回不足字节数的问题。recv_frame先读4字节头再按照头里声明的长度把消息体完整读出来最后用json.loads还原成字典。中间做了最大长度限制避免恶意或异常数据导致内存被撑爆。参数说明struct.pack的格式串“!I”表示用网络字节序大端打包一个无符号整数。网络字节序与机器无关两端都是同样的格式串才不至于读到错乱的长度值。字节序不一致是另一个隐蔽坑——有些开发者图省事直接发host字节序的整数在x86小端机器上自测没问题一换到ARM或交叉编译环境就乱套。3.3 客户端收发线程把网络事件降到主线程之外客户端是带界面的不能在UI线程里直接调recv否则界面会卡死。常见的做法是单独拉一个接收线程每次收到一条完整消息后通过队列扔给界面线程处理。这里给出一段客户端收发核心代码展示怎样用queue安全传递消息import threading import queue import socket class GameClient: def __init__(self, host, port): self.sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) self.msg_queue queue.Queue() self.running True threading.Thread(targetself.recv_loop, daemonTrue).start() def recv_loop(self): while self.running: try: msg recv_frame(self.sock) except (ConnectionError, ValueError): self.msg_queue.put({type: system, event: disconnect}) break self.msg_queue.put(msg) def send_move(self, src, dst): frame build_frame(json.dumps({ type: move, from: src, to: dst, role: self.my_role }).encode()) self.sock.sendall(frame)逻辑说明recv_loop是阻塞循环它持续调用recv_frame每解析出一条消息就放入标准库queue.Queue。queue是线程安全的可以放心把msg_queue传递到任何线程去读取。界面线程开一个QTimer定时从msg_queue里取消息一旦取到就更新棋盘这样网络线程和UI线程不会因为共享变量而打架。参数说明send_move里的my_role是客户端加入房间后服务端回传的执红/执黑标识。客户端所有走棋请求都带这个字段服务端可以据此判断当前请求是否为合法方发起——非回合方的走子请求应当被拒绝。这里有一个设计选择合法性第一道关在客户端本地做第二道关在服务端做。本地校验反馈快、能提示具体错误服务端校验防止有人修改客户端后绕过限制。4. 基于PyQt5的走棋界面绘制棋盘、点击判定与悔棋实现4.1 棋盘绘制用QPainter一次性画完棋盘和棋子界面这块用的PyQt5。绘制逻辑全部交给QPainter棋盘背景、网格线、九宫斜线、河界文字、棋子全部在一个paintEvent里画完。它的优势是不依赖额外图片资源切到哪台机器都能跑不用担心素材路径问题。核心绘制逻辑如下from PyQt5.QtWidgets import QWidget from PyQt5.QtGui import QPainter, QBrush, QColor, QPen from PyQt5.QtCore import Qt, QPoint class ChessBoard(QWidget): CELL_SIZE 64 ORIGIN_X 40 ORIGIN_Y 40 def paintEvent(self, event): p QPainter(self) p.setRenderHint(QPainter.Antialiasing) # 画横线10行共9条横线 for row in range(10): start QPoint(self.ORIGIN_X, self.ORIGIN_Y row * self.CELL_SIZE) end QPoint(self.ORIGIN_X 8 * self.CELL_SIZE, self.ORIGIN_Y row * self.CELL_SIZE) p.drawLine(start, end) # 画竖线9列中间一行是河界要断开 for col in range(9): x self.ORIGIN_X col * self.CELL_SIZE if col 0 or col 8: p.drawLine(x, self.ORIGIN_Y, x, self.ORIGIN_Y 9 * self.CELL_SIZE) else: p.drawLine(x, self.ORIGIN_Y, x, self.ORIGIN_Y 4 * self.CELL_SIZE) p.drawLine(x, self.ORIGIN_Y 5 * self.CELL_SIZE, x, self.ORIGIN_Y 9 * self.CELL_SIZE) # 画斜线九宫 p.drawLine( self.ORIGIN_X 3 * self.CELL_SIZE, self.ORIGIN_Y, self.ORIGIN_X 5 * self.CELL_SIZE, self.ORIGIN_Y 2 * self.CELL_SIZE ) # 画棋子、文字等 self.drawPieces(p)逻辑说明paintEvent里用CELL_SIZE做基准尺寸所有坐标都由行列换算得出拖动窗口缩放时不会出现图案错乱。横线10行9条、中间第5行断开是为了画河界竖线在断开的区域分两段画。九宫斜线只需要画两条主对角线中对应的那两条线段。参数说明CELL_SIZE64表示每个格子边长64像素这是鼠标点击转换坐标的基础。ORIGIN_X和ORIGIN_Y是棋局左上角距离窗口边缘的偏移量保证棋盘周边留白让用户操作时不会误点到边缘。p.setRenderHint(QPainter.Antialiasing)打开抗锯齿棋子上字边缘才不会有毛刺。4.2 点击判定与走棋流程鼠标事件到协议消息的转换鼠标点击棋盘首先要把屏幕坐标换算成行列坐标换算公式行列值等于坐标差值除以格子大小。这一步看似简单坑在取下整还是取上整。我通常会先判定点击落点离哪个交叉点最近再决定按下哪一格避免用户稍偏一点就选错棋子。def find_intersection(self, pos): # 对每个交叉点计算距鼠标的最短距离 best None best_dist 1e9 for row in range(10): for col in range(9): px self.ORIGIN_X col * self.CELL_SIZE py self.ORIGIN_Y row * self.CELL_SIZE dist (pos.x() - px) ** 2 (pos.y() - py) ** 2 if dist best_dist: best_dist dist best (row, col) if best_dist (self.CELL_SIZE // 2) ** 2: return None # 离得太远判定为空点 return best def mousePressEvent(self, event): pos event.pos() idx self.find_intersection(pos) if idx is None: return row, col idx if self.selected is None: # 未选中任何棋子尝试选择已方棋子 piece self.board[row][col] if piece and (piece[0] self.my_color): self.selected (row, col) self.update() else: # 已有选中尝试走棋 self.try_move(self.selected, (row, col))逻辑说明find_intersection用双重循环计算鼠标位置与棋盘上全部90个交叉点的平方距离取最近的那个。这个算法简单、不易错90个点遍历一次毫秒级完成完全没有性能压力。选棋规则做了限定只有落在自己方棋子上的点击才会被选中为起点这个判断放在鼠标事件层可以大幅减少后面无意义的协议通信。参数说明best_dist大于半个格子距离的平方说明点落在偏远处直接返回None这一条规则很实用——它避免了玩家在棋盘外区域乱点时引发空指针。try_move内部做了四步操作本地走子合法性检测、发送move消息到服务端、等待服务端回move_ok后更新棋盘、最后清空selected状态。4.3 悔棋的实现撤回协议与一次只能悔一步的约束悔棋是最容易破坏同步的功能因为悔棋本质上是对“权威局面”的修正。最常见方案是服务端维护一个step_history列表每步棋的结果都存进去。收到悔棋请求时服务端先把上一步棋从history里弹出计算出悔棋后的局面快照fen再把这个快照连同撤销的坐标一起广播给双方。两份数据缺一不可坐标告诉界面“把棋子放回去”快照则让双方本地棋盘可以做一次整体校对。def handle_retract(self, cid): room self.get_room(cid) if not room.history: return self.send_error(cid, no_step_to_retract) step room.history.pop() # 用的是栈结构后手悔棋只能悔最后一步 snapshot build_snapshot(room.board) payload build_frame(json.dumps({ type: retract, step: step, fen: snapshot }).encode()) self.send_to_room(room.id, excludeNone, payloadpayload)逻辑说明room.history保存的不是简单坐标对而是包含棋子和吃子信息的完整记录。弹出后需要恢复该位置的棋子再把被吃的棋子放回目标点。snapshot是合法走子后的棋盘状态序列化结果它让双方客户端不用逐条回放历史也能直接校准到最新局面。这种设计把“悔棋”从单端操作变成服务端权威操作最能避免出现两边棋盘不一致。参数说明悔棋时只允许撤回最后一步棋这是很多桌游平台的潜规则。如果你想做“可以连悔两步”或者“对方同意才能悔”的变体需要额外加房间配置把retract消息改成“请求-确认”两步交互而不是当前这种一步广播。从工作量来说资源里这份一步悔棋的实现已经能覆盖绝大多数交付场景。5. 联机对战实战排坑五条必须牢记的踩坑记录5.1 走了几步就解析失败粘包与半包问题现象客户端和服务端在单步走棋时一切正常连续走几步后就开始偶发“JSON解析失败”甚至棋盘上棋子位置错乱。让人头疼的是出错的频率并不固定有时候走十步没问题有时候第二步就崩。原因TCP是字节流协议发送端多次调用sendall后数据可能被TCP层合并成一个大数据段发出也可能被拆成多个小段。接收端如果按“一次recv就是一条消息”来读就必然会出现一次读到了两条消息或者半条消息的情况。这个现象行业内叫粘包/半包。典型错误代码是没有按长度读取直接data conn.recv(4096)然后json.loads。解决使用前面讲过的帧格式先收4字节长度头再按长度收消息体。核心点有两个接收循环必须按recv_exact的方式把长度头读满4字节、消息体读满N字节发送端每个sendall必须只发一条完整帧。只要两端都严格套用这个格式粘包半包就不可能出现。建议开发期在服务端写一个测试脚本随机连续发1000条消息再用抓包工具校验字节序。5.2 UI线程被recv阻塞后整个窗口假死现象客户端启动后一进入游戏就出现“界面上所有按钮都点了没反应、棋子拖不动”过几秒后突然恢复还会跳出大量消息。单机版正常、联机版才出现这是排查的第一个线索。原因在界面主线程里调了socket.recv()而这个函数在没有数据时会一直阻塞直到收到数据或超时。UI循环得不到执行机会整个窗口就进入假死状态。这在联机程序新手手里非常常见问题根源不是socket本身而是把网络IO放错了线程。解决能收网络消息的操作统一放进独立线程或QThread收到的消息放进queue.Queue。界面线程用QTimer每隔50毫秒检测一次队列如果有消息就批量取出处理。这样即便socket那边一秒收到200条消息界面线程仍然按自己的节奏消费不会出现卡顿。从那以后我做联机界面第一件事永远是问自己一句话“这个recv是放在哪个线程里跑的”5.3 选择棋子的坐标串错位导致马真的“飞”了现象红方马在棋盘上的移动不受蹩马腿约束黑方马却一切正常或者同一只棋子从A点移动到B点在单机测试时合法联机时服务端返回move_fail。原因走子规则模块里的蹩马腿判断被注释掉或者判断逻辑写错常见错误是只检查目标点横纵坐标差为1和2而没有检查马腿中间点是否有棋子。联机时服务端和客户端各自维护一份规则代码如果两份代码不一致就会出现“客户端本地校验通过服务端校验拒绝”的诡异局面。另一个制造错位的来源是坐标字符串传输中行列序不一致一方按“rowcol”发送另一方按“colrow”解析。解决把走子校验函数统一放在一个模块里客户端服务端共用同一份代码。在资源目录下是实现一个不依赖任何界面代码的rule.py里面就一个is_valid_move(board, my_color, src, dst)函数两边都直接import这个文件。如果项目要求服务端用另外一套语言重写那也要用同一套测试用例先做验证绝不能各写各的。坐标序这件事上协议字段名直接命名为from_row、from_col比再用拼接字符串要安全得多。5.4 客户端断线重连后服务端还把它当在线现象玩家A关闭客户端后重新打开、再次加入同一个房间A已经能正常收发消息但玩家B这边一下卡死、或者B发消息时抛“socket send failed”。检查服务端还发现连接列表里有两个完全一样的旧连接。原因TCP连接断开后如果服务端不及时读取数据可能根本不知道连接已经死亡。特别是客户端直接kill进程没有机会发送正常关闭帧服务端那个socket仍然处于半开状态发数据触发RST之前你根本感知不到异常。旧连接没被清理导致后来的广播被发送给了已失效的socket。解决服务端除了注册连接还必须做两件事一是每次send后检查写入是否抛异常一旦异常就立刻执行清理逻辑二是给每对客户端连接加心跳。心跳周期通常设10秒固定值客户端每10秒上报一次存活消息服务端每30秒检查一次所有连接的最后活跃时间超过阈值直接断开并回收连接。断线重连的客户端要生成新全局ID注册进房间而不是复用旧ID。清理函数必须带锁避免和广播同时操作rooms字典导致线程错乱。5.5 悔棋造成两端棋盘不一致忘了同步吃掉的子现象双方对战一段时间后执行一次悔棋B端棋盘被撤回的格子正确但B端被吃掉的棋子没有回到B的棋盘上。继续对弈几步之后发现双方局面开始明显不同步。原因后端存储的历史记录只有起点和终点坐标。悔棋时把棋盘起点恢复原棋、终点恢复原棋子但如果这一手吃掉了棋子历史记录里没有被吃掉棋子的信息无法在悔棋时把它挂回终点对应的格子导致撤回后的局面和撤棋前不一致。解决在step记录中除了from和to必须增加captured字段记录被吃掉的棋子到底是什么。悔棋时从captured里读到棋子身份和颜色把它写回终点格。同时每一次悔棋后的完整局面快照要广播给两端让任意一端发现自己棋盘和服务端权威快照不一致时可以直接灌注整盘状态而不是继续戴着错位跑。我实测下来悔棋功能如果不在协议层做完整单靠客户端本地拍脑袋还原是必然翻车的。6. 进阶实践用状态机驱动断线重连与发布打包建议到了这个阶段联机对弈的基本功能已经能跑通了。再往上走一截就是把“能玩”变成“好用”。我重点说说断线重连的状态机设计这是所有联机应用里最需要用“状态”来约束逻辑的地方。客户端对局状态可以抽象成四个Lobby大厅、Playing对局中、Reconnecting重连中、Ended已结束。切换规则是固定的Lobby收到match消息后进入PlayingPlaying中途网络异常进入ReconnectingReconnecting状态下尝试连接服务器并重新加入房间成功则回到Playing超过5次失败则进入Ended并告知对局已经无法继续。class GameState: def __init__(self): self.state Lobby self.retry_count 0 def on_event(self, event, payloadNone): if self.state Lobby and event matched: self.state Playing self.retry_count 0 elif self.state Playing and event disconnected: self.state Reconnecting self.retry_count 0 elif self.state Reconnecting and event joined: self.state Playing self.retry_count 0 elif self.state Reconnecting and event retry_fail: self.retry_count 1 if self.retry_count 5: self.state Ended逻辑说明状态机把“网络断开”和“重新连上”这件事从一个简单的bool值变成了可追踪的状态。在Reconnecting状态下客户端保留本地的当前棋盘和history但停止接收新消息只发起join请求。服务端要额外提供“继续对局”的协议支持当客户端重新join到已经存在的房间时服务端把当前权威局面快照和history长度一起回传客户端以此校准本地棋盘。参数说明重连尝试次数限制为5每次间隔用指数退避第一次等1秒、第二次2秒、第三次4秒最多加到8秒封顶。指数退避比固定间隔更聪明——前几次快速重试能解决临时闪断后面慢下来避免服务器在高负载下被打爆。这类参数在实际项目中最好做成配置文件而不是写死在代码里课程设计交付时也建议附一份config.ini把端口、超时、重连上限写清楚。打包成可分发程序同样有讲究。客户端用PyInstaller打包成单目录或单文件关键在collect数据文件如果窗口图标、棋盘背景图是从外部路径加载的打包后运行会出现找不到文件的错误。常见做法是把所有资源用qrc机制嵌进二进制这样打包出来只有一个exe或app复制到别的电脑直接能跑。服务端不带界面直接写一份requirements.txt部署时用pip按清单装依赖即可。每条报文在后面附真实抓包样例这份代码闭着眼都能跑通。希望帮到你。本文还有配套的精品资源点击获取