* **新增 Zulip 模块**:包含完整的集成服务,涵盖客户端池(client pool)、会话管理及事件处理。 * **新增 WebSocket 网关**:用于处理 Zulip 的实时事件监听与双向通信。 * **新增安全服务**:支持 API 密钥加密存储及凭据的安全管理。 * **新增配置管理服务**:支持配置热加载(hot-reload),实现动态配置更新。 * **新增错误处理与监控服务**:提升系统的可靠性与可观测性。 * **新增消息过滤服务**:用于内容校验及速率限制(流控)。 * **新增流初始化与会话清理服务**:优化资源管理与回收。 * **完善测试覆盖**:包含单元测试及端到端(e2e)集成测试。 * **完善详细文档**:包括 API 参考手册、配置指南及集成概述。 * **新增地图配置系统**:实现游戏地点与 Zulip Stream(频道)及 Topic(话题)的逻辑映射。 * **新增环境变量配置**:涵盖 Zulip 服务器地址、身份验证及监控相关设置。 * **更新 App 模块**:注册并启用新的 Zulip 集成模块。 * **更新 Redis 接口**:以支持增强型的会话管理功能。 * **实现 WebSocket 协议支持**:确保与 Zulip 之间的实时双向通信。
432 lines
9.2 KiB
Markdown
432 lines
9.2 KiB
Markdown
# 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<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();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 消息序列化
|
|
|
|
### 发送消息
|
|
|
|
```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<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
|