9.6 KiB
9.6 KiB
实时通信模块详设文档
文档版本: v1.0
撰写人: 通信架构师
创建日期: 2024年9月11日
1. WebSocket通信协议
1.1 协议选型
- WebSocket: 基于TCP的全双工通信协议,提供持久化连接,是实现游戏实时对战、状态同步的最佳选择。
- WSS (WebSocket Secure): 在生产环境强制使用WSS协议,确保所有通信数据都经过TLS加密,保障数据安全。
1.2 消息格式
所有客户端与服务端之间的WebSocket消息都采用统一的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连接。
- 收到
// 客户端PING消息
{
"type": "PING",
"payload": {},
"timestamp": "..."
}
// 服务端PONG消息
{
"type": "PONG",
"payload": {},
"timestamp": "..."
}
2. 消息类型与数据结构
2.1 消息类型枚举 (GameMessageType)
// 客户端 -> 服务端 (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:
interface AuthenticatePayload { token: string; // 从HTTP登录接口获取的JWT }
AUTHENTICATED (S2C)
- 描述: 服务端认证成功后返回的消息。
- Payload:
interface AuthenticatedPayload { userId: string; nickname: string; // ... 其他用户信息 }
CREATE_ROOM (C2S)
- 描述: 客户端请求创建新房间。
- Payload:
interface CreateRoomPayload { roomName: string; isPublic: boolean; password?: string; // 如果是私密房间 }
ROOM_STATE_UPDATE (S2C)
- 描述: 当房间状态(如玩家加入/退出/准备)发生变化时,服务端向房间内所有客户端广播。
- Payload:
RoomState对象 (详见后端设计文档)
PLACE_PLANES (C2S)
- 描述: 玩家在布置阶段提交飞机布局。
- Payload:
interface PlacePlanesPayload { planes: { center: { x: number, y: number }; direction: 'up' | 'down' | 'left' | 'right'; }[]; }
EXECUTE_ATTACK (C2S)
- 描述: 玩家在对战阶段执行攻击。
- Payload:
interface ExecuteAttackPayload { position: { x: number, y: number }; }
GAME_STATE_UPDATE (S2C)
- 描述: 游戏核心状态发生变化时,服务端向游戏内玩家广播。
- Payload:
GameState对象 (详见游戏核心逻辑设计文档),但会根据接收玩家进行数据裁剪(如隐藏对手未被攻击的飞机位置)。
ATTACK_RESULT (S2C)
- 描述: 服务端通知攻击结果。
- Payload:
interface AttackResultPayload { attackerId: string; position: { x: number, y: number }; result: 'miss' | 'hit' | 'destroy'; targetPlaneId?: string; // 如果击中 isGameOver: boolean; }
ERROR (S2C)
- 描述: 服务端向客户端发送错误信息。
- Payload:
interface ErrorPayload { code: number; // 错误码 message: string; // 错误信息 requestType?: string; // 导致错误的请求类型 }
3. 断线重连机制
3.1 核心流程
-
客户端检测断线:
- WebSocket
onclose事件被触发。 - 或,发送
PING后超过10秒未收到PONG。
- WebSocket
-
自动重连:
- 客户端立即尝试重新建立WebSocket连接。
- 采用指数退避算法进行重连尝试,例如:首次延迟1秒,然后2秒, 4秒, 8秒... 直到成功或达到最大重连次数(5次)。
-
重连后认证:
- 新连接建立后,客户端必须立即发送
AUTHENTICATE消息,并携带之前的JWT。
- 新连接建立后,客户端必须立即发送
-
服务端处理重连:
- 服务端通过JWT识别出这是同个玩家的重连请求。
- 服务端查找该玩家当前是否处于某个游戏会话中。
- 如果在游戏中,服务端将最新的
GameState发送给该玩家,并向房间内所有玩家广播PLAYER_RECONNECTED事件。
-
状态同步:
- 客户端收到完整的
GameState后,恢复游戏界面,确保与服务器状态一致。
- 客户端收到完整的
3.2 服务端实现要点
- 会话持久化: 玩家的
userId和其connectionId的映射关系需要存储在Redis中,并设置合理的过期时间(如5分钟),以便在断线期间保留会话信息。 - 游戏状态恢复:
GameStateMachine实例必须在玩家断线时保留在内存中,直到游戏结束或超时。当玩家重连时,可以从该实例获取最新状态。
// 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)机制作为消息总线,实现跨节点通信。
-
消息流:
- 客户端A向服务器S1发送消息(如
EXECUTE_ATTACK)。 - S1处理消息,更新游戏状态。
- S1将需要广播的消息(如
ATTACK_RESULT,GAME_STATE_UPDATE)发布到一个特定的Redis频道,例如game-room:{roomCode}。 - 所有后端服务器实例(S1, S2, ...)都订阅了相关的频道。
- S2接收到
game-room:{roomCode}频道的消息。
- 客户端A向服务器S1发送消息(如
-
消息投递:
- S2查找连接在本机且属于
roomCode房间的客户端(即客户端B)。 - S2将消息通过WebSocket连接发送给客户端B。
- S2查找连接在本机且属于
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逻辑。只需简单配置,即可实现多节点间的无缝消息广播。
示例配置
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处理,使业务代码可以专注于游戏逻辑,无需关心底层的消息路由。