feat: 移除Socket.IO依赖,实现原生WebSocket支持
- 移除所有Socket.IO相关装饰器和依赖 - 创建CleanWebSocketGateway使用原生WebSocket Server - 实现完整的多客户端实时同步功能 - 支持地图房间分组管理 - 支持本地和全局消息广播 - 支持位置更新实时同步 - 更新API文档和连接信息 - 完成多客户端同步测试验证 技术改进: - 使用原生ws库替代Socket.IO,减少依赖 - 实现更高效的消息路由和广播机制 - 添加地图房间自动管理功能 - 提供实时连接统计和监控接口 测试验证: - 多客户端连接和认证 - 聊天消息实时同步 - 位置更新广播 - 地图房间分组 - 系统状态监控
This commit is contained in:
346
src/business/zulip/clean_websocket.gateway.ts
Normal file
346
src/business/zulip/clean_websocket.gateway.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* 清洁的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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user