Files
whale-town-end/docs/systems/zulip/websocket-protocol.md
angjustinl 55cfda0532 feat(zulip): 添加全面的 Zulip 集成系统
* **新增 Zulip 模块**:包含完整的集成服务,涵盖客户端池(client pool)、会话管理及事件处理。
* **新增 WebSocket 网关**:用于处理 Zulip 的实时事件监听与双向通信。
* **新增安全服务**:支持 API 密钥加密存储及凭据的安全管理。
* **新增配置管理服务**:支持配置热加载(hot-reload),实现动态配置更新。
* **新增错误处理与监控服务**:提升系统的可靠性与可观测性。
* **新增消息过滤服务**:用于内容校验及速率限制(流控)。
* **新增流初始化与会话清理服务**:优化资源管理与回收。
* **完善测试覆盖**:包含单元测试及端到端(e2e)集成测试。
* **完善详细文档**:包括 API 参考手册、配置指南及集成概述。
* **新增地图配置系统**:实现游戏地点与 Zulip Stream(频道)及 Topic(话题)的逻辑映射。
* **新增环境变量配置**:涵盖 Zulip 服务器地址、身份验证及监控相关设置。
* **更新 App 模块**:注册并启用新的 Zulip 集成模块。
* **更新 Redis 接口**:以支持增强型的会话管理功能。
* **实现 WebSocket 协议支持**:确保与 Zulip 之间的实时双向通信。
2025-12-25 22:22:30 +08:00

9.2 KiB

WebSocket 协议详解

协议概述

Zulip 集成系统使用 WebSocket 协议实现游戏客户端与服务器之间的实时双向通信。所有消息采用 JSON 格式编码。

连接生命周期

1. 建立连接

Client                                Server
   |                                     |
   |-------- WebSocket Connect --------->|
   |                                     |
   |<------- Connection Accepted --------|
   |                                     |

2. 认证握手

Client                                Server
   |                                     |
   |-------- login message ------------->|
   |                                     |
   |         [验证 Token]                |
   |         [创建 Zulip Client]         |
   |         [注册 Event Queue]          |
   |         [创建 Session]              |
   |                                     |
   |<------- login_success --------------|
   |                                     |

3. 消息交换

Client                                Server                              Zulip
   |                                     |                                   |
   |-------- chat message -------------->|                                   |
   |                                     |-------- POST /messages ---------->|
   |                                     |<------- 200 OK -------------------|
   |<------- chat_sent ------------------|                                   |
   |                                     |                                   |
   |                                     |<------- Event Queue Message ------|
   |<------- chat_render ----------------|                                   |
   |                                     |                                   |

4. 断开连接

Client                                Server
   |                                     |
   |-------- logout message ------------>|
   |                                     |
   |         [清理 Session]              |
   |         [注销 Event Queue]          |
   |         [销毁 Zulip Client]         |
   |                                     |
   |<------- logout_success -------------|
   |                                     |
   |-------- WebSocket Close ----------->|
   |                                     |

消息格式规范

消息结构

所有消息都是 JSON 对象,包含以下基本字段:

字段 类型 说明
typet string 消息类型标识
其他字段 any 根据消息类型不同而变化

消息类型标识

  • 客户端发送的消息使用 typet 字段
  • 服务器响应的消息统一使用 t 字段

客户端消息

LOGIN - 登录认证

{
  "type": "login",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
字段 类型 必填 说明
type string 固定值 "login"
token string 游戏认证 Token

CHAT - 发送聊天消息

{
  "t": "chat",
  "content": "Hello, everyone!",
  "scope": "local"
}
字段 类型 必填 说明
t string 固定值 "chat"
content string 消息内容 (1-1000 字符)
scope string 消息范围

scope 取值:

  • "local": 当前地图的默认 Topic
  • "topic_name": 指定的 Topic 名称

POSITION - 位置更新

{
  "t": "position",
  "x": 150.5,
  "y": 200.3,
  "mapId": "novice_village"
}
字段 类型 必填 说明
t string 固定值 "position"
x number X 坐标
y number Y 坐标
mapId string 地图 ID

LOGOUT - 登出

{
  "type": "logout"
}
字段 类型 必填 说明
type string 固定值 "logout"

服务器消息

LOGIN_SUCCESS - 登录成功

{
  "t": "login_success",
  "sessionId": "sess_abc123def456",
  "currentMap": "novice_village",
  "username": "player_name",
  "stream": "Novice Village"
}
字段 类型 说明
t string 固定值 "login_success"
sessionId string 会话 ID
currentMap string 当前地图 ID
username string 用户名
stream string 当前 Zulip Stream

CHAT_SENT - 消息发送确认

{
  "t": "chat_sent",
  "messageId": "msg_789xyz",
  "timestamp": 1703500800000
}
字段 类型 说明
t string 固定值 "chat_sent"
messageId string Zulip 消息 ID
timestamp number 发送时间戳 (毫秒)

CHAT_RENDER - 接收聊天消息

{
  "t": "chat_render",
  "from": "other_player",
  "txt": "Hi there!",
  "bubble": true,
  "timestamp": 1703500800000,
  "stream": "Novice Village",
  "topic": "General"
}
字段 类型 说明
t string 固定值 "chat_render"
from string 发送者名称
txt string 消息内容
bubble boolean 是否显示气泡
timestamp number 消息时间戳
stream string 来源 Stream
topic string 来源 Topic

POSITION_UPDATED - 位置更新确认

{
  "t": "position_updated",
  "stream": "Novice Village",
  "topic": "General"
}
字段 类型 说明
t string 固定值 "position_updated"
stream string 新的 Zulip Stream
topic string 新的 Zulip Topic

LOGOUT_SUCCESS - 登出成功

{
  "t": "logout_success"
}

ERROR - 错误消息

{
  "t": "error",
  "code": "RATE_LIMIT",
  "message": "消息发送过于频繁,请稍后再试",
  "details": {
    "retryAfter": 60
  }
}
字段 类型 说明
t string 固定值 "error"
code string 错误码
message string 错误描述
details object 可选,额外错误信息

心跳机制

客户端心跳

客户端应每 30 秒发送一次心跳消息:

{
  "t": "ping"
}

服务器响应

{
  "t": "pong",
  "timestamp": 1703500800000
}

超时处理

  • 服务器在 60 秒内未收到任何消息将断开连接
  • 客户端应在连接断开后自动重连

重连策略

指数退避算法

重试间隔 = min(baseDelay * 2^attempt, maxDelay)

baseDelay = 1000ms
maxDelay = 30000ms

重连流程

  1. 检测到连接断开
  2. 等待重试间隔
  3. 尝试重新连接
  4. 连接成功后重新发送 login 消息
  5. 恢复会话状态

示例代码

class ReconnectingWebSocket {
  private baseDelay = 1000;
  private maxDelay = 30000;
  private attempt = 0;
  
  private getDelay(): number {
    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.attempt),
      this.maxDelay
    );
    this.attempt++;
    return delay;
  }
  
  private resetDelay(): void {
    this.attempt = 0;
  }
  
  async reconnect(): Promise<void> {
    const delay = this.getDelay();
    console.log(`等待 ${delay}ms 后重连...`);
    
    await new Promise(resolve => setTimeout(resolve, delay));
    
    try {
      await this.connect();
      this.resetDelay();
    } catch (error) {
      await this.reconnect();
    }
  }
}

消息序列化

发送消息

function sendMessage(socket: WebSocket, message: object): void {
  const json = JSON.stringify(message);
  socket.send(json);
}

接收消息

socket.onmessage = (event: MessageEvent) => {
  try {
    const message = JSON.parse(event.data);
    handleMessage(message);
  } catch (error) {
    console.error('消息解析失败:', error);
  }
};

并发处理

消息顺序

  • 同一客户端的消息按发送顺序处理
  • 不同客户端的消息可能并发处理
  • 服务器响应顺序可能与请求顺序不同

消息确认

对于需要确认的操作(如发送聊天消息),客户端应:

  1. 生成唯一的请求 ID
  2. 等待对应的响应
  3. 设置超时处理
async function sendChatWithConfirmation(
  socket: WebSocket,
  content: string,
  timeout: number = 5000
): Promise<void> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error('发送超时'));
    }, timeout);
    
    const handler = (event: MessageEvent) => {
      const message = JSON.parse(event.data);
      if (message.t === 'chat_sent') {
        clearTimeout(timer);
        socket.removeEventListener('message', handler);
        resolve();
      } else if (message.t === 'error') {
        clearTimeout(timer);
        socket.removeEventListener('message', handler);
        reject(new Error(message.message));
      }
    };
    
    socket.addEventListener('message', handler);
    
    socket.send(JSON.stringify({
      t: 'chat',
      content: content,
      scope: 'local'
    }));
  });
}

安全考虑

Token 安全

  • Token 仅在 login 消息中传输一次
  • 服务器验证后不再需要 Token
  • Token 应有合理的过期时间

消息验证

  • 服务器验证所有消息格式
  • 拒绝格式错误的消息
  • 记录异常消息日志

防重放攻击

  • 使用时间戳验证消息新鲜度
  • 拒绝过期的消息
  • 检测重复的消息 ID