Files
DFJ/02_详细设计文档/实时通信模块详设.md

9.6 KiB
Raw Blame History

实时通信模块详设文档

文档版本: 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 核心流程

  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实例必须在玩家断线时保留在内存中,直到游戏结束或超时。当玩家重连时,可以从该实例获取最新状态。
// 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.iosocket.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处理,使业务代码可以专注于游戏逻辑,无需关心底层的消息路由。