MineMonopoly 的混合 P2P 联机架构

MineMonopoly 将联机拆成房间发现和状态计算两部分。中央服务器维护房间注册并提供 PeerJS 信令、ICE 配置;房主客户端建立 WebRTC 连接并运行游戏进程,对局消息主要在房主与玩家之间传输。

房间控制平面

用户通过 /user/login 获得 JWT,受保护接口由 roleValidation 校验。/room-router 下的接口被列入免鉴权路径,因此加入房间本身可以在游客模式下执行。/room-router/join 查询内存房间表,返回房主 Peer ID、是否需要创建、心跳参数和 ICE 服务器。请求带有效 JWT 时,服务端按用户 ID 生成带时效的 TURN 凭证;游客只得到 STUN。登录和受保护 API 仍由 JWT 控制,但房间路由不要求 JWT。

房间表记录房间 ID、房主 Peer ID、状态和租约。房间不存在时,/join 创建记录并返回 needCreate;首个客户端创建 MonopolyHost,取得 PeerJS ID 后调用 /room-router/emit-host 注册,其他客户端再通过 /join 获得该 ID。房主定期调用 /heart 续租,退出调用 /delete。租约到期先进入 grace,宽限期结束才变为 expired;房主可携带旧令牌调用 /reclaim-host 重新注册。服务端据此完成发现和生命周期管理,但不保存完整游戏对象。

WebRTC 数据平面与房主权威

PeerClient.create 通过 PeerJS 信令端点获得本地 Peer ID,linkToHost 使用目标 ID 发起 reliable 数据连接。ICE 可以选择直连或 TURN 中继,客户端记录候选和实际连接模式;默认优先 P2P,连续重连失败后改用 force-relay。

房主页面持有 MonopolyHost 和 Room。客机先发送 JoinRoom,房主检查房间是否已开局、用户是否已存在以及人数上限,再决定加入或重连。Room 保存用户及其 DataConnection,sendToClient 负责单播,roomBroadcast 负责广播。

实际规则在房主的 GameProcessWorker 中运行。Worker 通过 SendToUsers 把消息交给 Room,Room 按玩家 ID 找到连接后发送。游戏开始时广播 GameStart,初始化完成后发送 GameInit;运行期间发送 GameData、RoundTurn、GameLog、RemainingTime、RoundTimeOut 和交互消息。客机根据收到的 GameData 更新本地状态并判断回合。规则计算只有一个权威来源,客户端负责展示和提交操作。

消息类型在 packages/types/interfaces/game/socket.ts 统一定义。SocketMessage<T, S> 将消息类型和来源参数化,ClientSocketMessage、ServerSocketMessage 是按 SocketMsgType 展开的联合类型。普通消息用 JSON 传输,Infinity 等特殊数值在两端转换;地图分块使用 Uint8Array。

自定义地图的分块传输

房主加载自定义地图后计算原始文件 SHA-256,生成包含 mapLoadSessionId、大小、格式和文件名的 CustomMapDescriptor。客机先检查本地缓存:命中回复 MapLocalCheck,未命中发送 MapTransferRequest。描述和字节分离,减少重复传输。

传输时,Room 按 64 KiB 切块,先发送 MapChunkStart(总块数、总字节数和动态超时),再通过 DataChannel 按序发送二进制包,最后发送 MapChunkEnd。二进制包包含类型、会话标识、四字节大端序索引和数据。DataConnection 使用可靠、有序的 SCTP 通道,代码明确依赖其重传和排序能力,应用层不再实现 ACK/重传;MapChunkAck 类型仍保留。客机按会话标识丢弃迟到的旧分块,组装后校验地图编辑器版本,成功才发送 MapResourceLoaded。两秒未收到本地匹配结果时,房主回退到旧协议。使用非官方地图前,其他玩家还需确认风险,拒绝者会被移出房间。

断线、重连与暂停

客机每三秒发送 Heart,五秒无回复进入重连;房主对每个玩家设置十秒无心跳超时。连接带 generation 编号,旧连接事件不会影响新连接。重连管理器每三秒尝试一次:先直连房主,失败后查询 /room-router/status;active 或 grace 才继续,第四次起强制 TURN。房间 closed 或 expired 时结束会话并返回房间路由。

房主维护用户连接版本。玩家断线后,Worker 收到 UserOffLine,可将其标记为离线或交给 AI;重连时房主重新下发地图,等待 MapResourceLoaded,再发送 UserReconnect。Worker 创建初始化会话并发送 GameInit 快照,超时则发送 GameInitAborted。当前没有主机迁移:房主销毁时通知其他玩家并关闭房间,新房主不会自动接管已有进程。

暂停属于游戏状态。房主切后台或执行暂停操作时,Worker 的 OperateListener 暂停倒计时并广播 PauseGame;恢复时补偿暂停时长并广播 ResumeGame。网络断线仍走重连流程,不等同于暂停。

取舍与边界

该架构把一致性责任集中到房主,服务器不验证每次掷骰子或地产变更。它减少服务端对地图脚本和规则的耦合,代价是房主成为单点,连接受 NAT、TURN 可达性和房主稳定性影响。租约、心跳、重连和 Worker 初始化屏障覆盖主要故障路径,但主机迁移与服务端权威回放尚未实现。