/** * 清洁的WebSocket网关 - 优化版本 * * 功能描述: * - 使用原生WebSocket,不依赖NestJS的WebSocket装饰器 * - 支持游戏内实时聊天广播 * - 与优化后的ZulipService集成 * * 核心优化: * - 🚀 实时消息广播:直接广播给同区域玩家 * - 🔄 与ZulipService的异步同步集成 * - ⚡ 低延迟聊天体验 * * 最近修改: * - 2026-01-10: 重构优化 - 适配优化后的ZulipService,支持实时广播 (修改者: moyin) */ import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import * as WebSocket from 'ws'; import { ZulipService } from './zulip.service'; import { SessionManagerService } from './services/session_manager.service'; interface ExtendedWebSocket extends WebSocket { id: string; isAlive?: boolean; authenticated?: boolean; userId?: string; username?: string; sessionId?: string; currentMap?: string; } @Injectable() export class CleanWebSocketGateway implements OnModuleInit, OnModuleDestroy { private server: WebSocket.Server; private readonly logger = new Logger(CleanWebSocketGateway.name); private clients = new Map(); private mapRooms = new Map>(); // mapId -> Set constructor( private readonly zulipService: ZulipService, private readonly sessionManager: SessionManagerService, ) {} async onModuleInit() { const port = process.env.WEBSOCKET_PORT ? parseInt(process.env.WEBSOCKET_PORT) : 3001; this.server = new WebSocket.Server({ port, path: '/game' // 统一使用 /game 路径 }); this.server.on('connection', (ws: ExtendedWebSocket) => { ws.id = this.generateClientId(); ws.isAlive = true; ws.authenticated = false; this.clients.set(ws.id, ws); this.logger.log(`新的WebSocket连接: ${ws.id}`); ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleMessage(ws, message); } catch (error) { this.logger.error('解析消息失败', error); this.sendError(ws, '消息格式错误'); } }); ws.on('close', (code, reason) => { this.logger.log(`WebSocket连接关闭: ${ws.id}`, { code, reason: reason?.toString(), authenticated: ws.authenticated, username: ws.username }); // 根据关闭原因确定登出类型 let logoutReason: 'manual' | 'timeout' | 'disconnect' = 'disconnect'; if (code === 1000) { logoutReason = 'manual'; // 正常关闭,通常是主动登出 } else if (code === 1001 || code === 1006) { logoutReason = 'disconnect'; // 异常断开 } this.cleanupClient(ws, logoutReason); }); ws.on('error', (error) => { this.logger.error(`WebSocket错误: ${ws.id}`, error); }); // 发送连接确认 this.sendMessage(ws, { type: 'connected', message: '连接成功', socketId: ws.id }); }); // 🔄 设置WebSocket网关引用到ZulipService this.zulipService.setWebSocketGateway(this); this.logger.log(`WebSocket服务器启动成功,端口: ${port},路径: /game`); } async onModuleDestroy() { if (this.server) { this.server.close(); this.logger.log('WebSocket服务器已关闭'); } } private async handleMessage(ws: ExtendedWebSocket, message: any) { this.logger.log(`收到消息: ${ws.id}`, message); const messageType = message.type || message.t; this.logger.log(`消息类型: ${messageType}`, { type: message.type, t: message.t }); switch (messageType) { case 'login': await this.handleLogin(ws, message); break; case 'logout': await this.handleLogout(ws, message); break; case 'chat': await this.handleChat(ws, message); break; case 'position': await this.handlePositionUpdate(ws, message); break; default: this.logger.warn(`未知消息类型: ${messageType}`, message); this.sendError(ws, `未知消息类型: ${messageType}`); } } private async handleLogin(ws: ExtendedWebSocket, message: any) { try { if (!message.token) { this.sendError(ws, 'Token不能为空'); return; } // 调用ZulipService进行登录 const result = await this.zulipService.handlePlayerLogin({ socketId: ws.id, token: message.token }); if (result.success) { ws.authenticated = true; ws.userId = result.userId; ws.username = result.username; ws.sessionId = result.sessionId; ws.currentMap = 'whale_port'; // 默认地图 // 加入默认地图房间 this.joinMapRoom(ws.id, ws.currentMap); this.sendMessage(ws, { t: 'login_success', sessionId: result.sessionId, userId: result.userId, username: result.username, currentMap: ws.currentMap }); this.logger.log(`用户登录成功: ${result.username} (${ws.id}) 进入地图: ${ws.currentMap}`); } else { this.sendMessage(ws, { t: 'login_error', message: result.error || '登录失败' }); } } catch (error) { this.logger.error('登录处理失败', error); this.sendError(ws, '登录处理失败'); } } /** * 处理主动登出请求 */ private async handleLogout(ws: ExtendedWebSocket, message: any) { try { if (!ws.authenticated) { this.sendError(ws, '用户未登录'); return; } this.logger.log(`用户主动登出: ${ws.username} (${ws.id})`); // 调用ZulipService处理登出,标记为主动登出 await this.zulipService.handlePlayerLogout(ws.id, 'manual'); // 清理WebSocket状态 this.cleanupClient(ws); this.sendMessage(ws, { t: 'logout_success', message: '登出成功' }); // 关闭WebSocket连接 ws.close(1000, '用户主动登出'); } catch (error) { this.logger.error('登出处理失败', error); this.sendError(ws, '登出处理失败'); } } private async handleChat(ws: ExtendedWebSocket, message: any) { try { if (!ws.authenticated) { this.sendError(ws, '请先登录'); return; } if (!message.content) { this.sendError(ws, '消息内容不能为空'); return; } // 🚀 调用优化后的ZulipService发送消息(实时广播+异步同步) const result = await this.zulipService.sendChatMessage({ socketId: ws.id, content: message.content, scope: message.scope || 'local' }); if (result.success) { this.sendMessage(ws, { t: 'chat_sent', messageId: result.messageId, message: '消息发送成功' }); this.logger.log(`消息发送成功: ${ws.username} -> ${message.content}`); } else { this.sendMessage(ws, { t: 'chat_error', message: result.error || '消息发送失败' }); } } catch (error) { this.logger.error('聊天处理失败', error); this.sendError(ws, '聊天处理失败'); } } private async handlePositionUpdate(ws: ExtendedWebSocket, message: any) { try { if (!ws.authenticated) { this.sendError(ws, '请先登录'); return; } // 简单的位置更新处理,这里可以添加更多逻辑 this.logger.log(`位置更新: ${ws.username} -> (${message.x}, ${message.y}) 在 ${message.mapId}`); // 如果用户切换了地图,更新房间 if (ws.currentMap !== message.mapId) { this.leaveMapRoom(ws.id, ws.currentMap); this.joinMapRoom(ws.id, message.mapId); ws.currentMap = message.mapId; this.logger.log(`用户 ${ws.username} 切换到地图: ${message.mapId}`); } // 广播位置更新给同一地图的其他用户 this.broadcastToMap(message.mapId, { t: 'position_update', userId: ws.userId, username: ws.username, x: message.x, y: message.y, mapId: message.mapId }, ws.id); } catch (error) { this.logger.error('位置更新处理失败', error); this.sendError(ws, '位置更新处理失败'); } } // 🚀 实现IWebSocketGateway接口方法,供ZulipService调用 /** * 向指定玩家发送消息 * * @param socketId 目标Socket ID * @param data 消息数据 */ public sendToPlayer(socketId: string, data: any): void { const client = this.clients.get(socketId); if (client && client.readyState === WebSocket.OPEN) { this.sendMessage(client, data); } } /** * 向指定地图广播消息 * * @param mapId 地图ID * @param data 消息数据 * @param excludeId 排除的Socket ID */ public broadcastToMap(mapId: string, data: any, excludeId?: string): void { const room = this.mapRooms.get(mapId); if (!room) return; room.forEach(clientId => { if (clientId !== excludeId) { const client = this.clients.get(clientId); if (client && client.authenticated && client.readyState === WebSocket.OPEN) { this.sendMessage(client, data); } } }); } // 原有的私有方法保持不变 private sendMessage(ws: ExtendedWebSocket, data: any) { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(data)); } } private sendError(ws: ExtendedWebSocket, message: string) { this.sendMessage(ws, { type: 'error', message: message }); } private broadcastMessage(data: any, excludeId?: string) { this.clients.forEach((client, id) => { if (id !== excludeId && client.authenticated) { this.sendMessage(client, data); } }); } private joinMapRoom(clientId: string, mapId: string) { if (!this.mapRooms.has(mapId)) { this.mapRooms.set(mapId, new Set()); } this.mapRooms.get(mapId).add(clientId); this.logger.log(`客户端 ${clientId} 加入地图房间: ${mapId}`); } private leaveMapRoom(clientId: string, mapId: string) { const room = this.mapRooms.get(mapId); if (room) { room.delete(clientId); if (room.size === 0) { this.mapRooms.delete(mapId); } this.logger.log(`客户端 ${clientId} 离开地图房间: ${mapId}`); } } private async cleanupClient(ws: ExtendedWebSocket, reason: 'manual' | 'timeout' | 'disconnect' = 'disconnect') { try { // 如果用户已认证,调用ZulipService处理登出 if (ws.authenticated && ws.id) { this.logger.log(`清理已认证用户: ${ws.username} (${ws.id})`, { reason }); await this.zulipService.handlePlayerLogout(ws.id, reason); } // 从地图房间中移除 if (ws.currentMap) { this.leaveMapRoom(ws.id, ws.currentMap); } // 从客户端列表中移除 this.clients.delete(ws.id); this.logger.log(`客户端清理完成: ${ws.id}`, { reason, wasAuthenticated: ws.authenticated, username: ws.username }); } catch (error) { this.logger.error(`清理客户端失败: ${ws.id}`, { error: (error as Error).message, reason, username: ws.username }); } } private generateClientId(): string { return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // 公共方法供其他服务调用 public getConnectionCount(): number { return this.clients.size; } public getAuthenticatedConnectionCount(): number { return Array.from(this.clients.values()).filter(client => client.authenticated).length; } public getMapPlayerCounts(): Record { const counts: Record = {}; this.mapRooms.forEach((clients, mapId) => { counts[mapId] = clients.size; }); return counts; } public getMapPlayers(mapId: string): string[] { const room = this.mapRooms.get(mapId); if (!room) return []; const players: string[] = []; room.forEach(clientId => { const client = this.clients.get(clientId); if (client && client.authenticated && client.username) { players.push(client.username); } }); return players; } }