# 实时通信模块详设文档 > **文档版本**: v1.0 > **撰写人**: 通信架构师 > **创建日期**: 2024年9月11日 ## 1. WebSocket通信协议 ### 1.1 协议选型 - **WebSocket**: 基于TCP的全双工通信协议,提供持久化连接,是实现游戏实时对战、状态同步的最佳选择。 - **WSS (WebSocket Secure)**: 在生产环境强制使用WSS协议,确保所有通信数据都经过TLS加密,保障数据安全。 ### 1.2 消息格式 所有客户端与服务端之间的WebSocket消息都采用统一的JSON格式进行封装,便于解析和扩展。 ```json { "type": "MESSAGE_TYPE_ENUM", "payload": { "key1": "value1", "key2": "value2" }, "timestamp": "2024-09-11T10:00:00.000Z", "client_message_id": "optional-client-uuid-for-ack" } ``` - `type`: 消息类型,用于路由到不同的处理逻辑。 - `payload`: 消息体,包含具体业务数据。 - `timestamp`: 消息发送的UTC时间戳。 - `client_message_id`: (可选) 客户端生成的消息ID,用于实现消息确认(ACK)机制。 ### 1.3 心跳机制 为维持连接活性并检测死链,采用双向心跳机制: - **客户端**: 每隔25秒向服务端发送一个`PING`消息。 - **服务端**: - 收到`PING`消息后,立即回复一个`PONG`消息。 - 如果在60秒内未收到任何客户端消息(包括`PING`),则认为连接已断开,主动关闭该WebSocket连接。 ```typescript // 客户端PING消息 { "type": "PING", "payload": {}, "timestamp": "..." } // 服务端PONG消息 { "type": "PONG", "payload": {}, "timestamp": "..." } ``` ## 2. 消息类型与数据结构 ### 2.1 消息类型枚举 (`GameMessageType`) ```typescript // 客户端 -> 服务端 (C2S) export enum ClientToServerMessageType { // --- 系统级 --- PING = 'PING', // 心跳检测 AUTHENTICATE = 'AUTHENTICATE', // 身份认证 // --- 房间管理 --- CREATE_ROOM = 'CREATE_ROOM', // 创建房间 JOIN_ROOM = 'JOIN_ROOM', // 加入房间 LEAVE_ROOM = 'LEAVE_ROOM', // 离开房间 GET_ROOM_LIST = 'GET_ROOM_LIST', // 获取房间列表 PLAYER_READY = 'PLAYER_READY', // 玩家准备 // --- 游戏逻辑 --- PLACE_PLANES = 'PLACE_PLANES', // 放置飞机 EXECUTE_ATTACK = 'EXECUTE_ATTACK', // 执行攻击 SURRENDER = 'SURRENDER' // 投降 } // 服务端 -> 客户端 (S2C) export enum ServerToClientMessageType { // --- 系统级 --- PONG = 'PONG', // 心跳响应 AUTHENTICATED = 'AUTHENTICATED', // 认证成功 ERROR = 'ERROR', // 错误通知 // --- 房间与游戏状态同步 --- ROOM_LIST_UPDATE = 'ROOM_LIST_UPDATE', // 房间列表更新 ROOM_STATE_UPDATE = 'ROOM_STATE_UPDATE',// 房间状态更新 GAME_STATE_UPDATE = 'GAME_STATE_UPDATE',// 游戏状态更新 // --- 游戏事件通知 --- GAME_STARTED = 'GAME_STARTED', // 游戏开始 PLACEMENT_PHASE_START = 'PLACEMENT_PHASE_START', // 放置阶段开始 BATTLE_PHASE_START = 'BATTLE_PHASE_START', // 对战阶段开始 TURN_CHANGE = 'TURN_CHANGE', // 回合变更 ATTACK_RESULT = 'ATTACK_RESULT', // 攻击结果 GAME_OVER = 'GAME_OVER', // 游戏结束 PLAYER_RECONNECTED = 'PLAYER_RECONNECTED',// 玩家重连 PLAYER_DISCONNECTED = 'PLAYER_DISCONNECTED'// 玩家断线 } ``` ### 2.2 核心消息体 (`Payload`) 详解 #### `AUTHENTICATE` (C2S) - **描述**: 客户端连接后发送的第一条消息,用于身份认证。 - **Payload**: ```typescript interface AuthenticatePayload { token: string; // 从HTTP登录接口获取的JWT } ``` #### `AUTHENTICATED` (S2C) - **描述**: 服务端认证成功后返回的消息。 - **Payload**: ```typescript interface AuthenticatedPayload { userId: string; nickname: string; // ... 其他用户信息 } ``` #### `CREATE_ROOM` (C2S) - **描述**: 客户端请求创建新房间。 - **Payload**: ```typescript interface CreateRoomPayload { roomName: string; isPublic: boolean; password?: string; // 如果是私密房间 } ``` #### `ROOM_STATE_UPDATE` (S2C) - **描述**: 当房间状态(如玩家加入/退出/准备)发生变化时,服务端向房间内所有客户端广播。 - **Payload**: `RoomState` 对象 (详见后端设计文档) #### `PLACE_PLANES` (C2S) - **描述**: 玩家在布置阶段提交飞机布局。 - **Payload**: ```typescript interface PlacePlanesPayload { planes: { center: { x: number, y: number }; direction: 'up' | 'down' | 'left' | 'right'; }[]; } ``` #### `EXECUTE_ATTACK` (C2S) - **描述**: 玩家在对战阶段执行攻击。 - **Payload**: ```typescript interface ExecuteAttackPayload { position: { x: number, y: number }; } ``` #### `GAME_STATE_UPDATE` (S2C) - **描述**: 游戏核心状态发生变化时,服务端向游戏内玩家广播。 - **Payload**: `GameState` 对象 (详见游戏核心逻辑设计文档),但会根据接收玩家进行数据裁剪(如隐藏对手未被攻击的飞机位置)。 #### `ATTACK_RESULT` (S2C) - **描述**: 服务端通知攻击结果。 - **Payload**: ```typescript interface AttackResultPayload { attackerId: string; position: { x: number, y: number }; result: 'miss' | 'hit' | 'destroy'; targetPlaneId?: string; // 如果击中 isGameOver: boolean; } ``` #### `ERROR` (S2C) - **描述**: 服务端向客户端发送错误信息。 - **Payload**: ```typescript interface ErrorPayload { code: number; // 错误码 message: string; // 错误信息 requestType?: string; // 导致错误的请求类型 } ``` ## 3. 断线重连机制 ### 3.1 核心流程 1. **客户端检测断线**: - WebSocket `onclose` 事件被触发。 - 或,发送`PING`后超过10秒未收到`PONG`。 2. **自动重连**: - 客户端立即尝试重新建立WebSocket连接。 - 采用**指数退避算法**进行重连尝试,例如:首次延迟1秒,然后2秒, 4秒, 8秒... 直到成功或达到最大重连次数(5次)。 3. **重连后认证**: - 新连接建立后,客户端必须立即发送`AUTHENTICATE`消息,并携带之前的JWT。 4. **服务端处理重连**: - 服务端通过JWT识别出这是同个玩家的重连请求。 - 服务端查找该玩家当前是否处于某个游戏会话中。 - 如果在游戏中,服务端将最新的`GameState`发送给该玩家,并向房间内所有玩家广播`PLAYER_RECONNECTED`事件。 5. **状态同步**: - 客户端收到完整的`GameState`后,恢复游戏界面,确保与服务器状态一致。 ### 3.2 服务端实现要点 - **会话持久化**: 玩家的`userId`和其`connectionId`的映射关系需要存储在Redis中,并设置合理的过期时间(如5分钟),以便在断线期间保留会话信息。 - **游戏状态恢复**: `GameStateMachine`实例必须在玩家断线时保留在内存中,直到游戏结束或超时。当玩家重连时,可以从该实例获取最新状态。 ```typescript // Redis中存储的重连会话信息 // Key: "reconnect:session:{userId}" // Value (HASH): // connectionId: "previous-connection-id" // gameId: "current-game-id" // roomCode: "current-room-code" // expireAt: "timestamp" ``` ## 4. 多节点部署与消息同步 ### 4.1 挑战 当后端WebSocket服务部署在多个节点上时,同一房间的两个玩家可能连接到不同的服务器实例。一个玩家发送的消息需要被广播给连接在另一台服务器上的对手。 ### 4.2 解决方案:Redis Pub/Sub 使用Redis的发布/订阅(Pub/Sub)机制作为消息总线,实现跨节点通信。 1. **消息流**: - 客户端A向服务器S1发送消息(如`EXECUTE_ATTACK`)。 - S1处理消息,更新游戏状态。 - S1将需要广播的消息(如`ATTACK_RESULT`, `GAME_STATE_UPDATE`)发布到一个特定的Redis频道,例如`game-room:{roomCode}`。 - 所有后端服务器实例(S1, S2, ...)都订阅了相关的频道。 - S2接收到`game-room:{roomCode}`频道的消息。 2. **消息投递**: - S2查找连接在本机且属于`roomCode`房间的客户端(即客户端B)。 - S2将消息通过WebSocket连接发送给客户端B。 ### 4.3 `socket.io` 与 `socket.io-redis-adapter` 为简化实现,推荐使用`socket.io`库及其官方Redis适配器`socket.io-redis-adapter`。 - **`socket.io`**: 提供了房间(room)、广播(broadcast)、命名空间(namespace)等高级抽象,并内置了心跳和自动重连机制。 - **`socket.io-redis-adapter`**: 自动处理了上述的Redis Pub/Sub逻辑。只需简单配置,即可实现多节点间的无缝消息广播。 #### 示例配置 ```typescript import { Server } from 'socket.io'; import { createAdapter } from '@socket.io/redis-adapter'; import { createClient } from 'redis'; const io = new Server(); const pubClient = createClient({ url: 'redis://localhost:6379' }); const subClient = pubClient.duplicate(); Promise.all([pubClient.connect(), subClient.connect()]).then(() => { io.adapter(createAdapter(pubClient, subClient)); io.listen(3000); }); // 使用方法 io.to('some-room-code').emit('event_name', { data: '...' }); ``` 此方案将所有跨节点通信的复杂性都交由`socket.io-redis-adapter`处理,使业务代码可以专注于游戏逻辑,无需关心底层的消息路由。