feat(gateway/chat): 新增聊天网关模块
范围:src/gateway/chat/ - 新增 ChatWebSocketGateway WebSocket 网关,处理实时聊天通信 - 新增 ChatController HTTP 控制器,提供聊天历史和系统状态接口 - 新增 ChatGatewayModule 模块配置,整合网关层组件 - 新增请求/响应 DTO 定义,提供数据验证和类型约束 - 新增完整的单元测试覆盖 - 新增模块 README 文档,包含接口说明、核心特性和风险评估
This commit is contained in:
333
src/gateway/chat/README.md
Normal file
333
src/gateway/chat/README.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# 聊天网关模块 (Chat Gateway Module)
|
||||
|
||||
聊天网关模块是聊天系统的协议入口,负责处理 WebSocket 和 HTTP 请求,提供统一的 API 接口。作为 Gateway Layer 的核心组件,它专注于协议转换和路由管理,将客户端请求转发到 Business Layer 处理,不包含业务逻辑。
|
||||
|
||||
## 架构层级
|
||||
|
||||
**Gateway Layer(网关层)**
|
||||
|
||||
## 职责定位
|
||||
|
||||
网关层负责:
|
||||
|
||||
1. **协议处理**:处理 WebSocket 和 HTTP 请求
|
||||
2. **数据验证**:使用 DTO 进行请求参数验证
|
||||
3. **路由管理**:定义 API 端点和消息路由
|
||||
4. **错误转换**:将业务错误转换为协议响应
|
||||
|
||||
## 模块组成
|
||||
|
||||
```
|
||||
src/gateway/chat/
|
||||
├── chat.gateway.ts # WebSocket 网关
|
||||
├── chat.controller.ts # HTTP 控制器
|
||||
├── chat.gateway.module.ts # 网关模块配置
|
||||
├── chat.dto.ts # 请求 DTO
|
||||
├── chat_response.dto.ts # 响应 DTO
|
||||
└── README.md # 模块文档
|
||||
```
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
Gateway Layer (chat.gateway.module)
|
||||
↓ 依赖
|
||||
Business Layer (chat.module)
|
||||
↓ 依赖
|
||||
Core Layer (zulip_core.module, redis.module)
|
||||
```
|
||||
|
||||
## 对外提供的接口
|
||||
|
||||
### ChatWebSocketGateway 类
|
||||
|
||||
#### sendToPlayer(socketId: string, data: any): void
|
||||
向指定玩家的 WebSocket 连接发送消息,用于单播通信。
|
||||
|
||||
#### broadcastToMap(mapId: string, data: any, excludeId?: string): void
|
||||
向指定地图内的所有玩家广播消息,支持排除特定玩家。
|
||||
|
||||
#### getConnectionCount(): number
|
||||
获取当前 WebSocket 总连接数,用于监控和统计。
|
||||
|
||||
#### getAuthenticatedConnectionCount(): number
|
||||
获取已认证的 WebSocket 连接数,用于在线玩家统计。
|
||||
|
||||
#### getMapPlayerCounts(): Record<string, number>
|
||||
获取各地图的在线玩家数量统计,用于负载监控。
|
||||
|
||||
#### getMapPlayers(mapId: string): string[]
|
||||
获取指定地图内的所有玩家用户名列表,用于房间成员查询。
|
||||
|
||||
### ChatController 类
|
||||
|
||||
#### getChatHistory(query: GetChatHistoryDto): Promise<ChatHistoryResponseDto>
|
||||
获取聊天历史记录,支持按地图筛选和分页查询。
|
||||
|
||||
#### getSystemStatus(): Promise<SystemStatusResponseDto>
|
||||
获取聊天系统状态,包括 WebSocket 连接数、Zulip 状态、内存使用等。
|
||||
|
||||
#### getWebSocketInfo(): Promise<object>
|
||||
获取 WebSocket 连接配置信息,包括连接地址、支持的事件类型等。
|
||||
|
||||
#### sendMessage(dto: SendChatMessageDto): Promise<ChatMessageResponseDto>
|
||||
通过 REST API 发送聊天消息(不推荐),该接口会返回错误提示使用 WebSocket 连接。
|
||||
|
||||
## WebSocket 事件接口
|
||||
|
||||
### 连接地址
|
||||
```
|
||||
wss://whaletownend.xinghangee.icu/game
|
||||
```
|
||||
|
||||
### 'connection'
|
||||
客户端建立 WebSocket 连接,服务器自动分配连接 ID。
|
||||
- 输入:无(自动触发)
|
||||
- 输出:`{ type: 'connected', message: '连接成功', socketId: string }`
|
||||
|
||||
### 'login'
|
||||
用户登录认证,验证 JWT token 并建立会话。
|
||||
- 输入:`{ type: 'login', token: string }`
|
||||
- 输出成功:`{ t: 'login_success', sessionId: string, userId: string, username: string, currentMap: string }`
|
||||
- 输出失败:`{ t: 'login_error', message: string }`
|
||||
|
||||
### 'logout'
|
||||
用户主动登出,清理会话和房间信息。
|
||||
- 输入:`{ type: 'logout' }`
|
||||
- 输出:`{ t: 'logout_success', message: '登出成功' }`
|
||||
|
||||
### 'chat'
|
||||
发送聊天消息,支持本地和全局范围。
|
||||
- 输入:`{ type: 'chat', content: string, scope?: 'local' | 'global' }`
|
||||
- 输出成功:`{ t: 'chat_sent', messageId: string, message: '消息发送成功' }`
|
||||
- 输出失败:`{ t: 'chat_error', message: string }`
|
||||
|
||||
### 'position'
|
||||
更新玩家位置,自动处理地图切换和位置广播。
|
||||
- 输入:`{ type: 'position', x: number, y: number, mapId: string }`
|
||||
- 输出:广播给地图内其他玩家 `{ t: 'position_update', userId: string, username: string, x: number, y: number, mapId: string }`
|
||||
|
||||
### 'chat_render'
|
||||
接收其他玩家的聊天消息(服务器推送)。
|
||||
- 输入:无(服务器推送)
|
||||
- 输出:`{ t: 'chat_render', from: string, txt: string, scope: string, mapId: string }`
|
||||
|
||||
### 'disconnect'
|
||||
客户端断开连接,自动清理资源和通知其他玩家。
|
||||
- 输入:无(自动触发)
|
||||
- 输出:无
|
||||
|
||||
### 'error'
|
||||
通用错误消息(服务器推送)。
|
||||
- 输入:无(服务器推送)
|
||||
- 输出:`{ type: 'error', message: string }`
|
||||
|
||||
## 对外 API 接口
|
||||
|
||||
### POST /chat/send
|
||||
通过 REST API 发送聊天消息(不推荐使用)。
|
||||
- 认证:需要 JWT Bearer Token
|
||||
- 请求体:`{ content: string, scope: string, mapId?: string }`
|
||||
- 响应:返回 400 错误,提示使用 WebSocket 接口
|
||||
- 说明:该接口仅用于提示,实际聊天消息需通过 WebSocket 发送
|
||||
|
||||
### GET /chat/history
|
||||
获取聊天历史记录,支持按地图筛选和分页查询。
|
||||
- 认证:需要 JWT Bearer Token
|
||||
- 查询参数:`mapId?: string, limit?: number, offset?: number`
|
||||
- 响应:聊天消息列表和总数统计
|
||||
|
||||
### GET /chat/status
|
||||
获取聊天系统状态,包括 WebSocket 连接数、Zulip 集成状态、内存使用等。
|
||||
- 认证:无需认证
|
||||
- 响应:系统状态详细信息
|
||||
|
||||
### GET /chat/websocket/info
|
||||
获取 WebSocket 连接配置信息,包括连接地址、支持的事件类型、认证方式等。
|
||||
- 认证:无需认证
|
||||
- 响应:WebSocket 连接配置
|
||||
|
||||
## 使用的项目内部依赖
|
||||
|
||||
### ChatService (来自 business/chat/chat.service)
|
||||
聊天业务服务,处理聊天消息发送、历史查询、玩家登录登出等业务逻辑。
|
||||
|
||||
### JwtAuthGuard (来自 gateway/auth/jwt_auth.guard)
|
||||
JWT 认证守卫,用于保护需要认证的 HTTP API 接口。
|
||||
|
||||
### SendChatMessageDto (本模块)
|
||||
发送聊天消息的请求 DTO,提供消息内容和范围的验证规则。
|
||||
|
||||
### GetChatHistoryDto (本模块)
|
||||
获取聊天历史的请求 DTO,提供地图筛选和分页参数的验证规则。
|
||||
|
||||
### ChatMessageResponseDto (本模块)
|
||||
聊天消息响应 DTO,定义消息发送结果的数据结构。
|
||||
|
||||
### ChatHistoryResponseDto (本模块)
|
||||
聊天历史响应 DTO,定义历史消息列表的数据结构。
|
||||
|
||||
### SystemStatusResponseDto (本模块)
|
||||
系统状态响应 DTO,定义系统状态信息的数据结构。
|
||||
|
||||
### LoginCoreModule (来自 core/login_core/login_core.module)
|
||||
登录核心模块,提供 JWT 验证和认证功能。
|
||||
|
||||
### ChatModule (来自 business/chat/chat.module)
|
||||
聊天业务模块,提供聊天相关的业务逻辑处理。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### WebSocket 连接管理
|
||||
- 原生 WebSocket 支持:基于 ws 库的原生 WebSocket 实现
|
||||
- 连接生命周期管理:自动处理连接建立、认证、断开和清理
|
||||
- 连接状态追踪:维护连接 ID、认证状态、用户信息等
|
||||
- 心跳检测机制:通过 isAlive 标记检测连接活性
|
||||
|
||||
### 地图房间系统
|
||||
- 动态房间管理:根据玩家所在地图自动创建和销毁房间
|
||||
- 房间成员追踪:维护每个地图的玩家列表
|
||||
- 自动房间切换:玩家切换地图时自动加入新房间并离开旧房间
|
||||
- 房间广播优化:仅向房间内的已认证玩家广播消息
|
||||
|
||||
### 实时消息广播
|
||||
- 单播通信:向指定玩家发送消息
|
||||
- 地图广播:向地图内所有玩家广播消息,支持排除发送者
|
||||
- 位置同步:实时广播玩家位置更新给房间成员
|
||||
- 聊天消息推送:接收业务层的聊天消息并推送给客户端
|
||||
|
||||
### 协议转换与路由
|
||||
- 消息类型路由:根据消息类型自动路由到对应处理方法
|
||||
- 协议格式统一:统一 WebSocket 和 HTTP 的响应格式
|
||||
- 错误转换:将业务层错误转换为客户端友好的错误消息
|
||||
- DTO 数据验证:使用 class-validator 进行请求参数验证
|
||||
|
||||
### 监控与统计
|
||||
- 连接数统计:实时统计总连接数和已认证连接数
|
||||
- 地图人数统计:统计各地图的在线玩家数量
|
||||
- 系统状态监控:提供内存使用、运行时间等系统指标
|
||||
- 日志记录:记录连接、消息、错误等关键事件
|
||||
|
||||
## 潜在风险
|
||||
|
||||
### WebSocket 连接管理风险
|
||||
- 大量并发连接可能导致内存占用过高
|
||||
- 连接泄漏风险:异常断开时可能未正确清理资源
|
||||
- 僵尸连接问题:网络异常时连接可能长时间挂起
|
||||
- 缓解措施:实现连接数限制、定期清理超时连接、完善错误处理
|
||||
|
||||
### 实时通信性能风险
|
||||
- 高频位置更新可能导致服务器 CPU 压力
|
||||
- 大房间广播延迟:房间人数过多时广播性能下降
|
||||
- 消息队列堆积:处理速度慢于接收速度时消息堆积
|
||||
- 缓解措施:位置更新限流、分片广播、消息优先级队列
|
||||
|
||||
### 认证与安全风险
|
||||
- JWT token 泄露风险:WebSocket 连接中 token 可能被截获
|
||||
- 未认证消息攻击:恶意客户端可能发送大量未认证消息
|
||||
- 消息内容安全:缺少消息内容的安全过滤
|
||||
- 缓解措施:使用 WSS 加密传输、限制未认证连接的消息频率、在业务层进行内容过滤
|
||||
|
||||
### 资源清理风险
|
||||
- 断开连接时资源清理不完整可能导致内存泄漏
|
||||
- 地图房间未及时清理导致空房间占用内存
|
||||
- 客户端映射未清理导致无效引用
|
||||
- 缓解措施:完善 cleanupClient 方法、定期清理空房间、使用 WeakMap 避免内存泄漏
|
||||
|
||||
### 错误处理风险
|
||||
- 业务层异常未正确捕获可能导致连接中断
|
||||
- 消息解析失败可能导致连接关闭
|
||||
- 错误信息泄露敏感信息
|
||||
- 缓解措施:完善 try-catch 覆盖、统一错误处理、脱敏错误消息
|
||||
|
||||
### 扩展性风险
|
||||
- 单实例 WebSocket 服务器无法水平扩展
|
||||
- 内存存储的房间信息无法跨实例共享
|
||||
- 负载均衡时 WebSocket 连接可能断开
|
||||
- 缓解措施:引入 Redis 共享房间信息、使用 Sticky Session、实现 WebSocket 集群
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 只做协议转换,不做业务逻辑
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:只做协议处理
|
||||
private async handleChat(ws: ExtendedWebSocket, message: any) {
|
||||
if (!ws.authenticated) {
|
||||
this.sendError(ws, '请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.chatService.sendChatMessage({
|
||||
socketId: ws.id,
|
||||
content: message.content,
|
||||
scope: message.scope || 'local'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.sendMessage(ws, { t: 'chat_sent', messageId: result.messageId });
|
||||
} else {
|
||||
this.sendMessage(ws, { t: 'chat_error', message: result.error });
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 错误:在网关中包含业务逻辑
|
||||
private async handleChat(ws: ExtendedWebSocket, message: any) {
|
||||
// 不应该在这里做敏感词过滤、频率限制等业务逻辑
|
||||
if (message.content.includes('敏感词')) {
|
||||
this.sendError(ws, '包含敏感词');
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 统一的错误处理
|
||||
|
||||
```typescript
|
||||
private sendError(ws: ExtendedWebSocket, message: string) {
|
||||
this.sendMessage(ws, { type: 'error', message });
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### WebSocket 连接示例
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game');
|
||||
|
||||
ws.onopen = () => {
|
||||
// 登录
|
||||
ws.send(JSON.stringify({
|
||||
type: 'login',
|
||||
token: 'your-jwt-token'
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.t || data.type) {
|
||||
case 'login_success':
|
||||
console.log('登录成功', data);
|
||||
break;
|
||||
case 'chat_render':
|
||||
console.log('收到消息', data.from, data.txt);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 发送聊天消息
|
||||
ws.send(JSON.stringify({
|
||||
type: 'chat',
|
||||
content: '大家好!',
|
||||
scope: 'local'
|
||||
}));
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 网关层不应该直接访问数据库
|
||||
- 网关层不应该包含复杂的业务逻辑
|
||||
- 所有业务逻辑都应该在 Business 层实现
|
||||
- WebSocket 连接需要先登录才能发送聊天消息
|
||||
Reference in New Issue
Block a user