传输层实现(客户端/服务端,链接管理和心跳机制,重连机制)
消息序列化(json序列化,消息压缩,消息ID和时间戳) 网络服务器核心(networkserver/基础room/链接状态同步) 网络客户端核心(networkclient/消息队列)
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom', // 客户端使用jsdom环境
|
||||
roots: ['<rootDir>/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: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||
testTimeout: 10000,
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/bin/',
|
||||
'<rootDir>/dist/',
|
||||
'<rootDir>/node_modules/'
|
||||
]
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
638
packages/network-client/src/core/ConnectionStateManager.ts
Normal file
638
packages/network-client/src/core/ConnectionStateManager.ts
Normal file
@@ -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<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态统计信息
|
||||
*/
|
||||
export interface StateStats {
|
||||
currentState: ConnectionState;
|
||||
totalTransitions: number;
|
||||
stateHistory: StateHistoryEntry[];
|
||||
stateDurations: Record<ConnectionState, number[]>;
|
||||
averageStateDurations: Record<ConnectionState, number>;
|
||||
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<ConnectionState, ITimer> = new Map();
|
||||
|
||||
// 恢复逻辑
|
||||
private recoveryAttempts = 0;
|
||||
private recoveryTimer?: ITimer;
|
||||
|
||||
// Timer管理器使用静态的NetworkTimerManager
|
||||
private recoveryCallback?: () => Promise<void>;
|
||||
|
||||
// 事件处理器
|
||||
private eventHandlers: Partial<ConnectionStateManagerEvents> = {};
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<ConnectionStateManagerConfig> = {}) {
|
||||
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<string, any>): 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>): 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<K extends keyof ConnectionStateManagerEvents>(event: K, handler: ConnectionStateManagerEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return super.on(event, handler as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof ConnectionStateManagerEvents>(event: K): this {
|
||||
delete this.eventHandlers[event];
|
||||
return super.off(event, this.eventHandlers[event] as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<ConnectionStateManagerConfig>): 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, ConnectionState[]> = {
|
||||
[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<string, any>): 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, string> = {
|
||||
[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';
|
||||
}
|
||||
}
|
||||
}
|
||||
680
packages/network-client/src/core/MessageQueue.ts
Normal file
680
packages/network-client/src/core/MessageQueue.ts
Normal file
@@ -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<MessagePriority, number>;
|
||||
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<MessagePriority, QueuedMessage[]> = new Map();
|
||||
private retryQueue: QueuedMessage[] = [];
|
||||
private processingMap: Map<string, QueuedMessage> = new Map();
|
||||
|
||||
// 定时器
|
||||
private processingTimer?: ITimer;
|
||||
private retryTimer?: ITimer;
|
||||
private cleanupTimer?: ITimer;
|
||||
|
||||
// 事件处理器
|
||||
private eventHandlers: Partial<MessageQueueEvents> = {};
|
||||
|
||||
// 发送回调
|
||||
private sendCallback?: (message: INetworkMessage) => Promise<boolean>;
|
||||
|
||||
// 性能统计
|
||||
private processingTimes: number[] = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<MessageQueueConfig> = {}) {
|
||||
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<boolean>): 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<K extends keyof MessageQueueEvents>(event: K, handler: MessageQueueEvents[K]): void {
|
||||
this.eventHandlers[event] = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
off<K extends keyof MessageQueueEvents>(event: K): void {
|
||||
delete this.eventHandlers[event];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<MessageQueueConfig>): 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<void> {
|
||||
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<void> {
|
||||
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]
|
||||
};
|
||||
}
|
||||
}
|
||||
698
packages/network-client/src/core/NetworkClient.ts
Normal file
698
packages/network-client/src/core/NetworkClient.ts
Normal file
@@ -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<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端状态
|
||||
*/
|
||||
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<NetworkClientEvents> = {};
|
||||
|
||||
// 消息队列
|
||||
private messageQueue: INetworkMessage[] = [];
|
||||
private isProcessingQueue = false;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<NetworkClientConfig> = {}) {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<T extends INetworkMessage>(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<K extends keyof NetworkClientEvents>(event: K, handler: NetworkClientEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof NetworkClientEvents>(event: K): this {
|
||||
delete this.eventHandlers[event];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<NetworkClientConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
this.logger.info('客户端配置已更新:', newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁客户端
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
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<INetworkMessage>(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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
410
packages/network-client/src/transport/ReconnectionManager.ts
Normal file
410
packages/network-client/src/transport/ReconnectionManager.ts
Normal file
@@ -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<ReconnectionEvents> = {};
|
||||
|
||||
private reconnectTimer?: ITimer;
|
||||
private reconnectCallback?: () => Promise<void>;
|
||||
private abortController?: AbortController;
|
||||
|
||||
// 策略相关
|
||||
private strategy: ReconnectionStrategy = ReconnectionStrategy.Exponential;
|
||||
private customDelayCalculator?: (attempt: number) => number;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<ReconnectionConfig> = {}) {
|
||||
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>): 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<ReconnectionConfig>): 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<K extends keyof ReconnectionEvents>(event: K, handler: ReconnectionEvents[K]): void {
|
||||
this.eventHandlers[event] = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
off<K extends keyof ReconnectionEvents>(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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
406
packages/network-client/src/transport/WebSocketClient.ts
Normal file
406
packages/network-client/src/transport/WebSocketClient.ts
Normal file
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
103
packages/network-client/src/utils/NetworkTimer.ts
Normal file
103
packages/network-client/src/utils/NetworkTimer.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 网络层专用Timer实现
|
||||
* 实现core库的ITimer接口,但使用浏览器/Node.js的原生定时器
|
||||
*/
|
||||
import { ITimer } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 网络层Timer实现
|
||||
*/
|
||||
export class NetworkTimer<TContext = unknown> implements ITimer<TContext> {
|
||||
public context: TContext;
|
||||
private timerId?: number;
|
||||
private callback: (timer: ITimer<TContext>) => void;
|
||||
private _isDone = false;
|
||||
|
||||
constructor(
|
||||
timeInMilliseconds: number,
|
||||
repeats: boolean,
|
||||
context: TContext,
|
||||
onTime: (timer: ITimer<TContext>) => 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>(): T {
|
||||
return this.context as unknown as T;
|
||||
}
|
||||
|
||||
get isDone(): boolean {
|
||||
return this._isDone;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络Timer管理器
|
||||
*/
|
||||
export class NetworkTimerManager {
|
||||
private static timers: Set<NetworkTimer> = new Set();
|
||||
|
||||
/**
|
||||
* 创建一个定时器
|
||||
*/
|
||||
static schedule<TContext = unknown>(
|
||||
timeInSeconds: number,
|
||||
repeats: boolean,
|
||||
context: TContext,
|
||||
onTime: (timer: ITimer<TContext>) => void
|
||||
): ITimer<TContext> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
4
packages/network-client/src/utils/index.ts
Normal file
4
packages/network-client/src/utils/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 网络客户端工具类
|
||||
*/
|
||||
export * from './NetworkTimer';
|
||||
@@ -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(() => {
|
||||
// 每个测试后的清理工作
|
||||
// 清理可能的网络连接、定时器等
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/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: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
||||
testTimeout: 15000, // 服务端测试可能需要更长时间
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/bin/',
|
||||
'<rootDir>/dist/',
|
||||
'<rootDir>/node_modules/'
|
||||
]
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
410
packages/network-server/src/core/ConnectionManager.ts
Normal file
410
packages/network-server/src/core/ConnectionManager.ts
Normal file
@@ -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<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接管理器配置
|
||||
*/
|
||||
export interface ConnectionManagerConfig {
|
||||
heartbeatInterval: number;
|
||||
heartbeatTimeout: number;
|
||||
maxIdleTime: number;
|
||||
cleanupInterval: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接管理器
|
||||
*/
|
||||
export class ConnectionManager extends EventEmitter {
|
||||
private logger = createLogger('ConnectionManager');
|
||||
private sessions: Map<string, ClientSession> = new Map();
|
||||
private config: ConnectionManagerConfig;
|
||||
private heartbeatTimer?: ITimer;
|
||||
private cleanupTimer?: ITimer;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<ConnectionManagerConfig> = {}) {
|
||||
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<IConnectionStats>): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(session.stats, stats);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话用户数据
|
||||
*/
|
||||
setSessionUserData(clientId: string, userData: Record<string, any>): 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<string, number> {
|
||||
const roomCounts: Record<string, number> = {};
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
701
packages/network-server/src/core/NetworkServer.ts
Normal file
701
packages/network-server/src/core/NetworkServer.ts
Normal file
@@ -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<NetworkServerEvents> = {};
|
||||
|
||||
// 速率限制
|
||||
private rateLimitMap: Map<string, { count: number; resetTime: number; banned: boolean }> = new Map();
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<NetworkServerConfig> = {}) {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<T extends INetworkMessage>(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<T extends INetworkMessage>(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<K extends keyof NetworkServerEvents>(event: K, handler: NetworkServerEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof NetworkServerEvents>(event: K): this {
|
||||
delete this.eventHandlers[event];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<NetworkServerConfig>): 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<INetworkMessage>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
507
packages/network-server/src/rooms/Room.ts
Normal file
507
packages/network-server/src/rooms/Room.ts
Normal file
@@ -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<string, any>;
|
||||
autoDestroy: boolean; // 是否在空房间时自动销毁
|
||||
customData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家信息
|
||||
*/
|
||||
export interface PlayerInfo {
|
||||
sessionId: string;
|
||||
name: string;
|
||||
isHost: boolean;
|
||||
joinTime: number;
|
||||
customData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间事件接口
|
||||
*/
|
||||
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<string, PlayerInfo> = new Map();
|
||||
private hostId?: string;
|
||||
private createTime: number = Date.now();
|
||||
private stats: RoomStats;
|
||||
|
||||
// 事件处理器
|
||||
private eventHandlers: Partial<RoomEvents> = {};
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
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<RoomConfig>): void {
|
||||
Object.assign(this.config, updates);
|
||||
this.logger.info(`房间配置已更新: ${this.config.id}`, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置玩家自定义数据
|
||||
*/
|
||||
setPlayerData(sessionId: string, data: Record<string, any>): 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<K extends keyof RoomEvents>(event: K, handler: RoomEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return super.on(event, handler as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof RoomEvents>(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);
|
||||
// 可以根据需要重置其他状态
|
||||
}
|
||||
}
|
||||
621
packages/network-server/src/rooms/RoomManager.ts
Normal file
621
packages/network-server/src/rooms/RoomManager.ts
Normal file
@@ -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<string, any>;
|
||||
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<RoomState, number>;
|
||||
roomsCreated: number;
|
||||
roomsDestroyed: number;
|
||||
playersJoined: number;
|
||||
playersLeft: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器
|
||||
*/
|
||||
export class RoomManager extends EventEmitter {
|
||||
private logger = createLogger('RoomManager');
|
||||
private config: RoomManagerConfig;
|
||||
private rooms: Map<string, Room> = new Map();
|
||||
private playerRoomMap: Map<string, string> = new Map(); // sessionId -> roomId
|
||||
private stats: RoomManagerStats;
|
||||
private cleanupTimer?: ITimer;
|
||||
|
||||
// 事件处理器
|
||||
private eventHandlers: Partial<RoomManagerEvents> = {};
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<RoomManagerConfig> = {}) {
|
||||
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<RoomManagerConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
this.logger.info('房间管理器配置已更新:', newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件处理器
|
||||
*/
|
||||
override on<K extends keyof RoomManagerEvents>(event: K, handler: RoomManagerEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return super.on(event, handler as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof RoomManagerEvents>(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);
|
||||
}
|
||||
}
|
||||
407
packages/network-server/src/transport/WebSocketTransport.ts
Normal file
407
packages/network-server/src/transport/WebSocketTransport.ts
Normal file
@@ -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<string, WebSocket> = new Map();
|
||||
private clientInfo: Map<string, ITransportClientInfo> = new Map();
|
||||
private config: ITransportConfig;
|
||||
private isRunning = false;
|
||||
|
||||
/**
|
||||
* 连接事件处理器
|
||||
*/
|
||||
private connectHandlers: ((clientInfo: ITransportClientInfo) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 断开连接事件处理器
|
||||
*/
|
||||
private disconnectHandlers: ((clientId: string, reason?: string) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 消息接收事件处理器
|
||||
*/
|
||||
private messageHandlers: ((clientId: string, data: ArrayBuffer | string) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 错误事件处理器
|
||||
*/
|
||||
private errorHandlers: ((error: Error) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: ITransportConfig) {
|
||||
super();
|
||||
this.config = {
|
||||
maxConnections: 1000,
|
||||
heartbeatInterval: 30000,
|
||||
connectionTimeout: 60000,
|
||||
maxMessageSize: 1024 * 1024, // 1MB
|
||||
compression: true,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动传输层
|
||||
*/
|
||||
async start(port: number, host?: string): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
this.logger.warn('WebSocket传输层已在运行');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.server = new WebSocketServer({
|
||||
port,
|
||||
host: host || '0.0.0.0',
|
||||
maxPayload: this.config.maxMessageSize,
|
||||
perMessageDeflate: this.config.compression ? {
|
||||
threshold: 1024,
|
||||
concurrencyLimit: 10,
|
||||
clientNoContextTakeover: false,
|
||||
serverNoContextTakeover: false
|
||||
} : false
|
||||
});
|
||||
|
||||
this.setupServerEvents();
|
||||
this.isRunning = true;
|
||||
|
||||
this.logger.info(`WebSocket服务器已启动: ${host || '0.0.0.0'}:${port}`);
|
||||
this.logger.info(`最大连接数: ${this.config.maxConnections}`);
|
||||
this.logger.info(`压缩: ${this.config.compression ? '启用' : '禁用'}`);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('启动WebSocket服务器失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止传输层
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning || !this.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 断开所有客户端连接
|
||||
for (const [clientId, ws] of this.clients) {
|
||||
ws.close(1001, '服务器关闭');
|
||||
this.handleClientDisconnect(clientId, '服务器关闭');
|
||||
}
|
||||
|
||||
// 关闭服务器
|
||||
this.server!.close(() => {
|
||||
this.isRunning = false;
|
||||
this.server = undefined;
|
||||
this.logger.info('WebSocket服务器已停止');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据到指定客户端
|
||||
*/
|
||||
send(clientId: string, data: ArrayBuffer | string): void {
|
||||
const ws = this.clients.get(clientId);
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
this.logger.warn(`尝试向未连接的客户端发送消息: ${clientId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (error) {
|
||||
this.logger.error(`发送消息到客户端 ${clientId} 失败:`, error);
|
||||
this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播数据到所有客户端
|
||||
*/
|
||||
broadcast(data: ArrayBuffer | string, exclude?: string[]): void {
|
||||
const excludeSet = new Set(exclude || []);
|
||||
|
||||
for (const [clientId, ws] of this.clients) {
|
||||
if (excludeSet.has(clientId) || ws.readyState !== WebSocket.OPEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (error) {
|
||||
this.logger.error(`广播消息到客户端 ${clientId} 失败:`, error);
|
||||
this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端连接事件
|
||||
*/
|
||||
onConnect(handler: (clientInfo: ITransportClientInfo) => void): void {
|
||||
this.connectHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端断开事件
|
||||
*/
|
||||
onDisconnect(handler: (clientId: string, reason?: string) => void): void {
|
||||
this.disconnectHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听消息接收事件
|
||||
*/
|
||||
onMessage(handler: (clientId: string, data: ArrayBuffer | string) => void): void {
|
||||
this.messageHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听错误事件
|
||||
*/
|
||||
onError(handler: (error: Error) => void): void {
|
||||
this.errorHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接的客户端数量
|
||||
*/
|
||||
getClientCount(): number {
|
||||
return this.clients.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否连接
|
||||
*/
|
||||
isClientConnected(clientId: string): boolean {
|
||||
const ws = this.clients.get(clientId);
|
||||
return ws !== undefined && ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端
|
||||
*/
|
||||
disconnectClient(clientId: string, reason?: string): void {
|
||||
const ws = this.clients.get(clientId);
|
||||
if (ws) {
|
||||
ws.close(1000, reason || '服务器主动断开');
|
||||
this.handleClientDisconnect(clientId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端信息
|
||||
*/
|
||||
getClientInfo(clientId: string): ITransportClientInfo | undefined {
|
||||
return this.clientInfo.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端信息
|
||||
*/
|
||||
getAllClients(): ITransportClientInfo[] {
|
||||
return Array.from(this.clientInfo.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器事件监听
|
||||
*/
|
||||
private setupServerEvents(): void {
|
||||
if (!this.server) return;
|
||||
|
||||
this.server.on('connection', (ws, request) => {
|
||||
this.handleNewConnection(ws, request);
|
||||
});
|
||||
|
||||
this.server.on('error', (error) => {
|
||||
this.logger.error('WebSocket服务器错误:', error);
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
this.server.on('close', () => {
|
||||
this.logger.info('WebSocket服务器已关闭');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新客户端连接
|
||||
*/
|
||||
private handleNewConnection(ws: WebSocket, request: any): void {
|
||||
// 检查连接数限制
|
||||
if (this.clients.size >= this.config.maxConnections!) {
|
||||
this.logger.warn('达到最大连接数限制,拒绝新连接');
|
||||
ws.close(1013, '服务器繁忙');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = crypto.randomUUID();
|
||||
const clientInfo: ITransportClientInfo = {
|
||||
id: clientId,
|
||||
remoteAddress: request.socket.remoteAddress || 'unknown',
|
||||
connectTime: Date.now(),
|
||||
userAgent: request.headers['user-agent'],
|
||||
headers: request.headers
|
||||
};
|
||||
|
||||
// 存储客户端连接和信息
|
||||
this.clients.set(clientId, ws);
|
||||
this.clientInfo.set(clientId, clientInfo);
|
||||
|
||||
// 设置WebSocket事件监听
|
||||
this.setupClientEvents(ws, clientId);
|
||||
|
||||
this.logger.info(`新客户端连接: ${clientId} from ${clientInfo.remoteAddress}`);
|
||||
|
||||
// 触发连接事件
|
||||
this.connectHandlers.forEach(handler => {
|
||||
try {
|
||||
handler(clientInfo);
|
||||
} catch (error) {
|
||||
this.logger.error('连接事件处理器错误:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端WebSocket事件监听
|
||||
*/
|
||||
private setupClientEvents(ws: WebSocket, clientId: string): void {
|
||||
// 消息接收
|
||||
ws.on('message', (data) => {
|
||||
this.handleClientMessage(clientId, data);
|
||||
});
|
||||
|
||||
// 连接关闭
|
||||
ws.on('close', (code, reason) => {
|
||||
this.handleClientDisconnect(clientId, reason?.toString() || `Code: ${code}`);
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
ws.on('error', (error) => {
|
||||
this.logger.error(`客户端 ${clientId} WebSocket错误:`, error);
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
// Pong响应(心跳)
|
||||
ws.on('pong', () => {
|
||||
// 记录客户端响应心跳
|
||||
const info = this.clientInfo.get(clientId);
|
||||
if (info) {
|
||||
// 可以更新延迟信息
|
||||
}
|
||||
});
|
||||
|
||||
// 设置连接超时
|
||||
if (this.config.connectionTimeout) {
|
||||
Core.schedule(this.config.connectionTimeout / 1000, false, this, () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.ping();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端消息
|
||||
*/
|
||||
private handleClientMessage(clientId: string, data: WebSocket.Data): void {
|
||||
try {
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Jest测试环境设置 - 服务端
|
||||
*/
|
||||
|
||||
// 导入reflect-metadata以支持装饰器
|
||||
import 'reflect-metadata';
|
||||
|
||||
// 全局测试配置
|
||||
beforeAll(() => {
|
||||
// 设置测试环境
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.NETWORK_ENV = 'server';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// 清理测试环境
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// 每个测试前的准备工作
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 每个测试后的清理工作
|
||||
// 清理可能的网络连接、定时器等
|
||||
});
|
||||
@@ -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';
|
||||
// 序列化系统
|
||||
export * from './serialization/JSONSerializer';
|
||||
export * from './serialization/MessageCompressor';
|
||||
|
||||
// 工具类
|
||||
export * from './utils';
|
||||
502
packages/network-shared/src/protocols/MessageManager.ts
Normal file
502
packages/network-shared/src/protocols/MessageManager.ts
Normal file
@@ -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<string> = new Set();
|
||||
private pendingMessages: Map<string, INetworkMessage> = new Map();
|
||||
private messageSequence: Map<string, number> = new Map();
|
||||
|
||||
// 清理定时器
|
||||
private cleanupTimer?: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<MessageManagerConfig> = {}) {
|
||||
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<T extends INetworkMessage>(
|
||||
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<MessageManagerConfig>): 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;
|
||||
}
|
||||
}
|
||||
550
packages/network-shared/src/serialization/JSONSerializer.ts
Normal file
550
packages/network-shared/src/serialization/JSONSerializer.ts
Normal file
@@ -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<string, ICustomSerializer>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义序列化器接口
|
||||
*/
|
||||
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<T = any> {
|
||||
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<SerializerConfig> = {}) {
|
||||
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<T extends INetworkMessage>(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<T extends INetworkMessage>(data: string | ArrayBuffer): DeserializationResult<T> {
|
||||
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<T extends INetworkMessage>(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<T extends INetworkMessage>(data: string | ArrayBuffer): DeserializationResult<T[]> {
|
||||
const result = this.deserialize<any>(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<SerializerConfig>): 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)]
|
||||
};
|
||||
}
|
||||
}
|
||||
498
packages/network-shared/src/serialization/MessageCompressor.ts
Normal file
498
packages/network-shared/src/serialization/MessageCompressor.ts
Normal file
@@ -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<CompressionAlgorithm, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息压缩器
|
||||
*/
|
||||
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<CompressionConfig> = {}) {
|
||||
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<CompressionResult> {
|
||||
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<Buffer> {
|
||||
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<CompressionResult[]> {
|
||||
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<CompressionResult> {
|
||||
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<CompressionConfig>): 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<Buffer> {
|
||||
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<Buffer> {
|
||||
if (this.config.enableAsync) {
|
||||
return await this.gunzipAsync(data);
|
||||
} else {
|
||||
return zlib.gunzipSync(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DEFLATE压缩
|
||||
*/
|
||||
private async compressDeflate(data: Buffer): Promise<Buffer> {
|
||||
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<Buffer> {
|
||||
if (this.config.enableAsync) {
|
||||
return await this.inflateAsync(data);
|
||||
} else {
|
||||
return zlib.inflateSync(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BROTLI压缩
|
||||
*/
|
||||
private async compressBrotli(data: Buffer): Promise<Buffer> {
|
||||
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<Buffer> {
|
||||
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 '压缩性能正常';
|
||||
}
|
||||
}
|
||||
}
|
||||
461
packages/network-shared/src/transport/ErrorHandler.ts
Normal file
461
packages/network-shared/src/transport/ErrorHandler.ts
Normal file
@@ -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<NetworkErrorType, number>;
|
||||
errorsBySeverity: Record<ErrorSeverity, number>;
|
||||
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<ErrorHandlerEvents> = {};
|
||||
|
||||
// 错误恢复状态
|
||||
private retryAttempts: Map<string, number> = new Map();
|
||||
private pendingRecoveries: Set<string> = new Set();
|
||||
|
||||
// 错误分类规则
|
||||
private severityRules: Map<NetworkErrorType, ErrorSeverity> = new Map();
|
||||
private recoveryRules: Map<NetworkErrorType, RecoveryStrategy> = new Map();
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<ErrorHandlerConfig> = {}) {
|
||||
this.config = {
|
||||
maxRetryAttempts: 3,
|
||||
retryDelay: 1000,
|
||||
enableAutoRecovery: true,
|
||||
enableErrorReporting: false,
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
totalErrors: 0,
|
||||
errorsByType: {} as Record<NetworkErrorType, number>,
|
||||
errorsBySeverity: {} as Record<ErrorSeverity, number>,
|
||||
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<NetworkErrorType, number>,
|
||||
errorsBySeverity: {} as Record<ErrorSeverity, number>,
|
||||
recoveredErrors: 0,
|
||||
unrecoveredErrors: 0
|
||||
};
|
||||
this.retryAttempts.clear();
|
||||
this.pendingRecoveries.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件处理器
|
||||
*/
|
||||
on<K extends keyof ErrorHandlerEvents>(event: K, handler: ErrorHandlerEvents[K]): void {
|
||||
this.eventHandlers[event] = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
off<K extends keyof ErrorHandlerEvents>(event: K): void {
|
||||
delete this.eventHandlers[event];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<ErrorHandlerConfig>): 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<void> {
|
||||
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: '错误处理正常' };
|
||||
}
|
||||
}
|
||||
}
|
||||
381
packages/network-shared/src/transport/HeartbeatManager.ts
Normal file
381
packages/network-shared/src/transport/HeartbeatManager.ts
Normal file
@@ -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<HeartbeatEvents> = {};
|
||||
|
||||
// 定时器
|
||||
private heartbeatTimer?: number;
|
||||
private timeoutTimer?: number;
|
||||
|
||||
// 延迟测量
|
||||
private pendingPings: Map<number, number> = new Map();
|
||||
private latencyHistory: number[] = [];
|
||||
private sequence = 0;
|
||||
|
||||
// 统计信息
|
||||
private sentCount = 0;
|
||||
private receivedCount = 0;
|
||||
|
||||
/**
|
||||
* 发送心跳回调
|
||||
*/
|
||||
private sendHeartbeat?: (message: HeartbeatMessage) => void;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<HeartbeatConfig> = {}) {
|
||||
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<K extends keyof HeartbeatEvents>(event: K, handler: HeartbeatEvents[K]): void {
|
||||
this.eventHandlers[event] = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
off<K extends keyof HeartbeatEvents>(event: K): void {
|
||||
delete this.eventHandlers[event];
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动发送心跳
|
||||
*/
|
||||
sendHeartbeatNow(): void {
|
||||
this.doSendHeartbeat();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<HeartbeatConfig>): 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 监听连接状态变化
|
||||
|
||||
85
packages/network-shared/src/utils/EventEmitter.ts
Normal file
85
packages/network-shared/src/utils/EventEmitter.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 网络层专用的EventEmitter实现
|
||||
* 继承自core库的Emitter,提供简单的事件API
|
||||
*/
|
||||
import { Emitter } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 网络事件发射器,专为网络层设计
|
||||
* 使用字符串或symbol作为事件类型,简化API
|
||||
*/
|
||||
export class EventEmitter extends Emitter<string | symbol, void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
4
packages/network-shared/src/utils/index.ts
Normal file
4
packages/network-shared/src/utils/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 网络层工具类
|
||||
*/
|
||||
export * from './EventEmitter';
|
||||
Reference in New Issue
Block a user