Files
whale-town-end/src/business/zulip/clean_websocket.gateway.ts
moyin 75ac7ac0f8 websocket:统一WebSocket网关配置
- 为CleanWebSocketGateway添加/game路径配置
- 支持通过环境变量WEBSOCKET_PORT配置端口
- 移除ZulipWebSocketGateway的模块引用
- 统一使用CleanWebSocketGateway作为唯一WebSocket网关
- 更新模块注释,反映当前架构
2026-01-09 17:45:51 +08:00

349 lines
9.8 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 = 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', () => {
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},路径: /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 '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;
}
}