Files
whale-town-end/src/business/zulip/clean_websocket.gateway.ts
moyin e9dc887c59 feat: 移除Socket.IO依赖,实现原生WebSocket支持
- 移除所有Socket.IO相关装饰器和依赖
- 创建CleanWebSocketGateway使用原生WebSocket Server
- 实现完整的多客户端实时同步功能
- 支持地图房间分组管理
- 支持本地和全局消息广播
- 支持位置更新实时同步
- 更新API文档和连接信息
- 完成多客户端同步测试验证

技术改进:
- 使用原生ws库替代Socket.IO,减少依赖
- 实现更高效的消息路由和广播机制
- 添加地图房间自动管理功能
- 提供实时连接统计和监控接口

测试验证:
-  多客户端连接和认证
-  聊天消息实时同步
-  位置更新广播
-  地图房间分组
-  系统状态监控
2026-01-09 17:00:23 +08:00

346 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 清洁的WebSocket网关
* 使用原生WebSocket不依赖NestJS的WebSocket装饰器
*/
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<string, ExtendedWebSocket>();
private mapRooms = new Map<string, Set<string>>(); // mapId -> Set<clientId>
constructor(
private readonly zulipService: ZulipService,
private readonly sessionManager: SessionManagerService,
) {}
async onModuleInit() {
const port = 3001;
this.server = new WebSocket.Server({ port });
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', () => {
this.logger.log(`WebSocket连接关闭: ${ws.id}`);
this.cleanupClient(ws);
});
ws.on('error', (error) => {
this.logger.error(`WebSocket错误: ${ws.id}`, error);
});
// 发送连接确认
this.sendMessage(ws, {
type: 'connected',
message: '连接成功',
socketId: ws.id
});
});
this.logger.log(`WebSocket服务器启动成功端口: ${port}`);
}
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 '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 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: '消息发送成功'
});
// 广播消息给其他用户根据scope决定范围
if (message.scope === 'global') {
// 全局消息:广播给所有已认证用户
this.broadcastMessage({
t: 'chat_render',
from: ws.username,
txt: message.content,
bubble: true,
scope: 'global'
}, ws.id);
} else {
// 本地消息:只广播给同一地图的用户
this.broadcastToMap(ws.currentMap, {
t: 'chat_render',
from: ws.username,
txt: message.content,
bubble: true,
scope: 'local',
mapId: ws.currentMap
}, ws.id);
}
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, '位置更新处理失败');
}
}
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 broadcastToMap(mapId: string, data: any, excludeId?: string) {
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) {
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 cleanupClient(ws: ExtendedWebSocket) {
// 从地图房间中移除
if (ws.currentMap) {
this.leaveMapRoom(ws.id, ws.currentMap);
}
// 从客户端列表中移除
this.clients.delete(ws.id);
}
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<string, number> {
const counts: Record<string, number> = {};
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;
}
}