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