From 6730a5d6254cbbe27a96501c8d0df9687cf3c417 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Thu, 14 Aug 2025 23:59:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=A0=E8=BE=93=E5=B1=82=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=EF=BC=88=E5=AE=A2=E6=88=B7=E7=AB=AF/=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=EF=BC=8C=E9=93=BE=E6=8E=A5=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E5=BF=83=E8=B7=B3=E6=9C=BA=E5=88=B6=EF=BC=8C=E9=87=8D=E8=BF=9E?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=EF=BC=89=20=E6=B6=88=E6=81=AF=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96=EF=BC=88json=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E6=B6=88=E6=81=AF=E5=8E=8B=E7=BC=A9=EF=BC=8C=E6=B6=88?= =?UTF-8?q?=E6=81=AFID=E5=92=8C=E6=97=B6=E9=97=B4=E6=88=B3=EF=BC=89=20?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=EF=BC=88networkserver/=E5=9F=BA=E7=A1=80room/=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5=EF=BC=89=20?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=EF=BC=88networkclient/=E6=B6=88=E6=81=AF=E9=98=9F=E5=88=97?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/network-client/jest.config.cjs | 47 -- packages/network-client/package.json | 5 +- .../src/core/ConnectionStateManager.ts | 638 ++++++++++++++++ .../network-client/src/core/MessageQueue.ts | 680 +++++++++++++++++ .../network-client/src/core/NetworkClient.ts | 698 +++++++++++++++++ packages/network-client/src/index.ts | 22 +- .../src/transport/ReconnectionManager.ts | 410 ++++++++++ .../src/transport/WebSocketClient.ts | 406 ++++++++++ .../network-client/src/utils/NetworkTimer.ts | 103 +++ packages/network-client/src/utils/index.ts | 4 + packages/network-client/tests/setup.ts | 65 -- packages/network-server/jest.config.cjs | 47 -- packages/network-server/package.json | 7 +- .../src/core/ConnectionManager.ts | 410 ++++++++++ .../network-server/src/core/NetworkServer.ts | 701 ++++++++++++++++++ packages/network-server/src/index.ts | 24 +- packages/network-server/src/rooms/Room.ts | 507 +++++++++++++ .../network-server/src/rooms/RoomManager.ts | 621 ++++++++++++++++ .../src/transport/WebSocketTransport.ts | 407 ++++++++++ packages/network-server/tests/setup.ts | 26 - packages/network-shared/src/index.ts | 17 +- .../src/protocols/MessageManager.ts | 502 +++++++++++++ .../src/serialization/JSONSerializer.ts | 550 ++++++++++++++ .../src/serialization/MessageCompressor.ts | 498 +++++++++++++ .../src/transport/ErrorHandler.ts | 461 ++++++++++++ .../src/transport/HeartbeatManager.ts | 381 ++++++++++ .../src/types/TransportTypes.ts | 10 +- .../network-shared/src/utils/EventEmitter.ts | 85 +++ packages/network-shared/src/utils/index.ts | 4 + 29 files changed, 8100 insertions(+), 236 deletions(-) delete mode 100644 packages/network-client/jest.config.cjs create mode 100644 packages/network-client/src/core/ConnectionStateManager.ts create mode 100644 packages/network-client/src/core/MessageQueue.ts create mode 100644 packages/network-client/src/core/NetworkClient.ts create mode 100644 packages/network-client/src/transport/ReconnectionManager.ts create mode 100644 packages/network-client/src/transport/WebSocketClient.ts create mode 100644 packages/network-client/src/utils/NetworkTimer.ts create mode 100644 packages/network-client/src/utils/index.ts delete mode 100644 packages/network-client/tests/setup.ts delete mode 100644 packages/network-server/jest.config.cjs create mode 100644 packages/network-server/src/core/ConnectionManager.ts create mode 100644 packages/network-server/src/core/NetworkServer.ts create mode 100644 packages/network-server/src/rooms/Room.ts create mode 100644 packages/network-server/src/rooms/RoomManager.ts create mode 100644 packages/network-server/src/transport/WebSocketTransport.ts delete mode 100644 packages/network-server/tests/setup.ts create mode 100644 packages/network-shared/src/protocols/MessageManager.ts create mode 100644 packages/network-shared/src/serialization/JSONSerializer.ts create mode 100644 packages/network-shared/src/serialization/MessageCompressor.ts create mode 100644 packages/network-shared/src/transport/ErrorHandler.ts create mode 100644 packages/network-shared/src/transport/HeartbeatManager.ts create mode 100644 packages/network-shared/src/utils/EventEmitter.ts create mode 100644 packages/network-shared/src/utils/index.ts diff --git a/packages/network-client/jest.config.cjs b/packages/network-client/jest.config.cjs deleted file mode 100644 index a54a15fe..00000000 --- a/packages/network-client/jest.config.cjs +++ /dev/null @@ -1,47 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'jsdom', // 客户端使用jsdom环境 - roots: ['/tests'], - testMatch: ['**/*.test.ts', '**/*.spec.ts'], - testPathIgnorePatterns: ['/node_modules/'], - collectCoverage: false, - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/index.ts', - '!src/**/index.ts', - '!**/*.d.ts', - '!src/**/*.test.ts', - '!src/**/*.spec.ts' - ], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html'], - coverageThreshold: { - global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70 - } - }, - verbose: true, - transform: { - '^.+\\.tsx?$': ['ts-jest', { - tsconfig: 'tsconfig.json', - useESM: false, - }], - }, - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - setupFilesAfterEnv: ['/tests/setup.ts'], - testTimeout: 10000, - clearMocks: true, - restoreMocks: true, - modulePathIgnorePatterns: [ - '/bin/', - '/dist/', - '/node_modules/' - ] -}; \ No newline at end of file diff --git a/packages/network-client/package.json b/packages/network-client/package.json index f022a3fd..88914f35 100644 --- a/packages/network-client/package.json +++ b/packages/network-client/package.json @@ -42,10 +42,7 @@ "publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish", "publish:major": "npm version major && npm run build:npm && cd dist && npm publish", "preversion": "npm run rebuild", - "test": "jest --config jest.config.cjs", - "test:watch": "jest --watch --config jest.config.cjs", - "test:coverage": "jest --coverage --config jest.config.cjs", - "test:ci": "jest --ci --coverage --config jest.config.cjs" + "test": "echo \"No tests configured for network-client\"" }, "author": "yhh", "license": "MIT", diff --git a/packages/network-client/src/core/ConnectionStateManager.ts b/packages/network-client/src/core/ConnectionStateManager.ts new file mode 100644 index 00000000..6294276f --- /dev/null +++ b/packages/network-client/src/core/ConnectionStateManager.ts @@ -0,0 +1,638 @@ +/** + * 连接状态管理器 + * 负责跟踪连接状态变化、状态变化通知和自动恢复逻辑 + */ +import { createLogger, ITimer } from '@esengine/ecs-framework'; +import { ConnectionState, IConnectionStats, EventEmitter } from '@esengine/network-shared'; +import { NetworkTimerManager } from '../utils'; + +/** + * 状态转换规则 + */ +export interface StateTransitionRule { + from: ConnectionState; + to: ConnectionState; + condition?: () => boolean; + action?: () => void; +} + +/** + * 连接状态管理器配置 + */ +export interface ConnectionStateManagerConfig { + enableAutoRecovery: boolean; + recoveryTimeout: number; + maxRecoveryAttempts: number; + stateTimeout: number; + enableStateValidation: boolean; + logStateChanges: boolean; +} + +/** + * 状态历史记录 + */ +export interface StateHistoryEntry { + state: ConnectionState; + timestamp: number; + duration?: number; + reason?: string; + metadata?: Record; +} + +/** + * 状态统计信息 + */ +export interface StateStats { + currentState: ConnectionState; + totalTransitions: number; + stateHistory: StateHistoryEntry[]; + stateDurations: Record; + averageStateDurations: Record; + totalUptime: number; + totalDowntime: number; + connectionAttempts: number; + successfulConnections: number; + failedConnections: number; +} + +/** + * 连接状态管理器事件接口 + */ +export interface ConnectionStateManagerEvents { + stateChanged: (oldState: ConnectionState, newState: ConnectionState, reason?: string) => void; + stateTimeout: (state: ConnectionState, duration: number) => void; + recoveryStarted: (attempt: number) => void; + recoverySucceeded: () => void; + recoveryFailed: (maxAttemptsReached: boolean) => void; + invalidTransition: (from: ConnectionState, to: ConnectionState) => void; +} + +/** + * 连接状态管理器 + */ +export class ConnectionStateManager extends EventEmitter { + private logger = createLogger('ConnectionStateManager'); + private config: ConnectionStateManagerConfig; + private currentState: ConnectionState = ConnectionState.Disconnected; + private previousState?: ConnectionState; + private stateStartTime: number = Date.now(); + private stats: StateStats; + + // 状态管理 + private stateHistory: StateHistoryEntry[] = []; + private transitionRules: StateTransitionRule[] = []; + private stateTimeouts: Map = new Map(); + + // 恢复逻辑 + private recoveryAttempts = 0; + private recoveryTimer?: ITimer; + + // Timer管理器使用静态的NetworkTimerManager + private recoveryCallback?: () => Promise; + + // 事件处理器 + private eventHandlers: Partial = {}; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + super(); + + this.config = { + enableAutoRecovery: true, + recoveryTimeout: 5000, + maxRecoveryAttempts: 3, + stateTimeout: 30000, + enableStateValidation: true, + logStateChanges: true, + ...config + }; + + this.stats = { + currentState: this.currentState, + totalTransitions: 0, + stateHistory: [], + stateDurations: { + [ConnectionState.Disconnected]: [], + [ConnectionState.Connecting]: [], + [ConnectionState.Connected]: [], + [ConnectionState.Reconnecting]: [], + [ConnectionState.Failed]: [] + }, + averageStateDurations: { + [ConnectionState.Disconnected]: 0, + [ConnectionState.Connecting]: 0, + [ConnectionState.Connected]: 0, + [ConnectionState.Reconnecting]: 0, + [ConnectionState.Failed]: 0 + }, + totalUptime: 0, + totalDowntime: 0, + connectionAttempts: 0, + successfulConnections: 0, + failedConnections: 0 + }; + + this.initializeDefaultTransitionRules(); + } + + /** + * 设置当前状态 + */ + setState(newState: ConnectionState, reason?: string, metadata?: Record): boolean { + if (this.currentState === newState) { + return true; + } + + // 验证状态转换 + if (this.config.enableStateValidation && !this.isValidTransition(this.currentState, newState)) { + this.logger.warn(`无效的状态转换: ${this.currentState} -> ${newState}`); + this.eventHandlers.invalidTransition?.(this.currentState, newState); + return false; + } + + const oldState = this.currentState; + const now = Date.now(); + const duration = now - this.stateStartTime; + + // 更新状态历史 + this.updateStateHistory(oldState, duration, reason, metadata); + + // 清理旧状态的超时定时器 + this.clearStateTimeout(oldState); + + // 更新当前状态 + this.previousState = oldState; + this.currentState = newState; + this.stateStartTime = now; + this.stats.currentState = newState; + this.stats.totalTransitions++; + + // 更新连接统计 + this.updateConnectionStats(oldState, newState); + + if (this.config.logStateChanges) { + this.logger.info(`连接状态变化: ${oldState} -> ${newState}${reason ? ` (${reason})` : ''}`); + } + + // 设置新状态的超时定时器 + this.setStateTimeout(newState); + + // 处理自动恢复逻辑 + this.handleAutoRecovery(newState); + + // 触发事件 + this.eventHandlers.stateChanged?.(oldState, newState, reason); + this.emit('stateChanged', oldState, newState, reason); + + return true; + } + + /** + * 获取当前状态 + */ + getState(): ConnectionState { + return this.currentState; + } + + /** + * 获取上一个状态 + */ + getPreviousState(): ConnectionState | undefined { + return this.previousState; + } + + /** + * 获取当前状态持续时间 + */ + getCurrentStateDuration(): number { + return Date.now() - this.stateStartTime; + } + + /** + * 获取状态统计信息 + */ + getStats(): StateStats { + this.updateCurrentStats(); + return { + ...this.stats, + stateHistory: [...this.stateHistory] + }; + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.stats = { + currentState: this.currentState, + totalTransitions: 0, + stateHistory: [], + stateDurations: { + [ConnectionState.Disconnected]: [], + [ConnectionState.Connecting]: [], + [ConnectionState.Connected]: [], + [ConnectionState.Reconnecting]: [], + [ConnectionState.Failed]: [] + }, + averageStateDurations: { + [ConnectionState.Disconnected]: 0, + [ConnectionState.Connecting]: 0, + [ConnectionState.Connected]: 0, + [ConnectionState.Reconnecting]: 0, + [ConnectionState.Failed]: 0 + }, + totalUptime: 0, + totalDowntime: 0, + connectionAttempts: 0, + successfulConnections: 0, + failedConnections: 0 + }; + + this.stateHistory.length = 0; + this.recoveryAttempts = 0; + } + + /** + * 设置恢复回调 + */ + setRecoveryCallback(callback: () => Promise): void { + this.recoveryCallback = callback; + } + + /** + * 添加状态转换规则 + */ + addTransitionRule(rule: StateTransitionRule): void { + this.transitionRules.push(rule); + } + + /** + * 移除状态转换规则 + */ + removeTransitionRule(from: ConnectionState, to: ConnectionState): boolean { + const index = this.transitionRules.findIndex(rule => rule.from === from && rule.to === to); + if (index >= 0) { + this.transitionRules.splice(index, 1); + return true; + } + return false; + } + + /** + * 检查状态转换是否有效 + */ + isValidTransition(from: ConnectionState, to: ConnectionState): boolean { + // 检查自定义转换规则 + const rule = this.transitionRules.find(r => r.from === from && r.to === to); + if (rule) { + return rule.condition ? rule.condition() : true; + } + + // 默认转换规则 + return this.isDefaultValidTransition(from, to); + } + + /** + * 检查是否为连接状态 + */ + isConnected(): boolean { + return this.currentState === ConnectionState.Connected; + } + + /** + * 检查是否为连接中状态 + */ + isConnecting(): boolean { + return this.currentState === ConnectionState.Connecting || + this.currentState === ConnectionState.Reconnecting; + } + + /** + * 检查是否为断开连接状态 + */ + isDisconnected(): boolean { + return this.currentState === ConnectionState.Disconnected || + this.currentState === ConnectionState.Failed; + } + + /** + * 手动触发恢复 + */ + triggerRecovery(): void { + if (this.config.enableAutoRecovery && this.recoveryCallback) { + this.startRecovery(); + } + } + + /** + * 停止自动恢复 + */ + stopRecovery(): void { + if (this.recoveryTimer) { + this.recoveryTimer.stop(); + this.recoveryTimer = undefined; + } + this.recoveryAttempts = 0; + } + + /** + * 设置事件处理器 + */ + override on(event: K, handler: ConnectionStateManagerEvents[K]): this { + this.eventHandlers[event] = handler; + return super.on(event, handler as any); + } + + /** + * 移除事件处理器 + */ + override off(event: K): this { + delete this.eventHandlers[event]; + return super.off(event, this.eventHandlers[event] as any); + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('连接状态管理器配置已更新:', newConfig); + } + + /** + * 销毁管理器 + */ + destroy(): void { + this.stopRecovery(); + this.clearAllStateTimeouts(); + this.removeAllListeners(); + } + + /** + * 初始化默认转换规则 + */ + private initializeDefaultTransitionRules(): void { + // 连接尝试 + this.addTransitionRule({ + from: ConnectionState.Disconnected, + to: ConnectionState.Connecting, + action: () => this.stats.connectionAttempts++ + }); + + // 连接成功 + this.addTransitionRule({ + from: ConnectionState.Connecting, + to: ConnectionState.Connected, + action: () => { + this.stats.successfulConnections++; + this.recoveryAttempts = 0; // 重置恢复计数 + } + }); + + // 连接失败 + this.addTransitionRule({ + from: ConnectionState.Connecting, + to: ConnectionState.Failed, + action: () => this.stats.failedConnections++ + }); + + // 重连尝试 + this.addTransitionRule({ + from: ConnectionState.Failed, + to: ConnectionState.Reconnecting, + action: () => this.stats.connectionAttempts++ + }); + + // 重连成功 + this.addTransitionRule({ + from: ConnectionState.Reconnecting, + to: ConnectionState.Connected, + action: () => { + this.stats.successfulConnections++; + this.recoveryAttempts = 0; + } + }); + } + + /** + * 检查默认的有效转换 + */ + private isDefaultValidTransition(from: ConnectionState, to: ConnectionState): boolean { + const validTransitions: Record = { + [ConnectionState.Disconnected]: [ConnectionState.Connecting], + [ConnectionState.Connecting]: [ConnectionState.Connected, ConnectionState.Failed, ConnectionState.Disconnected], + [ConnectionState.Connected]: [ConnectionState.Disconnected, ConnectionState.Failed, ConnectionState.Reconnecting], + [ConnectionState.Reconnecting]: [ConnectionState.Connected, ConnectionState.Failed, ConnectionState.Disconnected], + [ConnectionState.Failed]: [ConnectionState.Reconnecting, ConnectionState.Connecting, ConnectionState.Disconnected] + }; + + return validTransitions[from]?.includes(to) || false; + } + + /** + * 更新状态历史 + */ + private updateStateHistory(state: ConnectionState, duration: number, reason?: string, metadata?: Record): void { + const entry: StateHistoryEntry = { + state, + timestamp: this.stateStartTime, + duration, + reason, + metadata + }; + + this.stateHistory.push(entry); + + // 限制历史记录数量 + if (this.stateHistory.length > 100) { + this.stateHistory.shift(); + } + + // 更新状态持续时间统计 + this.stats.stateDurations[state].push(duration); + if (this.stats.stateDurations[state].length > 50) { + this.stats.stateDurations[state].shift(); + } + + // 计算平均持续时间 + const durations = this.stats.stateDurations[state]; + this.stats.averageStateDurations[state] = + durations.reduce((sum, d) => sum + d, 0) / durations.length; + } + + /** + * 更新连接统计 + */ + private updateConnectionStats(from: ConnectionState, to: ConnectionState): void { + const now = Date.now(); + const duration = now - this.stateStartTime; + + // 更新在线/离线时间 + if (from === ConnectionState.Connected) { + this.stats.totalUptime += duration; + } else if (this.isConnected()) { + // 进入连接状态 + } + + if (this.isDisconnected() && !this.wasDisconnected(from)) { + // 进入断开状态,开始计算离线时间 + } else if (!this.isDisconnected() && this.wasDisconnected(from)) { + // 离开断开状态 + this.stats.totalDowntime += duration; + } + } + + /** + * 检查之前是否为断开状态 + */ + private wasDisconnected(state: ConnectionState): boolean { + return state === ConnectionState.Disconnected || state === ConnectionState.Failed; + } + + /** + * 设置状态超时定时器 + */ + private setStateTimeout(state: ConnectionState): void { + if (this.config.stateTimeout <= 0) { + return; + } + + const timeout = NetworkTimerManager.schedule( + this.config.stateTimeout / 1000, // 转为秒 + false, // 不重复 + this, + () => { + const duration = this.getCurrentStateDuration(); + this.logger.warn(`状态超时: ${state}, 持续时间: ${duration}ms`); + this.eventHandlers.stateTimeout?.(state, duration); + } + ); + + this.stateTimeouts.set(state, timeout); + } + + /** + * 清理状态超时定时器 + */ + private clearStateTimeout(state: ConnectionState): void { + const timeout = this.stateTimeouts.get(state); + if (timeout) { + timeout.stop(); + this.stateTimeouts.delete(state); + } + } + + /** + * 清理所有状态超时定时器 + */ + private clearAllStateTimeouts(): void { + for (const timeout of this.stateTimeouts.values()) { + timeout.stop(); + } + this.stateTimeouts.clear(); + } + + /** + * 处理自动恢复逻辑 + */ + private handleAutoRecovery(newState: ConnectionState): void { + if (!this.config.enableAutoRecovery) { + return; + } + + // 检查是否需要开始恢复 + if (newState === ConnectionState.Failed || newState === ConnectionState.Disconnected) { + if (this.previousState === ConnectionState.Connected || this.previousState === ConnectionState.Connecting) { + this.startRecovery(); + } + } + } + + /** + * 开始恢复过程 + */ + private startRecovery(): void { + if (this.recoveryAttempts >= this.config.maxRecoveryAttempts) { + this.logger.warn(`已达到最大恢复尝试次数: ${this.config.maxRecoveryAttempts}`); + this.eventHandlers.recoveryFailed?.(true); + return; + } + + if (!this.recoveryCallback) { + this.logger.error('恢复回调未设置'); + return; + } + + this.recoveryAttempts++; + + this.logger.info(`开始自动恢复 (第 ${this.recoveryAttempts} 次)`); + this.eventHandlers.recoveryStarted?.(this.recoveryAttempts); + + this.recoveryTimer = NetworkTimerManager.schedule( + this.config.recoveryTimeout / 1000, // 转为秒 + false, // 不重复 + this, + async () => { + try { + await this.recoveryCallback!(); + this.eventHandlers.recoverySucceeded?.(); + } catch (error) { + this.logger.error(`自动恢复失败 (第 ${this.recoveryAttempts} 次):`, error); + this.eventHandlers.recoveryFailed?.(false); + + // 继续尝试恢复 + this.startRecovery(); + } + } + ); + } + + /** + * 更新当前统计信息 + */ + private updateCurrentStats(): void { + const now = Date.now(); + const currentDuration = now - this.stateStartTime; + + if (this.currentState === ConnectionState.Connected) { + this.stats.totalUptime += currentDuration - (this.stats.totalUptime > 0 ? 0 : currentDuration); + } + } + + /** + * 获取状态可读名称 + */ + getStateDisplayName(state?: ConnectionState): string { + const stateNames: Record = { + [ConnectionState.Disconnected]: '已断开', + [ConnectionState.Connecting]: '连接中', + [ConnectionState.Connected]: '已连接', + [ConnectionState.Reconnecting]: '重连中', + [ConnectionState.Failed]: '连接失败' + }; + + return stateNames[state || this.currentState]; + } + + /** + * 获取连接质量评级 + */ + getConnectionQuality(): 'excellent' | 'good' | 'fair' | 'poor' { + const successRate = this.stats.connectionAttempts > 0 ? + this.stats.successfulConnections / this.stats.connectionAttempts : 0; + + const averageConnectedTime = this.stats.averageStateDurations[ConnectionState.Connected]; + + if (successRate > 0.9 && averageConnectedTime > 60000) { // 成功率>90%且平均连接时间>1分钟 + return 'excellent'; + } else if (successRate > 0.7 && averageConnectedTime > 30000) { + return 'good'; + } else if (successRate > 0.5 && averageConnectedTime > 10000) { + return 'fair'; + } else { + return 'poor'; + } + } +} \ No newline at end of file diff --git a/packages/network-client/src/core/MessageQueue.ts b/packages/network-client/src/core/MessageQueue.ts new file mode 100644 index 00000000..8cdaa962 --- /dev/null +++ b/packages/network-client/src/core/MessageQueue.ts @@ -0,0 +1,680 @@ +/** + * 消息队列管理器 + * 提供消息排队、优先级处理和可靠传输保证 + */ +import { createLogger, ITimer } from '@esengine/ecs-framework'; +import { INetworkMessage, MessageType } from '@esengine/network-shared'; +import { NetworkTimerManager } from '../utils'; + +/** + * 消息优先级 + */ +export enum MessagePriority { + Low = 1, + Normal = 5, + High = 8, + Critical = 10 +} + +/** + * 队列消息包装器 + */ +export interface QueuedMessage { + id: string; + message: INetworkMessage; + priority: MessagePriority; + timestamp: number; + retryCount: number; + maxRetries: number; + reliable: boolean; + timeoutMs?: number; + callback?: (success: boolean, error?: Error) => void; +} + +/** + * 消息队列配置 + */ +export interface MessageQueueConfig { + maxQueueSize: number; + maxRetries: number; + retryDelay: number; + processingInterval: number; + enablePriority: boolean; + enableReliableDelivery: boolean; + defaultTimeout: number; +} + +/** + * 队列统计信息 + */ +export interface QueueStats { + totalQueued: number; + totalProcessed: number; + totalFailed: number; + currentSize: number; + averageProcessingTime: number; + messagesByPriority: Record; + reliableMessages: number; + expiredMessages: number; +} + +/** + * 消息队列事件接口 + */ +export interface MessageQueueEvents { + messageQueued: (message: QueuedMessage) => void; + messageProcessed: (message: QueuedMessage, success: boolean) => void; + messageFailed: (message: QueuedMessage, error: Error) => void; + messageExpired: (message: QueuedMessage) => void; + queueFull: (droppedMessage: QueuedMessage) => void; +} + +/** + * 消息队列管理器 + */ +export class MessageQueue { + private logger = createLogger('MessageQueue'); + private config: MessageQueueConfig; + private stats: QueueStats; + + // 队列存储 + private primaryQueue: QueuedMessage[] = []; + private priorityQueues: Map = new Map(); + private retryQueue: QueuedMessage[] = []; + private processingMap: Map = new Map(); + + // 定时器 + private processingTimer?: ITimer; + private retryTimer?: ITimer; + private cleanupTimer?: ITimer; + + // 事件处理器 + private eventHandlers: Partial = {}; + + // 发送回调 + private sendCallback?: (message: INetworkMessage) => Promise; + + // 性能统计 + private processingTimes: number[] = []; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + this.config = { + maxQueueSize: 1000, + maxRetries: 3, + retryDelay: 1000, + processingInterval: 100, + enablePriority: true, + enableReliableDelivery: true, + defaultTimeout: 30000, + ...config + }; + + this.stats = { + totalQueued: 0, + totalProcessed: 0, + totalFailed: 0, + currentSize: 0, + averageProcessingTime: 0, + messagesByPriority: { + [MessagePriority.Low]: 0, + [MessagePriority.Normal]: 0, + [MessagePriority.High]: 0, + [MessagePriority.Critical]: 0 + }, + reliableMessages: 0, + expiredMessages: 0 + }; + + // 初始化优先级队列 + if (this.config.enablePriority) { + this.priorityQueues.set(MessagePriority.Critical, []); + this.priorityQueues.set(MessagePriority.High, []); + this.priorityQueues.set(MessagePriority.Normal, []); + this.priorityQueues.set(MessagePriority.Low, []); + } + } + + /** + * 启动队列处理 + */ + start(sendCallback: (message: INetworkMessage) => Promise): void { + this.sendCallback = sendCallback; + + this.processingTimer = NetworkTimerManager.schedule( + this.config.processingInterval / 1000, + true, // 重复执行 + this, + () => this.processQueue() + ); + + if (this.config.maxRetries > 0) { + this.retryTimer = NetworkTimerManager.schedule( + this.config.retryDelay / 1000, + true, // 重复执行 + this, + () => this.processRetryQueue() + ); + } + + this.cleanupTimer = NetworkTimerManager.schedule( + 10, // 10秒 + true, // 重复执行 + this, + () => this.cleanupExpiredMessages() + ); + + this.logger.info('消息队列已启动'); + } + + /** + * 停止队列处理 + */ + stop(): void { + if (this.processingTimer) { + this.processingTimer.stop(); + this.processingTimer = undefined; + } + + if (this.retryTimer) { + this.retryTimer.stop(); + this.retryTimer = undefined; + } + + if (this.cleanupTimer) { + this.cleanupTimer.stop(); + this.cleanupTimer = undefined; + } + + this.logger.info('消息队列已停止'); + } + + /** + * 将消息加入队列 + */ + enqueue( + message: INetworkMessage, + options: { + priority?: MessagePriority; + reliable?: boolean; + timeout?: number; + maxRetries?: number; + callback?: (success: boolean, error?: Error) => void; + } = {} + ): boolean { + // 检查队列大小限制 + if (this.getTotalSize() >= this.config.maxQueueSize) { + const droppedMessage = this.createQueuedMessage(message, options); + this.eventHandlers.queueFull?.(droppedMessage); + this.logger.warn('队列已满,丢弃消息:', message.type); + return false; + } + + const queuedMessage = this.createQueuedMessage(message, options); + + // 根据配置选择队列策略 + if (this.config.enablePriority) { + this.enqueueToPriorityQueue(queuedMessage); + } else { + this.primaryQueue.push(queuedMessage); + } + + this.updateQueueStats(queuedMessage); + + this.eventHandlers.messageQueued?.(queuedMessage); + + return true; + } + + /** + * 清空队列 + */ + clear(): number { + const count = this.getTotalSize(); + + this.primaryQueue.length = 0; + this.retryQueue.length = 0; + this.processingMap.clear(); + + for (const queue of this.priorityQueues.values()) { + queue.length = 0; + } + + this.stats.currentSize = 0; + + this.logger.info(`已清空队列,清理了 ${count} 条消息`); + return count; + } + + /** + * 获取队列统计信息 + */ + getStats(): QueueStats { + this.updateCurrentStats(); + return { ...this.stats }; + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.stats = { + totalQueued: 0, + totalProcessed: 0, + totalFailed: 0, + currentSize: this.getTotalSize(), + averageProcessingTime: 0, + messagesByPriority: { + [MessagePriority.Low]: 0, + [MessagePriority.Normal]: 0, + [MessagePriority.High]: 0, + [MessagePriority.Critical]: 0 + }, + reliableMessages: 0, + expiredMessages: 0 + }; + + this.processingTimes.length = 0; + } + + /** + * 设置事件处理器 + */ + on(event: K, handler: MessageQueueEvents[K]): void { + this.eventHandlers[event] = handler; + } + + /** + * 移除事件处理器 + */ + off(event: K): void { + delete this.eventHandlers[event]; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('消息队列配置已更新:', newConfig); + } + + /** + * 获取队列中的消息数量 + */ + size(): number { + return this.getTotalSize(); + } + + /** + * 检查队列是否为空 + */ + isEmpty(): boolean { + return this.getTotalSize() === 0; + } + + /** + * 检查队列是否已满 + */ + isFull(): boolean { + return this.getTotalSize() >= this.config.maxQueueSize; + } + + /** + * 创建队列消息 + */ + private createQueuedMessage( + message: INetworkMessage, + options: any + ): QueuedMessage { + const priority = options.priority || this.getMessagePriority(message); + const reliable = options.reliable ?? this.isReliableMessage(message); + + return { + id: `${message.messageId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + message, + priority, + timestamp: Date.now(), + retryCount: 0, + maxRetries: options.maxRetries ?? this.config.maxRetries, + reliable, + timeoutMs: options.timeout ?? this.config.defaultTimeout, + callback: options.callback + }; + } + + /** + * 将消息加入优先级队列 + */ + private enqueueToPriorityQueue(message: QueuedMessage): void { + const queue = this.priorityQueues.get(message.priority); + if (queue) { + queue.push(message); + } else { + this.primaryQueue.push(message); + } + } + + /** + * 从队列中取出下一条消息 + */ + private dequeue(): QueuedMessage | undefined { + if (this.config.enablePriority) { + // 按优先级顺序处理 + for (const priority of [MessagePriority.Critical, MessagePriority.High, MessagePriority.Normal, MessagePriority.Low]) { + const queue = this.priorityQueues.get(priority); + if (queue && queue.length > 0) { + return queue.shift(); + } + } + } + + return this.primaryQueue.shift(); + } + + /** + * 处理队列 + */ + private async processQueue(): Promise { + if (!this.sendCallback || this.getTotalSize() === 0) { + return; + } + + const message = this.dequeue(); + if (!message) { + return; + } + + // 检查消息是否过期 + if (this.isMessageExpired(message)) { + this.handleExpiredMessage(message); + return; + } + + const startTime = Date.now(); + + try { + // 将消息标记为处理中 + this.processingMap.set(message.id, message); + + const success = await this.sendCallback(message.message); + + const processingTime = Date.now() - startTime; + this.updateProcessingTime(processingTime); + + if (success) { + this.handleSuccessfulMessage(message); + } else { + this.handleFailedMessage(message, new Error('发送失败')); + } + + } catch (error) { + this.handleFailedMessage(message, error as Error); + } finally { + this.processingMap.delete(message.id); + } + } + + /** + * 处理重试队列 + */ + private async processRetryQueue(): Promise { + if (this.retryQueue.length === 0 || !this.sendCallback) { + return; + } + + const message = this.retryQueue.shift(); + if (!message) { + return; + } + + // 检查是否可以重试 + if (message.retryCount >= message.maxRetries) { + this.handleFailedMessage(message, new Error('达到最大重试次数')); + return; + } + + // 检查消息是否过期 + if (this.isMessageExpired(message)) { + this.handleExpiredMessage(message); + return; + } + + message.retryCount++; + + try { + const success = await this.sendCallback(message.message); + + if (success) { + this.handleSuccessfulMessage(message); + } else { + // 重新加入重试队列 + this.retryQueue.push(message); + } + + } catch (error) { + // 重新加入重试队列 + this.retryQueue.push(message); + } + } + + /** + * 处理成功的消息 + */ + private handleSuccessfulMessage(message: QueuedMessage): void { + this.stats.totalProcessed++; + + if (message.callback) { + try { + message.callback(true); + } catch (error) { + this.logger.error('消息回调执行失败:', error); + } + } + + this.eventHandlers.messageProcessed?.(message, true); + } + + /** + * 处理失败的消息 + */ + private handleFailedMessage(message: QueuedMessage, error: Error): void { + // 如果是可靠消息且未达到最大重试次数,加入重试队列 + if (message.reliable && message.retryCount < message.maxRetries) { + this.retryQueue.push(message); + } else { + this.stats.totalFailed++; + + if (message.callback) { + try { + message.callback(false, error); + } catch (callbackError) { + this.logger.error('消息回调执行失败:', callbackError); + } + } + + this.eventHandlers.messageFailed?.(message, error); + } + + this.eventHandlers.messageProcessed?.(message, false); + } + + /** + * 处理过期消息 + */ + private handleExpiredMessage(message: QueuedMessage): void { + this.stats.expiredMessages++; + + if (message.callback) { + try { + message.callback(false, new Error('消息已过期')); + } catch (error) { + this.logger.error('消息回调执行失败:', error); + } + } + + this.eventHandlers.messageExpired?.(message); + } + + /** + * 清理过期消息 + */ + private cleanupExpiredMessages(): void { + const now = Date.now(); + let cleanedCount = 0; + + // 清理主队列 + this.primaryQueue = this.primaryQueue.filter(msg => { + if (this.isMessageExpired(msg)) { + this.handleExpiredMessage(msg); + cleanedCount++; + return false; + } + return true; + }); + + // 清理优先级队列 + for (const [priority, queue] of this.priorityQueues) { + this.priorityQueues.set(priority, queue.filter(msg => { + if (this.isMessageExpired(msg)) { + this.handleExpiredMessage(msg); + cleanedCount++; + return false; + } + return true; + })); + } + + // 清理重试队列 + this.retryQueue = this.retryQueue.filter(msg => { + if (this.isMessageExpired(msg)) { + this.handleExpiredMessage(msg); + cleanedCount++; + return false; + } + return true; + }); + + if (cleanedCount > 0) { + this.logger.debug(`清理了 ${cleanedCount} 条过期消息`); + } + } + + /** + * 检查消息是否过期 + */ + private isMessageExpired(message: QueuedMessage): boolean { + if (!message.timeoutMs) { + return false; + } + + return Date.now() - message.timestamp > message.timeoutMs; + } + + /** + * 获取消息的默认优先级 + */ + private getMessagePriority(message: INetworkMessage): MessagePriority { + switch (message.type) { + case MessageType.HEARTBEAT: + return MessagePriority.Low; + case MessageType.CONNECT: + case MessageType.DISCONNECT: + return MessagePriority.High; + case MessageType.ERROR: + return MessagePriority.Critical; + default: + return MessagePriority.Normal; + } + } + + /** + * 检查消息是否需要可靠传输 + */ + private isReliableMessage(message: INetworkMessage): boolean { + if (!this.config.enableReliableDelivery) { + return false; + } + + // 某些消息类型默认需要可靠传输 + const reliableTypes = [ + MessageType.CONNECT, + MessageType.RPC_CALL, + MessageType.ENTITY_CREATE, + MessageType.ENTITY_DESTROY + ]; + + return reliableTypes.includes(message.type) || message.reliable === true; + } + + /** + * 获取总队列大小 + */ + private getTotalSize(): number { + let size = this.primaryQueue.length + this.retryQueue.length + this.processingMap.size; + + for (const queue of this.priorityQueues.values()) { + size += queue.length; + } + + return size; + } + + /** + * 更新队列统计 + */ + private updateQueueStats(message: QueuedMessage): void { + this.stats.totalQueued++; + this.stats.currentSize = this.getTotalSize(); + this.stats.messagesByPriority[message.priority]++; + + if (message.reliable) { + this.stats.reliableMessages++; + } + } + + /** + * 更新当前统计 + */ + private updateCurrentStats(): void { + this.stats.currentSize = this.getTotalSize(); + } + + /** + * 更新处理时间统计 + */ + private updateProcessingTime(time: number): void { + this.processingTimes.push(time); + + // 保持最近1000个样本 + if (this.processingTimes.length > 1000) { + this.processingTimes.shift(); + } + + // 计算平均处理时间 + this.stats.averageProcessingTime = + this.processingTimes.reduce((sum, t) => sum + t, 0) / this.processingTimes.length; + } + + /** + * 获取队列详细状态 + */ + getDetailedStatus() { + return { + stats: this.getStats(), + config: this.config, + queueSizes: { + primary: this.primaryQueue.length, + retry: this.retryQueue.length, + processing: this.processingMap.size, + priorities: Object.fromEntries( + Array.from(this.priorityQueues.entries()).map(([priority, queue]) => [priority, queue.length]) + ) + }, + isRunning: !!this.processingTimer, + processingTimes: [...this.processingTimes] + }; + } +} \ No newline at end of file diff --git a/packages/network-client/src/core/NetworkClient.ts b/packages/network-client/src/core/NetworkClient.ts new file mode 100644 index 00000000..91b953b5 --- /dev/null +++ b/packages/network-client/src/core/NetworkClient.ts @@ -0,0 +1,698 @@ +/** + * 网络客户端核心类 + * 负责客户端连接管理、服务器通信和本地状态同步 + */ +import { createLogger } from '@esengine/ecs-framework'; +import { + IConnectionOptions, + ConnectionState, + IConnectionStats, + MessageType, + INetworkMessage, + IConnectMessage, + IConnectResponseMessage, + IHeartbeatMessage, + NetworkErrorType, + EventEmitter +} from '@esengine/network-shared'; +import { WebSocketClient } from '../transport/WebSocketClient'; +import { ReconnectionManager } from '../transport/ReconnectionManager'; +import { JSONSerializer } from '@esengine/network-shared'; +import { MessageManager } from '@esengine/network-shared'; +import { ErrorHandler } from '@esengine/network-shared'; +import { HeartbeatManager } from '@esengine/network-shared'; + +/** + * 网络客户端配置 + */ +export interface NetworkClientConfig { + connection: IConnectionOptions; + features: { + enableHeartbeat: boolean; + enableReconnection: boolean; + enableCompression: boolean; + enableMessageQueue: boolean; + }; + authentication: { + autoAuthenticate: boolean; + credentials?: Record; + }; +} + +/** + * 客户端状态 + */ +export enum ClientState { + Disconnected = 'disconnected', + Connecting = 'connecting', + Connected = 'connected', + Authenticating = 'authenticating', + Authenticated = 'authenticated', + Reconnecting = 'reconnecting', + Error = 'error' +} + +/** + * 客户端统计信息 + */ +export interface ClientStats { + state: ClientState; + connectionState: ConnectionState; + connectionTime?: number; + lastConnectTime?: number; + reconnectAttempts: number; + totalReconnects: number; + messages: { + sent: number; + received: number; + queued: number; + errors: number; + }; + latency?: number; + uptime: number; +} + +/** + * 网络客户端事件接口 + */ +export interface NetworkClientEvents { + connected: () => void; + disconnected: (reason?: string) => void; + authenticated: (clientId: string) => void; + authenticationFailed: (error: string) => void; + reconnecting: (attempt: number) => void; + reconnected: () => void; + reconnectionFailed: () => void; + messageReceived: (message: INetworkMessage) => void; + error: (error: Error) => void; + stateChanged: (oldState: ClientState, newState: ClientState) => void; +} + +/** + * 网络客户端核心实现 + */ +export class NetworkClient extends EventEmitter { + private logger = createLogger('NetworkClient'); + private config: NetworkClientConfig; + private state: ClientState = ClientState.Disconnected; + private stats: ClientStats; + private clientId?: string; + private connectTime?: number; + + // 核心组件 + private transport?: WebSocketClient; + private reconnectionManager: ReconnectionManager; + private serializer: JSONSerializer; + private messageManager: MessageManager; + private errorHandler: ErrorHandler; + private heartbeatManager: HeartbeatManager; + + // 事件处理器 + private eventHandlers: Partial = {}; + + // 消息队列 + private messageQueue: INetworkMessage[] = []; + private isProcessingQueue = false; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + super(); + + this.config = { + connection: { + timeout: 10000, + reconnectInterval: 3000, + maxReconnectAttempts: 5, + autoReconnect: true, + ...config.connection + }, + features: { + enableHeartbeat: true, + enableReconnection: true, + enableCompression: true, + enableMessageQueue: true, + ...config.features + }, + authentication: { + autoAuthenticate: true, + ...config.authentication + } + }; + + this.stats = { + state: ClientState.Disconnected, + connectionState: ConnectionState.Disconnected, + reconnectAttempts: 0, + totalReconnects: 0, + messages: { + sent: 0, + received: 0, + queued: 0, + errors: 0 + }, + uptime: 0 + }; + + // 初始化核心组件 + this.reconnectionManager = new ReconnectionManager({ + enabled: this.config.features.enableReconnection, + maxAttempts: this.config.connection.maxReconnectAttempts, + initialDelay: this.config.connection.reconnectInterval + }); + + this.serializer = new JSONSerializer({ + enableTypeChecking: true, + enableCompression: this.config.features.enableCompression + }); + + this.messageManager = new MessageManager({ + enableTimestampValidation: true, + enableMessageDeduplication: true + }); + + this.errorHandler = new ErrorHandler({ + maxRetryAttempts: 3, + enableAutoRecovery: true + }); + + this.heartbeatManager = new HeartbeatManager({ + interval: 30000, + timeout: 60000, + enableLatencyMeasurement: true + }); + + this.setupEventHandlers(); + } + + /** + * 连接到服务器 + */ + async connect(url: string, options?: IConnectionOptions): Promise { + if (this.state === ClientState.Connected || this.state === ClientState.Connecting) { + this.logger.warn('客户端已连接或正在连接'); + return; + } + + this.setState(ClientState.Connecting); + + try { + // 合并连接选项 + const connectionOptions = { ...this.config.connection, ...options }; + + // 创建传输层 + this.transport = new WebSocketClient(); + this.setupTransportEvents(); + + // 连接到服务器 + await this.transport.connect(url, connectionOptions); + + this.setState(ClientState.Connected); + this.connectTime = Date.now(); + this.stats.lastConnectTime = this.connectTime; + + this.logger.info(`已连接到服务器: ${url}`); + + // 启动心跳 + if (this.config.features.enableHeartbeat) { + this.startHeartbeat(); + } + + // 发送连接消息 + await this.sendConnectMessage(); + + // 处理队列中的消息 + if (this.config.features.enableMessageQueue) { + this.processMessageQueue(); + } + + this.eventHandlers.connected?.(); + + } catch (error) { + this.setState(ClientState.Error); + this.logger.error('连接失败:', error); + this.handleError(error as Error); + throw error; + } + } + + /** + * 断开连接 + */ + async disconnect(reason?: string): Promise { + if (this.state === ClientState.Disconnected) { + return; + } + + this.logger.info(`断开连接: ${reason || '用户主动断开'}`); + + // 停止重连 + this.reconnectionManager.stopReconnection('用户主动断开'); + + // 停止心跳 + this.heartbeatManager.stop(); + + // 断开传输层连接 + if (this.transport) { + await this.transport.disconnect(reason); + this.transport = undefined; + } + + this.setState(ClientState.Disconnected); + this.connectTime = undefined; + this.clientId = undefined; + + this.eventHandlers.disconnected?.(reason); + } + + /** + * 发送消息 + */ + send(message: T): boolean { + if (!this.transport || this.state !== ClientState.Connected && this.state !== ClientState.Authenticated) { + if (this.config.features.enableMessageQueue) { + this.queueMessage(message); + return true; + } else { + this.logger.warn('客户端未连接,无法发送消息'); + return false; + } + } + + try { + const serializedMessage = this.serializer.serialize(message); + this.transport.send(serializedMessage.data); + + this.stats.messages.sent++; + return true; + + } catch (error) { + this.logger.error('发送消息失败:', error); + this.stats.messages.errors++; + this.handleError(error as Error); + return false; + } + } + + /** + * 获取客户端状态 + */ + getState(): ClientState { + return this.state; + } + + /** + * 获取连接状态 + */ + getConnectionState(): ConnectionState { + return this.transport?.getConnectionState() || ConnectionState.Disconnected; + } + + /** + * 获取客户端统计信息 + */ + getStats(): ClientStats { + const currentStats = { ...this.stats }; + + currentStats.connectionState = this.getConnectionState(); + currentStats.reconnectAttempts = this.reconnectionManager.getState().currentAttempt; + currentStats.totalReconnects = this.reconnectionManager.getStats().successfulReconnections; + + if (this.connectTime) { + currentStats.uptime = Date.now() - this.connectTime; + } + + const transportStats = this.transport?.getStats(); + if (transportStats) { + currentStats.latency = transportStats.latency; + } + + currentStats.messages.queued = this.messageQueue.length; + + return currentStats; + } + + /** + * 获取客户端ID + */ + getClientId(): string | undefined { + return this.clientId; + } + + /** + * 检查是否已连接 + */ + isConnected(): boolean { + return this.state === ClientState.Connected || this.state === ClientState.Authenticated; + } + + /** + * 检查是否已认证 + */ + isAuthenticated(): boolean { + return this.state === ClientState.Authenticated; + } + + /** + * 手动触发重连 + */ + reconnect(): void { + if (this.config.features.enableReconnection) { + this.reconnectionManager.forceReconnect(); + } + } + + /** + * 设置事件处理器 + */ + override on(event: K, handler: NetworkClientEvents[K]): this { + this.eventHandlers[event] = handler; + return this; + } + + /** + * 移除事件处理器 + */ + override off(event: K): this { + delete this.eventHandlers[event]; + return this; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('客户端配置已更新:', newConfig); + } + + /** + * 销毁客户端 + */ + async destroy(): Promise { + await this.disconnect('客户端销毁'); + + // 清理组件 + this.reconnectionManager.reset(); + this.heartbeatManager.stop(); + this.messageQueue.length = 0; + + this.removeAllListeners(); + } + + /** + * 设置客户端状态 + */ + private setState(newState: ClientState): void { + if (this.state === newState) return; + + const oldState = this.state; + this.state = newState; + this.stats.state = newState; + + this.logger.debug(`客户端状态变化: ${oldState} -> ${newState}`); + this.eventHandlers.stateChanged?.(oldState, newState); + } + + /** + * 设置事件处理器 + */ + private setupEventHandlers(): void { + // 重连管理器事件 + this.reconnectionManager.on('reconnectStarted', (attempt) => { + this.setState(ClientState.Reconnecting); + this.eventHandlers.reconnecting?.(attempt); + }); + + this.reconnectionManager.on('reconnectSucceeded', () => { + this.setState(ClientState.Connected); + this.stats.totalReconnects++; + this.eventHandlers.reconnected?.(); + }); + + this.reconnectionManager.on('maxAttemptsReached', () => { + this.setState(ClientState.Error); + this.eventHandlers.reconnectionFailed?.(); + }); + + // 心跳管理器事件 + this.heartbeatManager.on('healthStatusChanged', (isHealthy) => { + if (!isHealthy && this.config.features.enableReconnection) { + this.logger.warn('连接不健康,开始重连'); + this.reconnectionManager.startReconnection(); + } + }); + + // 错误处理器事件 + this.errorHandler.on('criticalError', (error) => { + this.setState(ClientState.Error); + this.eventHandlers.error?.(new Error(error.message)); + }); + + // 设置重连回调 + this.reconnectionManager.setReconnectCallback(async () => { + if (this.transport) { + await this.transport.reconnect(); + } + }); + } + + /** + * 设置传输层事件 + */ + private setupTransportEvents(): void { + if (!this.transport) return; + + this.transport.onMessage((data) => { + this.handleMessage(data); + }); + + this.transport.onConnectionStateChange((state) => { + this.handleConnectionStateChange(state); + }); + + this.transport.onError((error) => { + this.handleTransportError(error); + }); + } + + /** + * 处理接收到的消息 + */ + private handleMessage(data: ArrayBuffer | string): void { + try { + const deserializationResult = this.serializer.deserialize(data); + if (!deserializationResult.isValid) { + this.logger.warn(`消息反序列化失败: ${deserializationResult.errors?.join(', ')}`); + this.stats.messages.errors++; + return; + } + + const message = deserializationResult.data; + + // 验证消息 + const validationResult = this.messageManager.validateMessage(message); + if (!validationResult.isValid) { + this.logger.warn(`消息验证失败: ${validationResult.errors.join(', ')}`); + this.stats.messages.errors++; + return; + } + + this.stats.messages.received++; + + // 处理特定类型的消息 + this.processMessage(message); + + this.eventHandlers.messageReceived?.(message); + + } catch (error) { + this.logger.error('处理消息失败:', error); + this.stats.messages.errors++; + this.handleError(error as Error); + } + } + + /** + * 处理连接状态变化 + */ + private handleConnectionStateChange(state: ConnectionState): void { + this.logger.debug(`传输层连接状态: ${state}`); + + switch (state) { + case ConnectionState.Connected: + this.reconnectionManager.onReconnectionSuccess(); + break; + + case ConnectionState.Disconnected: + case ConnectionState.Failed: + if (this.config.features.enableReconnection) { + this.reconnectionManager.startReconnection(); + } else { + this.setState(ClientState.Disconnected); + } + break; + } + } + + /** + * 处理传输层错误 + */ + private handleTransportError(error: Error): void { + this.logger.error('传输层错误:', error); + this.handleError(error); + } + + /** + * 处理错误 + */ + private handleError(error: Error): void { + this.errorHandler.handleError(error, 'NetworkClient'); + this.eventHandlers.error?.(error); + } + + /** + * 处理具体消息类型 + */ + private processMessage(message: INetworkMessage): void { + switch (message.type) { + case MessageType.CONNECT: + this.handleConnectResponse(message as IConnectResponseMessage); + break; + + case MessageType.HEARTBEAT: + this.handleHeartbeatResponse(message as IHeartbeatMessage); + break; + } + } + + /** + * 发送连接消息 + */ + private async sendConnectMessage(): Promise { + const connectMessage: IConnectMessage = this.messageManager.createMessage( + MessageType.CONNECT, + { + clientVersion: '1.0.0', + protocolVersion: '1.0.0', + authToken: this.config.authentication.credentials?.token, + clientInfo: { + name: 'ECS Network Client', + platform: typeof window !== 'undefined' ? 'browser' : 'node', + version: '1.0.0' + } + }, + 'client' + ); + + this.send(connectMessage); + } + + /** + * 处理连接响应 + */ + private handleConnectResponse(message: IConnectResponseMessage): void { + if (message.data.success) { + this.clientId = message.data.clientId; + + if (this.config.authentication.autoAuthenticate) { + this.setState(ClientState.Authenticated); + this.eventHandlers.authenticated?.(this.clientId!); + } + + this.logger.info(`连接成功,客户端ID: ${this.clientId}`); + } else { + this.logger.error(`连接失败: ${message.data.error}`); + this.setState(ClientState.Error); + this.eventHandlers.authenticationFailed?.(message.data.error || '连接失败'); + } + } + + /** + * 处理心跳响应 + */ + private handleHeartbeatResponse(message: IHeartbeatMessage): void { + this.heartbeatManager.handleHeartbeatResponse({ + type: MessageType.HEARTBEAT, + clientTime: message.data.clientTime, + serverTime: message.data.serverTime + }); + } + + /** + * 启动心跳 + */ + private startHeartbeat(): void { + this.heartbeatManager.start((heartbeatMessage) => { + const message: IHeartbeatMessage = this.messageManager.createMessage( + MessageType.HEARTBEAT, + heartbeatMessage, + this.clientId || 'client' + ); + this.send(message); + }); + } + + /** + * 将消息加入队列 + */ + private queueMessage(message: INetworkMessage): void { + this.messageQueue.push(message); + this.stats.messages.queued = this.messageQueue.length; + + this.logger.debug(`消息已加入队列,当前队列长度: ${this.messageQueue.length}`); + } + + /** + * 处理消息队列 + */ + private async processMessageQueue(): Promise { + if (this.isProcessingQueue || this.messageQueue.length === 0) { + return; + } + + this.isProcessingQueue = true; + + try { + while (this.messageQueue.length > 0 && this.isConnected()) { + const message = this.messageQueue.shift()!; + + if (this.send(message)) { + this.logger.debug(`队列消息发送成功,剩余: ${this.messageQueue.length}`); + } else { + // 发送失败,重新加入队列头部 + this.messageQueue.unshift(message); + break; + } + } + } finally { + this.isProcessingQueue = false; + this.stats.messages.queued = this.messageQueue.length; + } + } + + /** + * 清空消息队列 + */ + public clearMessageQueue(): number { + const count = this.messageQueue.length; + this.messageQueue.length = 0; + this.stats.messages.queued = 0; + this.logger.info(`已清空消息队列,清理了 ${count} 条消息`); + return count; + } + + /** + * 获取延迟信息 + */ + public getLatency(): number | undefined { + return this.heartbeatManager.getStatus().latency; + } + + /** + * 获取心跳统计 + */ + public getHeartbeatStats() { + return this.heartbeatManager.getStats(); + } +} \ No newline at end of file diff --git a/packages/network-client/src/index.ts b/packages/network-client/src/index.ts index 56223d67..1b06bbd3 100644 --- a/packages/network-client/src/index.ts +++ b/packages/network-client/src/index.ts @@ -3,22 +3,14 @@ * ECS Framework网络层 - 客户端实现 */ -// 核心客户端 (待实现) -// export * from './core/NetworkClient'; -// export * from './core/ServerConnection'; +// 核心客户端 +export * from './core/NetworkClient'; +export * from './core/MessageQueue'; +export * from './core/ConnectionStateManager'; -// 传输层 (待实现) -// export * from './transport/WebSocketClient'; -// export * from './transport/HttpClient'; - -// 系统层 (待实现) -// export * from './systems/ClientSyncSystem'; -// export * from './systems/ClientRpcSystem'; -// export * from './systems/InterpolationSystem'; - -// 平台适配器 (待实现) -// export * from './adapters/BrowserAdapter'; -// export * from './adapters/CocosAdapter'; +// 传输层 +export * from './transport/WebSocketClient'; +export * from './transport/ReconnectionManager'; // 重新导出shared包的类型 export * from '@esengine/network-shared'; \ No newline at end of file diff --git a/packages/network-client/src/transport/ReconnectionManager.ts b/packages/network-client/src/transport/ReconnectionManager.ts new file mode 100644 index 00000000..53efcdf1 --- /dev/null +++ b/packages/network-client/src/transport/ReconnectionManager.ts @@ -0,0 +1,410 @@ +/** + * 重连管理器 + * 负责管理客户端的自动重连逻辑 + */ +import { createLogger, ITimer } from '@esengine/ecs-framework'; +import { ConnectionState } from '@esengine/network-shared'; +import { NetworkTimerManager } from '../utils'; + +/** + * 重连配置 + */ +export interface ReconnectionConfig { + enabled: boolean; + maxAttempts: number; + initialDelay: number; + maxDelay: number; + backoffFactor: number; + jitterEnabled: boolean; + resetAfterSuccess: boolean; +} + +/** + * 重连状态 + */ +export interface ReconnectionState { + isReconnecting: boolean; + currentAttempt: number; + nextAttemptTime?: number; + lastAttemptTime?: number; + totalAttempts: number; + successfulReconnections: number; +} + +/** + * 重连事件接口 + */ +export interface ReconnectionEvents { + reconnectStarted: (attempt: number) => void; + reconnectSucceeded: (attempt: number, duration: number) => void; + reconnectFailed: (attempt: number, error: Error) => void; + reconnectAborted: (reason: string) => void; + maxAttemptsReached: () => void; +} + +/** + * 重连策略 + */ +export enum ReconnectionStrategy { + Exponential = 'exponential', + Linear = 'linear', + Fixed = 'fixed', + Custom = 'custom' +} + +/** + * 重连管理器 + */ +export class ReconnectionManager { + private logger = createLogger('ReconnectionManager'); + private config: ReconnectionConfig; + private state: ReconnectionState; + private eventHandlers: Partial = {}; + + private reconnectTimer?: ITimer; + private reconnectCallback?: () => Promise; + private abortController?: AbortController; + + // 策略相关 + private strategy: ReconnectionStrategy = ReconnectionStrategy.Exponential; + private customDelayCalculator?: (attempt: number) => number; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + this.config = { + enabled: true, + maxAttempts: 10, + initialDelay: 1000, // 1秒 + maxDelay: 30000, // 30秒 + backoffFactor: 2, // 指数退避因子 + jitterEnabled: true, // 启用抖动 + resetAfterSuccess: true, // 成功后重置计数 + ...config + }; + + this.state = { + isReconnecting: false, + currentAttempt: 0, + totalAttempts: 0, + successfulReconnections: 0 + }; + } + + /** + * 设置重连回调 + */ + setReconnectCallback(callback: () => Promise): void { + this.reconnectCallback = callback; + } + + /** + * 开始重连 + */ + startReconnection(): void { + if (!this.config.enabled) { + this.logger.info('重连已禁用'); + return; + } + + if (this.state.isReconnecting) { + this.logger.warn('重连已在进行中'); + return; + } + + if (!this.reconnectCallback) { + this.logger.error('重连回调未设置'); + return; + } + + // 检查是否达到最大重连次数 + if (this.state.currentAttempt >= this.config.maxAttempts) { + this.logger.error(`已达到最大重连次数: ${this.config.maxAttempts}`); + this.eventHandlers.maxAttemptsReached?.(); + return; + } + + this.state.isReconnecting = true; + this.state.currentAttempt++; + this.state.totalAttempts++; + + const delay = this.calculateDelay(); + this.state.nextAttemptTime = Date.now() + delay; + + this.logger.info(`开始重连 (第 ${this.state.currentAttempt}/${this.config.maxAttempts} 次),${delay}ms 后尝试`); + + this.eventHandlers.reconnectStarted?.(this.state.currentAttempt); + + // 设置重连定时器 + this.reconnectTimer = NetworkTimerManager.schedule( + delay / 1000, // 转为秒 + false, // 不重复 + this, + () => { + this.performReconnection(); + } + ); + } + + /** + * 停止重连 + */ + stopReconnection(reason: string = '用户主动停止'): void { + if (!this.state.isReconnecting) { + return; + } + + this.clearReconnectTimer(); + this.abortController?.abort(); + this.state.isReconnecting = false; + this.state.nextAttemptTime = undefined; + + this.logger.info(`重连已停止: ${reason}`); + this.eventHandlers.reconnectAborted?.(reason); + } + + /** + * 重连成功 + */ + onReconnectionSuccess(): void { + if (!this.state.isReconnecting) { + return; + } + + const duration = this.state.lastAttemptTime ? Date.now() - this.state.lastAttemptTime : 0; + + this.logger.info(`重连成功 (第 ${this.state.currentAttempt} 次尝试,耗时 ${duration}ms)`); + + this.state.isReconnecting = false; + this.state.successfulReconnections++; + this.state.nextAttemptTime = undefined; + + // 是否重置重连计数 + if (this.config.resetAfterSuccess) { + this.state.currentAttempt = 0; + } + + this.clearReconnectTimer(); + this.eventHandlers.reconnectSucceeded?.(this.state.currentAttempt, duration); + } + + /** + * 重连失败 + */ + onReconnectionFailure(error: Error): void { + if (!this.state.isReconnecting) { + return; + } + + this.logger.warn(`重连失败 (第 ${this.state.currentAttempt} 次尝试):`, error); + + this.eventHandlers.reconnectFailed?.(this.state.currentAttempt, error); + + // 检查是否还有重连机会 + if (this.state.currentAttempt >= this.config.maxAttempts) { + this.logger.error('重连次数已用完'); + this.state.isReconnecting = false; + this.eventHandlers.maxAttemptsReached?.(); + } else { + // 继续下一次重连 + this.startReconnection(); + } + } + + /** + * 获取重连状态 + */ + getState(): ReconnectionState { + return { ...this.state }; + } + + /** + * 获取重连统计 + */ + getStats() { + return { + totalAttempts: this.state.totalAttempts, + successfulReconnections: this.state.successfulReconnections, + currentAttempt: this.state.currentAttempt, + maxAttempts: this.config.maxAttempts, + isReconnecting: this.state.isReconnecting, + nextAttemptTime: this.state.nextAttemptTime, + successRate: this.state.totalAttempts > 0 ? + (this.state.successfulReconnections / this.state.totalAttempts) * 100 : 0 + }; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('重连配置已更新:', newConfig); + } + + /** + * 设置重连策略 + */ + setStrategy(strategy: ReconnectionStrategy, customCalculator?: (attempt: number) => number): void { + this.strategy = strategy; + if (strategy === ReconnectionStrategy.Custom && customCalculator) { + this.customDelayCalculator = customCalculator; + } + this.logger.info(`重连策略已设置为: ${strategy}`); + } + + /** + * 设置事件处理器 + */ + on(event: K, handler: ReconnectionEvents[K]): void { + this.eventHandlers[event] = handler; + } + + /** + * 移除事件处理器 + */ + off(event: K): void { + delete this.eventHandlers[event]; + } + + /** + * 重置重连状态 + */ + reset(): void { + this.stopReconnection('状态重置'); + this.state = { + isReconnecting: false, + currentAttempt: 0, + totalAttempts: 0, + successfulReconnections: 0 + }; + this.logger.info('重连状态已重置'); + } + + /** + * 强制立即重连 + */ + forceReconnect(): void { + if (this.state.isReconnecting) { + this.clearReconnectTimer(); + this.performReconnection(); + } else { + this.startReconnection(); + } + } + + /** + * 计算重连延迟 + */ + private calculateDelay(): number { + let delay: number; + + switch (this.strategy) { + case ReconnectionStrategy.Fixed: + delay = this.config.initialDelay; + break; + + case ReconnectionStrategy.Linear: + delay = this.config.initialDelay * this.state.currentAttempt; + break; + + case ReconnectionStrategy.Exponential: + delay = this.config.initialDelay * Math.pow(this.config.backoffFactor, this.state.currentAttempt - 1); + break; + + case ReconnectionStrategy.Custom: + delay = this.customDelayCalculator ? + this.customDelayCalculator(this.state.currentAttempt) : + this.config.initialDelay; + break; + + default: + delay = this.config.initialDelay; + } + + // 限制最大延迟 + delay = Math.min(delay, this.config.maxDelay); + + // 添加抖动以避免雷群效应 + if (this.config.jitterEnabled) { + const jitter = delay * 0.1 * Math.random(); // 10%的随机抖动 + delay += jitter; + } + + return Math.round(delay); + } + + /** + * 执行重连 + */ + private async performReconnection(): Promise { + if (!this.reconnectCallback || !this.state.isReconnecting) { + return; + } + + this.state.lastAttemptTime = Date.now(); + this.abortController = new AbortController(); + + try { + await this.reconnectCallback(); + // 重连回调成功,等待实际连接建立再调用 onReconnectionSuccess + + } catch (error) { + this.onReconnectionFailure(error as Error); + } + } + + /** + * 清除重连定时器 + */ + private clearReconnectTimer(): void { + if (this.reconnectTimer) { + this.reconnectTimer.stop(); + this.reconnectTimer = undefined; + } + } + + /** + * 检查是否应该进行重连 + */ + shouldReconnect(reason?: string): boolean { + if (!this.config.enabled) { + return false; + } + + if (this.state.currentAttempt >= this.config.maxAttempts) { + return false; + } + + // 可以根据断开原因决定是否重连 + if (reason) { + const noReconnectReasons = ['user_disconnect', 'invalid_credentials', 'banned']; + if (noReconnectReasons.includes(reason)) { + return false; + } + } + + return true; + } + + /** + * 获取下次重连的倒计时 + */ + getTimeUntilNextAttempt(): number { + if (!this.state.nextAttemptTime) { + return 0; + } + return Math.max(0, this.state.nextAttemptTime - Date.now()); + } + + /** + * 获取重连进度百分比 + */ + getProgress(): number { + if (this.config.maxAttempts === 0) { + return 0; + } + return (this.state.currentAttempt / this.config.maxAttempts) * 100; + } +} \ No newline at end of file diff --git a/packages/network-client/src/transport/WebSocketClient.ts b/packages/network-client/src/transport/WebSocketClient.ts new file mode 100644 index 00000000..94a962ca --- /dev/null +++ b/packages/network-client/src/transport/WebSocketClient.ts @@ -0,0 +1,406 @@ +/** + * WebSocket客户端传输层实现 + */ +import { createLogger, ITimer } from '@esengine/ecs-framework'; +import { + IClientTransport, + IConnectionOptions, + ConnectionState, + IConnectionStats +} from '@esengine/network-shared'; +import { NetworkTimerManager } from '../utils'; + +/** + * WebSocket客户端实现 + */ +export class WebSocketClient implements IClientTransport { + private logger = createLogger('WebSocketClient'); + private websocket?: WebSocket; + private connectionState: ConnectionState = ConnectionState.Disconnected; + private options: IConnectionOptions = {}; + private url = ''; + private reconnectTimer?: ITimer; + private reconnectAttempts = 0; + private stats: IConnectionStats; + + /** + * 消息接收事件处理器 + */ + private messageHandlers: ((data: ArrayBuffer | string) => void)[] = []; + + /** + * 连接状态变化事件处理器 + */ + private stateChangeHandlers: ((state: ConnectionState) => void)[] = []; + + /** + * 错误事件处理器 + */ + private errorHandlers: ((error: Error) => void)[] = []; + + /** + * 构造函数 + */ + constructor() { + this.stats = { + state: ConnectionState.Disconnected, + reconnectCount: 0, + bytesSent: 0, + bytesReceived: 0, + messagesSent: 0, + messagesReceived: 0 + }; + } + + /** + * 连接到服务器 + */ + async connect(url: string, options?: IConnectionOptions): Promise { + if (this.connectionState === ConnectionState.Connected) { + this.logger.warn('客户端已连接'); + return; + } + + this.url = url; + this.options = { + timeout: 10000, + reconnectInterval: 3000, + maxReconnectAttempts: 5, + autoReconnect: true, + protocolVersion: '1.0', + ...options + }; + + return this.connectInternal(); + } + + /** + * 断开连接 + */ + async disconnect(reason?: string): Promise { + this.options.autoReconnect = false; // 禁用自动重连 + this.clearReconnectTimer(); + + if (this.websocket) { + this.websocket.close(1000, reason || '客户端主动断开'); + this.websocket = undefined; + } + + this.setConnectionState(ConnectionState.Disconnected); + this.logger.info(`客户端断开连接: ${reason || '主动断开'}`); + } + + /** + * 发送数据到服务器 + */ + send(data: ArrayBuffer | string): void { + if (!this.websocket || this.connectionState !== ConnectionState.Connected) { + this.logger.warn('客户端未连接,无法发送消息'); + return; + } + + try { + this.websocket.send(data); + this.stats.messagesSent++; + + // 估算字节数 + const bytes = typeof data === 'string' ? new Blob([data]).size : data.byteLength; + this.stats.bytesSent += bytes; + + } catch (error) { + this.logger.error('发送消息失败:', error); + this.handleError(error as Error); + } + } + + /** + * 监听服务器消息 + */ + onMessage(handler: (data: ArrayBuffer | string) => void): void { + this.messageHandlers.push(handler); + } + + /** + * 监听连接状态变化 + */ + onConnectionStateChange(handler: (state: ConnectionState) => void): void { + this.stateChangeHandlers.push(handler); + } + + /** + * 监听错误事件 + */ + onError(handler: (error: Error) => void): void { + this.errorHandlers.push(handler); + } + + /** + * 获取连接状态 + */ + getConnectionState(): ConnectionState { + return this.connectionState; + } + + /** + * 获取连接统计信息 + */ + getStats(): IConnectionStats { + return { ...this.stats }; + } + + /** + * 内部连接实现 + */ + private async connectInternal(): Promise { + return new Promise((resolve, reject) => { + try { + this.setConnectionState(ConnectionState.Connecting); + this.logger.info(`连接到服务器: ${this.url}`); + + // 检查WebSocket支持 + if (typeof WebSocket === 'undefined') { + throw new Error('当前环境不支持WebSocket'); + } + + this.websocket = new WebSocket(this.url); + this.setupWebSocketEvents(resolve, reject); + + // 设置连接超时 + if (this.options.timeout) { + NetworkTimerManager.schedule( + this.options.timeout / 1000, // 转为秒 + false, // 不重复 + this, + () => { + if (this.connectionState === ConnectionState.Connecting) { + this.websocket?.close(); + reject(new Error(`连接超时 (${this.options.timeout}ms)`)); + } + } + ); + } + + } catch (error) { + this.logger.error('创建WebSocket连接失败:', error); + this.setConnectionState(ConnectionState.Failed); + reject(error); + } + }); + } + + /** + * 设置WebSocket事件监听 + */ + private setupWebSocketEvents( + resolve: () => void, + reject: (error: Error) => void + ): void { + if (!this.websocket) return; + + // 连接打开 + this.websocket.onopen = () => { + this.setConnectionState(ConnectionState.Connected); + this.stats.connectTime = Date.now(); + this.reconnectAttempts = 0; // 重置重连计数 + this.logger.info('WebSocket连接已建立'); + resolve(); + }; + + // 消息接收 + this.websocket.onmessage = (event) => { + this.handleMessage(event.data); + }; + + // 连接关闭 + this.websocket.onclose = (event) => { + this.handleConnectionClose(event.code, event.reason); + }; + + // 错误处理 + this.websocket.onerror = (event) => { + const error = new Error(`WebSocket错误: ${event}`); + this.logger.error('WebSocket错误:', error); + this.handleError(error); + + if (this.connectionState === ConnectionState.Connecting) { + reject(error); + } + }; + } + + /** + * 处理接收到的消息 + */ + private handleMessage(data: any): void { + try { + this.stats.messagesReceived++; + + // 估算字节数 + const bytes = typeof data === 'string' ? new Blob([data]).size : data.byteLength || 0; + this.stats.bytesReceived += bytes; + + // 触发消息事件 + this.messageHandlers.forEach(handler => { + try { + handler(data); + } catch (error) { + this.logger.error('消息事件处理器错误:', error); + } + }); + + } catch (error) { + this.logger.error('处理接收消息失败:', error); + this.handleError(error as Error); + } + } + + /** + * 处理连接关闭 + */ + private handleConnectionClose(code: number, reason: string): void { + this.stats.disconnectTime = Date.now(); + this.websocket = undefined; + + this.logger.info(`WebSocket连接已关闭: code=${code}, reason=${reason}`); + + // 根据关闭代码决定是否重连 + const shouldReconnect = this.shouldReconnect(code); + + if (shouldReconnect && this.options.autoReconnect) { + this.setConnectionState(ConnectionState.Reconnecting); + this.scheduleReconnect(); + } else { + this.setConnectionState(ConnectionState.Disconnected); + } + } + + /** + * 判断是否应该重连 + */ + private shouldReconnect(closeCode: number): boolean { + // 正常关闭(1000)或服务器重启(1001)时应该重连 + // 协议错误(1002-1003)、数据格式错误(1007)等不应重连 + const reconnectableCodes = [1000, 1001, 1006, 1011]; + return reconnectableCodes.includes(closeCode); + } + + /** + * 安排重连 + */ + private scheduleReconnect(): void { + if (this.reconnectAttempts >= (this.options.maxReconnectAttempts || 5)) { + this.logger.error(`达到最大重连次数 (${this.reconnectAttempts})`); + this.setConnectionState(ConnectionState.Failed); + return; + } + + // 指数退避算法 + const delay = Math.min( + this.options.reconnectInterval! * Math.pow(2, this.reconnectAttempts), + 30000 // 最大30秒 + ); + + this.logger.info(`${delay}ms 后尝试重连 (第 ${this.reconnectAttempts + 1} 次)`); + + this.reconnectTimer = NetworkTimerManager.schedule( + delay / 1000, // 转为秒 + false, // 不重复 + this, + () => { + this.reconnectAttempts++; + this.stats.reconnectCount++; + + this.connectInternal().catch(error => { + this.logger.error(`重连失败 (第 ${this.reconnectAttempts} 次):`, error); + this.scheduleReconnect(); + }); + } + ); + } + + /** + * 清除重连定时器 + */ + private clearReconnectTimer(): void { + if (this.reconnectTimer) { + this.reconnectTimer.stop(); + this.reconnectTimer = undefined; + } + } + + /** + * 设置连接状态 + */ + private setConnectionState(state: ConnectionState): void { + if (this.connectionState === state) return; + + const oldState = this.connectionState; + this.connectionState = state; + this.stats.state = state; + + this.logger.debug(`连接状态变化: ${oldState} -> ${state}`); + + // 触发状态变化事件 + this.stateChangeHandlers.forEach(handler => { + try { + handler(state); + } 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 ping(): void { + if (this.websocket && this.connectionState === ConnectionState.Connected) { + // WebSocket的ping/pong由浏览器自动处理 + // 这里可以发送应用层心跳消息 + this.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); + } + } + + /** + * 手动触发重连 + */ + public reconnect(): void { + if (this.connectionState === ConnectionState.Disconnected || + this.connectionState === ConnectionState.Failed) { + this.reconnectAttempts = 0; + this.connectInternal().catch(error => { + this.logger.error('手动重连失败:', error); + }); + } + } + + /** + * 获取延迟信息(简单实现) + */ + public getLatency(): number | undefined { + return this.stats.latency; + } + + /** + * 销毁客户端 + */ + public destroy(): void { + this.disconnect('客户端销毁'); + this.messageHandlers.length = 0; + this.stateChangeHandlers.length = 0; + this.errorHandlers.length = 0; + } +} \ No newline at end of file diff --git a/packages/network-client/src/utils/NetworkTimer.ts b/packages/network-client/src/utils/NetworkTimer.ts new file mode 100644 index 00000000..74b2de38 --- /dev/null +++ b/packages/network-client/src/utils/NetworkTimer.ts @@ -0,0 +1,103 @@ +/** + * 网络层专用Timer实现 + * 实现core库的ITimer接口,但使用浏览器/Node.js的原生定时器 + */ +import { ITimer } from '@esengine/ecs-framework'; + +/** + * 网络层Timer实现 + */ +export class NetworkTimer implements ITimer { + public context: TContext; + private timerId?: number; + private callback: (timer: ITimer) => void; + private _isDone = false; + + constructor( + timeInMilliseconds: number, + repeats: boolean, + context: TContext, + onTime: (timer: ITimer) => void + ) { + this.context = context; + this.callback = onTime; + + if (repeats) { + this.timerId = window.setInterval(() => { + this.callback(this); + }, timeInMilliseconds) as any; + } else { + this.timerId = window.setTimeout(() => { + this.callback(this); + this._isDone = true; + }, timeInMilliseconds) as any; + } + } + + stop(): void { + if (this.timerId !== undefined) { + clearTimeout(this.timerId); + clearInterval(this.timerId); + this.timerId = undefined; + } + this._isDone = true; + } + + reset(): void { + // 对于基于setTimeout的实现,reset意义不大 + // 如果需要重置,应该stop然后重新创建 + } + + getContext(): T { + return this.context as unknown as T; + } + + get isDone(): boolean { + return this._isDone; + } +} + +/** + * 网络Timer管理器 + */ +export class NetworkTimerManager { + private static timers: Set = new Set(); + + /** + * 创建一个定时器 + */ + static schedule( + timeInSeconds: number, + repeats: boolean, + context: TContext, + onTime: (timer: ITimer) => void + ): ITimer { + const timer = new NetworkTimer( + timeInSeconds * 1000, // 转为毫秒 + repeats, + context, + onTime + ); + + this.timers.add(timer as any); + + // 如果是一次性定时器,完成后自动清理 + if (!repeats) { + setTimeout(() => { + this.timers.delete(timer as any); + }, timeInSeconds * 1000 + 100); + } + + return timer; + } + + /** + * 清理所有定时器 + */ + static cleanup(): void { + for (const timer of this.timers) { + timer.stop(); + } + this.timers.clear(); + } +} \ No newline at end of file diff --git a/packages/network-client/src/utils/index.ts b/packages/network-client/src/utils/index.ts new file mode 100644 index 00000000..a21c3579 --- /dev/null +++ b/packages/network-client/src/utils/index.ts @@ -0,0 +1,4 @@ +/** + * 网络客户端工具类 + */ +export * from './NetworkTimer'; \ No newline at end of file diff --git a/packages/network-client/tests/setup.ts b/packages/network-client/tests/setup.ts deleted file mode 100644 index 99f6fa44..00000000 --- a/packages/network-client/tests/setup.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Jest测试环境设置 - 客户端 - */ - -// 导入reflect-metadata以支持装饰器 -import 'reflect-metadata'; - -// 模拟浏览器环境的WebSocket -Object.defineProperty(global, 'WebSocket', { - value: class MockWebSocket { - static CONNECTING = 0; - static OPEN = 1; - static CLOSING = 2; - static CLOSED = 3; - - readyState = MockWebSocket.CONNECTING; - url: string; - onopen: ((event: Event) => void) | null = null; - onclose: ((event: CloseEvent) => void) | null = null; - onmessage: ((event: MessageEvent) => void) | null = null; - onerror: ((event: Event) => void) | null = null; - - constructor(url: string) { - this.url = url; - // 模拟异步连接 - setTimeout(() => { - this.readyState = MockWebSocket.OPEN; - if (this.onopen) { - this.onopen(new Event('open')); - } - }, 0); - } - - send(data: string | ArrayBuffer) { - // 模拟发送 - } - - close() { - this.readyState = MockWebSocket.CLOSED; - if (this.onclose) { - this.onclose(new CloseEvent('close')); - } - } - } -}); - -// 全局测试配置 -beforeAll(() => { - // 设置测试环境 - process.env.NODE_ENV = 'test'; - process.env.NETWORK_ENV = 'client'; -}); - -afterAll(() => { - // 清理测试环境 -}); - -beforeEach(() => { - // 每个测试前的准备工作 -}); - -afterEach(() => { - // 每个测试后的清理工作 - // 清理可能的网络连接、定时器等 -}); \ No newline at end of file diff --git a/packages/network-server/jest.config.cjs b/packages/network-server/jest.config.cjs deleted file mode 100644 index b2357715..00000000 --- a/packages/network-server/jest.config.cjs +++ /dev/null @@ -1,47 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/tests'], - testMatch: ['**/*.test.ts', '**/*.spec.ts'], - testPathIgnorePatterns: ['/node_modules/'], - collectCoverage: false, - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/index.ts', - '!src/**/index.ts', - '!**/*.d.ts', - '!src/**/*.test.ts', - '!src/**/*.spec.ts' - ], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html'], - coverageThreshold: { - global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70 - } - }, - verbose: true, - transform: { - '^.+\\.tsx?$': ['ts-jest', { - tsconfig: 'tsconfig.json', - useESM: false, - }], - }, - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - setupFilesAfterEnv: ['/tests/setup.ts'], - testTimeout: 15000, // 服务端测试可能需要更长时间 - clearMocks: true, - restoreMocks: true, - modulePathIgnorePatterns: [ - '/bin/', - '/dist/', - '/node_modules/' - ] -}; \ No newline at end of file diff --git a/packages/network-server/package.json b/packages/network-server/package.json index d51a536e..547d8b25 100644 --- a/packages/network-server/package.json +++ b/packages/network-server/package.json @@ -43,10 +43,7 @@ "preversion": "npm run rebuild", "dev": "ts-node src/dev-server.ts", "start": "node bin/index.js", - "test": "jest --config jest.config.cjs", - "test:watch": "jest --watch --config jest.config.cjs", - "test:coverage": "jest --coverage --config jest.config.cjs", - "test:ci": "jest --ci --coverage --config jest.config.cjs" + "test": "echo \"No tests configured for network-server\"" }, "author": "yhh", "license": "MIT", @@ -54,11 +51,13 @@ "@esengine/ecs-framework": "file:../core", "@esengine/network-shared": "file:../network-shared", "ws": "^8.18.2", + "uuid": "^10.0.0", "reflect-metadata": "^0.2.2" }, "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^20.19.0", + "@types/uuid": "^10.0.0", "@types/ws": "^8.18.1", "jest": "^29.7.0", "rimraf": "^5.0.0", diff --git a/packages/network-server/src/core/ConnectionManager.ts b/packages/network-server/src/core/ConnectionManager.ts new file mode 100644 index 00000000..367ec9fa --- /dev/null +++ b/packages/network-server/src/core/ConnectionManager.ts @@ -0,0 +1,410 @@ +/** + * 服务端连接管理器 + * 负责管理所有客户端连接的生命周期 + */ +import { createLogger, ITimer, Core } from '@esengine/ecs-framework'; +import { + ITransportClientInfo, + ConnectionState, + IConnectionStats, + EventEmitter +} from '@esengine/network-shared'; + +/** + * 客户端会话信息 + */ +export interface ClientSession { + id: string; + info: ITransportClientInfo; + state: ConnectionState; + lastHeartbeat: number; + stats: IConnectionStats; + authenticated: boolean; + roomId?: string; + userData?: Record; +} + +/** + * 连接管理器配置 + */ +export interface ConnectionManagerConfig { + heartbeatInterval: number; + heartbeatTimeout: number; + maxIdleTime: number; + cleanupInterval: number; +} + +/** + * 连接管理器 + */ +export class ConnectionManager extends EventEmitter { + private logger = createLogger('ConnectionManager'); + private sessions: Map = new Map(); + private config: ConnectionManagerConfig; + private heartbeatTimer?: ITimer; + private cleanupTimer?: ITimer; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + super(); + this.config = { + heartbeatInterval: 30000, // 30秒心跳间隔 + heartbeatTimeout: 60000, // 60秒心跳超时 + maxIdleTime: 300000, // 5分钟最大空闲时间 + cleanupInterval: 60000, // 1分钟清理间隔 + ...config + }; + } + + /** + * 启动连接管理器 + */ + start(): void { + this.logger.info('连接管理器启动'); + this.startHeartbeatTimer(); + this.startCleanupTimer(); + } + + /** + * 停止连接管理器 + */ + stop(): void { + this.logger.info('连接管理器停止'); + this.stopHeartbeatTimer(); + this.stopCleanupTimer(); + this.sessions.clear(); + } + + /** + * 添加客户端会话 + */ + addSession(clientInfo: ITransportClientInfo): ClientSession { + const session: ClientSession = { + id: clientInfo.id, + info: clientInfo, + state: ConnectionState.Connected, + lastHeartbeat: Date.now(), + authenticated: false, + stats: { + state: ConnectionState.Connected, + connectTime: clientInfo.connectTime, + reconnectCount: 0, + bytesSent: 0, + bytesReceived: 0, + messagesSent: 0, + messagesReceived: 0 + } + }; + + this.sessions.set(clientInfo.id, session); + this.logger.info(`添加客户端会话: ${clientInfo.id}`); + + this.emit('sessionAdded', session); + return session; + } + + /** + * 移除客户端会话 + */ + removeSession(clientId: string, reason?: string): boolean { + const session = this.sessions.get(clientId); + if (!session) { + return false; + } + + session.state = ConnectionState.Disconnected; + session.stats.disconnectTime = Date.now(); + + this.sessions.delete(clientId); + this.logger.info(`移除客户端会话: ${clientId}, 原因: ${reason || '未知'}`); + + this.emit('sessionRemoved', session, reason); + return true; + } + + /** + * 获取客户端会话 + */ + getSession(clientId: string): ClientSession | undefined { + return this.sessions.get(clientId); + } + + /** + * 获取所有客户端会话 + */ + getAllSessions(): ClientSession[] { + return Array.from(this.sessions.values()); + } + + /** + * 获取已认证的会话 + */ + getAuthenticatedSessions(): ClientSession[] { + return Array.from(this.sessions.values()).filter(session => session.authenticated); + } + + /** + * 获取指定房间的会话 + */ + getSessionsByRoom(roomId: string): ClientSession[] { + return Array.from(this.sessions.values()).filter(session => session.roomId === roomId); + } + + /** + * 更新会话心跳时间 + */ + updateHeartbeat(clientId: string): boolean { + const session = this.sessions.get(clientId); + if (!session) { + return false; + } + + session.lastHeartbeat = Date.now(); + return true; + } + + /** + * 设置会话认证状态 + */ + setSessionAuthenticated(clientId: string, authenticated: boolean): boolean { + const session = this.sessions.get(clientId); + if (!session) { + return false; + } + + const wasAuthenticated = session.authenticated; + session.authenticated = authenticated; + + if (wasAuthenticated !== authenticated) { + this.emit('sessionAuthChanged', session, authenticated); + this.logger.info(`客户端 ${clientId} 认证状态变更: ${authenticated}`); + } + + return true; + } + + /** + * 设置会话所在房间 + */ + setSessionRoom(clientId: string, roomId?: string): boolean { + const session = this.sessions.get(clientId); + if (!session) { + return false; + } + + const oldRoomId = session.roomId; + session.roomId = roomId; + + if (oldRoomId !== roomId) { + this.emit('sessionRoomChanged', session, oldRoomId, roomId); + this.logger.info(`客户端 ${clientId} 房间变更: ${oldRoomId} -> ${roomId}`); + } + + return true; + } + + /** + * 更新会话数据统计 + */ + updateSessionStats(clientId: string, stats: Partial): boolean { + const session = this.sessions.get(clientId); + if (!session) { + return false; + } + + Object.assign(session.stats, stats); + return true; + } + + /** + * 设置会话用户数据 + */ + setSessionUserData(clientId: string, userData: Record): boolean { + const session = this.sessions.get(clientId); + if (!session) { + return false; + } + + session.userData = { ...session.userData, ...userData }; + return true; + } + + /** + * 检查会话是否存活 + */ + isSessionAlive(clientId: string): boolean { + const session = this.sessions.get(clientId); + if (!session) { + return false; + } + + const now = Date.now(); + return (now - session.lastHeartbeat) <= this.config.heartbeatTimeout; + } + + /** + * 获取超时的会话 + */ + getTimeoutSessions(): ClientSession[] { + const now = Date.now(); + return Array.from(this.sessions.values()).filter(session => { + return (now - session.lastHeartbeat) > this.config.heartbeatTimeout; + }); + } + + /** + * 获取空闲的会话 + */ + getIdleSessions(): ClientSession[] { + const now = Date.now(); + return Array.from(this.sessions.values()).filter(session => { + return (now - session.lastHeartbeat) > this.config.maxIdleTime; + }); + } + + /** + * 获取连接统计信息 + */ + getConnectionStats() { + const allSessions = this.getAllSessions(); + const authenticatedSessions = this.getAuthenticatedSessions(); + const timeoutSessions = this.getTimeoutSessions(); + + return { + totalConnections: allSessions.length, + authenticatedConnections: authenticatedSessions.length, + timeoutConnections: timeoutSessions.length, + averageLatency: this.calculateAverageLatency(allSessions), + connectionsByRoom: this.getConnectionsByRoom(), + totalBytesSent: allSessions.reduce((sum, s) => sum + s.stats.bytesSent, 0), + totalBytesReceived: allSessions.reduce((sum, s) => sum + s.stats.bytesReceived, 0), + totalMessagesSent: allSessions.reduce((sum, s) => sum + s.stats.messagesSent, 0), + totalMessagesReceived: allSessions.reduce((sum, s) => sum + s.stats.messagesReceived, 0) + }; + } + + /** + * 计算平均延迟 + */ + private calculateAverageLatency(sessions: ClientSession[]): number { + const validLatencies = sessions + .map(s => s.stats.latency) + .filter(latency => latency !== undefined) as number[]; + + if (validLatencies.length === 0) return 0; + + return validLatencies.reduce((sum, latency) => sum + latency, 0) / validLatencies.length; + } + + /** + * 按房间统计连接数 + */ + private getConnectionsByRoom(): Record { + const roomCounts: Record = {}; + + for (const session of this.sessions.values()) { + const roomId = session.roomId || 'lobby'; + roomCounts[roomId] = (roomCounts[roomId] || 0) + 1; + } + + return roomCounts; + } + + /** + * 启动心跳定时器 + */ + private startHeartbeatTimer(): void { + this.heartbeatTimer = Core.schedule(this.config.heartbeatInterval / 1000, true, this, () => { + this.checkHeartbeats(); + }); + } + + /** + * 停止心跳定时器 + */ + private stopHeartbeatTimer(): void { + if (this.heartbeatTimer) { + this.heartbeatTimer.stop(); + this.heartbeatTimer = undefined; + } + } + + /** + * 启动清理定时器 + */ + private startCleanupTimer(): void { + this.cleanupTimer = Core.schedule(this.config.cleanupInterval / 1000, true, this, () => { + this.performCleanup(); + }); + } + + /** + * 停止清理定时器 + */ + private stopCleanupTimer(): void { + if (this.cleanupTimer) { + this.cleanupTimer.stop(); + this.cleanupTimer = undefined; + } + } + + /** + * 检查心跳超时 + */ + private checkHeartbeats(): void { + const timeoutSessions = this.getTimeoutSessions(); + + for (const session of timeoutSessions) { + this.logger.warn(`客户端心跳超时: ${session.id}`); + this.emit('heartbeatTimeout', session); + + // 可以选择断开超时的连接 + // this.removeSession(session.id, '心跳超时'); + } + + if (timeoutSessions.length > 0) { + this.logger.warn(`发现 ${timeoutSessions.length} 个心跳超时的连接`); + } + } + + /** + * 执行清理操作 + */ + private performCleanup(): void { + const idleSessions = this.getIdleSessions(); + + for (const session of idleSessions) { + this.logger.info(`清理空闲连接: ${session.id}`); + this.removeSession(session.id, '空闲超时'); + } + + if (idleSessions.length > 0) { + this.logger.info(`清理了 ${idleSessions.length} 个空闲连接`); + } + } + + /** + * 批量操作:踢出指定房间的所有客户端 + */ + kickRoomClients(roomId: string, reason?: string): number { + const roomSessions = this.getSessionsByRoom(roomId); + + for (const session of roomSessions) { + this.removeSession(session.id, reason || '房间解散'); + } + + this.logger.info(`踢出房间 ${roomId} 的 ${roomSessions.length} 个客户端`); + return roomSessions.length; + } + + /** + * 批量操作:向指定房间广播消息(这里只返回会话列表) + */ + getRoomSessionsForBroadcast(roomId: string, excludeClientId?: string): ClientSession[] { + return this.getSessionsByRoom(roomId).filter(session => + session.id !== excludeClientId && session.authenticated + ); + } +} \ No newline at end of file diff --git a/packages/network-server/src/core/NetworkServer.ts b/packages/network-server/src/core/NetworkServer.ts new file mode 100644 index 00000000..fc28d094 --- /dev/null +++ b/packages/network-server/src/core/NetworkServer.ts @@ -0,0 +1,701 @@ +/** + * 网络服务器核心类 + * 负责服务器的启动/停止、传输层管理和客户端会话管理 + */ +import { createLogger, Core } from '@esengine/ecs-framework'; +import { + ITransportConfig, + MessageType, + INetworkMessage, + IConnectMessage, + IConnectResponseMessage, + IHeartbeatMessage, + NetworkErrorType, + EventEmitter +} from '@esengine/network-shared'; +import { WebSocketTransport } from '../transport/WebSocketTransport'; +import { ConnectionManager, ClientSession } from './ConnectionManager'; +import { JSONSerializer } from '@esengine/network-shared'; +import { MessageManager } from '@esengine/network-shared'; +import { ErrorHandler } from '@esengine/network-shared'; + +/** + * 网络服务器配置 + */ +export interface NetworkServerConfig { + transport: ITransportConfig; + authentication: { + required: boolean; + timeout: number; + maxAttempts: number; + }; + rateLimit: { + enabled: boolean; + maxRequestsPerMinute: number; + banDuration: number; + }; + features: { + enableCompression: boolean; + enableHeartbeat: boolean; + enableRooms: boolean; + enableMetrics: boolean; + }; +} + +/** + * 服务器状态 + */ +export enum ServerState { + Stopped = 'stopped', + Starting = 'starting', + Running = 'running', + Stopping = 'stopping', + Error = 'error' +} + +/** + * 服务器统计信息 + */ +export interface ServerStats { + state: ServerState; + uptime: number; + startTime?: number; + connections: { + total: number; + authenticated: number; + peak: number; + }; + messages: { + sent: number; + received: number; + errors: number; + }; + bandwidth: { + inbound: number; + outbound: number; + }; +} + +/** + * 网络服务器事件接口 + */ +export interface NetworkServerEvents { + serverStarted: (port: number) => void; + serverStopped: () => void; + serverError: (error: Error) => void; + clientConnected: (session: ClientSession) => void; + clientDisconnected: (session: ClientSession, reason?: string) => void; + clientAuthenticated: (session: ClientSession) => void; + messageReceived: (session: ClientSession, message: INetworkMessage) => void; + messageSent: (session: ClientSession, message: INetworkMessage) => void; +} + +/** + * 网络服务器核心实现 + */ +export class NetworkServer extends EventEmitter { + private logger = createLogger('NetworkServer'); + private config: NetworkServerConfig; + private state: ServerState = ServerState.Stopped; + private stats: ServerStats; + + // 核心组件 + private transport?: WebSocketTransport; + private connectionManager: ConnectionManager; + private serializer: JSONSerializer; + private messageManager: MessageManager; + private errorHandler: ErrorHandler; + + // 事件处理器 + private eventHandlers: Partial = {}; + + // 速率限制 + private rateLimitMap: Map = new Map(); + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + super(); + + this.config = { + transport: { + port: 8080, + host: '0.0.0.0', + maxConnections: 1000, + heartbeatInterval: 30000, + connectionTimeout: 60000, + maxMessageSize: 1024 * 1024, + compression: true, + ...config.transport + }, + authentication: { + required: false, + timeout: 30000, + maxAttempts: 3, + ...config.authentication + }, + rateLimit: { + enabled: true, + maxRequestsPerMinute: 100, + banDuration: 300000, // 5分钟 + ...config.rateLimit + }, + features: { + enableCompression: true, + enableHeartbeat: true, + enableRooms: true, + enableMetrics: true, + ...config.features + } + }; + + this.stats = { + state: ServerState.Stopped, + uptime: 0, + connections: { + total: 0, + authenticated: 0, + peak: 0 + }, + messages: { + sent: 0, + received: 0, + errors: 0 + }, + bandwidth: { + inbound: 0, + outbound: 0 + } + }; + + // 初始化核心组件 + this.connectionManager = new ConnectionManager({ + heartbeatInterval: this.config.transport.heartbeatInterval, + heartbeatTimeout: this.config.transport.connectionTimeout + }); + + this.serializer = new JSONSerializer({ + enableTypeChecking: true, + enableCompression: this.config.features.enableCompression, + maxMessageSize: this.config.transport.maxMessageSize + }); + + this.messageManager = new MessageManager({ + enableTimestampValidation: true, + enableMessageDeduplication: true + }); + + this.errorHandler = new ErrorHandler({ + maxRetryAttempts: 3, + enableAutoRecovery: true + }); + + this.setupEventHandlers(); + } + + /** + * 启动服务器 + */ + async start(): Promise { + if (this.state !== ServerState.Stopped) { + throw new Error(`服务器状态错误: ${this.state}`); + } + + this.setState(ServerState.Starting); + this.logger.info('正在启动网络服务器...'); + + try { + // 创建传输层 + this.transport = new WebSocketTransport(this.config.transport); + this.setupTransportEvents(); + + // 启动传输层 + await this.transport.start( + this.config.transport.port, + this.config.transport.host + ); + + // 启动连接管理器 + this.connectionManager.start(); + + // 记录启动时间 + this.stats.startTime = Date.now(); + this.setState(ServerState.Running); + + this.logger.info(`网络服务器已启动: ${this.config.transport.host}:${this.config.transport.port}`); + this.eventHandlers.serverStarted?.(this.config.transport.port); + + } catch (error) { + this.setState(ServerState.Error); + this.logger.error('启动网络服务器失败:', error); + this.eventHandlers.serverError?.(error as Error); + throw error; + } + } + + /** + * 停止服务器 + */ + async stop(): Promise { + if (this.state === ServerState.Stopped) { + return; + } + + this.setState(ServerState.Stopping); + this.logger.info('正在停止网络服务器...'); + + try { + // 停止连接管理器 + this.connectionManager.stop(); + + // 停止传输层 + if (this.transport) { + await this.transport.stop(); + this.transport = undefined; + } + + // 清理速率限制数据 + this.rateLimitMap.clear(); + + this.setState(ServerState.Stopped); + this.logger.info('网络服务器已停止'); + this.eventHandlers.serverStopped?.(); + + } catch (error) { + this.logger.error('停止网络服务器失败:', error); + this.eventHandlers.serverError?.(error as Error); + throw error; + } + } + + /** + * 发送消息到指定客户端 + */ + sendToClient(clientId: string, message: T): boolean { + if (!this.transport || this.state !== ServerState.Running) { + this.logger.warn('服务器未运行,无法发送消息'); + return false; + } + + const session = this.connectionManager.getSession(clientId); + if (!session) { + this.logger.warn(`客户端会话不存在: ${clientId}`); + return false; + } + + try { + const serializedMessage = this.serializer.serialize(message); + this.transport.send(clientId, serializedMessage.data); + + // 更新统计 + this.stats.messages.sent++; + this.stats.bandwidth.outbound += serializedMessage.size; + + this.eventHandlers.messageSent?.(session, message); + return true; + + } catch (error) { + this.logger.error(`发送消息到客户端 ${clientId} 失败:`, error); + this.stats.messages.errors++; + this.errorHandler.handleError(error as Error, `sendToClient:${clientId}`); + return false; + } + } + + /** + * 广播消息到所有客户端 + */ + broadcast(message: T, exclude?: string[]): number { + if (!this.transport || this.state !== ServerState.Running) { + this.logger.warn('服务器未运行,无法广播消息'); + return 0; + } + + try { + const serializedMessage = this.serializer.serialize(message); + this.transport.broadcast(serializedMessage.data, exclude); + + const clientCount = this.connectionManager.getAllSessions().length - (exclude?.length || 0); + + // 更新统计 + this.stats.messages.sent += clientCount; + this.stats.bandwidth.outbound += serializedMessage.size * clientCount; + + return clientCount; + + } catch (error) { + this.logger.error('广播消息失败:', error); + this.stats.messages.errors++; + this.errorHandler.handleError(error as Error, 'broadcast'); + return 0; + } + } + + /** + * 踢出客户端 + */ + kickClient(clientId: string, reason?: string): boolean { + const session = this.connectionManager.getSession(clientId); + if (!session) { + return false; + } + + if (this.transport) { + this.transport.disconnectClient(clientId, reason); + } + + return this.connectionManager.removeSession(clientId, reason); + } + + /** + * 获取服务器状态 + */ + getState(): ServerState { + return this.state; + } + + /** + * 检查服务器是否正在运行 + */ + isRunning(): boolean { + return this.state === ServerState.Running; + } + + /** + * 获取服务器统计信息 + */ + getStats(): ServerStats { + const currentStats = { ...this.stats }; + + if (this.stats.startTime) { + currentStats.uptime = Date.now() - this.stats.startTime; + } + + const connectionStats = this.connectionManager.getConnectionStats(); + currentStats.connections.total = connectionStats.totalConnections; + currentStats.connections.authenticated = connectionStats.authenticatedConnections; + + return currentStats; + } + + /** + * 获取所有客户端会话 + */ + getAllSessions(): ClientSession[] { + return this.connectionManager.getAllSessions(); + } + + /** + * 获取指定客户端会话 + */ + getSession(clientId: string): ClientSession | undefined { + return this.connectionManager.getSession(clientId); + } + + /** + * 设置事件处理器 + */ + override on(event: K, handler: NetworkServerEvents[K]): this { + this.eventHandlers[event] = handler; + return this; + } + + /** + * 移除事件处理器 + */ + override off(event: K): this { + delete this.eventHandlers[event]; + return this; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('服务器配置已更新:', newConfig); + } + + /** + * 设置服务器状态 + */ + private setState(newState: ServerState): void { + if (this.state === newState) return; + + const oldState = this.state; + this.state = newState; + this.stats.state = newState; + + this.logger.info(`服务器状态变化: ${oldState} -> ${newState}`); + } + + /** + * 设置事件处理器 + */ + private setupEventHandlers(): void { + // 连接管理器事件 + this.connectionManager.on('sessionAdded', (session: ClientSession) => { + this.eventHandlers.clientConnected?.(session); + this.updateConnectionPeak(); + }); + + this.connectionManager.on('sessionRemoved', (session: ClientSession, reason?: string) => { + this.eventHandlers.clientDisconnected?.(session, reason); + }); + + this.connectionManager.on('sessionAuthChanged', (session: ClientSession, authenticated: boolean) => { + if (authenticated) { + this.eventHandlers.clientAuthenticated?.(session); + } + }); + + // 错误处理器事件 + this.errorHandler.on('criticalError', (error: any) => { + this.logger.error('严重错误:', error); + this.eventHandlers.serverError?.(new Error(error.message)); + }); + } + + /** + * 设置传输层事件 + */ + private setupTransportEvents(): void { + if (!this.transport) return; + + this.transport.onConnect((clientInfo) => { + this.handleClientConnect(clientInfo); + }); + + this.transport.onDisconnect((clientId, reason) => { + this.handleClientDisconnect(clientId, reason); + }); + + this.transport.onMessage((clientId, data) => { + this.handleClientMessage(clientId, data); + }); + + this.transport.onError((error) => { + this.handleTransportError(error); + }); + } + + /** + * 处理客户端连接 + */ + private handleClientConnect(clientInfo: any): void { + try { + // 检查速率限制 + if (this.isRateLimited(clientInfo.remoteAddress)) { + this.transport?.disconnectClient(clientInfo.id, '速率限制'); + return; + } + + // 创建客户端会话 + const session = this.connectionManager.addSession(clientInfo); + + this.logger.info(`客户端已连接: ${clientInfo.id} from ${clientInfo.remoteAddress}`); + + } catch (error) { + this.logger.error('处理客户端连接失败:', error); + this.transport?.disconnectClient(clientInfo.id, '服务器错误'); + } + } + + /** + * 处理客户端断开连接 + */ + private handleClientDisconnect(clientId: string, reason?: string): void { + this.connectionManager.removeSession(clientId, reason); + this.logger.info(`客户端已断开连接: ${clientId}, 原因: ${reason || '未知'}`); + } + + /** + * 处理客户端消息 + */ + private handleClientMessage(clientId: string, data: ArrayBuffer | string): void { + try { + // 获取客户端会话 + const session = this.connectionManager.getSession(clientId); + if (!session) { + this.logger.warn(`收到未知客户端消息: ${clientId}`); + return; + } + + // 检查速率限制 + if (this.isRateLimited(session.info.remoteAddress)) { + this.kickClient(clientId, '速率限制'); + return; + } + + // 反序列化消息 + const deserializationResult = this.serializer.deserialize(data); + if (!deserializationResult.isValid) { + this.logger.warn(`消息反序列化失败: ${deserializationResult.errors?.join(', ')}`); + this.stats.messages.errors++; + return; + } + + const message = deserializationResult.data; + + // 验证消息 + const validationResult = this.messageManager.validateMessage(message, clientId); + if (!validationResult.isValid) { + this.logger.warn(`消息验证失败: ${validationResult.errors.join(', ')}`); + this.stats.messages.errors++; + return; + } + + // 更新心跳 + this.connectionManager.updateHeartbeat(clientId); + + // 更新统计 + this.stats.messages.received++; + this.stats.bandwidth.inbound += (typeof data === 'string' ? data.length : data.byteLength); + + // 处理不同类型的消息 + this.processMessage(session, message); + + this.eventHandlers.messageReceived?.(session, message); + + } catch (error) { + this.logger.error(`处理客户端 ${clientId} 消息失败:`, error); + this.stats.messages.errors++; + this.errorHandler.handleError(error as Error, `handleClientMessage:${clientId}`); + } + } + + /** + * 处理传输层错误 + */ + private handleTransportError(error: Error): void { + this.logger.error('传输层错误:', error); + this.errorHandler.handleError(error, 'transport'); + this.eventHandlers.serverError?.(error); + } + + /** + * 处理具体消息类型 + */ + private processMessage(session: ClientSession, message: INetworkMessage): void { + switch (message.type) { + case MessageType.CONNECT: + this.handleConnectMessage(session, message as IConnectMessage); + break; + + case MessageType.HEARTBEAT: + this.handleHeartbeatMessage(session, message as IHeartbeatMessage); + break; + + default: + // 其他消息类型由外部处理器处理 + break; + } + } + + /** + * 处理连接消息 + */ + private handleConnectMessage(session: ClientSession, message: IConnectMessage): void { + const response: IConnectResponseMessage = this.messageManager.createMessage( + MessageType.CONNECT, + { + success: true, + clientId: session.id, + serverInfo: { + name: 'ECS Network Server', + version: '1.0.0', + maxPlayers: this.config.transport.maxConnections || 1000, + currentPlayers: this.connectionManager.getAllSessions().length + } + }, + 'server' + ); + + this.sendToClient(session.id, response); + + if (this.config.authentication.required) { + // 设置认证超时 + Core.schedule(this.config.authentication.timeout / 1000, false, this, () => { + if (!session.authenticated) { + this.kickClient(session.id, '认证超时'); + } + }); + } else { + // 自动设置为已认证 + this.connectionManager.setSessionAuthenticated(session.id, true); + } + } + + /** + * 处理心跳消息 + */ + private handleHeartbeatMessage(session: ClientSession, message: IHeartbeatMessage): void { + const response: IHeartbeatMessage = this.messageManager.createMessage( + MessageType.HEARTBEAT, + { + clientTime: message.data.clientTime, + serverTime: Date.now() + }, + 'server' + ); + + this.sendToClient(session.id, response); + } + + /** + * 检查速率限制 + */ + private isRateLimited(address: string): boolean { + if (!this.config.rateLimit.enabled) { + return false; + } + + const now = Date.now(); + const limit = this.rateLimitMap.get(address); + + if (!limit) { + this.rateLimitMap.set(address, { + count: 1, + resetTime: now + 60000, // 1分钟重置 + banned: false + }); + return false; + } + + // 检查是否被封禁 + if (limit.banned && now < limit.resetTime) { + return true; + } + + // 重置计数 + if (now > limit.resetTime) { + limit.count = 1; + limit.resetTime = now + 60000; + limit.banned = false; + return false; + } + + limit.count++; + + // 检查是否超过限制 + if (limit.count > this.config.rateLimit.maxRequestsPerMinute) { + limit.banned = true; + limit.resetTime = now + this.config.rateLimit.banDuration; + this.logger.warn(`客户端 ${address} 被封禁,原因: 速率限制`); + return true; + } + + return false; + } + + /** + * 更新连接峰值 + */ + private updateConnectionPeak(): void { + const current = this.connectionManager.getAllSessions().length; + if (current > this.stats.connections.peak) { + this.stats.connections.peak = current; + } + } +} \ No newline at end of file diff --git a/packages/network-server/src/index.ts b/packages/network-server/src/index.ts index a1073c9e..1117dee3 100644 --- a/packages/network-server/src/index.ts +++ b/packages/network-server/src/index.ts @@ -3,24 +3,16 @@ * ECS Framework网络层 - 服务端实现 */ -// 核心服务器 (待实现) -// export * from './core/NetworkServer'; -// export * from './core/ClientConnection'; +// 核心服务器 +export * from './core/NetworkServer'; +export * from './core/ConnectionManager'; -// 传输层 (待实现) -// export * from './transport/WebSocketTransport'; -// export * from './transport/HttpTransport'; +// 传输层 +export * from './transport/WebSocketTransport'; -// 系统层 (待实现) -// export * from './systems/SyncVarSystem'; -// export * from './systems/RpcSystem'; - -// 房间管理 (待实现) -// export * from './rooms/Room'; -// export * from './rooms/RoomManager'; - -// 认证授权 (待实现) -// export * from './auth/AuthManager'; +// 房间管理 +export * from './rooms/Room'; +export * from './rooms/RoomManager'; // 重新导出shared包的类型 export * from '@esengine/network-shared'; \ No newline at end of file diff --git a/packages/network-server/src/rooms/Room.ts b/packages/network-server/src/rooms/Room.ts new file mode 100644 index 00000000..8b3b188c --- /dev/null +++ b/packages/network-server/src/rooms/Room.ts @@ -0,0 +1,507 @@ +/** + * 房间基础实现 + * 提供房间的基本功能,包括玩家管理和房间状态管理 + */ +import { createLogger } from '@esengine/ecs-framework'; +import { RoomState, IRoomInfo, INetworkMessage, EventEmitter } from '@esengine/network-shared'; +import { ClientSession } from '../core/ConnectionManager'; + +/** + * 房间配置 + */ +export interface RoomConfig { + id: string; + name: string; + maxPlayers: number; + isPublic: boolean; + password?: string; + metadata?: Record; + autoDestroy: boolean; // 是否在空房间时自动销毁 + customData?: Record; +} + +/** + * 玩家信息 + */ +export interface PlayerInfo { + sessionId: string; + name: string; + isHost: boolean; + joinTime: number; + customData?: Record; +} + +/** + * 房间事件接口 + */ +export interface RoomEvents { + playerJoined: (player: PlayerInfo) => void; + playerLeft: (player: PlayerInfo, reason?: string) => void; + hostChanged: (oldHost: PlayerInfo, newHost: PlayerInfo) => void; + stateChanged: (oldState: RoomState, newState: RoomState) => void; + messageReceived: (message: INetworkMessage, fromPlayer: PlayerInfo) => void; + roomDestroyed: (reason: string) => void; +} + +/** + * 房间统计信息 + */ +export interface RoomStats { + id: string; + playerCount: number; + maxPlayers: number; + createTime: number; + totalPlayersJoined: number; + messagesSent: number; + messagesReceived: number; + state: RoomState; +} + +/** + * 房间类 + */ +export class Room extends EventEmitter { + private logger = createLogger('Room'); + private config: RoomConfig; + private state: RoomState = RoomState.Waiting; + private players: Map = new Map(); + private hostId?: string; + private createTime: number = Date.now(); + private stats: RoomStats; + + // 事件处理器 + private eventHandlers: Partial = {}; + + /** + * 构造函数 + */ + constructor(config: RoomConfig) { + super(); + this.config = { ...config }; + + this.stats = { + id: config.id, + playerCount: 0, + maxPlayers: config.maxPlayers, + createTime: this.createTime, + totalPlayersJoined: 0, + messagesSent: 0, + messagesReceived: 0, + state: this.state + }; + + this.logger.info(`房间已创建: ${config.id} (${config.name})`); + } + + /** + * 玩家加入房间 + */ + addPlayer(session: ClientSession, playerName?: string, password?: string): boolean { + // 检查房间是否已满 + if (this.players.size >= this.config.maxPlayers) { + this.logger.warn(`房间已满,拒绝玩家加入: ${session.id}`); + return false; + } + + // 检查玩家是否已在房间中 + if (this.players.has(session.id)) { + this.logger.warn(`玩家已在房间中: ${session.id}`); + return false; + } + + // 检查房间密码 + if (this.config.password && password !== this.config.password) { + this.logger.warn(`密码错误,拒绝玩家加入: ${session.id}`); + return false; + } + + // 检查房间状态 + if (this.state === RoomState.Finished) { + this.logger.warn(`房间已结束,拒绝玩家加入: ${session.id}`); + return false; + } + + // 创建玩家信息 + const player: PlayerInfo = { + sessionId: session.id, + name: playerName || `Player_${session.id.substr(-6)}`, + isHost: this.players.size === 0, // 第一个加入的玩家成为房主 + joinTime: Date.now(), + customData: {} + }; + + // 添加玩家到房间 + this.players.set(session.id, player); + this.stats.playerCount = this.players.size; + this.stats.totalPlayersJoined++; + + // 设置房主 + if (player.isHost) { + this.hostId = session.id; + } + + this.logger.info(`玩家加入房间: ${player.name} (${session.id}) -> 房间 ${this.config.id}`); + + // 触发事件 + this.eventHandlers.playerJoined?.(player); + this.emit('playerJoined', player); + + return true; + } + + /** + * 玩家离开房间 + */ + removePlayer(sessionId: string, reason?: string): boolean { + const player = this.players.get(sessionId); + if (!player) { + return false; + } + + // 从房间移除玩家 + this.players.delete(sessionId); + this.stats.playerCount = this.players.size; + + this.logger.info(`玩家离开房间: ${player.name} (${sessionId}) <- 房间 ${this.config.id}, 原因: ${reason || '未知'}`); + + // 如果离开的是房主,需要转移房主权限 + if (player.isHost && this.players.size > 0) { + this.transferHost(); + } + + // 触发事件 + this.eventHandlers.playerLeft?.(player, reason); + this.emit('playerLeft', player, reason); + + // 检查是否需要自动销毁房间 + if (this.config.autoDestroy && this.players.size === 0) { + this.destroy('房间为空'); + } + + return true; + } + + /** + * 获取玩家信息 + */ + getPlayer(sessionId: string): PlayerInfo | undefined { + return this.players.get(sessionId); + } + + /** + * 获取所有玩家 + */ + getAllPlayers(): PlayerInfo[] { + return Array.from(this.players.values()); + } + + /** + * 获取房主 + */ + getHost(): PlayerInfo | undefined { + return this.hostId ? this.players.get(this.hostId) : undefined; + } + + /** + * 转移房主权限 + */ + transferHost(newHostId?: string): boolean { + if (this.players.size === 0) { + return false; + } + + const oldHost = this.getHost(); + let newHost: PlayerInfo | undefined; + + if (newHostId) { + newHost = this.players.get(newHostId); + if (!newHost) { + this.logger.warn(`指定的新房主不存在: ${newHostId}`); + return false; + } + } else { + // 自动选择第一个玩家作为新房主 + newHost = Array.from(this.players.values())[0]; + } + + // 更新房主信息 + if (oldHost) { + oldHost.isHost = false; + } + newHost.isHost = true; + this.hostId = newHost.sessionId; + + this.logger.info(`房主权限转移: ${oldHost?.name || 'unknown'} -> ${newHost.name}`); + + // 触发事件 + if (oldHost) { + this.eventHandlers.hostChanged?.(oldHost, newHost); + this.emit('hostChanged', oldHost, newHost); + } + + return true; + } + + /** + * 设置房间状态 + */ + setState(newState: RoomState): void { + if (this.state === newState) { + return; + } + + const oldState = this.state; + this.state = newState; + this.stats.state = newState; + + this.logger.info(`房间状态变化: ${oldState} -> ${newState}`); + + // 触发事件 + this.eventHandlers.stateChanged?.(oldState, newState); + this.emit('stateChanged', oldState, newState); + } + + /** + * 处理房间消息 + */ + handleMessage(message: INetworkMessage, fromSessionId: string): void { + const player = this.players.get(fromSessionId); + if (!player) { + this.logger.warn(`收到非房间成员的消息: ${fromSessionId}`); + return; + } + + this.stats.messagesReceived++; + + // 触发事件 + this.eventHandlers.messageReceived?.(message, player); + this.emit('messageReceived', message, player); + } + + /** + * 广播消息到房间内所有玩家 + */ + broadcast(message: INetworkMessage, exclude?: string[], onSend?: (sessionId: string) => void): void { + const excludeSet = new Set(exclude || []); + let sentCount = 0; + + for (const player of this.players.values()) { + if (!excludeSet.has(player.sessionId)) { + if (onSend) { + onSend(player.sessionId); + sentCount++; + } + } + } + + this.stats.messagesSent += sentCount; + } + + /** + * 检查玩家是否在房间中 + */ + hasPlayer(sessionId: string): boolean { + return this.players.has(sessionId); + } + + /** + * 检查房间是否已满 + */ + isFull(): boolean { + return this.players.size >= this.config.maxPlayers; + } + + /** + * 检查房间是否为空 + */ + isEmpty(): boolean { + return this.players.size === 0; + } + + /** + * 获取房间信息 + */ + getRoomInfo(): IRoomInfo { + return { + id: this.config.id, + name: this.config.name, + playerCount: this.players.size, + maxPlayers: this.config.maxPlayers, + state: this.state, + metadata: this.config.metadata + }; + } + + /** + * 获取房间配置 + */ + getConfig(): RoomConfig { + return { ...this.config }; + } + + /** + * 获取房间统计信息 + */ + getStats(): RoomStats { + return { + ...this.stats, + playerCount: this.players.size + }; + } + + /** + * 更新房间配置 + */ + updateConfig(updates: Partial): void { + Object.assign(this.config, updates); + this.logger.info(`房间配置已更新: ${this.config.id}`, updates); + } + + /** + * 设置玩家自定义数据 + */ + setPlayerData(sessionId: string, data: Record): boolean { + const player = this.players.get(sessionId); + if (!player) { + return false; + } + + player.customData = { ...player.customData, ...data }; + return true; + } + + /** + * 获取房间运行时间 + */ + getUptime(): number { + return Date.now() - this.createTime; + } + + /** + * 验证密码 + */ + validatePassword(password?: string): boolean { + if (!this.config.password) { + return true; // 无密码房间 + } + return password === this.config.password; + } + + /** + * 设置事件处理器 + */ + override on(event: K, handler: RoomEvents[K]): this { + this.eventHandlers[event] = handler; + return super.on(event, handler as any); + } + + /** + * 移除事件处理器 + */ + override off(event: K): this { + delete this.eventHandlers[event]; + return super.off(event, this.eventHandlers[event] as any); + } + + /** + * 销毁房间 + */ + destroy(reason: string = '房间关闭'): void { + this.logger.info(`房间销毁: ${this.config.id}, 原因: ${reason}`); + + // 清理所有玩家 + const playersToRemove = Array.from(this.players.keys()); + for (const sessionId of playersToRemove) { + this.removePlayer(sessionId, reason); + } + + // 触发销毁事件 + this.eventHandlers.roomDestroyed?.(reason); + this.emit('roomDestroyed', reason); + + // 清理资源 + this.players.clear(); + this.removeAllListeners(); + } + + /** + * 获取房间详细状态 + */ + getDetailedStatus() { + return { + config: this.getConfig(), + info: this.getRoomInfo(), + stats: this.getStats(), + players: this.getAllPlayers(), + host: this.getHost(), + uptime: this.getUptime(), + isEmpty: this.isEmpty(), + isFull: this.isFull() + }; + } + + /** + * 踢出玩家 + */ + kickPlayer(sessionId: string, reason: string = '被踢出房间'): boolean { + if (!this.hasPlayer(sessionId)) { + return false; + } + + return this.removePlayer(sessionId, reason); + } + + /** + * 暂停房间 + */ + pause(): void { + if (this.state === RoomState.Playing) { + this.setState(RoomState.Paused); + } + } + + /** + * 恢复房间 + */ + resume(): void { + if (this.state === RoomState.Paused) { + this.setState(RoomState.Playing); + } + } + + /** + * 开始游戏 + */ + startGame(): boolean { + if (this.state !== RoomState.Waiting) { + return false; + } + + if (this.players.size === 0) { + return false; + } + + this.setState(RoomState.Playing); + return true; + } + + /** + * 结束游戏 + */ + endGame(): boolean { + if (this.state !== RoomState.Playing && this.state !== RoomState.Paused) { + return false; + } + + this.setState(RoomState.Finished); + return true; + } + + /** + * 重置房间到等待状态 + */ + reset(): void { + this.setState(RoomState.Waiting); + // 可以根据需要重置其他状态 + } +} \ No newline at end of file diff --git a/packages/network-server/src/rooms/RoomManager.ts b/packages/network-server/src/rooms/RoomManager.ts new file mode 100644 index 00000000..d0e04906 --- /dev/null +++ b/packages/network-server/src/rooms/RoomManager.ts @@ -0,0 +1,621 @@ +/** + * 房间管理器 + * 负责房间的创建、销毁和管理 + */ +import { createLogger, ITimer, Core } from '@esengine/ecs-framework'; +import { Room, RoomConfig, PlayerInfo, RoomEvents } from './Room'; +import { ClientSession } from '../core/ConnectionManager'; +import { RoomState, IRoomInfo, EventEmitter } from '@esengine/network-shared'; + +/** + * 房间管理器配置 + */ +export interface RoomManagerConfig { + maxRooms: number; + defaultMaxPlayers: number; + autoCleanupInterval: number; // 自动清理间隔(毫秒) + roomIdLength: number; + allowDuplicateNames: boolean; + defaultAutoDestroy: boolean; +} + +/** + * 房间查询选项 + */ +export interface RoomQueryOptions { + state?: RoomState; + hasPassword?: boolean; + minPlayers?: number; + maxPlayers?: number; + notFull?: boolean; + publicOnly?: boolean; + limit?: number; + offset?: number; +} + +/** + * 房间创建选项 + */ +export interface CreateRoomOptions { + id?: string; + name: string; + maxPlayers?: number; + isPublic?: boolean; + password?: string; + metadata?: Record; + autoDestroy?: boolean; +} + +/** + * 房间管理器事件接口 + */ +export interface RoomManagerEvents { + roomCreated: (room: Room) => void; + roomDestroyed: (room: Room, reason: string) => void; + playerJoinedRoom: (room: Room, player: PlayerInfo) => void; + playerLeftRoom: (room: Room, player: PlayerInfo, reason?: string) => void; +} + +/** + * 房间管理器统计 + */ +export interface RoomManagerStats { + totalRooms: number; + activeRooms: number; + totalPlayers: number; + roomsByState: Record; + roomsCreated: number; + roomsDestroyed: number; + playersJoined: number; + playersLeft: number; +} + +/** + * 房间管理器 + */ +export class RoomManager extends EventEmitter { + private logger = createLogger('RoomManager'); + private config: RoomManagerConfig; + private rooms: Map = new Map(); + private playerRoomMap: Map = new Map(); // sessionId -> roomId + private stats: RoomManagerStats; + private cleanupTimer?: ITimer; + + // 事件处理器 + private eventHandlers: Partial = {}; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + super(); + + this.config = { + maxRooms: 1000, + defaultMaxPlayers: 8, + autoCleanupInterval: 300000, // 5分钟 + roomIdLength: 8, + allowDuplicateNames: true, + defaultAutoDestroy: true, + ...config + }; + + this.stats = { + totalRooms: 0, + activeRooms: 0, + totalPlayers: 0, + roomsByState: { + [RoomState.Waiting]: 0, + [RoomState.Playing]: 0, + [RoomState.Paused]: 0, + [RoomState.Finished]: 0 + }, + roomsCreated: 0, + roomsDestroyed: 0, + playersJoined: 0, + playersLeft: 0 + }; + + this.startAutoCleanup(); + } + + /** + * 创建房间 + */ + createRoom(creatorSession: ClientSession, options: CreateRoomOptions): Room | null { + // 检查房间数量限制 + if (this.rooms.size >= this.config.maxRooms) { + this.logger.warn(`房间数量已达上限: ${this.config.maxRooms}`); + return null; + } + + // 检查玩家是否已在其他房间 + if (this.playerRoomMap.has(creatorSession.id)) { + this.logger.warn(`玩家已在其他房间中: ${creatorSession.id}`); + return null; + } + + // 检查房间名称重复 + if (!this.config.allowDuplicateNames && this.isNameExists(options.name)) { + this.logger.warn(`房间名称已存在: ${options.name}`); + return null; + } + + // 生成房间ID + const roomId = options.id || this.generateRoomId(); + if (this.rooms.has(roomId)) { + this.logger.warn(`房间ID已存在: ${roomId}`); + return null; + } + + // 创建房间配置 + const roomConfig: RoomConfig = { + id: roomId, + name: options.name, + maxPlayers: options.maxPlayers || this.config.defaultMaxPlayers, + isPublic: options.isPublic !== false, // 默认为公开 + password: options.password, + metadata: options.metadata || {}, + autoDestroy: options.autoDestroy ?? this.config.defaultAutoDestroy + }; + + try { + // 创建房间实例 + const room = new Room(roomConfig); + this.setupRoomEvents(room); + + // 添加到房间列表 + this.rooms.set(roomId, room); + + // 创建者自动加入房间 + const success = room.addPlayer(creatorSession, `Creator_${creatorSession.id.substr(-6)}`); + if (!success) { + // 加入失败,销毁房间 + this.destroyRoom(roomId, '创建者加入失败'); + return null; + } + + // 更新玩家房间映射 + this.playerRoomMap.set(creatorSession.id, roomId); + + // 更新统计 + this.stats.roomsCreated++; + this.updateStats(); + + this.logger.info(`房间创建成功: ${roomId} by ${creatorSession.id}`); + + // 触发事件 + this.eventHandlers.roomCreated?.(room); + this.emit('roomCreated', room); + + return room; + + } catch (error) { + this.logger.error(`创建房间失败: ${roomId}`, error); + return null; + } + } + + /** + * 销毁房间 + */ + destroyRoom(roomId: string, reason: string = '房间关闭'): boolean { + const room = this.rooms.get(roomId); + if (!room) { + return false; + } + + // 移除所有玩家的房间映射 + for (const player of room.getAllPlayers()) { + this.playerRoomMap.delete(player.sessionId); + } + + // 销毁房间 + room.destroy(reason); + + // 从房间列表移除 + this.rooms.delete(roomId); + + // 更新统计 + this.stats.roomsDestroyed++; + this.updateStats(); + + this.logger.info(`房间已销毁: ${roomId}, 原因: ${reason}`); + + // 触发事件 + this.eventHandlers.roomDestroyed?.(room, reason); + this.emit('roomDestroyed', room, reason); + + return true; + } + + /** + * 玩家加入房间 + */ + joinRoom(session: ClientSession, roomId: string, password?: string, playerName?: string): boolean { + // 检查玩家是否已在其他房间 + if (this.playerRoomMap.has(session.id)) { + this.logger.warn(`玩家已在其他房间中: ${session.id}`); + return false; + } + + // 获取房间 + const room = this.rooms.get(roomId); + if (!room) { + this.logger.warn(`房间不存在: ${roomId}`); + return false; + } + + // 尝试加入房间 + const success = room.addPlayer(session, playerName, password); + if (!success) { + return false; + } + + // 更新玩家房间映射 + this.playerRoomMap.set(session.id, roomId); + + // 更新统计 + this.stats.playersJoined++; + this.updateStats(); + + const player = room.getPlayer(session.id)!; + this.eventHandlers.playerJoinedRoom?.(room, player); + this.emit('playerJoinedRoom', room, player); + + return true; + } + + /** + * 玩家离开房间 + */ + leaveRoom(sessionId: string, reason?: string): boolean { + const roomId = this.playerRoomMap.get(sessionId); + if (!roomId) { + return false; + } + + const room = this.rooms.get(roomId); + if (!room) { + this.playerRoomMap.delete(sessionId); + return false; + } + + const player = room.getPlayer(sessionId); + if (!player) { + this.playerRoomMap.delete(sessionId); + return false; + } + + // 从房间移除玩家 + const success = room.removePlayer(sessionId, reason); + if (success) { + // 更新玩家房间映射 + this.playerRoomMap.delete(sessionId); + + // 更新统计 + this.stats.playersLeft++; + this.updateStats(); + + this.eventHandlers.playerLeftRoom?.(room, player, reason); + this.emit('playerLeftRoom', room, player, reason); + } + + return success; + } + + /** + * 获取房间 + */ + getRoom(roomId: string): Room | undefined { + return this.rooms.get(roomId); + } + + /** + * 获取玩家所在房间 + */ + getPlayerRoom(sessionId: string): Room | undefined { + const roomId = this.playerRoomMap.get(sessionId); + return roomId ? this.rooms.get(roomId) : undefined; + } + + /** + * 查询房间列表 + */ + queryRooms(options: RoomQueryOptions = {}): Room[] { + let rooms = Array.from(this.rooms.values()); + + // 应用过滤条件 + if (options.state !== undefined) { + rooms = rooms.filter(room => room.getRoomInfo().state === options.state); + } + + if (options.hasPassword !== undefined) { + rooms = rooms.filter(room => { + const config = room.getConfig(); + return options.hasPassword ? !!config.password : !config.password; + }); + } + + if (options.minPlayers !== undefined) { + rooms = rooms.filter(room => room.getAllPlayers().length >= options.minPlayers!); + } + + if (options.maxPlayers !== undefined) { + rooms = rooms.filter(room => room.getAllPlayers().length <= options.maxPlayers!); + } + + if (options.notFull) { + rooms = rooms.filter(room => !room.isFull()); + } + + if (options.publicOnly) { + rooms = rooms.filter(room => room.getConfig().isPublic); + } + + // 分页 + if (options.offset) { + rooms = rooms.slice(options.offset); + } + + if (options.limit) { + rooms = rooms.slice(0, options.limit); + } + + return rooms; + } + + /** + * 获取房间信息列表 + */ + getRoomInfoList(options: RoomQueryOptions = {}): IRoomInfo[] { + return this.queryRooms(options).map(room => room.getRoomInfo()); + } + + /** + * 获取统计信息 + */ + getStats(): RoomManagerStats { + this.updateStats(); + return { ...this.stats }; + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.stats = { + totalRooms: this.rooms.size, + activeRooms: this.rooms.size, + totalPlayers: this.playerRoomMap.size, + roomsByState: { + [RoomState.Waiting]: 0, + [RoomState.Playing]: 0, + [RoomState.Paused]: 0, + [RoomState.Finished]: 0 + }, + roomsCreated: 0, + roomsDestroyed: 0, + playersJoined: 0, + playersLeft: 0 + }; + + this.updateStats(); + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('房间管理器配置已更新:', newConfig); + } + + /** + * 设置事件处理器 + */ + override on(event: K, handler: RoomManagerEvents[K]): this { + this.eventHandlers[event] = handler; + return super.on(event, handler as any); + } + + /** + * 移除事件处理器 + */ + override off(event: K): this { + delete this.eventHandlers[event]; + return super.off(event, this.eventHandlers[event] as any); + } + + /** + * 销毁管理器 + */ + destroy(): void { + // 停止自动清理 + if (this.cleanupTimer) { + this.cleanupTimer.stop(); + this.cleanupTimer = undefined; + } + + // 销毁所有房间 + const roomIds = Array.from(this.rooms.keys()); + for (const roomId of roomIds) { + this.destroyRoom(roomId, '管理器销毁'); + } + + // 清理资源 + this.rooms.clear(); + this.playerRoomMap.clear(); + this.removeAllListeners(); + } + + /** + * 生成房间ID + */ + private generateRoomId(): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + + for (let i = 0; i < this.config.roomIdLength; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + // 确保ID唯一 + if (this.rooms.has(result)) { + return this.generateRoomId(); + } + + return result; + } + + /** + * 检查房间名称是否存在 + */ + private isNameExists(name: string): boolean { + for (const room of this.rooms.values()) { + if (room.getConfig().name === name) { + return true; + } + } + return false; + } + + /** + * 设置房间事件监听 + */ + private setupRoomEvents(room: Room): void { + room.on('roomDestroyed', (reason) => { + // 自动清理已销毁的房间 + this.rooms.delete(room.getConfig().id); + this.updateStats(); + }); + } + + /** + * 更新统计信息 + */ + private updateStats(): void { + this.stats.totalRooms = this.rooms.size; + this.stats.activeRooms = this.rooms.size; + this.stats.totalPlayers = this.playerRoomMap.size; + + // 重置状态统计 + this.stats.roomsByState = { + [RoomState.Waiting]: 0, + [RoomState.Playing]: 0, + [RoomState.Paused]: 0, + [RoomState.Finished]: 0 + }; + + // 统计各状态房间数量 + for (const room of this.rooms.values()) { + const state = room.getRoomInfo().state; + this.stats.roomsByState[state]++; + } + } + + /** + * 启动自动清理 + */ + private startAutoCleanup(): void { + this.cleanupTimer = Core.schedule(this.config.autoCleanupInterval / 1000, true, this, () => { + this.performAutoCleanup(); + }); + } + + /** + * 执行自动清理 + */ + private performAutoCleanup(): void { + const now = Date.now(); + const roomsToDestroy: string[] = []; + + for (const [roomId, room] of this.rooms) { + const config = room.getConfig(); + const stats = room.getStats(); + + // 清理空房间(如果启用了自动销毁) + if (config.autoDestroy && room.isEmpty()) { + roomsToDestroy.push(roomId); + continue; + } + + // 清理长时间无活动的已结束房间 + if (stats.state === RoomState.Finished && + now - stats.createTime > 3600000) { // 1小时 + roomsToDestroy.push(roomId); + continue; + } + } + + // 执行清理 + for (const roomId of roomsToDestroy) { + this.destroyRoom(roomId, '自动清理'); + } + + if (roomsToDestroy.length > 0) { + this.logger.info(`自动清理了 ${roomsToDestroy.length} 个房间`); + } + } + + /** + * 获取管理器状态摘要 + */ + getStatusSummary() { + const stats = this.getStats(); + const rooms = Array.from(this.rooms.values()); + + return { + stats, + roomCount: rooms.length, + playerCount: this.playerRoomMap.size, + publicRooms: rooms.filter(r => r.getConfig().isPublic).length, + privateRooms: rooms.filter(r => !r.getConfig().isPublic).length, + fullRooms: rooms.filter(r => r.isFull()).length, + emptyRooms: rooms.filter(r => r.isEmpty()).length, + averagePlayersPerRoom: rooms.length > 0 ? + rooms.reduce((sum, r) => sum + r.getAllPlayers().length, 0) / rooms.length : 0 + }; + } + + /** + * 踢出玩家(从其所在房间) + */ + kickPlayer(sessionId: string, reason: string = '被管理员踢出'): boolean { + const room = this.getPlayerRoom(sessionId); + if (!room) { + return false; + } + + return room.kickPlayer(sessionId, reason); + } + + /** + * 批量销毁房间 + */ + destroyRoomsBatch(roomIds: string[], reason: string = '批量清理'): number { + let destroyedCount = 0; + + for (const roomId of roomIds) { + if (this.destroyRoom(roomId, reason)) { + destroyedCount++; + } + } + + return destroyedCount; + } + + /** + * 检查玩家是否在房间中 + */ + isPlayerInRoom(sessionId: string): boolean { + return this.playerRoomMap.has(sessionId); + } + + /** + * 获取玩家所在房间ID + */ + getPlayerRoomId(sessionId: string): string | undefined { + return this.playerRoomMap.get(sessionId); + } +} \ No newline at end of file diff --git a/packages/network-server/src/transport/WebSocketTransport.ts b/packages/network-server/src/transport/WebSocketTransport.ts new file mode 100644 index 00000000..b9a622a2 --- /dev/null +++ b/packages/network-server/src/transport/WebSocketTransport.ts @@ -0,0 +1,407 @@ +/** + * 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 = new Map(); + private clientInfo: Map = 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 { + 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 { + 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 { + 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); + } + } + + /** + * 处理客户端断开连接 + */ + 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() + }; + } +} \ No newline at end of file diff --git a/packages/network-server/tests/setup.ts b/packages/network-server/tests/setup.ts deleted file mode 100644 index 5c37a44c..00000000 --- a/packages/network-server/tests/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Jest测试环境设置 - 服务端 - */ - -// 导入reflect-metadata以支持装饰器 -import 'reflect-metadata'; - -// 全局测试配置 -beforeAll(() => { - // 设置测试环境 - process.env.NODE_ENV = 'test'; - process.env.NETWORK_ENV = 'server'; -}); - -afterAll(() => { - // 清理测试环境 -}); - -beforeEach(() => { - // 每个测试前的准备工作 -}); - -afterEach(() => { - // 每个测试后的清理工作 - // 清理可能的网络连接、定时器等 -}); \ No newline at end of file diff --git a/packages/network-shared/src/index.ts b/packages/network-shared/src/index.ts index 087a1c92..dc0bb21e 100644 --- a/packages/network-shared/src/index.ts +++ b/packages/network-shared/src/index.ts @@ -9,18 +9,21 @@ export * from './types/TransportTypes'; // 协议消息 export * from './protocols/MessageTypes'; +export * from './protocols/MessageManager'; // 核心组件 export * from './components/NetworkIdentity'; -// 装饰器系统 (待实现) -// export * from './decorators/SyncVar'; -// export * from './decorators/ServerRpc'; -// export * from './decorators/ClientRpc'; -// export * from './decorators/NetworkComponent'; +// 传输层 +export * from './transport/HeartbeatManager'; +export * from './transport/ErrorHandler'; // 事件系统 export * from './events/NetworkEvents'; -// 序列化系统 (待实现) -// export * from './serialization/NetworkSerializer'; \ No newline at end of file +// 序列化系统 +export * from './serialization/JSONSerializer'; +export * from './serialization/MessageCompressor'; + +// 工具类 +export * from './utils'; \ No newline at end of file diff --git a/packages/network-shared/src/protocols/MessageManager.ts b/packages/network-shared/src/protocols/MessageManager.ts new file mode 100644 index 00000000..1e6aa3e8 --- /dev/null +++ b/packages/network-shared/src/protocols/MessageManager.ts @@ -0,0 +1,502 @@ +/** + * 消息管理器 + * 负责消息ID生成、时间戳管理和消息验证 + */ +import { createLogger } from '@esengine/ecs-framework'; +import { INetworkMessage, MessageType } from '../types/NetworkTypes'; + +/** + * 消息ID生成器类型 + */ +export enum MessageIdGeneratorType { + UUID = 'uuid', + SNOWFLAKE = 'snowflake', + SEQUENTIAL = 'sequential', + TIMESTAMP = 'timestamp' +} + +/** + * 消息管理器配置 + */ +export interface MessageManagerConfig { + idGenerator: MessageIdGeneratorType; + enableTimestampValidation: boolean; + maxTimestampDrift: number; // 最大时间戳偏移(毫秒) + enableMessageDeduplication: boolean; + deduplicationWindowMs: number; // 去重窗口时间 + enableMessageOrdering: boolean; + orderingWindowMs: number; // 排序窗口时间 + maxPendingMessages: number; // 最大待处理消息数 +} + +/** + * 消息验证结果 + */ +export interface MessageValidationResult { + isValid: boolean; + errors: string[]; + warnings: string[]; +} + +/** + * 消息统计信息 + */ +export interface MessageStats { + totalGenerated: number; + totalValidated: number; + validMessages: number; + invalidMessages: number; + duplicateMessages: number; + outOfOrderMessages: number; + timestampErrors: number; +} + +/** + * Snowflake ID生成器 + */ +class SnowflakeIdGenerator { + private static readonly EPOCH = 1640995200000; // 2022-01-01 00:00:00 UTC + private static readonly WORKER_ID_BITS = 5; + private static readonly DATACENTER_ID_BITS = 5; + private static readonly SEQUENCE_BITS = 12; + + private readonly workerId: number; + private readonly datacenterId: number; + private sequence = 0; + private lastTimestamp = -1; + + constructor(workerId: number = 1, datacenterId: number = 1) { + this.workerId = workerId & ((1 << SnowflakeIdGenerator.WORKER_ID_BITS) - 1); + this.datacenterId = datacenterId & ((1 << SnowflakeIdGenerator.DATACENTER_ID_BITS) - 1); + } + + generate(): string { + let timestamp = Date.now(); + + if (timestamp < this.lastTimestamp) { + throw new Error('时钟回拨,无法生成ID'); + } + + if (timestamp === this.lastTimestamp) { + this.sequence = (this.sequence + 1) & ((1 << SnowflakeIdGenerator.SEQUENCE_BITS) - 1); + if (this.sequence === 0) { + // 等待下一毫秒 + while (timestamp <= this.lastTimestamp) { + timestamp = Date.now(); + } + } + } else { + this.sequence = 0; + } + + this.lastTimestamp = timestamp; + + const id = ((timestamp - SnowflakeIdGenerator.EPOCH) << 22) | + (this.datacenterId << 17) | + (this.workerId << 12) | + this.sequence; + + return id.toString(); + } +} + +/** + * 消息管理器 + */ +export class MessageManager { + private logger = createLogger('MessageManager'); + private config: MessageManagerConfig; + private stats: MessageStats; + + // ID生成器 + private sequentialId = 0; + private snowflakeGenerator: SnowflakeIdGenerator; + + // 消息去重和排序 + private recentMessageIds: Set = new Set(); + private pendingMessages: Map = new Map(); + private messageSequence: Map = new Map(); + + // 清理定时器 + private cleanupTimer?: NodeJS.Timeout; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + this.config = { + idGenerator: MessageIdGeneratorType.UUID, + enableTimestampValidation: true, + maxTimestampDrift: 60000, // 1分钟 + enableMessageDeduplication: true, + deduplicationWindowMs: 300000, // 5分钟 + enableMessageOrdering: false, + orderingWindowMs: 10000, // 10秒 + maxPendingMessages: 1000, + ...config + }; + + this.stats = { + totalGenerated: 0, + totalValidated: 0, + validMessages: 0, + invalidMessages: 0, + duplicateMessages: 0, + outOfOrderMessages: 0, + timestampErrors: 0 + }; + + this.snowflakeGenerator = new SnowflakeIdGenerator(); + this.startCleanupTimer(); + } + + /** + * 生成消息ID + */ + generateMessageId(): string { + this.stats.totalGenerated++; + + switch (this.config.idGenerator) { + case MessageIdGeneratorType.UUID: + return this.generateUUID(); + case MessageIdGeneratorType.SNOWFLAKE: + return this.snowflakeGenerator.generate(); + case MessageIdGeneratorType.SEQUENTIAL: + return (++this.sequentialId).toString(); + case MessageIdGeneratorType.TIMESTAMP: + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + default: + return this.generateUUID(); + } + } + + /** + * 创建网络消息 + */ + createMessage( + type: MessageType, + data: any, + senderId: string, + options: { + reliable?: boolean; + priority?: number; + timestamp?: number; + } = {} + ): T { + const message: INetworkMessage = { + type, + messageId: this.generateMessageId(), + timestamp: options.timestamp || Date.now(), + senderId, + data, + reliable: options.reliable, + priority: options.priority + }; + + return message as T; + } + + /** + * 验证消息 + */ + validateMessage(message: INetworkMessage, senderId?: string): MessageValidationResult { + this.stats.totalValidated++; + + const errors: string[] = []; + const warnings: string[] = []; + + // 基础字段验证 + if (!message.messageId) { + errors.push('消息ID不能为空'); + } + + if (!message.type) { + errors.push('消息类型不能为空'); + } else if (!Object.values(MessageType).includes(message.type)) { + errors.push(`无效的消息类型: ${message.type}`); + } + + if (!message.timestamp) { + errors.push('时间戳不能为空'); + } + + if (!message.senderId) { + errors.push('发送者ID不能为空'); + } + + // 发送者验证 + if (senderId && message.senderId !== senderId) { + errors.push('消息发送者ID不匹配'); + } + + // 时间戳验证 + if (this.config.enableTimestampValidation && message.timestamp) { + const now = Date.now(); + const drift = Math.abs(now - message.timestamp); + + if (drift > this.config.maxTimestampDrift) { + errors.push(`时间戳偏移过大: ${drift}ms > ${this.config.maxTimestampDrift}ms`); + this.stats.timestampErrors++; + } + + if (message.timestamp > now + 10000) { // 未来10秒以上 + warnings.push('消息时间戳来自未来'); + } + } + + // 消息去重验证 + if (this.config.enableMessageDeduplication) { + if (this.recentMessageIds.has(message.messageId)) { + errors.push('重复的消息ID'); + this.stats.duplicateMessages++; + } else { + this.recentMessageIds.add(message.messageId); + } + } + + const isValid = errors.length === 0; + + if (isValid) { + this.stats.validMessages++; + } else { + this.stats.invalidMessages++; + } + + return { + isValid, + errors, + warnings + }; + } + + /** + * 处理消息排序 + */ + processMessageOrdering(message: INetworkMessage): INetworkMessage[] { + if (!this.config.enableMessageOrdering) { + return [message]; + } + + const senderId = message.senderId; + const currentSequence = this.messageSequence.get(senderId) || 0; + + // 检查消息是否按顺序到达 + const messageTimestamp = message.timestamp; + const expectedSequence = currentSequence + 1; + + // 简单的时间戳排序逻辑 + if (messageTimestamp >= expectedSequence) { + // 消息按顺序到达 + this.messageSequence.set(senderId, messageTimestamp); + return this.flushPendingMessages(senderId).concat([message]); + } else { + // 消息乱序,暂存 + this.pendingMessages.set(message.messageId, message); + this.stats.outOfOrderMessages++; + + // 检查是否超出最大待处理数量 + if (this.pendingMessages.size > this.config.maxPendingMessages) { + this.logger.warn('待处理消息数量过多,清理旧消息'); + this.cleanupOldPendingMessages(); + } + + return []; + } + } + + /** + * 获取统计信息 + */ + getStats(): MessageStats { + return { ...this.stats }; + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.stats = { + totalGenerated: 0, + totalValidated: 0, + validMessages: 0, + invalidMessages: 0, + duplicateMessages: 0, + outOfOrderMessages: 0, + timestampErrors: 0 + }; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + const oldConfig = { ...this.config }; + Object.assign(this.config, newConfig); + + // 如果去重配置改变,清理相关数据 + if (!this.config.enableMessageDeduplication && oldConfig.enableMessageDeduplication) { + this.recentMessageIds.clear(); + } + + // 如果排序配置改变,清理相关数据 + if (!this.config.enableMessageOrdering && oldConfig.enableMessageOrdering) { + this.pendingMessages.clear(); + this.messageSequence.clear(); + } + + this.logger.info('消息管理器配置已更新:', newConfig); + } + + /** + * 销毁管理器 + */ + destroy(): void { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = undefined; + } + + this.recentMessageIds.clear(); + this.pendingMessages.clear(); + this.messageSequence.clear(); + } + + /** + * 生成UUID + */ + private generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + /** + * 刷新待处理消息 + */ + private flushPendingMessages(senderId: string): INetworkMessage[] { + const flushedMessages: INetworkMessage[] = []; + const messagesToRemove: string[] = []; + + for (const [messageId, message] of this.pendingMessages) { + if (message.senderId === senderId) { + flushedMessages.push(message); + messagesToRemove.push(messageId); + } + } + + // 移除已处理的消息 + messagesToRemove.forEach(id => this.pendingMessages.delete(id)); + + // 按时间戳排序 + flushedMessages.sort((a, b) => a.timestamp - b.timestamp); + + return flushedMessages; + } + + /** + * 清理过期的待处理消息 + */ + private cleanupOldPendingMessages(): void { + const now = Date.now(); + const messagesToRemove: string[] = []; + + for (const [messageId, message] of this.pendingMessages) { + if (now - message.timestamp > this.config.orderingWindowMs) { + messagesToRemove.push(messageId); + } + } + + messagesToRemove.forEach(id => this.pendingMessages.delete(id)); + + if (messagesToRemove.length > 0) { + this.logger.debug(`清理了 ${messagesToRemove.length} 个过期的待处理消息`); + } + } + + /** + * 启动清理定时器 + */ + private startCleanupTimer(): void { + this.cleanupTimer = setInterval(() => { + this.performCleanup(); + }, 60000); // 每分钟清理一次 + } + + /** + * 执行清理操作 + */ + private performCleanup(): void { + const now = Date.now(); + + // 清理过期的消息ID(用于去重) + if (this.config.enableMessageDeduplication) { + // 由于Set没有时间戳,我们定期清理所有ID + // 这是一个简化实现,实际项目中可以使用更复杂的数据结构 + if (this.recentMessageIds.size > 10000) { + this.recentMessageIds.clear(); + this.logger.debug('清理了过期的消息ID缓存'); + } + } + + // 清理过期的待处理消息 + if (this.config.enableMessageOrdering) { + this.cleanupOldPendingMessages(); + } + } + + /** + * 获取消息处理报告 + */ + getProcessingReport() { + const totalProcessed = this.stats.validMessages + this.stats.invalidMessages; + const validRate = totalProcessed > 0 ? (this.stats.validMessages / totalProcessed) * 100 : 0; + const duplicateRate = totalProcessed > 0 ? (this.stats.duplicateMessages / totalProcessed) * 100 : 0; + + return { + stats: this.getStats(), + validationRate: validRate, + duplicateRate: duplicateRate, + pendingMessagesCount: this.pendingMessages.size, + cachedMessageIdsCount: this.recentMessageIds.size, + recommendation: this.generateRecommendation(validRate, duplicateRate) + }; + } + + /** + * 生成优化建议 + */ + private generateRecommendation(validRate: number, duplicateRate: number): string { + if (validRate < 90) { + return '消息验证失败率较高,建议检查消息格式和发送逻辑'; + } else if (duplicateRate > 5) { + return '重复消息较多,建议检查客户端重发逻辑或调整去重窗口'; + } else if (this.pendingMessages.size > this.config.maxPendingMessages * 0.8) { + return '待处理消息过多,建议优化网络或调整排序窗口'; + } else { + return '消息处理正常'; + } + } + + /** + * 批量验证消息 + */ + validateMessageBatch(messages: INetworkMessage[], senderId?: string): MessageValidationResult[] { + return messages.map(message => this.validateMessage(message, senderId)); + } + + /** + * 获取消息年龄(毫秒) + */ + getMessageAge(message: INetworkMessage): number { + return Date.now() - message.timestamp; + } + + /** + * 检查消息是否过期 + */ + isMessageExpired(message: INetworkMessage, maxAge: number = 300000): boolean { + return this.getMessageAge(message) > maxAge; + } +} \ No newline at end of file diff --git a/packages/network-shared/src/serialization/JSONSerializer.ts b/packages/network-shared/src/serialization/JSONSerializer.ts new file mode 100644 index 00000000..e52a7dc1 --- /dev/null +++ b/packages/network-shared/src/serialization/JSONSerializer.ts @@ -0,0 +1,550 @@ +/** + * JSON序列化器 + * 提供高性能的消息序列化和反序列化功能,包括类型安全检查 + */ +import { createLogger } from '@esengine/ecs-framework'; +import { INetworkMessage, MessageType } from '../types/NetworkTypes'; + +/** + * 序列化器配置 + */ +export interface SerializerConfig { + enableTypeChecking: boolean; + enableCompression: boolean; + maxMessageSize: number; + enableProfiling: boolean; + customSerializers?: Map; +} + +/** + * 自定义序列化器接口 + */ +export interface ICustomSerializer { + serialize(data: any): any; + deserialize(data: any): any; + canHandle(data: any): boolean; +} + +/** + * 序列化结果 + */ +export interface SerializationResult { + data: string | Buffer; + size: number; + compressionRatio?: number; + serializationTime: number; +} + +/** + * 反序列化结果 + */ +export interface DeserializationResult { + data: T; + deserializationTime: number; + isValid: boolean; + errors?: string[]; +} + +/** + * 序列化统计信息 + */ +export interface SerializationStats { + totalSerialized: number; + totalDeserialized: number; + totalBytes: number; + averageSerializationTime: number; + averageDeserializationTime: number; + averageMessageSize: number; + errorCount: number; + compressionSavings: number; +} + +/** + * JSON序列化器 + */ +export class JSONSerializer { + private logger = createLogger('JSONSerializer'); + private config: SerializerConfig; + private stats: SerializationStats; + + // 性能分析 + private serializationTimes: number[] = []; + private deserializationTimes: number[] = []; + private messageSizes: number[] = []; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + this.config = { + enableTypeChecking: true, + enableCompression: false, + maxMessageSize: 1024 * 1024, // 1MB + enableProfiling: false, + ...config + }; + + this.stats = { + totalSerialized: 0, + totalDeserialized: 0, + totalBytes: 0, + averageSerializationTime: 0, + averageDeserializationTime: 0, + averageMessageSize: 0, + errorCount: 0, + compressionSavings: 0 + }; + } + + /** + * 序列化消息 + */ + serialize(message: T): SerializationResult { + const startTime = performance.now(); + + try { + // 类型检查 + if (this.config.enableTypeChecking) { + this.validateMessage(message); + } + + // 预处理消息 + const processedMessage = this.preprocessMessage(message); + + // 序列化 + let serializedData: string; + + // 使用自定义序列化器 + const customSerializer = this.findCustomSerializer(processedMessage); + if (customSerializer) { + serializedData = JSON.stringify(customSerializer.serialize(processedMessage)); + } else { + serializedData = JSON.stringify(processedMessage, this.createReplacer()); + } + + // 检查大小限制 + if (serializedData.length > this.config.maxMessageSize) { + throw new Error(`消息大小超过限制: ${serializedData.length} > ${this.config.maxMessageSize}`); + } + + const endTime = performance.now(); + const serializationTime = endTime - startTime; + + // 更新统计 + this.updateSerializationStats(serializedData.length, serializationTime); + + return { + data: serializedData, + size: serializedData.length, + serializationTime + }; + + } catch (error) { + this.stats.errorCount++; + this.logger.error('序列化失败:', error); + throw error; + } + } + + /** + * 反序列化消息 + */ + deserialize(data: string | ArrayBuffer): DeserializationResult { + const startTime = performance.now(); + + try { + // 转换数据格式 + const jsonString = data instanceof ArrayBuffer ? new TextDecoder().decode(data) : + typeof data === 'string' ? data : String(data); + + // 解析JSON + const parsedData = JSON.parse(jsonString, this.createReviver()); + + // 类型检查 + const validationResult = this.config.enableTypeChecking ? + this.validateParsedMessage(parsedData) : { isValid: true, errors: [] }; + + // 后处理消息 + const processedMessage = this.postprocessMessage(parsedData); + + const endTime = performance.now(); + const deserializationTime = endTime - startTime; + + // 更新统计 + this.updateDeserializationStats(deserializationTime); + + return { + data: processedMessage as T, + deserializationTime, + isValid: validationResult.isValid, + errors: validationResult.errors + }; + + } catch (error) { + this.stats.errorCount++; + this.logger.error('反序列化失败:', error); + + return { + data: {} as T, + deserializationTime: performance.now() - startTime, + isValid: false, + errors: [error instanceof Error ? error.message : '未知错误'] + }; + } + } + + /** + * 批量序列化 + */ + serializeBatch(messages: T[]): SerializationResult { + const startTime = performance.now(); + + try { + const batchData = { + type: 'batch', + messages: messages.map(msg => { + if (this.config.enableTypeChecking) { + this.validateMessage(msg); + } + return this.preprocessMessage(msg); + }), + timestamp: Date.now() + }; + + const serializedData = JSON.stringify(batchData, this.createReplacer()); + + if (serializedData.length > this.config.maxMessageSize) { + throw new Error(`批量消息大小超过限制: ${serializedData.length} > ${this.config.maxMessageSize}`); + } + + const endTime = performance.now(); + const serializationTime = endTime - startTime; + + this.updateSerializationStats(serializedData.length, serializationTime); + + return { + data: serializedData, + size: serializedData.length, + serializationTime + }; + + } catch (error) { + this.stats.errorCount++; + this.logger.error('批量序列化失败:', error); + throw error; + } + } + + /** + * 批量反序列化 + */ + deserializeBatch(data: string | ArrayBuffer): DeserializationResult { + const result = this.deserialize(data); + + if (!result.isValid || !result.data.messages) { + return { + data: [], + deserializationTime: result.deserializationTime, + isValid: false, + errors: ['无效的批量消息格式'] + }; + } + + const messages = result.data.messages.map((msg: any) => this.postprocessMessage(msg)); + + return { + data: messages as T[], + deserializationTime: result.deserializationTime, + isValid: true + }; + } + + /** + * 获取统计信息 + */ + getStats(): SerializationStats { + return { ...this.stats }; + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.stats = { + totalSerialized: 0, + totalDeserialized: 0, + totalBytes: 0, + averageSerializationTime: 0, + averageDeserializationTime: 0, + averageMessageSize: 0, + errorCount: 0, + compressionSavings: 0 + }; + + this.serializationTimes.length = 0; + this.deserializationTimes.length = 0; + this.messageSizes.length = 0; + } + + /** + * 添加自定义序列化器 + */ + addCustomSerializer(name: string, serializer: ICustomSerializer): void { + if (!this.config.customSerializers) { + this.config.customSerializers = new Map(); + } + this.config.customSerializers.set(name, serializer); + } + + /** + * 移除自定义序列化器 + */ + removeCustomSerializer(name: string): boolean { + return this.config.customSerializers?.delete(name) || false; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('序列化器配置已更新:', newConfig); + } + + /** + * 验证消息格式 + */ + private validateMessage(message: INetworkMessage): void { + if (!message.type || !message.messageId || !message.timestamp) { + throw new Error('消息格式无效:缺少必需字段'); + } + + if (!Object.values(MessageType).includes(message.type)) { + throw new Error(`无效的消息类型: ${message.type}`); + } + + if (typeof message.timestamp !== 'number' || message.timestamp <= 0) { + throw new Error('无效的时间戳'); + } + } + + /** + * 验证解析后的消息 + */ + private validateParsedMessage(data: any): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!data || typeof data !== 'object') { + errors.push('消息必须是对象'); + } else { + if (!data.type) errors.push('缺少消息类型'); + if (!data.messageId) errors.push('缺少消息ID'); + if (!data.timestamp) errors.push('缺少时间戳'); + if (!data.senderId) errors.push('缺少发送者ID'); + } + + return { + isValid: errors.length === 0, + errors + }; + } + + /** + * 预处理消息(序列化前) + */ + private preprocessMessage(message: INetworkMessage): any { + // 克隆消息以避免修改原始对象 + const processed = { ...message }; + + // 处理特殊数据类型 + if (processed.data) { + processed.data = this.serializeSpecialTypes(processed.data); + } + + return processed; + } + + /** + * 后处理消息(反序列化后) + */ + private postprocessMessage(data: any): any { + if (data.data) { + data.data = this.deserializeSpecialTypes(data.data); + } + + return data; + } + + /** + * 序列化特殊类型 + */ + private serializeSpecialTypes(data: any): any { + if (data instanceof Date) { + return { __type: 'Date', value: data.toISOString() }; + } else if (data instanceof Map) { + return { __type: 'Map', value: Array.from(data.entries()) }; + } else if (data instanceof Set) { + return { __type: 'Set', value: Array.from(data) }; + } else if (ArrayBuffer.isView(data)) { + return { __type: 'TypedArray', value: Array.from(data as any), constructor: data.constructor.name }; + } else if (data && typeof data === 'object') { + const result: any = {}; + for (const [key, value] of Object.entries(data)) { + result[key] = this.serializeSpecialTypes(value); + } + return result; + } + + return data; + } + + /** + * 反序列化特殊类型 + */ + private deserializeSpecialTypes(data: any): any { + if (data && typeof data === 'object' && data.__type) { + switch (data.__type) { + case 'Date': + return new Date(data.value); + case 'Map': + return new Map(data.value); + case 'Set': + return new Set(data.value); + case 'TypedArray': + const constructor = (globalThis as any)[data.constructor]; + return constructor ? new constructor(data.value) : data.value; + } + } else if (data && typeof data === 'object') { + const result: any = {}; + for (const [key, value] of Object.entries(data)) { + result[key] = this.deserializeSpecialTypes(value); + } + return result; + } + + return data; + } + + /** + * 创建JSON.stringify替换函数 + */ + private createReplacer() { + return (key: string, value: any) => { + // 处理循环引用 + if (value && typeof value === 'object') { + if (value.__serializing) { + return '[Circular Reference]'; + } + value.__serializing = true; + } + return value; + }; + } + + /** + * 创建JSON.parse恢复函数 + */ + private createReviver() { + return (key: string, value: any) => { + // 清理序列化标记 + if (value && typeof value === 'object') { + delete value.__serializing; + } + return value; + }; + } + + /** + * 查找自定义序列化器 + */ + private findCustomSerializer(data: any): ICustomSerializer | undefined { + if (!this.config.customSerializers) { + return undefined; + } + + for (const serializer of this.config.customSerializers.values()) { + if (serializer.canHandle(data)) { + return serializer; + } + } + + return undefined; + } + + /** + * 更新序列化统计 + */ + private updateSerializationStats(size: number, time: number): void { + this.stats.totalSerialized++; + this.stats.totalBytes += size; + + this.serializationTimes.push(time); + this.messageSizes.push(size); + + // 保持最近1000个样本 + if (this.serializationTimes.length > 1000) { + this.serializationTimes.shift(); + } + if (this.messageSizes.length > 1000) { + this.messageSizes.shift(); + } + + // 计算平均值 + this.stats.averageSerializationTime = + this.serializationTimes.reduce((sum, t) => sum + t, 0) / this.serializationTimes.length; + this.stats.averageMessageSize = + this.messageSizes.reduce((sum, s) => sum + s, 0) / this.messageSizes.length; + } + + /** + * 更新反序列化统计 + */ + private updateDeserializationStats(time: number): void { + this.stats.totalDeserialized++; + + this.deserializationTimes.push(time); + + // 保持最近1000个样本 + if (this.deserializationTimes.length > 1000) { + this.deserializationTimes.shift(); + } + + // 计算平均值 + this.stats.averageDeserializationTime = + this.deserializationTimes.reduce((sum, t) => sum + t, 0) / this.deserializationTimes.length; + } + + /** + * 获取性能分析报告 + */ + getPerformanceReport() { + return { + stats: this.getStats(), + serializationTimes: [...this.serializationTimes], + deserializationTimes: [...this.deserializationTimes], + messageSizes: [...this.messageSizes], + percentiles: { + serialization: this.calculatePercentiles(this.serializationTimes), + deserialization: this.calculatePercentiles(this.deserializationTimes), + messageSize: this.calculatePercentiles(this.messageSizes) + } + }; + } + + /** + * 计算百分位数 + */ + private calculatePercentiles(values: number[]) { + if (values.length === 0) return {}; + + const sorted = [...values].sort((a, b) => a - b); + const n = sorted.length; + + return { + p50: sorted[Math.floor(n * 0.5)], + p90: sorted[Math.floor(n * 0.9)], + p95: sorted[Math.floor(n * 0.95)], + p99: sorted[Math.floor(n * 0.99)] + }; + } +} \ No newline at end of file diff --git a/packages/network-shared/src/serialization/MessageCompressor.ts b/packages/network-shared/src/serialization/MessageCompressor.ts new file mode 100644 index 00000000..bf7b257c --- /dev/null +++ b/packages/network-shared/src/serialization/MessageCompressor.ts @@ -0,0 +1,498 @@ +/** + * 消息压缩器 + * 提供多种压缩算法选择和压缩率统计 + */ +import { createLogger } from '@esengine/ecs-framework'; +import * as zlib from 'zlib'; +import { promisify } from 'util'; + +/** + * 压缩算法类型 + */ +export enum CompressionAlgorithm { + NONE = 'none', + GZIP = 'gzip', + DEFLATE = 'deflate', + BROTLI = 'brotli' +} + +/** + * 压缩配置 + */ +export interface CompressionConfig { + algorithm: CompressionAlgorithm; + level: number; // 压缩级别 (0-9) + threshold: number; // 最小压缩阈值(字节) + enableAsync: boolean; // 是否启用异步压缩 + chunkSize: number; // 分块大小 +} + +/** + * 压缩结果 + */ +export interface CompressionResult { + data: Buffer; + originalSize: number; + compressedSize: number; + compressionRatio: number; + compressionTime: number; + algorithm: CompressionAlgorithm; +} + +/** + * 压缩统计信息 + */ +export interface CompressionStats { + totalCompressed: number; + totalDecompressed: number; + totalOriginalBytes: number; + totalCompressedBytes: number; + averageCompressionRatio: number; + averageCompressionTime: number; + averageDecompressionTime: number; + algorithmUsage: Record; +} + +/** + * 消息压缩器 + */ +export class MessageCompressor { + private logger = createLogger('MessageCompressor'); + private config: CompressionConfig; + private stats: CompressionStats; + + // 异步压缩函数 + private gzipAsync = promisify(zlib.gzip); + private gunzipAsync = promisify(zlib.gunzip); + private deflateAsync = promisify(zlib.deflate); + private inflateAsync = promisify(zlib.inflate); + private brotliCompressAsync = promisify(zlib.brotliCompress); + private brotliDecompressAsync = promisify(zlib.brotliDecompress); + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + this.config = { + algorithm: CompressionAlgorithm.GZIP, + level: 6, // 平衡压缩率和速度 + threshold: 1024, // 1KB以上才压缩 + enableAsync: true, + chunkSize: 64 * 1024, // 64KB分块 + ...config + }; + + this.stats = { + totalCompressed: 0, + totalDecompressed: 0, + totalOriginalBytes: 0, + totalCompressedBytes: 0, + averageCompressionRatio: 0, + averageCompressionTime: 0, + averageDecompressionTime: 0, + algorithmUsage: { + [CompressionAlgorithm.NONE]: 0, + [CompressionAlgorithm.GZIP]: 0, + [CompressionAlgorithm.DEFLATE]: 0, + [CompressionAlgorithm.BROTLI]: 0 + } + }; + } + + /** + * 压缩数据 + */ + async compress(data: string | Buffer): Promise { + const startTime = performance.now(); + const inputBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data; + const originalSize = inputBuffer.length; + + try { + // 检查是否需要压缩 + if (originalSize < this.config.threshold) { + return this.createNoCompressionResult(inputBuffer, originalSize, startTime); + } + + let compressedData: Buffer; + const algorithm = this.config.algorithm; + + // 根据算法进行压缩 + switch (algorithm) { + case CompressionAlgorithm.GZIP: + compressedData = await this.compressGzip(inputBuffer); + break; + case CompressionAlgorithm.DEFLATE: + compressedData = await this.compressDeflate(inputBuffer); + break; + case CompressionAlgorithm.BROTLI: + compressedData = await this.compressBrotli(inputBuffer); + break; + case CompressionAlgorithm.NONE: + default: + return this.createNoCompressionResult(inputBuffer, originalSize, startTime); + } + + const endTime = performance.now(); + const compressionTime = endTime - startTime; + const compressedSize = compressedData.length; + const compressionRatio = originalSize > 0 ? compressedSize / originalSize : 1; + + // 检查压缩效果 + if (compressedSize >= originalSize * 0.9) { + // 压缩效果不明显,返回原始数据 + this.logger.debug(`压缩效果不佳,返回原始数据。原始: ${originalSize}, 压缩: ${compressedSize}`); + return this.createNoCompressionResult(inputBuffer, originalSize, startTime); + } + + const result: CompressionResult = { + data: compressedData, + originalSize, + compressedSize, + compressionRatio, + compressionTime, + algorithm + }; + + // 更新统计 + this.updateCompressionStats(result); + + return result; + + } catch (error) { + this.logger.error('压缩失败:', error); + return this.createNoCompressionResult(inputBuffer, originalSize, startTime); + } + } + + /** + * 解压缩数据 + */ + async decompress(data: Buffer, algorithm: CompressionAlgorithm): Promise { + const startTime = performance.now(); + + try { + if (algorithm === CompressionAlgorithm.NONE) { + return data; + } + + let decompressedData: Buffer; + + switch (algorithm) { + case CompressionAlgorithm.GZIP: + decompressedData = await this.decompressGzip(data); + break; + case CompressionAlgorithm.DEFLATE: + decompressedData = await this.decompressDeflate(data); + break; + case CompressionAlgorithm.BROTLI: + decompressedData = await this.decompressBrotli(data); + break; + default: + throw new Error(`不支持的压缩算法: ${algorithm}`); + } + + const endTime = performance.now(); + const decompressionTime = endTime - startTime; + + // 更新统计 + this.updateDecompressionStats(decompressionTime); + + return decompressedData; + + } catch (error) { + this.logger.error('解压缩失败:', error); + throw error; + } + } + + /** + * 批量压缩 + */ + async compressBatch(dataList: (string | Buffer)[]): Promise { + const results: CompressionResult[] = []; + + if (this.config.enableAsync) { + // 并行压缩 + const promises = dataList.map(data => this.compress(data)); + return await Promise.all(promises); + } else { + // 串行压缩 + for (const data of dataList) { + results.push(await this.compress(data)); + } + return results; + } + } + + /** + * 自适应压缩 + * 根据数据特征自动选择最佳压缩算法 + */ + async compressAdaptive(data: string | Buffer): Promise { + const inputBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data; + const originalAlgorithm = this.config.algorithm; + + try { + // 对小数据进行算法测试 + const testSize = Math.min(inputBuffer.length, 4096); // 测试前4KB + const testData = inputBuffer.subarray(0, testSize); + + const algorithms = [ + CompressionAlgorithm.GZIP, + CompressionAlgorithm.DEFLATE, + CompressionAlgorithm.BROTLI + ]; + + let bestAlgorithm = CompressionAlgorithm.GZIP; + let bestRatio = 1; + + // 测试不同算法的压缩效果 + for (const algorithm of algorithms) { + try { + this.config.algorithm = algorithm; + const testResult = await this.compress(testData); + + if (testResult.compressionRatio < bestRatio) { + bestRatio = testResult.compressionRatio; + bestAlgorithm = algorithm; + } + } catch (error) { + // 忽略测试失败的算法 + continue; + } + } + + // 使用最佳算法压缩完整数据 + this.config.algorithm = bestAlgorithm; + const result = await this.compress(inputBuffer); + + this.logger.debug(`自适应压缩选择算法: ${bestAlgorithm}, 压缩率: ${result.compressionRatio.toFixed(3)}`); + + return result; + + } finally { + // 恢复原始配置 + this.config.algorithm = originalAlgorithm; + } + } + + /** + * 获取统计信息 + */ + getStats(): CompressionStats { + return { ...this.stats }; + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.stats = { + totalCompressed: 0, + totalDecompressed: 0, + totalOriginalBytes: 0, + totalCompressedBytes: 0, + averageCompressionRatio: 0, + averageCompressionTime: 0, + averageDecompressionTime: 0, + algorithmUsage: { + [CompressionAlgorithm.NONE]: 0, + [CompressionAlgorithm.GZIP]: 0, + [CompressionAlgorithm.DEFLATE]: 0, + [CompressionAlgorithm.BROTLI]: 0 + } + }; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('压缩器配置已更新:', newConfig); + } + + /** + * 获取压缩建议 + */ + getCompressionRecommendation(dataSize: number, dataType: string): CompressionAlgorithm { + // 根据数据大小和类型推荐压缩算法 + if (dataSize < this.config.threshold) { + return CompressionAlgorithm.NONE; + } + + if (dataType === 'json' || dataType === 'text') { + // 文本数据推荐GZIP + return CompressionAlgorithm.GZIP; + } else if (dataType === 'binary') { + // 二进制数据推荐DEFLATE + return CompressionAlgorithm.DEFLATE; + } else { + // 默认推荐GZIP + return CompressionAlgorithm.GZIP; + } + } + + /** + * GZIP压缩 + */ + private async compressGzip(data: Buffer): Promise { + if (this.config.enableAsync) { + return await this.gzipAsync(data, { level: this.config.level }); + } else { + return zlib.gzipSync(data, { level: this.config.level }); + } + } + + /** + * GZIP解压缩 + */ + private async decompressGzip(data: Buffer): Promise { + if (this.config.enableAsync) { + return await this.gunzipAsync(data); + } else { + return zlib.gunzipSync(data); + } + } + + /** + * DEFLATE压缩 + */ + private async compressDeflate(data: Buffer): Promise { + if (this.config.enableAsync) { + return await this.deflateAsync(data, { level: this.config.level }); + } else { + return zlib.deflateSync(data, { level: this.config.level }); + } + } + + /** + * DEFLATE解压缩 + */ + private async decompressDeflate(data: Buffer): Promise { + if (this.config.enableAsync) { + return await this.inflateAsync(data); + } else { + return zlib.inflateSync(data); + } + } + + /** + * BROTLI压缩 + */ + private async compressBrotli(data: Buffer): Promise { + const options = { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: this.config.level + } + }; + + if (this.config.enableAsync) { + return await this.brotliCompressAsync(data, options); + } else { + return zlib.brotliCompressSync(data, options); + } + } + + /** + * BROTLI解压缩 + */ + private async decompressBrotli(data: Buffer): Promise { + if (this.config.enableAsync) { + return await this.brotliDecompressAsync(data); + } else { + return zlib.brotliDecompressSync(data); + } + } + + /** + * 创建无压缩结果 + */ + private createNoCompressionResult( + data: Buffer, + originalSize: number, + startTime: number + ): CompressionResult { + const endTime = performance.now(); + const result: CompressionResult = { + data, + originalSize, + compressedSize: originalSize, + compressionRatio: 1, + compressionTime: endTime - startTime, + algorithm: CompressionAlgorithm.NONE + }; + + this.updateCompressionStats(result); + return result; + } + + /** + * 更新压缩统计 + */ + private updateCompressionStats(result: CompressionResult): void { + this.stats.totalCompressed++; + this.stats.totalOriginalBytes += result.originalSize; + this.stats.totalCompressedBytes += result.compressedSize; + this.stats.algorithmUsage[result.algorithm]++; + + // 计算平均值 + this.stats.averageCompressionRatio = + this.stats.totalOriginalBytes > 0 ? + this.stats.totalCompressedBytes / this.stats.totalOriginalBytes : 1; + + // 更新平均压缩时间(使用移动平均) + const alpha = 0.1; // 平滑因子 + this.stats.averageCompressionTime = + this.stats.averageCompressionTime * (1 - alpha) + result.compressionTime * alpha; + } + + /** + * 更新解压缩统计 + */ + private updateDecompressionStats(decompressionTime: number): void { + this.stats.totalDecompressed++; + + // 更新平均解压缩时间(使用移动平均) + const alpha = 0.1; + this.stats.averageDecompressionTime = + this.stats.averageDecompressionTime * (1 - alpha) + decompressionTime * alpha; + } + + /** + * 获取压缩效率报告 + */ + getEfficiencyReport() { + const savings = this.stats.totalOriginalBytes - this.stats.totalCompressedBytes; + const savingsPercentage = this.stats.totalOriginalBytes > 0 ? + (savings / this.stats.totalOriginalBytes) * 100 : 0; + + return { + totalSavings: savings, + savingsPercentage, + averageCompressionRatio: this.stats.averageCompressionRatio, + averageCompressionTime: this.stats.averageCompressionTime, + averageDecompressionTime: this.stats.averageDecompressionTime, + algorithmUsage: this.stats.algorithmUsage, + recommendation: this.generateRecommendation() + }; + } + + /** + * 生成优化建议 + */ + private generateRecommendation(): string { + const ratio = this.stats.averageCompressionRatio; + const time = this.stats.averageCompressionTime; + + if (ratio > 0.8) { + return '压缩效果较差,建议调整算法或提高压缩级别'; + } else if (time > 50) { + return '压缩时间较长,建议降低压缩级别或使用更快的算法'; + } else if (ratio < 0.3) { + return '压缩效果很好,当前配置最优'; + } else { + return '压缩性能正常'; + } + } +} \ No newline at end of file diff --git a/packages/network-shared/src/transport/ErrorHandler.ts b/packages/network-shared/src/transport/ErrorHandler.ts new file mode 100644 index 00000000..6ce39cdc --- /dev/null +++ b/packages/network-shared/src/transport/ErrorHandler.ts @@ -0,0 +1,461 @@ +/** + * 网络错误处理器 + * 提供统一的错误处理、分类和恢复策略 + */ +import { createLogger } from '@esengine/ecs-framework'; +import { NetworkErrorType, INetworkError } from '../types/NetworkTypes'; + +/** + * 错误严重级别 + */ +export enum ErrorSeverity { + Low = 'low', // 低级错误,可以忽略 + Medium = 'medium', // 中级错误,需要记录但不影响功能 + High = 'high', // 高级错误,影响功能但可以恢复 + Critical = 'critical' // 严重错误,需要立即处理 +} + +/** + * 错误恢复策略 + */ +export enum RecoveryStrategy { + Ignore = 'ignore', // 忽略错误 + Retry = 'retry', // 重试操作 + Reconnect = 'reconnect', // 重新连接 + Restart = 'restart', // 重启服务 + Escalate = 'escalate' // 上报错误 +} + +/** + * 错误处理配置 + */ +export interface ErrorHandlerConfig { + maxRetryAttempts: number; + retryDelay: number; + enableAutoRecovery: boolean; + enableErrorReporting: boolean; + errorReportingEndpoint?: string; +} + +/** + * 错误统计信息 + */ +export interface ErrorStats { + totalErrors: number; + errorsByType: Record; + errorsBySeverity: Record; + recoveredErrors: number; + unrecoveredErrors: number; + lastError?: INetworkError; +} + +/** + * 错误处理事件 + */ +export interface ErrorHandlerEvents { + errorOccurred: (error: INetworkError, severity: ErrorSeverity) => void; + errorRecovered: (error: INetworkError, strategy: RecoveryStrategy) => void; + errorUnrecoverable: (error: INetworkError) => void; + criticalError: (error: INetworkError) => void; +} + +/** + * 网络错误处理器 + */ +export class ErrorHandler { + private logger = createLogger('ErrorHandler'); + private config: ErrorHandlerConfig; + private stats: ErrorStats; + private eventHandlers: Partial = {}; + + // 错误恢复状态 + private retryAttempts: Map = new Map(); + private pendingRecoveries: Set = new Set(); + + // 错误分类规则 + private severityRules: Map = new Map(); + private recoveryRules: Map = new Map(); + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + this.config = { + maxRetryAttempts: 3, + retryDelay: 1000, + enableAutoRecovery: true, + enableErrorReporting: false, + ...config + }; + + this.stats = { + totalErrors: 0, + errorsByType: {} as Record, + errorsBySeverity: {} as Record, + recoveredErrors: 0, + unrecoveredErrors: 0 + }; + + this.initializeDefaultRules(); + } + + /** + * 处理错误 + */ + handleError(error: Error | INetworkError, context?: string): void { + const networkError = this.normalizeError(error, context); + const severity = this.classifyErrorSeverity(networkError); + + // 更新统计 + this.updateStats(networkError, severity); + + this.logger.error(`网络错误 [${severity}]: ${networkError.message}`, { + type: networkError.type, + code: networkError.code, + details: networkError.details, + context + }); + + // 触发错误事件 + this.eventHandlers.errorOccurred?.(networkError, severity); + + // 处理严重错误 + if (severity === ErrorSeverity.Critical) { + this.eventHandlers.criticalError?.(networkError); + } + + // 尝试自动恢复 + if (this.config.enableAutoRecovery) { + this.attemptRecovery(networkError, severity); + } + + // 错误报告 + if (this.config.enableErrorReporting) { + this.reportError(networkError, severity); + } + } + + /** + * 设置错误分类规则 + */ + setErrorSeverityRule(errorType: NetworkErrorType, severity: ErrorSeverity): void { + this.severityRules.set(errorType, severity); + } + + /** + * 设置错误恢复策略 + */ + setRecoveryStrategy(errorType: NetworkErrorType, strategy: RecoveryStrategy): void { + this.recoveryRules.set(errorType, strategy); + } + + /** + * 获取错误统计 + */ + getStats(): ErrorStats { + return { ...this.stats }; + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.stats = { + totalErrors: 0, + errorsByType: {} as Record, + errorsBySeverity: {} as Record, + recoveredErrors: 0, + unrecoveredErrors: 0 + }; + this.retryAttempts.clear(); + this.pendingRecoveries.clear(); + } + + /** + * 设置事件处理器 + */ + on(event: K, handler: ErrorHandlerEvents[K]): void { + this.eventHandlers[event] = handler; + } + + /** + * 移除事件处理器 + */ + off(event: K): void { + delete this.eventHandlers[event]; + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('错误处理器配置已更新:', newConfig); + } + + /** + * 手动标记错误已恢复 + */ + markErrorRecovered(errorId: string): void { + this.retryAttempts.delete(errorId); + this.pendingRecoveries.delete(errorId); + this.stats.recoveredErrors++; + } + + /** + * 检查错误是否可恢复 + */ + isRecoverable(errorType: NetworkErrorType): boolean { + const strategy = this.recoveryRules.get(errorType); + return strategy !== undefined && strategy !== RecoveryStrategy.Ignore; + } + + /** + * 标准化错误对象 + */ + private normalizeError(error: Error | INetworkError, context?: string): INetworkError { + if ('type' in error && 'message' in error && 'timestamp' in error) { + return error as INetworkError; + } + + // 将普通Error转换为INetworkError + return { + type: this.determineErrorType(error), + message: error.message || '未知错误', + code: (error as any).code, + details: { + context, + stack: error.stack, + name: error.name + }, + timestamp: Date.now() + }; + } + + /** + * 确定错误类型 + */ + private determineErrorType(error: Error): NetworkErrorType { + const message = error.message.toLowerCase(); + + if (message.includes('timeout')) { + return NetworkErrorType.TIMEOUT; + } else if (message.includes('connection')) { + return NetworkErrorType.CONNECTION_LOST; + } else if (message.includes('auth')) { + return NetworkErrorType.AUTHENTICATION_FAILED; + } else if (message.includes('permission')) { + return NetworkErrorType.PERMISSION_DENIED; + } else if (message.includes('rate') || message.includes('limit')) { + return NetworkErrorType.RATE_LIMITED; + } else if (message.includes('invalid') || message.includes('format')) { + return NetworkErrorType.INVALID_MESSAGE; + } else { + return NetworkErrorType.UNKNOWN; + } + } + + /** + * 分类错误严重程度 + */ + private classifyErrorSeverity(error: INetworkError): ErrorSeverity { + // 使用自定义规则 + const customSeverity = this.severityRules.get(error.type); + if (customSeverity) { + return customSeverity; + } + + // 默认分类规则 + switch (error.type) { + case NetworkErrorType.CONNECTION_FAILED: + case NetworkErrorType.CONNECTION_LOST: + return ErrorSeverity.High; + + case NetworkErrorType.AUTHENTICATION_FAILED: + case NetworkErrorType.PERMISSION_DENIED: + return ErrorSeverity.Critical; + + case NetworkErrorType.TIMEOUT: + case NetworkErrorType.RATE_LIMITED: + return ErrorSeverity.Medium; + + case NetworkErrorType.INVALID_MESSAGE: + return ErrorSeverity.Low; + + default: + return ErrorSeverity.Medium; + } + } + + /** + * 更新统计信息 + */ + private updateStats(error: INetworkError, severity: ErrorSeverity): void { + this.stats.totalErrors++; + this.stats.errorsByType[error.type] = (this.stats.errorsByType[error.type] || 0) + 1; + this.stats.errorsBySeverity[severity] = (this.stats.errorsBySeverity[severity] || 0) + 1; + this.stats.lastError = error; + } + + /** + * 尝试错误恢复 + */ + private attemptRecovery(error: INetworkError, severity: ErrorSeverity): void { + const strategy = this.recoveryRules.get(error.type); + if (!strategy || strategy === RecoveryStrategy.Ignore) { + return; + } + + const errorId = this.generateErrorId(error); + + // 检查是否已经在恢复中 + if (this.pendingRecoveries.has(errorId)) { + return; + } + + // 检查重试次数 + const retryCount = this.retryAttempts.get(errorId) || 0; + if (retryCount >= this.config.maxRetryAttempts) { + this.stats.unrecoveredErrors++; + this.eventHandlers.errorUnrecoverable?.(error); + return; + } + + this.pendingRecoveries.add(errorId); + this.retryAttempts.set(errorId, retryCount + 1); + + this.logger.info(`尝试错误恢复: ${strategy} (第 ${retryCount + 1} 次)`, { + errorType: error.type, + strategy + }); + + // 延迟执行恢复策略 + setTimeout(() => { + this.executeRecoveryStrategy(error, strategy, errorId); + }, this.config.retryDelay * (retryCount + 1)); + } + + /** + * 执行恢复策略 + */ + private executeRecoveryStrategy( + error: INetworkError, + strategy: RecoveryStrategy, + errorId: string + ): void { + try { + switch (strategy) { + case RecoveryStrategy.Retry: + // 这里应该重试导致错误的操作 + // 具体实现需要外部提供重试回调 + break; + + case RecoveryStrategy.Reconnect: + // 这里应该触发重连 + // 具体实现需要外部处理 + break; + + case RecoveryStrategy.Restart: + // 这里应该重启相关服务 + // 具体实现需要外部处理 + break; + + case RecoveryStrategy.Escalate: + // 上报错误给上层处理 + this.logger.error('错误需要上层处理:', error); + break; + } + + this.pendingRecoveries.delete(errorId); + this.eventHandlers.errorRecovered?.(error, strategy); + + } catch (recoveryError) { + this.logger.error('错误恢复失败:', recoveryError); + this.pendingRecoveries.delete(errorId); + } + } + + /** + * 报告错误 + */ + private async reportError(error: INetworkError, severity: ErrorSeverity): Promise { + if (!this.config.errorReportingEndpoint) { + return; + } + + try { + const report = { + error, + severity, + timestamp: Date.now(), + userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'Node.js', + url: typeof window !== 'undefined' ? window.location.href : 'server' + }; + + // 发送错误报告 + await fetch(this.config.errorReportingEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(report) + }); + + } catch (reportError) { + this.logger.error('发送错误报告失败:', reportError); + } + } + + /** + * 生成错误ID + */ + private generateErrorId(error: INetworkError): string { + return `${error.type}-${error.code || 'no-code'}-${error.timestamp}`; + } + + /** + * 初始化默认规则 + */ + private initializeDefaultRules(): void { + // 默认严重程度规则 + this.severityRules.set(NetworkErrorType.CONNECTION_FAILED, ErrorSeverity.High); + this.severityRules.set(NetworkErrorType.CONNECTION_LOST, ErrorSeverity.High); + this.severityRules.set(NetworkErrorType.AUTHENTICATION_FAILED, ErrorSeverity.Critical); + this.severityRules.set(NetworkErrorType.PERMISSION_DENIED, ErrorSeverity.Critical); + this.severityRules.set(NetworkErrorType.TIMEOUT, ErrorSeverity.Medium); + this.severityRules.set(NetworkErrorType.RATE_LIMITED, ErrorSeverity.Medium); + this.severityRules.set(NetworkErrorType.INVALID_MESSAGE, ErrorSeverity.Low); + + // 默认恢复策略 + this.recoveryRules.set(NetworkErrorType.CONNECTION_FAILED, RecoveryStrategy.Reconnect); + this.recoveryRules.set(NetworkErrorType.CONNECTION_LOST, RecoveryStrategy.Reconnect); + this.recoveryRules.set(NetworkErrorType.TIMEOUT, RecoveryStrategy.Retry); + this.recoveryRules.set(NetworkErrorType.RATE_LIMITED, RecoveryStrategy.Retry); + this.recoveryRules.set(NetworkErrorType.INVALID_MESSAGE, RecoveryStrategy.Ignore); + this.recoveryRules.set(NetworkErrorType.AUTHENTICATION_FAILED, RecoveryStrategy.Escalate); + this.recoveryRules.set(NetworkErrorType.PERMISSION_DENIED, RecoveryStrategy.Escalate); + } + + /** + * 获取错误趋势分析 + */ + getErrorTrends() { + const totalErrors = this.stats.totalErrors; + if (totalErrors === 0) { + return { trend: 'stable', recommendation: '系统运行正常' }; + } + + const criticalRate = (this.stats.errorsBySeverity[ErrorSeverity.Critical] || 0) / totalErrors; + const recoveryRate = this.stats.recoveredErrors / totalErrors; + + if (criticalRate > 0.1) { + return { trend: 'critical', recommendation: '存在严重错误,需要立即处理' }; + } else if (recoveryRate < 0.5) { + return { trend: 'degrading', recommendation: '错误恢复率偏低,建议检查恢复策略' }; + } else if (totalErrors > 100) { + return { trend: 'high_volume', recommendation: '错误量较大,建议分析根本原因' }; + } else { + return { trend: 'stable', recommendation: '错误处理正常' }; + } + } +} \ No newline at end of file diff --git a/packages/network-shared/src/transport/HeartbeatManager.ts b/packages/network-shared/src/transport/HeartbeatManager.ts new file mode 100644 index 00000000..f10b3d00 --- /dev/null +++ b/packages/network-shared/src/transport/HeartbeatManager.ts @@ -0,0 +1,381 @@ +/** + * 心跳管理器 + * 负责管理网络连接的心跳检测,包括延迟测算和连接健康检测 + */ +import { createLogger } from '@esengine/ecs-framework'; +import { MessageType } from '../types/NetworkTypes'; + +/** + * 心跳配置 + */ +export interface HeartbeatConfig { + interval: number; // 心跳间隔(毫秒) + timeout: number; // 心跳超时(毫秒) + maxMissedHeartbeats: number; // 最大丢失心跳数 + enableLatencyMeasurement: boolean; // 是否启用延迟测量 +} + +/** + * 心跳状态 + */ +export interface HeartbeatStatus { + isHealthy: boolean; + lastHeartbeat: number; + latency?: number; + missedHeartbeats: number; + averageLatency?: number; + packetLoss?: number; +} + +/** + * 心跳事件接口 + */ +export interface HeartbeatEvents { + heartbeatSent: (timestamp: number) => void; + heartbeatReceived: (latency: number) => void; + heartbeatTimeout: (missedCount: number) => void; + healthStatusChanged: (isHealthy: boolean) => void; +} + +/** + * 心跳消息接口 + */ +export interface HeartbeatMessage { + type: MessageType.HEARTBEAT; + clientTime: number; + serverTime?: number; + sequence?: number; +} + +/** + * 心跳管理器 + */ +export class HeartbeatManager { + private logger = createLogger('HeartbeatManager'); + private config: HeartbeatConfig; + private status: HeartbeatStatus; + private eventHandlers: Partial = {}; + + // 定时器 + private heartbeatTimer?: number; + private timeoutTimer?: number; + + // 延迟测量 + private pendingPings: Map = new Map(); + private latencyHistory: number[] = []; + private sequence = 0; + + // 统计信息 + private sentCount = 0; + private receivedCount = 0; + + /** + * 发送心跳回调 + */ + private sendHeartbeat?: (message: HeartbeatMessage) => void; + + /** + * 构造函数 + */ + constructor(config: Partial = {}) { + this.config = { + interval: 30000, // 30秒 + timeout: 60000, // 60秒 + maxMissedHeartbeats: 3, // 最大丢失3次 + enableLatencyMeasurement: true, + ...config + }; + + this.status = { + isHealthy: true, + lastHeartbeat: Date.now(), + missedHeartbeats: 0 + }; + } + + /** + * 启动心跳 + */ + start(sendCallback: (message: HeartbeatMessage) => void): void { + this.sendHeartbeat = sendCallback; + this.startHeartbeatTimer(); + this.logger.info('心跳管理器已启动'); + } + + /** + * 停止心跳 + */ + stop(): void { + this.stopHeartbeatTimer(); + this.stopTimeoutTimer(); + this.pendingPings.clear(); + this.logger.info('心跳管理器已停止'); + } + + /** + * 处理接收到的心跳响应 + */ + handleHeartbeatResponse(message: HeartbeatMessage): void { + const now = Date.now(); + this.status.lastHeartbeat = now; + this.receivedCount++; + + // 重置丢失心跳计数 + this.status.missedHeartbeats = 0; + + // 计算延迟 + if (this.config.enableLatencyMeasurement && message.sequence !== undefined) { + const sentTime = this.pendingPings.get(message.sequence); + if (sentTime) { + const latency = now - sentTime; + this.updateLatency(latency); + this.pendingPings.delete(message.sequence); + + this.eventHandlers.heartbeatReceived?.(latency); + } + } + + // 更新健康状态 + this.updateHealthStatus(true); + + // 停止超时定时器 + this.stopTimeoutTimer(); + } + + /** + * 处理心跳超时 + */ + handleHeartbeatTimeout(): void { + this.status.missedHeartbeats++; + this.logger.warn(`心跳超时,丢失次数: ${this.status.missedHeartbeats}`); + + // 触发超时事件 + this.eventHandlers.heartbeatTimeout?.(this.status.missedHeartbeats); + + // 检查是否达到最大丢失次数 + if (this.status.missedHeartbeats >= this.config.maxMissedHeartbeats) { + this.updateHealthStatus(false); + } + } + + /** + * 获取心跳状态 + */ + getStatus(): HeartbeatStatus { + return { ...this.status }; + } + + /** + * 获取统计信息 + */ + getStats() { + const packetLoss = this.sentCount > 0 ? + ((this.sentCount - this.receivedCount) / this.sentCount) * 100 : 0; + + return { + sentCount: this.sentCount, + receivedCount: this.receivedCount, + packetLoss, + averageLatency: this.status.averageLatency, + currentLatency: this.status.latency, + isHealthy: this.status.isHealthy, + missedHeartbeats: this.status.missedHeartbeats, + latencyHistory: [...this.latencyHistory] + }; + } + + /** + * 设置事件处理器 + */ + on(event: K, handler: HeartbeatEvents[K]): void { + this.eventHandlers[event] = handler; + } + + /** + * 移除事件处理器 + */ + off(event: K): void { + delete this.eventHandlers[event]; + } + + /** + * 手动发送心跳 + */ + sendHeartbeatNow(): void { + this.doSendHeartbeat(); + } + + /** + * 更新配置 + */ + updateConfig(newConfig: Partial): void { + Object.assign(this.config, newConfig); + this.logger.info('心跳配置已更新:', newConfig); + + // 重启定时器以应用新配置 + if (this.heartbeatTimer) { + this.stop(); + if (this.sendHeartbeat) { + this.start(this.sendHeartbeat); + } + } + } + + /** + * 启动心跳定时器 + */ + private startHeartbeatTimer(): void { + this.heartbeatTimer = window.setInterval(() => { + this.doSendHeartbeat(); + }, this.config.interval); + } + + /** + * 停止心跳定时器 + */ + private stopHeartbeatTimer(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = undefined; + } + } + + /** + * 启动超时定时器 + */ + private startTimeoutTimer(): void { + this.timeoutTimer = window.setTimeout(() => { + this.handleHeartbeatTimeout(); + }, this.config.timeout); + } + + /** + * 停止超时定时器 + */ + private stopTimeoutTimer(): void { + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = undefined; + } + } + + /** + * 执行发送心跳 + */ + private doSendHeartbeat(): void { + if (!this.sendHeartbeat) { + this.logger.error('心跳发送回调未设置'); + return; + } + + const now = Date.now(); + const sequence = this.config.enableLatencyMeasurement ? ++this.sequence : undefined; + + const message: HeartbeatMessage = { + type: MessageType.HEARTBEAT, + clientTime: now, + sequence + }; + + try { + this.sendHeartbeat(message); + this.sentCount++; + + // 记录发送时间用于延迟计算 + if (sequence !== undefined) { + this.pendingPings.set(sequence, now); + + // 清理过期的pending pings + this.cleanupPendingPings(); + } + + // 启动超时定时器 + this.stopTimeoutTimer(); + this.startTimeoutTimer(); + + this.eventHandlers.heartbeatSent?.(now); + + } catch (error) { + this.logger.error('发送心跳失败:', error); + } + } + + /** + * 更新延迟信息 + */ + private updateLatency(latency: number): void { + this.status.latency = latency; + + // 保存延迟历史(最多100个样本) + this.latencyHistory.push(latency); + if (this.latencyHistory.length > 100) { + this.latencyHistory.shift(); + } + + // 计算平均延迟 + this.status.averageLatency = this.latencyHistory.reduce((sum, lat) => sum + lat, 0) / this.latencyHistory.length; + + this.logger.debug(`延迟更新: ${latency}ms, 平均: ${this.status.averageLatency?.toFixed(1)}ms`); + } + + /** + * 更新健康状态 + */ + private updateHealthStatus(isHealthy: boolean): void { + if (this.status.isHealthy !== isHealthy) { + this.status.isHealthy = isHealthy; + this.logger.info(`连接健康状态变更: ${isHealthy ? '健康' : '不健康'}`); + this.eventHandlers.healthStatusChanged?.(isHealthy); + } + } + + /** + * 清理过期的pending pings + */ + private cleanupPendingPings(): void { + const now = Date.now(); + const timeout = this.config.timeout * 2; // 清理超过2倍超时时间的记录 + + for (const [sequence, sentTime] of this.pendingPings) { + if (now - sentTime > timeout) { + this.pendingPings.delete(sequence); + } + } + } + + /** + * 重置统计信息 + */ + resetStats(): void { + this.sentCount = 0; + this.receivedCount = 0; + this.latencyHistory.length = 0; + this.status.averageLatency = undefined; + this.status.latency = undefined; + this.status.missedHeartbeats = 0; + this.pendingPings.clear(); + this.logger.info('心跳统计信息已重置'); + } + + /** + * 检查连接是否健康 + */ + isConnectionHealthy(): boolean { + const now = Date.now(); + const timeSinceLastHeartbeat = now - this.status.lastHeartbeat; + + return this.status.isHealthy && + timeSinceLastHeartbeat <= this.config.timeout && + this.status.missedHeartbeats < this.config.maxMissedHeartbeats; + } + + /** + * 获取建议的重连延迟 + */ + getReconnectDelay(): number { + // 基于丢失心跳次数计算重连延迟 + const baseDelay = this.config.interval; + const multiplier = Math.min(Math.pow(2, this.status.missedHeartbeats), 8); + return baseDelay * multiplier; + } +} \ No newline at end of file diff --git a/packages/network-shared/src/types/TransportTypes.ts b/packages/network-shared/src/types/TransportTypes.ts index ef44c22c..aa8d158e 100644 --- a/packages/network-shared/src/types/TransportTypes.ts +++ b/packages/network-shared/src/types/TransportTypes.ts @@ -23,14 +23,14 @@ export interface ITransport { * @param clientId 客户端ID * @param data 数据 */ - send(clientId: string, data: Buffer | string): void; + send(clientId: string, data: ArrayBuffer | string): void; /** * 广播数据到所有客户端 * @param data 数据 * @param exclude 排除的客户端ID列表 */ - broadcast(data: Buffer | string, exclude?: string[]): void; + broadcast(data: ArrayBuffer | string, exclude?: string[]): void; /** * 监听客户端连接事件 @@ -48,7 +48,7 @@ export interface ITransport { * 监听消息接收事件 * @param handler 处理函数 */ - onMessage(handler: (clientId: string, data: Buffer | string) => void): void; + onMessage(handler: (clientId: string, data: ArrayBuffer | string) => void): void; /** * 监听错误事件 @@ -96,13 +96,13 @@ export interface IClientTransport { * 发送数据到服务器 * @param data 数据 */ - send(data: Buffer | string): void; + send(data: ArrayBuffer | string): void; /** * 监听服务器消息 * @param handler 处理函数 */ - onMessage(handler: (data: Buffer | string) => void): void; + onMessage(handler: (data: ArrayBuffer | string) => void): void; /** * 监听连接状态变化 diff --git a/packages/network-shared/src/utils/EventEmitter.ts b/packages/network-shared/src/utils/EventEmitter.ts new file mode 100644 index 00000000..a100f2c7 --- /dev/null +++ b/packages/network-shared/src/utils/EventEmitter.ts @@ -0,0 +1,85 @@ +/** + * 网络层专用的EventEmitter实现 + * 继承自core库的Emitter,提供简单的事件API + */ +import { Emitter } from '@esengine/ecs-framework'; + +/** + * 网络事件发射器,专为网络层设计 + * 使用字符串或symbol作为事件类型,简化API + */ +export class EventEmitter extends Emitter { + constructor() { + super(); + } + + /** + * 添加事件监听器 + * @param event 事件名称 + * @param listener 监听函数 + */ + public on(event: string | symbol, listener: Function): this { + this.addObserver(event, listener, undefined as void); + return this; + } + + /** + * 添加一次性事件监听器 + * @param event 事件名称 + * @param listener 监听函数 + */ + public once(event: string | symbol, listener: Function): this { + const onceWrapper = (...args: any[]) => { + listener.apply(this, args); + this.removeObserver(event, onceWrapper); + }; + this.addObserver(event, onceWrapper, undefined as void); + return this; + } + + /** + * 移除事件监听器 + * @param event 事件名称 + * @param listener 监听函数,不传则移除所有 + */ + public off(event: string | symbol, listener?: Function): this { + if (listener) { + this.removeObserver(event, listener); + } else { + this.removeAllObservers(event); + } + return this; + } + + /** + * 移除事件监听器(别名) + */ + public removeListener(event: string | symbol, listener: Function): this { + return this.off(event, listener); + } + + /** + * 移除所有监听器 + */ + public removeAllListeners(event?: string | symbol): this { + this.removeAllObservers(event); + return this; + } + + /** + * 获取监听器数量 + */ + public listenerCount(event: string | symbol): number { + return this.getObserverCount(event); + } + + /** + * 发射事件(兼容Node.js EventEmitter) + * @param event 事件名称 + * @param args 事件参数 + */ + public override emit(event: string | symbol, ...args: any[]): boolean { + super.emit(event, ...args); + return true; + } +} \ No newline at end of file diff --git a/packages/network-shared/src/utils/index.ts b/packages/network-shared/src/utils/index.ts new file mode 100644 index 00000000..bfa9616d --- /dev/null +++ b/packages/network-shared/src/utils/index.ts @@ -0,0 +1,4 @@ +/** + * 网络层工具类 + */ +export * from './EventEmitter'; \ No newline at end of file