# 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 对象,包含以下基本字段: | 字段 | 类型 | 说明 | |-----|------|------| | `type` 或 `t` | string | 消息类型标识 | | 其他字段 | any | 根据消息类型不同而变化 | ### 消息类型标识 - 客户端发送的消息使用 `type` 或 `t` 字段 - 服务器响应的消息统一使用 `t` 字段 ## 客户端消息 ### LOGIN - 登录认证 ```json { "type": "login", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` | 字段 | 类型 | 必填 | 说明 | |-----|------|-----|------| | type | string | 是 | 固定值 "login" | | token | string | 是 | 游戏认证 Token | ### CHAT - 发送聊天消息 ```json { "t": "chat", "content": "Hello, everyone!", "scope": "local" } ``` | 字段 | 类型 | 必填 | 说明 | |-----|------|-----|------| | t | string | 是 | 固定值 "chat" | | content | string | 是 | 消息内容 (1-1000 字符) | | scope | string | 是 | 消息范围 | **scope 取值:** - `"local"`: 当前地图的默认 Topic - `"topic_name"`: 指定的 Topic 名称 ### POSITION - 位置更新 ```json { "t": "position", "x": 150.5, "y": 200.3, "mapId": "novice_village" } ``` | 字段 | 类型 | 必填 | 说明 | |-----|------|-----|------| | t | string | 是 | 固定值 "position" | | x | number | 是 | X 坐标 | | y | number | 是 | Y 坐标 | | mapId | string | 是 | 地图 ID | ### LOGOUT - 登出 ```json { "type": "logout" } ``` | 字段 | 类型 | 必填 | 说明 | |-----|------|-----|------| | type | string | 是 | 固定值 "logout" | ## 服务器消息 ### LOGIN_SUCCESS - 登录成功 ```json { "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 - 消息发送确认 ```json { "t": "chat_sent", "messageId": "msg_789xyz", "timestamp": 1703500800000 } ``` | 字段 | 类型 | 说明 | |-----|------|------| | t | string | 固定值 "chat_sent" | | messageId | string | Zulip 消息 ID | | timestamp | number | 发送时间戳 (毫秒) | ### CHAT_RENDER - 接收聊天消息 ```json { "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 - 位置更新确认 ```json { "t": "position_updated", "stream": "Novice Village", "topic": "General" } ``` | 字段 | 类型 | 说明 | |-----|------|------| | t | string | 固定值 "position_updated" | | stream | string | 新的 Zulip Stream | | topic | string | 新的 Zulip Topic | ### LOGOUT_SUCCESS - 登出成功 ```json { "t": "logout_success" } ``` ### ERROR - 错误消息 ```json { "t": "error", "code": "RATE_LIMIT", "message": "消息发送过于频繁,请稍后再试", "details": { "retryAfter": 60 } } ``` | 字段 | 类型 | 说明 | |-----|------|------| | t | string | 固定值 "error" | | code | string | 错误码 | | message | string | 错误描述 | | details | object | 可选,额外错误信息 | ## 心跳机制 ### 客户端心跳 客户端应每 30 秒发送一次心跳消息: ```json { "t": "ping" } ``` ### 服务器响应 ```json { "t": "pong", "timestamp": 1703500800000 } ``` ### 超时处理 - 服务器在 60 秒内未收到任何消息将断开连接 - 客户端应在连接断开后自动重连 ## 重连策略 ### 指数退避算法 ``` 重试间隔 = min(baseDelay * 2^attempt, maxDelay) baseDelay = 1000ms maxDelay = 30000ms ``` ### 重连流程 1. 检测到连接断开 2. 等待重试间隔 3. 尝试重新连接 4. 连接成功后重新发送 login 消息 5. 恢复会话状态 ### 示例代码 ```typescript 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 { 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(); } } } ``` ## 消息序列化 ### 发送消息 ```typescript function sendMessage(socket: WebSocket, message: object): void { const json = JSON.stringify(message); socket.send(json); } ``` ### 接收消息 ```typescript socket.onmessage = (event: MessageEvent) => { try { const message = JSON.parse(event.data); handleMessage(message); } catch (error) { console.error('消息解析失败:', error); } }; ``` ## 并发处理 ### 消息顺序 - 同一客户端的消息按发送顺序处理 - 不同客户端的消息可能并发处理 - 服务器响应顺序可能与请求顺序不同 ### 消息确认 对于需要确认的操作(如发送聊天消息),客户端应: 1. 生成唯一的请求 ID 2. 等待对应的响应 3. 设置超时处理 ```typescript async function sendChatWithConfirmation( socket: WebSocket, content: string, timeout: number = 5000 ): Promise { 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