2025-08-14 23:59:00 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* WebSocket传输层服务端实现
|
|
|
|
|
|
*/
|
|
|
|
|
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
|
|
|
|
import { createLogger, Core } from '@esengine/ecs-framework';
|
|
|
|
|
|
import {
|
|
|
|
|
|
ITransport,
|
|
|
|
|
|
ITransportClientInfo,
|
|
|
|
|
|
ITransportConfig,
|
|
|
|
|
|
ConnectionState,
|
|
|
|
|
|
EventEmitter
|
|
|
|
|
|
} from '@esengine/network-shared';
|
|
|
|
|
|
import * as crypto from 'crypto';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* WebSocket传输层实现
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class WebSocketTransport extends EventEmitter implements ITransport {
|
|
|
|
|
|
private logger = createLogger('WebSocketTransport');
|
|
|
|
|
|
private server?: WebSocketServer;
|
|
|
|
|
|
private clients: Map<string, WebSocket> = new Map();
|
|
|
|
|
|
private clientInfo: Map<string, ITransportClientInfo> = new Map();
|
|
|
|
|
|
private config: ITransportConfig;
|
|
|
|
|
|
private isRunning = false;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 连接事件处理器
|
|
|
|
|
|
*/
|
|
|
|
|
|
private connectHandlers: ((clientInfo: ITransportClientInfo) => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 断开连接事件处理器
|
|
|
|
|
|
*/
|
|
|
|
|
|
private disconnectHandlers: ((clientId: string, reason?: string) => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 消息接收事件处理器
|
|
|
|
|
|
*/
|
|
|
|
|
|
private messageHandlers: ((clientId: string, data: ArrayBuffer | string) => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 错误事件处理器
|
|
|
|
|
|
*/
|
|
|
|
|
|
private errorHandlers: ((error: Error) => void)[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构造函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
constructor(config: ITransportConfig) {
|
|
|
|
|
|
super();
|
|
|
|
|
|
this.config = {
|
|
|
|
|
|
maxConnections: 1000,
|
|
|
|
|
|
heartbeatInterval: 30000,
|
|
|
|
|
|
connectionTimeout: 60000,
|
|
|
|
|
|
maxMessageSize: 1024 * 1024, // 1MB
|
|
|
|
|
|
compression: true,
|
|
|
|
|
|
...config
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动传输层
|
|
|
|
|
|
*/
|
|
|
|
|
|
async start(port: number, host?: string): Promise<void> {
|
|
|
|
|
|
if (this.isRunning) {
|
|
|
|
|
|
this.logger.warn('WebSocket传输层已在运行');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.server = new WebSocketServer({
|
|
|
|
|
|
port,
|
|
|
|
|
|
host: host || '0.0.0.0',
|
|
|
|
|
|
maxPayload: this.config.maxMessageSize,
|
|
|
|
|
|
perMessageDeflate: this.config.compression ? {
|
|
|
|
|
|
threshold: 1024,
|
|
|
|
|
|
concurrencyLimit: 10,
|
|
|
|
|
|
clientNoContextTakeover: false,
|
|
|
|
|
|
serverNoContextTakeover: false
|
|
|
|
|
|
} : false
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.setupServerEvents();
|
|
|
|
|
|
this.isRunning = true;
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`WebSocket服务器已启动: ${host || '0.0.0.0'}:${port}`);
|
|
|
|
|
|
this.logger.info(`最大连接数: ${this.config.maxConnections}`);
|
|
|
|
|
|
this.logger.info(`压缩: ${this.config.compression ? '启用' : '禁用'}`);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('启动WebSocket服务器失败:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 停止传输层
|
|
|
|
|
|
*/
|
|
|
|
|
|
async stop(): Promise<void> {
|
|
|
|
|
|
if (!this.isRunning || !this.server) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
// 断开所有客户端连接
|
|
|
|
|
|
for (const [clientId, ws] of this.clients) {
|
|
|
|
|
|
ws.close(1001, '服务器关闭');
|
|
|
|
|
|
this.handleClientDisconnect(clientId, '服务器关闭');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭服务器
|
|
|
|
|
|
this.server!.close(() => {
|
|
|
|
|
|
this.isRunning = false;
|
|
|
|
|
|
this.server = undefined;
|
|
|
|
|
|
this.logger.info('WebSocket服务器已停止');
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送数据到指定客户端
|
|
|
|
|
|
*/
|
|
|
|
|
|
send(clientId: string, data: ArrayBuffer | string): void {
|
|
|
|
|
|
const ws = this.clients.get(clientId);
|
|
|
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
|
|
|
|
this.logger.warn(`尝试向未连接的客户端发送消息: ${clientId}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
ws.send(data);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`发送消息到客户端 ${clientId} 失败:`, error);
|
|
|
|
|
|
this.handleError(error as Error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 广播数据到所有客户端
|
|
|
|
|
|
*/
|
|
|
|
|
|
broadcast(data: ArrayBuffer | string, exclude?: string[]): void {
|
|
|
|
|
|
const excludeSet = new Set(exclude || []);
|
|
|
|
|
|
|
|
|
|
|
|
for (const [clientId, ws] of this.clients) {
|
|
|
|
|
|
if (excludeSet.has(clientId) || ws.readyState !== WebSocket.OPEN) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
ws.send(data);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`广播消息到客户端 ${clientId} 失败:`, error);
|
|
|
|
|
|
this.handleError(error as Error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 监听客户端连接事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
onConnect(handler: (clientInfo: ITransportClientInfo) => void): void {
|
|
|
|
|
|
this.connectHandlers.push(handler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 监听客户端断开事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
onDisconnect(handler: (clientId: string, reason?: string) => void): void {
|
|
|
|
|
|
this.disconnectHandlers.push(handler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 监听消息接收事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
onMessage(handler: (clientId: string, data: ArrayBuffer | string) => void): void {
|
|
|
|
|
|
this.messageHandlers.push(handler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 监听错误事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
onError(handler: (error: Error) => void): void {
|
|
|
|
|
|
this.errorHandlers.push(handler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取连接的客户端数量
|
|
|
|
|
|
*/
|
|
|
|
|
|
getClientCount(): number {
|
|
|
|
|
|
return this.clients.size;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查客户端是否连接
|
|
|
|
|
|
*/
|
|
|
|
|
|
isClientConnected(clientId: string): boolean {
|
|
|
|
|
|
const ws = this.clients.get(clientId);
|
|
|
|
|
|
return ws !== undefined && ws.readyState === WebSocket.OPEN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 断开指定客户端
|
|
|
|
|
|
*/
|
|
|
|
|
|
disconnectClient(clientId: string, reason?: string): void {
|
|
|
|
|
|
const ws = this.clients.get(clientId);
|
|
|
|
|
|
if (ws) {
|
|
|
|
|
|
ws.close(1000, reason || '服务器主动断开');
|
|
|
|
|
|
this.handleClientDisconnect(clientId, reason);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取客户端信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
getClientInfo(clientId: string): ITransportClientInfo | undefined {
|
|
|
|
|
|
return this.clientInfo.get(clientId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有客户端信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
getAllClients(): ITransportClientInfo[] {
|
|
|
|
|
|
return Array.from(this.clientInfo.values());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置服务器事件监听
|
|
|
|
|
|
*/
|
|
|
|
|
|
private setupServerEvents(): void {
|
|
|
|
|
|
if (!this.server) return;
|
|
|
|
|
|
|
|
|
|
|
|
this.server.on('connection', (ws, request) => {
|
|
|
|
|
|
this.handleNewConnection(ws, request);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.server.on('error', (error) => {
|
|
|
|
|
|
this.logger.error('WebSocket服务器错误:', error);
|
|
|
|
|
|
this.handleError(error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.server.on('close', () => {
|
|
|
|
|
|
this.logger.info('WebSocket服务器已关闭');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理新客户端连接
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleNewConnection(ws: WebSocket, request: any): void {
|
|
|
|
|
|
// 检查连接数限制
|
|
|
|
|
|
if (this.clients.size >= this.config.maxConnections!) {
|
|
|
|
|
|
this.logger.warn('达到最大连接数限制,拒绝新连接');
|
|
|
|
|
|
ws.close(1013, '服务器繁忙');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const clientId = crypto.randomUUID();
|
|
|
|
|
|
const clientInfo: ITransportClientInfo = {
|
|
|
|
|
|
id: clientId,
|
|
|
|
|
|
remoteAddress: request.socket.remoteAddress || 'unknown',
|
|
|
|
|
|
connectTime: Date.now(),
|
|
|
|
|
|
userAgent: request.headers['user-agent'],
|
|
|
|
|
|
headers: request.headers
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 存储客户端连接和信息
|
|
|
|
|
|
this.clients.set(clientId, ws);
|
|
|
|
|
|
this.clientInfo.set(clientId, clientInfo);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置WebSocket事件监听
|
|
|
|
|
|
this.setupClientEvents(ws, clientId);
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`新客户端连接: ${clientId} from ${clientInfo.remoteAddress}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 触发连接事件
|
|
|
|
|
|
this.connectHandlers.forEach(handler => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
handler(clientInfo);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('连接事件处理器错误:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置客户端WebSocket事件监听
|
|
|
|
|
|
*/
|
|
|
|
|
|
private setupClientEvents(ws: WebSocket, clientId: string): void {
|
|
|
|
|
|
// 消息接收
|
|
|
|
|
|
ws.on('message', (data) => {
|
|
|
|
|
|
this.handleClientMessage(clientId, data);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 连接关闭
|
|
|
|
|
|
ws.on('close', (code, reason) => {
|
|
|
|
|
|
this.handleClientDisconnect(clientId, reason?.toString() || `Code: ${code}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 错误处理
|
|
|
|
|
|
ws.on('error', (error) => {
|
|
|
|
|
|
this.logger.error(`客户端 ${clientId} WebSocket错误:`, error);
|
|
|
|
|
|
this.handleError(error);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Pong响应(心跳)
|
|
|
|
|
|
ws.on('pong', () => {
|
|
|
|
|
|
// 记录客户端响应心跳
|
|
|
|
|
|
const info = this.clientInfo.get(clientId);
|
|
|
|
|
|
if (info) {
|
|
|
|
|
|
// 可以更新延迟信息
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 设置连接超时
|
|
|
|
|
|
if (this.config.connectionTimeout) {
|
|
|
|
|
|
Core.schedule(this.config.connectionTimeout / 1000, false, this, () => {
|
|
|
|
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
|
ws.ping();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理客户端消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleClientMessage(clientId: string, data: WebSocket.Data): void {
|
|
|
|
|
|
try {
|
2025-09-04 16:25:18 +08:00
|
|
|
|
// 检查是否为有效的应用消息
|
|
|
|
|
|
if (!this.isApplicationMessage(data)) {
|
|
|
|
|
|
this.logger.debug(`忽略非应用消息 (${clientId}): ${typeof data} ${data instanceof ArrayBuffer ? data.byteLength : data.toString().length} bytes`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 23:59:00 +08:00
|
|
|
|
const message = data instanceof ArrayBuffer ? data : new TextEncoder().encode(data.toString()).buffer;
|
|
|
|
|
|
|
|
|
|
|
|
// 触发消息事件
|
|
|
|
|
|
this.messageHandlers.forEach(handler => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
handler(clientId, message);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('消息事件处理器错误:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`处理客户端 ${clientId} 消息失败:`, error);
|
|
|
|
|
|
this.handleError(error as Error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 16:25:18 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否为有效的应用消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
private isApplicationMessage(data: WebSocket.Data): boolean {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 转换为字符串进行检查
|
|
|
|
|
|
const jsonString = data instanceof ArrayBuffer
|
|
|
|
|
|
? new TextDecoder().decode(data)
|
|
|
|
|
|
: data.toString();
|
|
|
|
|
|
|
|
|
|
|
|
// 基本长度检查 - 空消息或过短消息通常不是应用消息
|
|
|
|
|
|
if (!jsonString || jsonString.length < 10) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试解析JSON
|
|
|
|
|
|
const parsed = JSON.parse(jsonString);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有基本的消息结构
|
|
|
|
|
|
return parsed &&
|
|
|
|
|
|
typeof parsed === 'object' &&
|
|
|
|
|
|
(parsed.type || parsed.messageId || parsed.data);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// JSON解析失败,可能是握手数据或其他非JSON消息
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-14 23:59:00 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理客户端断开连接
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleClientDisconnect(clientId: string, reason?: string): void {
|
|
|
|
|
|
// 清理客户端数据
|
|
|
|
|
|
this.clients.delete(clientId);
|
|
|
|
|
|
this.clientInfo.delete(clientId);
|
|
|
|
|
|
|
|
|
|
|
|
this.logger.info(`客户端断开连接: ${clientId}, 原因: ${reason || '未知'}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 触发断开连接事件
|
|
|
|
|
|
this.disconnectHandlers.forEach(handler => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
handler(clientId, reason);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error('断开连接事件处理器错误:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理错误
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleError(error: Error): void {
|
|
|
|
|
|
this.errorHandlers.forEach(handler => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
handler(error);
|
|
|
|
|
|
} catch (handlerError) {
|
|
|
|
|
|
this.logger.error('错误事件处理器错误:', handlerError);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 发送心跳到所有客户端
|
|
|
|
|
|
*/
|
|
|
|
|
|
public sendHeartbeat(): void {
|
|
|
|
|
|
for (const [clientId, ws] of this.clients) {
|
|
|
|
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
ws.ping();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.logger.error(`发送心跳到客户端 ${clientId} 失败:`, error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取传输层统计信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
public getStats() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
isRunning: this.isRunning,
|
|
|
|
|
|
clientCount: this.clients.size,
|
|
|
|
|
|
maxConnections: this.config.maxConnections,
|
|
|
|
|
|
compressionEnabled: this.config.compression,
|
|
|
|
|
|
clients: this.getAllClients()
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|