Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统 * feat(engine): 完善2D渲染管线和编辑器视口功能 * feat(editor): 实现Viewport变换工具系统 * feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示 * feat(editor): 实现Run on Device移动预览功能 * feat(editor): 添加组件属性控制和依赖关系系统 * feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器 * feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(ci): 迁移项目到pnpm并修复CI构建问题 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 移除 network 相关包 * chore: 移除 network 相关包
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建 @esengine/network-client 包...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('npx rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ @esengine/network-client 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 构建失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePackageJson() {
|
||||
const sourcePackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const distPackage = {
|
||||
name: sourcePackage.name,
|
||||
version: sourcePackage.version,
|
||||
description: sourcePackage.description,
|
||||
main: 'index.cjs',
|
||||
module: 'index.mjs',
|
||||
unpkg: 'index.umd.js',
|
||||
types: 'index.d.ts',
|
||||
exports: {
|
||||
'.': {
|
||||
import: './index.mjs',
|
||||
require: './index.cjs',
|
||||
types: './index.d.ts'
|
||||
}
|
||||
},
|
||||
files: [
|
||||
'index.mjs',
|
||||
'index.mjs.map',
|
||||
'index.cjs',
|
||||
'index.cjs.map',
|
||||
'index.umd.js',
|
||||
'index.umd.js.map',
|
||||
'index.d.ts',
|
||||
'README.md',
|
||||
'LICENSE'
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'network',
|
||||
'client',
|
||||
'multiplayer',
|
||||
'game',
|
||||
'browser',
|
||||
'cocos',
|
||||
'typescript'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
dependencies: sourcePackage.dependencies,
|
||||
publishConfig: sourcePackage.publishConfig,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
sideEffects: false
|
||||
};
|
||||
|
||||
fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2));
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
if (fs.existsSync(src)) {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` ✓ 复制: ${path.basename(dest)}`);
|
||||
} else {
|
||||
console.log(` ⚠️ 文件不存在: ${src}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showBuildResults() {
|
||||
const distDir = './dist';
|
||||
const files = ['index.mjs', 'index.cjs', 'index.umd.js', 'index.d.ts'];
|
||||
|
||||
console.log('\n📊 构建结果:');
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(distDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const size = fs.statSync(filePath).size;
|
||||
console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/network-client",
|
||||
"version": "1.0.1",
|
||||
"description": "ECS Framework网络层 - 客户端实现",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./bin/index.d.ts",
|
||||
"import": "./bin/index.js",
|
||||
"development": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"bin/**/*",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"network",
|
||||
"client",
|
||||
"multiplayer",
|
||||
"game",
|
||||
"browser",
|
||||
"cocos",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
"build:watch": "tsc --watch",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build:npm": "npm run build && node build-rollup.cjs",
|
||||
"publish:npm": "npm run build:npm && cd dist && npm publish",
|
||||
"publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish",
|
||||
"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": "echo \"No tests configured for network-client\""
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"@esengine/network-shared": "file:../network-shared",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/network-client"
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
const resolve = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const terser = require('@rollup/plugin-terser');
|
||||
const dts = require('rollup-plugin-dts').default;
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/network-client v${pkg.version}
|
||||
* ECS网络层客户端实现
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
// 外部依赖,不打包进bundle
|
||||
const external = [
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/network-shared',
|
||||
'reflect-metadata'
|
||||
];
|
||||
|
||||
const commonPlugins = [
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs({
|
||||
include: /node_modules/
|
||||
})
|
||||
];
|
||||
|
||||
module.exports = [
|
||||
// ES模块构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.mjs',
|
||||
format: 'es',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
unknownGlobalSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// CommonJS构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.cjs',
|
||||
format: 'cjs',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// UMD构建 - 用于浏览器直接使用
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.umd.js',
|
||||
format: 'umd',
|
||||
name: 'ECSNetworkClient',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
globals: {
|
||||
'@esengine/ecs-framework': 'ECS',
|
||||
'@esengine/network-shared': 'ECSNetworkShared',
|
||||
'reflect-metadata': 'Reflect'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// 类型定义构建
|
||||
{
|
||||
input: 'bin/index.d.ts',
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/network-client v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
respectExternal: true
|
||||
})
|
||||
],
|
||||
external
|
||||
}
|
||||
];
|
||||
@@ -1,638 +0,0 @@
|
||||
/**
|
||||
* 连接状态管理器
|
||||
* 负责跟踪连接状态变化、状态变化通知和自动恢复逻辑
|
||||
*/
|
||||
import { createLogger, ITimer } from '@esengine/ecs-framework';
|
||||
import { ConnectionState, 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,680 +0,0 @@
|
||||
/**
|
||||
* 消息队列管理器
|
||||
* 提供消息排队、优先级处理和可靠传输保证
|
||||
*/
|
||||
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]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,899 +0,0 @@
|
||||
/**
|
||||
* 网络客户端核心类
|
||||
* 负责客户端连接管理、服务器通信和本地状态同步
|
||||
*/
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import {
|
||||
IConnectionOptions,
|
||||
ConnectionState,
|
||||
MessageType,
|
||||
INetworkMessage,
|
||||
IConnectMessage,
|
||||
IConnectResponseMessage,
|
||||
IHeartbeatMessage,
|
||||
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;
|
||||
};
|
||||
messageQueue?: {
|
||||
maxSize: number;
|
||||
flushOnAuthentication: boolean;
|
||||
processInterval: number;
|
||||
};
|
||||
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
|
||||
},
|
||||
messageQueue: {
|
||||
maxSize: 100,
|
||||
flushOnAuthentication: true,
|
||||
processInterval: 1000,
|
||||
...config.messageQueue
|
||||
},
|
||||
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.validateMessage(message)) {
|
||||
this.logger.warn('消息格式无效,发送失败');
|
||||
this.stats.messages.errors++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据状态决定发送策略
|
||||
switch (this.state) {
|
||||
case ClientState.Authenticated:
|
||||
// 已认证,直接发送
|
||||
return this.sendImmediate(message);
|
||||
|
||||
case ClientState.Connected:
|
||||
case ClientState.Connecting:
|
||||
// 已连接但未认证,缓存消息
|
||||
if (this.config.features.enableMessageQueue) {
|
||||
this.queueMessage(message);
|
||||
this.logger.debug('消息已缓存,等待认证完成');
|
||||
return true;
|
||||
} else {
|
||||
this.logger.warn('未启用消息队列,消息被丢弃');
|
||||
return false;
|
||||
}
|
||||
|
||||
case ClientState.Reconnecting:
|
||||
// 重连中,缓存消息
|
||||
if (this.config.features.enableMessageQueue) {
|
||||
this.queueMessage(message);
|
||||
this.logger.debug('重连中,消息已缓存');
|
||||
return true;
|
||||
} else {
|
||||
this.logger.warn('重连中且未启用消息队列,消息被丢弃');
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
this.logger.warn(`客户端状态 ${this.state},无法发送消息`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即发送消息(不进行状态检查)
|
||||
*/
|
||||
private sendImmediate<T extends INetworkMessage>(message: T): boolean {
|
||||
if (!this.transport) {
|
||||
this.logger.error('传输层未初始化');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const serializedMessage = this.serializer.serialize(message);
|
||||
// 确保发送的数据类型正确
|
||||
const dataToSend = typeof serializedMessage.data === 'string'
|
||||
? serializedMessage.data
|
||||
: serializedMessage.data.toString();
|
||||
this.transport.send(dataToSend);
|
||||
|
||||
this.stats.messages.sent++;
|
||||
this.logger.debug(`消息发送成功: ${message.type}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('发送消息失败:', error);
|
||||
this.stats.messages.errors++;
|
||||
this.handleError(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息格式
|
||||
*/
|
||||
private validateMessage<T extends INetworkMessage>(message: T): boolean {
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!message.messageId) {
|
||||
this.logger.warn('消息缺少messageId');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!message.type) {
|
||||
this.logger.warn('消息缺少type');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!message.timestamp) {
|
||||
this.logger.warn('消息缺少timestamp');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 对于游戏消息,验证senderId
|
||||
if (message.type === MessageType.GAME_EVENT) {
|
||||
if (!message.senderId) {
|
||||
this.logger.warn('游戏消息缺少senderId');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查senderId是否与当前clientId一致
|
||||
if (this.clientId && message.senderId !== this.clientId) {
|
||||
this.logger.warn(`消息发送者ID不匹配: 期望 ${this.clientId}, 实际 ${message.senderId}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端状态
|
||||
*/
|
||||
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.handleStateTransition(oldState, newState);
|
||||
|
||||
this.eventHandlers.stateChanged?.(oldState, newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态转换的副作用
|
||||
*/
|
||||
private handleStateTransition(oldState: ClientState, newState: ClientState): void {
|
||||
switch (newState) {
|
||||
case ClientState.Authenticated:
|
||||
// 认证成功后自动处理消息队列
|
||||
if (oldState !== ClientState.Authenticated) {
|
||||
this.logger.info('认证完成,处理缓存消息队列');
|
||||
this.processMessageQueue();
|
||||
}
|
||||
break;
|
||||
|
||||
case ClientState.Disconnected:
|
||||
// 断开连接时清理资源
|
||||
if (this.config.features.enableMessageQueue && this.messageQueue.length > 0) {
|
||||
this.logger.info(`连接断开,清理 ${this.messageQueue.length} 条缓存消息`);
|
||||
this.clearMessageQueue();
|
||||
}
|
||||
this.clientId = undefined;
|
||||
break;
|
||||
|
||||
case ClientState.Connecting:
|
||||
// 连接开始时重置统计
|
||||
this.stats.connectionTime = Date.now();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件处理器
|
||||
*/
|
||||
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 {
|
||||
// 检查队列大小限制
|
||||
const maxSize = this.config.messageQueue?.maxSize || 100;
|
||||
if (this.messageQueue.length >= maxSize) {
|
||||
// 移除最旧的消息
|
||||
const removed = this.messageQueue.shift();
|
||||
this.logger.warn(`消息队列已满 (${maxSize}),移除最旧消息:`, removed?.type);
|
||||
}
|
||||
|
||||
this.messageQueue.push(message);
|
||||
this.stats.messages.queued = this.messageQueue.length;
|
||||
|
||||
this.logger.debug(`消息已加入队列,当前队列长度: ${this.messageQueue.length}/${maxSize}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息队列
|
||||
*/
|
||||
private async processMessageQueue(): Promise<void> {
|
||||
if (this.isProcessingQueue || this.messageQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state !== ClientState.Authenticated) {
|
||||
this.logger.debug('未认证,跳过消息队列处理');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessingQueue = true;
|
||||
const startQueueSize = this.messageQueue.length;
|
||||
|
||||
this.logger.info(`开始处理消息队列,共 ${startQueueSize} 条消息`);
|
||||
|
||||
try {
|
||||
let processedCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
while (this.messageQueue.length > 0 && this.state === ClientState.Authenticated) {
|
||||
const message = this.messageQueue.shift()!;
|
||||
|
||||
// 使用sendImmediate避免递归调用
|
||||
if (this.sendImmediate(message)) {
|
||||
processedCount++;
|
||||
this.logger.debug(`队列消息发送成功 [${processedCount}/${startQueueSize}]`);
|
||||
} else {
|
||||
failedCount++;
|
||||
// 发送失败,重新加入队列头部
|
||||
this.messageQueue.unshift(message);
|
||||
this.logger.warn(`队列消息发送失败,剩余: ${this.messageQueue.length}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 避免阻塞,每处理一定数量消息后暂停
|
||||
if (processedCount % 10 === 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`消息队列处理完成: 成功 ${processedCount}, 失败 ${failedCount}, 剩余 ${this.messageQueue.length}`);
|
||||
|
||||
} 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否已认证并可以发送消息
|
||||
*/
|
||||
public isReady(): boolean {
|
||||
return this.state === ClientState.Authenticated && !!this.clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息队列状态
|
||||
*/
|
||||
public getQueueStatus(): { length: number; isProcessing: boolean } {
|
||||
return {
|
||||
length: this.messageQueue.length,
|
||||
isProcessing: this.isProcessingQueue
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发消息队列处理(调试用)
|
||||
*/
|
||||
public flushMessageQueue(): void {
|
||||
if (this.state === ClientState.Authenticated) {
|
||||
this.processMessageQueue();
|
||||
} else {
|
||||
this.logger.warn('客户端未认证,无法处理消息队列');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置消息队列最大大小
|
||||
*/
|
||||
public setMaxQueueSize(size: number): void {
|
||||
if (size > 0) {
|
||||
// 如果新大小比当前队列小,截取队列
|
||||
if (size < this.messageQueue.length) {
|
||||
const removed = this.messageQueue.length - size;
|
||||
this.messageQueue.splice(0, removed);
|
||||
this.logger.info(`消息队列大小调整为 ${size},移除了 ${removed} 条最旧消息`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* @esengine/network-client
|
||||
* ECS Framework网络层 - 客户端实现
|
||||
*/
|
||||
|
||||
// 核心客户端
|
||||
export * from './core/NetworkClient';
|
||||
export * from './core/MessageQueue';
|
||||
export * from './core/ConnectionStateManager';
|
||||
|
||||
// 传输层
|
||||
export * from './transport/WebSocketClient';
|
||||
export * from './transport/ReconnectionManager';
|
||||
|
||||
// 系统
|
||||
export * from './systems';
|
||||
|
||||
// 同步模块
|
||||
export * from './sync';
|
||||
|
||||
// 重新导出shared包的类型
|
||||
export * from '@esengine/network-shared';
|
||||
@@ -1,521 +0,0 @@
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { EventEmitter } from '@esengine/network-shared';
|
||||
|
||||
/**
|
||||
* 冲突类型
|
||||
*/
|
||||
export enum ConflictType {
|
||||
/** 位置冲突 */
|
||||
Position = 'position',
|
||||
/** 属性值冲突 */
|
||||
Property = 'property',
|
||||
/** 状态冲突 */
|
||||
State = 'state',
|
||||
/** 时间冲突 */
|
||||
Timing = 'timing'
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突解决策略
|
||||
*/
|
||||
export enum ConflictResolutionStrategy {
|
||||
/** 服务端权威 */
|
||||
ServerAuthority = 'server_authority',
|
||||
/** 客户端预测 */
|
||||
ClientPrediction = 'client_prediction',
|
||||
/** 插值平滑 */
|
||||
Interpolation = 'interpolation',
|
||||
/** 回滚重放 */
|
||||
RollbackReplay = 'rollback_replay',
|
||||
/** 自定义逻辑 */
|
||||
Custom = 'custom'
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突信息
|
||||
*/
|
||||
export interface ConflictInfo {
|
||||
instanceId: string;
|
||||
propertyKey: string;
|
||||
conflictType: ConflictType;
|
||||
localValue: any;
|
||||
serverValue: any;
|
||||
timestamp: number;
|
||||
severity: number; // 0-1, 1为最严重
|
||||
}
|
||||
|
||||
/**
|
||||
* 解决结果
|
||||
*/
|
||||
export interface ResolutionResult {
|
||||
success: boolean;
|
||||
finalValue: any;
|
||||
strategy: ConflictResolutionStrategy;
|
||||
confidence: number; // 0-1, 1为最有信心
|
||||
rollbackRequired: boolean;
|
||||
interpolationTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值配置
|
||||
*/
|
||||
export interface InterpolationConfig {
|
||||
duration: number;
|
||||
easing: (t: number) => number;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚配置
|
||||
*/
|
||||
export interface RollbackConfig {
|
||||
maxFrames: number;
|
||||
timeWindow: number;
|
||||
priorityThreshold: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突解决器配置
|
||||
*/
|
||||
export interface ConflictResolverConfig {
|
||||
/** 默认解决策略 */
|
||||
defaultStrategy: ConflictResolutionStrategy;
|
||||
/** 冲突检测阈值 */
|
||||
conflictThreshold: number;
|
||||
/** 插值配置 */
|
||||
interpolation: InterpolationConfig;
|
||||
/** 回滚配置 */
|
||||
rollback: RollbackConfig;
|
||||
/** 是否启用自适应策略 */
|
||||
enableAdaptiveStrategy: boolean;
|
||||
/** 性能监控采样率 */
|
||||
performanceSampleRate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略性能统计
|
||||
*/
|
||||
interface StrategyPerformance {
|
||||
strategy: ConflictResolutionStrategy;
|
||||
successRate: number;
|
||||
averageTime: number;
|
||||
conflictCount: number;
|
||||
userSatisfaction: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲突解决器
|
||||
* 负责处理客户端预测和服务端权威之间的冲突
|
||||
*/
|
||||
export class ConflictResolver extends EventEmitter {
|
||||
private logger = createLogger('ConflictResolver');
|
||||
private config: ConflictResolverConfig;
|
||||
|
||||
/** 策略映射 */
|
||||
private strategyMap = new Map<string, ConflictResolutionStrategy>();
|
||||
|
||||
/** 自定义解决器 */
|
||||
private customResolvers = new Map<string, (conflict: ConflictInfo) => ResolutionResult>();
|
||||
|
||||
/** 性能统计 */
|
||||
private performance = new Map<ConflictResolutionStrategy, StrategyPerformance>();
|
||||
|
||||
/** 冲突历史 */
|
||||
private conflictHistory: Array<{
|
||||
conflict: ConflictInfo;
|
||||
result: ResolutionResult;
|
||||
timestamp: number;
|
||||
}> = [];
|
||||
|
||||
constructor(config: Partial<ConflictResolverConfig> = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
defaultStrategy: ConflictResolutionStrategy.ServerAuthority,
|
||||
conflictThreshold: 0.1,
|
||||
interpolation: {
|
||||
duration: 100,
|
||||
easing: (t: number) => t * t * (3 - 2 * t), // smoothstep
|
||||
threshold: 0.01
|
||||
},
|
||||
rollback: {
|
||||
maxFrames: 60,
|
||||
timeWindow: 1000,
|
||||
priorityThreshold: 0.7
|
||||
},
|
||||
enableAdaptiveStrategy: true,
|
||||
performanceSampleRate: 0.1,
|
||||
...config
|
||||
};
|
||||
|
||||
this.initializePerformanceTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测冲突
|
||||
*/
|
||||
public detectConflict(
|
||||
instanceId: string,
|
||||
propertyKey: string,
|
||||
localValue: any,
|
||||
serverValue: any,
|
||||
timestamp: number = Date.now()
|
||||
): ConflictInfo | null {
|
||||
const conflictType = this.determineConflictType(propertyKey, localValue, serverValue);
|
||||
const severity = this.calculateConflictSeverity(localValue, serverValue);
|
||||
|
||||
if (severity < this.config.conflictThreshold) {
|
||||
return null; // 差异太小,不算冲突
|
||||
}
|
||||
|
||||
return {
|
||||
instanceId,
|
||||
propertyKey,
|
||||
conflictType,
|
||||
localValue,
|
||||
serverValue,
|
||||
timestamp,
|
||||
severity
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解决冲突
|
||||
*/
|
||||
public resolveConflict(conflict: ConflictInfo): ResolutionResult {
|
||||
const startTime = performance.now();
|
||||
|
||||
// 选择解决策略
|
||||
const strategy = this.selectStrategy(conflict);
|
||||
|
||||
// 执行解决
|
||||
let result: ResolutionResult;
|
||||
|
||||
switch (strategy) {
|
||||
case ConflictResolutionStrategy.ServerAuthority:
|
||||
result = this.resolveWithServerAuthority(conflict);
|
||||
break;
|
||||
|
||||
case ConflictResolutionStrategy.ClientPrediction:
|
||||
result = this.resolveWithClientPrediction(conflict);
|
||||
break;
|
||||
|
||||
case ConflictResolutionStrategy.Interpolation:
|
||||
result = this.resolveWithInterpolation(conflict);
|
||||
break;
|
||||
|
||||
case ConflictResolutionStrategy.RollbackReplay:
|
||||
result = this.resolveWithRollbackReplay(conflict);
|
||||
break;
|
||||
|
||||
case ConflictResolutionStrategy.Custom:
|
||||
result = this.resolveWithCustomLogic(conflict);
|
||||
break;
|
||||
|
||||
default:
|
||||
result = this.resolveWithServerAuthority(conflict);
|
||||
}
|
||||
|
||||
result.strategy = strategy;
|
||||
|
||||
// 记录性能
|
||||
const resolveTime = performance.now() - startTime;
|
||||
this.recordPerformance(strategy, result.success, resolveTime);
|
||||
|
||||
// 记录历史
|
||||
this.conflictHistory.push({
|
||||
conflict,
|
||||
result,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 清理旧历史
|
||||
if (this.conflictHistory.length > 1000) {
|
||||
this.conflictHistory.shift();
|
||||
}
|
||||
|
||||
// 发出事件
|
||||
this.emit('conflictResolved', conflict, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性策略
|
||||
*/
|
||||
public setPropertyStrategy(propertyKey: string, strategy: ConflictResolutionStrategy): void {
|
||||
this.strategyMap.set(propertyKey, strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义解决器
|
||||
*/
|
||||
public setCustomResolver(
|
||||
propertyKey: string,
|
||||
resolver: (conflict: ConflictInfo) => ResolutionResult
|
||||
): void {
|
||||
this.customResolvers.set(propertyKey, resolver);
|
||||
this.strategyMap.set(propertyKey, ConflictResolutionStrategy.Custom);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能统计
|
||||
*/
|
||||
public getPerformanceStats(): Map<ConflictResolutionStrategy, StrategyPerformance> {
|
||||
return new Map(this.performance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冲突历史
|
||||
*/
|
||||
public getConflictHistory(limit: number = 100): Array<{
|
||||
conflict: ConflictInfo;
|
||||
result: ResolutionResult;
|
||||
timestamp: number;
|
||||
}> {
|
||||
return this.conflictHistory.slice(-limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计
|
||||
*/
|
||||
public resetStats(): void {
|
||||
this.performance.clear();
|
||||
this.conflictHistory.length = 0;
|
||||
this.initializePerformanceTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
public updateConfig(newConfig: Partial<ConflictResolverConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁解决器
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.strategyMap.clear();
|
||||
this.customResolvers.clear();
|
||||
this.performance.clear();
|
||||
this.conflictHistory.length = 0;
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定冲突类型
|
||||
*/
|
||||
private determineConflictType(propertyKey: string, localValue: any, serverValue: any): ConflictType {
|
||||
if (propertyKey.includes('position') || propertyKey.includes('location')) {
|
||||
return ConflictType.Position;
|
||||
}
|
||||
|
||||
if (propertyKey.includes('state') || propertyKey.includes('status')) {
|
||||
return ConflictType.State;
|
||||
}
|
||||
|
||||
if (propertyKey.includes('time') || propertyKey.includes('timestamp')) {
|
||||
return ConflictType.Timing;
|
||||
}
|
||||
|
||||
return ConflictType.Property;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算冲突严重性
|
||||
*/
|
||||
private calculateConflictSeverity(localValue: any, serverValue: any): number {
|
||||
if (typeof localValue === 'number' && typeof serverValue === 'number') {
|
||||
const diff = Math.abs(localValue - serverValue);
|
||||
const max = Math.max(Math.abs(localValue), Math.abs(serverValue), 1);
|
||||
return Math.min(diff / max, 1);
|
||||
}
|
||||
|
||||
if (typeof localValue === 'object' && typeof serverValue === 'object') {
|
||||
// 简化的对象比较
|
||||
return localValue === serverValue ? 0 : 1;
|
||||
}
|
||||
|
||||
return localValue === serverValue ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择解决策略
|
||||
*/
|
||||
private selectStrategy(conflict: ConflictInfo): ConflictResolutionStrategy {
|
||||
// 检查是否有属性特定策略
|
||||
const propertyStrategy = this.strategyMap.get(conflict.propertyKey);
|
||||
if (propertyStrategy) {
|
||||
return propertyStrategy;
|
||||
}
|
||||
|
||||
// 自适应策略选择
|
||||
if (this.config.enableAdaptiveStrategy) {
|
||||
return this.selectAdaptiveStrategy(conflict);
|
||||
}
|
||||
|
||||
return this.config.defaultStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应策略选择
|
||||
*/
|
||||
private selectAdaptiveStrategy(conflict: ConflictInfo): ConflictResolutionStrategy {
|
||||
const strategies = Array.from(this.performance.keys());
|
||||
|
||||
if (strategies.length === 0) {
|
||||
return this.config.defaultStrategy;
|
||||
}
|
||||
|
||||
// 根据冲突类型和性能选择最佳策略
|
||||
let bestStrategy = this.config.defaultStrategy;
|
||||
let bestScore = 0;
|
||||
|
||||
for (const strategy of strategies) {
|
||||
const perf = this.performance.get(strategy)!;
|
||||
|
||||
// 计算策略得分:成功率 * 用户满意度 / 平均时间
|
||||
const score = (perf.successRate * perf.userSatisfaction) / Math.max(perf.averageTime, 1);
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestStrategy = strategy;
|
||||
}
|
||||
}
|
||||
|
||||
return bestStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端权威解决
|
||||
*/
|
||||
private resolveWithServerAuthority(conflict: ConflictInfo): ResolutionResult {
|
||||
return {
|
||||
success: true,
|
||||
finalValue: conflict.serverValue,
|
||||
strategy: ConflictResolutionStrategy.ServerAuthority,
|
||||
confidence: 1.0,
|
||||
rollbackRequired: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端预测解决
|
||||
*/
|
||||
private resolveWithClientPrediction(conflict: ConflictInfo): ResolutionResult {
|
||||
// 如果冲突严重性较低,保持客户端值
|
||||
const keepClient = conflict.severity < 0.5;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
finalValue: keepClient ? conflict.localValue : conflict.serverValue,
|
||||
strategy: ConflictResolutionStrategy.ClientPrediction,
|
||||
confidence: keepClient ? 0.7 : 0.9,
|
||||
rollbackRequired: !keepClient
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值解决
|
||||
*/
|
||||
private resolveWithInterpolation(conflict: ConflictInfo): ResolutionResult {
|
||||
if (typeof conflict.localValue !== 'number' || typeof conflict.serverValue !== 'number') {
|
||||
// 非数值类型,回退到服务端权威
|
||||
return this.resolveWithServerAuthority(conflict);
|
||||
}
|
||||
|
||||
// 检查差异是否足够大需要插值
|
||||
const diff = Math.abs(conflict.localValue - conflict.serverValue);
|
||||
if (diff < this.config.interpolation.threshold) {
|
||||
return {
|
||||
success: true,
|
||||
finalValue: conflict.serverValue,
|
||||
strategy: ConflictResolutionStrategy.Interpolation,
|
||||
confidence: 1.0,
|
||||
rollbackRequired: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
finalValue: conflict.serverValue,
|
||||
strategy: ConflictResolutionStrategy.Interpolation,
|
||||
confidence: 0.8,
|
||||
rollbackRequired: false,
|
||||
interpolationTime: this.config.interpolation.duration
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚重放解决
|
||||
*/
|
||||
private resolveWithRollbackReplay(conflict: ConflictInfo): ResolutionResult {
|
||||
// 检查是否满足回滚条件
|
||||
const shouldRollback = conflict.severity >= this.config.rollback.priorityThreshold;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
finalValue: conflict.serverValue,
|
||||
strategy: ConflictResolutionStrategy.RollbackReplay,
|
||||
confidence: shouldRollback ? 0.9 : 0.6,
|
||||
rollbackRequired: shouldRollback
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义逻辑解决
|
||||
*/
|
||||
private resolveWithCustomLogic(conflict: ConflictInfo): ResolutionResult {
|
||||
const customResolver = this.customResolvers.get(conflict.propertyKey);
|
||||
|
||||
if (!customResolver) {
|
||||
this.logger.warn(`未找到属性 ${conflict.propertyKey} 的自定义解决器,使用服务端权威`);
|
||||
return this.resolveWithServerAuthority(conflict);
|
||||
}
|
||||
|
||||
try {
|
||||
return customResolver(conflict);
|
||||
} catch (error) {
|
||||
this.logger.error('自定义解决器执行失败:', error);
|
||||
return this.resolveWithServerAuthority(conflict);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化性能跟踪
|
||||
*/
|
||||
private initializePerformanceTracking(): void {
|
||||
const strategies = Object.values(ConflictResolutionStrategy);
|
||||
|
||||
for (const strategy of strategies) {
|
||||
this.performance.set(strategy, {
|
||||
strategy,
|
||||
successRate: 1.0,
|
||||
averageTime: 1.0,
|
||||
conflictCount: 0,
|
||||
userSatisfaction: 0.8
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能
|
||||
*/
|
||||
private recordPerformance(strategy: ConflictResolutionStrategy, success: boolean, time: number): void {
|
||||
const perf = this.performance.get(strategy);
|
||||
if (!perf) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新统计(使用滑动平均)
|
||||
const alpha = 0.1; // 学习率
|
||||
|
||||
perf.conflictCount++;
|
||||
perf.successRate = perf.successRate * (1 - alpha) + (success ? 1 : 0) * alpha;
|
||||
perf.averageTime = perf.averageTime * (1 - alpha) + time * alpha;
|
||||
|
||||
// 用户满意度基于成功率和时间
|
||||
perf.userSatisfaction = Math.max(0, perf.successRate - (time / 100) * 0.1);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 客户端同步模块导出
|
||||
*/
|
||||
|
||||
export * from './ConflictResolver';
|
||||
@@ -1,601 +0,0 @@
|
||||
import { EntitySystem, createLogger } from '@esengine/ecs-framework';
|
||||
import {
|
||||
SyncVarManager,
|
||||
SyncBatch,
|
||||
SyncVarSerializer,
|
||||
MessageType,
|
||||
INetworkMessage
|
||||
} from '@esengine/network-shared';
|
||||
import { NetworkClient } from '../core/NetworkClient';
|
||||
|
||||
/**
|
||||
* 客户端同步系统配置
|
||||
*/
|
||||
export interface ClientSyncSystemConfig {
|
||||
/** 是否启用本地预测 */
|
||||
enablePrediction: boolean;
|
||||
/** 是否启用插值 */
|
||||
enableInterpolation: boolean;
|
||||
/** 插值时间窗口(毫秒) */
|
||||
interpolationWindow: number;
|
||||
/** 外推时间限制(毫秒) */
|
||||
extrapolationLimit: number;
|
||||
/** 是否启用回滚 */
|
||||
enableRollback: boolean;
|
||||
/** 最大回滚历史帧数 */
|
||||
maxRollbackFrames: number;
|
||||
/** 服务端权威检查阈值 */
|
||||
authorityThreshold: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程实体状态
|
||||
*/
|
||||
interface RemoteEntityState {
|
||||
instanceId: string;
|
||||
instanceType: string;
|
||||
lastUpdateTime: number;
|
||||
serverTime: number;
|
||||
properties: { [key: string]: any };
|
||||
history: Array<{
|
||||
timestamp: number;
|
||||
properties: { [key: string]: any };
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地预测状态
|
||||
*/
|
||||
interface PredictionState {
|
||||
instanceId: string;
|
||||
predictedValues: { [key: string]: any };
|
||||
lastConfirmedValues: { [key: string]: any };
|
||||
confirmationTime: number;
|
||||
pendingInputs: Array<{
|
||||
timestamp: number;
|
||||
input: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插值状态
|
||||
*/
|
||||
interface InterpolationState {
|
||||
instanceId: string;
|
||||
fromValues: { [key: string]: any };
|
||||
toValues: { [key: string]: any };
|
||||
fromTime: number;
|
||||
toTime: number;
|
||||
currentValues: { [key: string]: any };
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步统计
|
||||
*/
|
||||
interface ClientSyncStats {
|
||||
remoteEntities: number;
|
||||
predictedEntities: number;
|
||||
interpolatingEntities: number;
|
||||
totalUpdatesReceived: number;
|
||||
predictionCorrections: number;
|
||||
averageLatency: number;
|
||||
networkJitter: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端同步系统
|
||||
* 负责接收和应用服务端同步数据,处理本地预测和插值
|
||||
*/
|
||||
export class ClientSyncSystem extends EntitySystem {
|
||||
private config: ClientSyncSystemConfig;
|
||||
private syncVarManager: SyncVarManager;
|
||||
private serializer: SyncVarSerializer;
|
||||
private networkClient?: NetworkClient;
|
||||
|
||||
/** 远程实体状态 */
|
||||
private remoteEntities = new Map<string, RemoteEntityState>();
|
||||
|
||||
/** 本地预测状态 */
|
||||
private predictions = new Map<string, PredictionState>();
|
||||
|
||||
/** 插值状态 */
|
||||
private interpolations = new Map<string, InterpolationState>();
|
||||
|
||||
/** 本地实体映射 */
|
||||
private localEntityMap = new Map<string, any>();
|
||||
|
||||
/** 时间同步 */
|
||||
private serverTimeOffset = 0;
|
||||
private lastServerTime = 0;
|
||||
|
||||
/** 统计信息 */
|
||||
private stats: ClientSyncStats = {
|
||||
remoteEntities: 0,
|
||||
predictedEntities: 0,
|
||||
interpolatingEntities: 0,
|
||||
totalUpdatesReceived: 0,
|
||||
predictionCorrections: 0,
|
||||
averageLatency: 0,
|
||||
networkJitter: 0
|
||||
};
|
||||
|
||||
constructor(config: Partial<ClientSyncSystemConfig> = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
enablePrediction: true,
|
||||
enableInterpolation: true,
|
||||
interpolationWindow: 100,
|
||||
extrapolationLimit: 50,
|
||||
enableRollback: true,
|
||||
maxRollbackFrames: 60,
|
||||
authorityThreshold: 0.1,
|
||||
...config
|
||||
};
|
||||
|
||||
this.syncVarManager = SyncVarManager.getInstance();
|
||||
this.serializer = new SyncVarSerializer({
|
||||
enableCompression: true,
|
||||
enableDeltaSync: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统
|
||||
*/
|
||||
public override initialize(): void {
|
||||
super.initialize();
|
||||
this.logger.info('客户端同步系统初始化');
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统更新
|
||||
*/
|
||||
protected override process(): void {
|
||||
const currentTime = this.getCurrentTime();
|
||||
|
||||
this.updateInterpolations(currentTime);
|
||||
this.updatePredictions(currentTime);
|
||||
this.cleanupOldData(currentTime);
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网络客户端
|
||||
*/
|
||||
public setNetworkClient(client: NetworkClient): void {
|
||||
this.networkClient = client;
|
||||
|
||||
// 监听同步消息
|
||||
client.on('messageReceived', (message: INetworkMessage) => {
|
||||
if (message.type === MessageType.SYNC_BATCH) {
|
||||
this.handleSyncBatch(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册本地实体
|
||||
*/
|
||||
public registerLocalEntity(instanceId: string, entity: any): void {
|
||||
this.localEntityMap.set(instanceId, entity);
|
||||
this.syncVarManager.registerInstance(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销本地实体
|
||||
*/
|
||||
public unregisterLocalEntity(instanceId: string): void {
|
||||
const entity = this.localEntityMap.get(instanceId);
|
||||
if (entity) {
|
||||
this.localEntityMap.delete(instanceId);
|
||||
this.syncVarManager.unregisterInstance(entity);
|
||||
}
|
||||
|
||||
this.remoteEntities.delete(instanceId);
|
||||
this.predictions.delete(instanceId);
|
||||
this.interpolations.delete(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远程实体
|
||||
*/
|
||||
public createRemoteEntity(instanceId: string, instanceType: string, properties: any): any {
|
||||
// 这里需要根据instanceType创建对应的实体
|
||||
// 简化实现,返回一个通用对象
|
||||
const entity = {
|
||||
instanceId,
|
||||
instanceType,
|
||||
...properties
|
||||
};
|
||||
|
||||
this.localEntityMap.set(instanceId, entity);
|
||||
|
||||
// 注册到SyncVar管理器
|
||||
this.syncVarManager.registerInstance(entity);
|
||||
|
||||
this.logger.debug(`创建远程实体: ${instanceId} (${instanceType})`);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁远程实体
|
||||
*/
|
||||
public destroyRemoteEntity(instanceId: string): void {
|
||||
const entity = this.localEntityMap.get(instanceId);
|
||||
if (entity) {
|
||||
this.unregisterLocalEntity(instanceId);
|
||||
this.logger.debug(`销毁远程实体: ${instanceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始本地预测
|
||||
*/
|
||||
public startPrediction(instanceId: string, input: any): void {
|
||||
if (!this.config.enablePrediction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = this.localEntityMap.get(instanceId);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prediction = this.predictions.get(instanceId);
|
||||
if (!prediction) {
|
||||
prediction = {
|
||||
instanceId,
|
||||
predictedValues: {},
|
||||
lastConfirmedValues: {},
|
||||
confirmationTime: this.getCurrentTime(),
|
||||
pendingInputs: []
|
||||
};
|
||||
this.predictions.set(instanceId, prediction);
|
||||
}
|
||||
|
||||
// 记录输入
|
||||
prediction.pendingInputs.push({
|
||||
timestamp: this.getCurrentTime(),
|
||||
input
|
||||
});
|
||||
|
||||
// 应用本地预测
|
||||
this.applyPrediction(entity, prediction, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务端时间偏移
|
||||
*/
|
||||
public setServerTimeOffset(offset: number): void {
|
||||
this.serverTimeOffset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
public getStats(): ClientSyncStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计信息
|
||||
*/
|
||||
public resetStats(): void {
|
||||
this.stats = {
|
||||
remoteEntities: this.remoteEntities.size,
|
||||
predictedEntities: this.predictions.size,
|
||||
interpolatingEntities: this.interpolations.size,
|
||||
totalUpdatesReceived: 0,
|
||||
predictionCorrections: 0,
|
||||
averageLatency: 0,
|
||||
networkJitter: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
public updateConfig(newConfig: Partial<ClientSyncSystemConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁系统
|
||||
*/
|
||||
public override destroy(): void {
|
||||
this.remoteEntities.clear();
|
||||
this.predictions.clear();
|
||||
this.interpolations.clear();
|
||||
this.localEntityMap.clear();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
protected override getLoggerName(): string {
|
||||
return 'ClientSyncSystem';
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理同步批次
|
||||
*/
|
||||
private handleSyncBatch(message: INetworkMessage): void {
|
||||
try {
|
||||
const result = this.serializer.parseSyncMessage(message);
|
||||
if (!result.success || !result.data) {
|
||||
this.logger.warn('解析同步消息失败:', result.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const batch = result.data;
|
||||
this.applySyncBatch(batch);
|
||||
|
||||
this.stats.totalUpdatesReceived++;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('处理同步批次失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用同步批次
|
||||
*/
|
||||
private applySyncBatch(batch: SyncBatch): void {
|
||||
const entity = this.localEntityMap.get(batch.instanceId);
|
||||
if (!entity) {
|
||||
// 尝试创建远程实体
|
||||
this.createRemoteEntity(batch.instanceId, batch.instanceType, batch.changes);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = this.getCurrentTime();
|
||||
|
||||
// 更新远程实体状态
|
||||
let remoteState = this.remoteEntities.get(batch.instanceId);
|
||||
if (!remoteState) {
|
||||
remoteState = {
|
||||
instanceId: batch.instanceId,
|
||||
instanceType: batch.instanceType,
|
||||
lastUpdateTime: currentTime,
|
||||
serverTime: batch.timestamp,
|
||||
properties: {},
|
||||
history: []
|
||||
};
|
||||
this.remoteEntities.set(batch.instanceId, remoteState);
|
||||
}
|
||||
|
||||
// 更新历史记录
|
||||
remoteState.history.push({
|
||||
timestamp: batch.timestamp,
|
||||
properties: { ...batch.changes }
|
||||
});
|
||||
|
||||
// 保持历史记录大小
|
||||
if (remoteState.history.length > this.config.maxRollbackFrames) {
|
||||
remoteState.history.shift();
|
||||
}
|
||||
|
||||
remoteState.lastUpdateTime = currentTime;
|
||||
remoteState.serverTime = batch.timestamp;
|
||||
Object.assign(remoteState.properties, batch.changes);
|
||||
|
||||
// 处理本地预测确认
|
||||
if (this.config.enablePrediction) {
|
||||
this.confirmPrediction(batch.instanceId, batch.changes, batch.timestamp);
|
||||
}
|
||||
|
||||
// 开始插值
|
||||
if (this.config.enableInterpolation) {
|
||||
this.startInterpolation(batch.instanceId, batch.changes);
|
||||
} else {
|
||||
// 直接应用值
|
||||
this.applyValuesToEntity(entity, batch.changes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认本地预测
|
||||
*/
|
||||
private confirmPrediction(instanceId: string, serverValues: any, serverTime: number): void {
|
||||
const prediction = this.predictions.get(instanceId);
|
||||
if (!prediction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查预测准确性
|
||||
let hasCorrection = false;
|
||||
for (const [key, serverValue] of Object.entries(serverValues)) {
|
||||
const predictedValue = prediction.predictedValues[key];
|
||||
if (predictedValue !== undefined &&
|
||||
typeof predictedValue === 'number' &&
|
||||
typeof serverValue === 'number' &&
|
||||
Math.abs(predictedValue - serverValue) > this.config.authorityThreshold) {
|
||||
hasCorrection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCorrection) {
|
||||
this.stats.predictionCorrections++;
|
||||
|
||||
// 执行回滚和重放
|
||||
if (this.config.enableRollback) {
|
||||
this.performRollbackAndReplay(instanceId, serverValues, serverTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新确认值
|
||||
Object.assign(prediction.lastConfirmedValues, serverValues);
|
||||
prediction.confirmationTime = serverTime;
|
||||
|
||||
// 清理已确认的输入
|
||||
prediction.pendingInputs = prediction.pendingInputs.filter(
|
||||
(input) => input.timestamp > serverTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行回滚和重放
|
||||
*/
|
||||
private performRollbackAndReplay(instanceId: string, serverValues: any, serverTime: number): void {
|
||||
const entity = this.localEntityMap.get(instanceId);
|
||||
const prediction = this.predictions.get(instanceId);
|
||||
|
||||
if (!entity || !prediction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 回滚到服务端状态
|
||||
this.applyValuesToEntity(entity, serverValues);
|
||||
|
||||
// 重放未确认的输入
|
||||
for (const input of prediction.pendingInputs) {
|
||||
if (input.timestamp > serverTime) {
|
||||
this.applyPrediction(entity, prediction, input.input);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`执行回滚和重放: ${instanceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始插值
|
||||
*/
|
||||
private startInterpolation(instanceId: string, targetValues: any): void {
|
||||
const entity = this.localEntityMap.get(instanceId);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前值
|
||||
const currentValues: { [key: string]: any } = {};
|
||||
for (const key of Object.keys(targetValues)) {
|
||||
currentValues[key] = entity[key];
|
||||
}
|
||||
|
||||
const interpolation: InterpolationState = {
|
||||
instanceId,
|
||||
fromValues: currentValues,
|
||||
toValues: targetValues,
|
||||
fromTime: this.getCurrentTime(),
|
||||
toTime: this.getCurrentTime() + this.config.interpolationWindow,
|
||||
currentValues: { ...currentValues }
|
||||
};
|
||||
|
||||
this.interpolations.set(instanceId, interpolation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新插值
|
||||
*/
|
||||
private updateInterpolations(currentTime: number): void {
|
||||
for (const [instanceId, interpolation] of this.interpolations) {
|
||||
const entity = this.localEntityMap.get(instanceId);
|
||||
if (!entity) {
|
||||
this.interpolations.delete(instanceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const progress = Math.min(
|
||||
(currentTime - interpolation.fromTime) /
|
||||
(interpolation.toTime - interpolation.fromTime),
|
||||
1
|
||||
);
|
||||
|
||||
// 计算插值
|
||||
for (const [key, fromValue] of Object.entries(interpolation.fromValues)) {
|
||||
const toValue = interpolation.toValues[key];
|
||||
|
||||
if (typeof fromValue === 'number' && typeof toValue === 'number') {
|
||||
interpolation.currentValues[key] = fromValue + (toValue - fromValue) * progress;
|
||||
} else {
|
||||
interpolation.currentValues[key] = progress < 0.5 ? fromValue : toValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用插值结果
|
||||
this.applyValuesToEntity(entity, interpolation.currentValues);
|
||||
|
||||
// 检查插值是否完成
|
||||
if (progress >= 1) {
|
||||
this.interpolations.delete(instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新本地预测
|
||||
*/
|
||||
private updatePredictions(currentTime: number): void {
|
||||
for (const [instanceId, prediction] of this.predictions) {
|
||||
// 清理过期的输入
|
||||
prediction.pendingInputs = prediction.pendingInputs.filter(
|
||||
(input) => currentTime - input.timestamp < 1000
|
||||
);
|
||||
|
||||
// 如果没有待处理的输入,移除预测状态
|
||||
if (prediction.pendingInputs.length === 0 &&
|
||||
currentTime - prediction.confirmationTime > 1000) {
|
||||
this.predictions.delete(instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用本地预测
|
||||
*/
|
||||
private applyPrediction(entity: any, prediction: PredictionState, input: any): void {
|
||||
// 这里应该根据具体的游戏逻辑实现预测
|
||||
// 简化实现,直接应用输入
|
||||
if (input.movement) {
|
||||
prediction.predictedValues.x = (entity.x || 0) + input.movement.x;
|
||||
prediction.predictedValues.y = (entity.y || 0) + input.movement.y;
|
||||
|
||||
this.applyValuesToEntity(entity, prediction.predictedValues);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用值到实体
|
||||
*/
|
||||
private applyValuesToEntity(entity: any, values: any): void {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
entity[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期数据
|
||||
*/
|
||||
private cleanupOldData(currentTime: number): void {
|
||||
const maxAge = 5000; // 5秒
|
||||
|
||||
// 清理远程实体历史
|
||||
for (const [instanceId, remoteState] of this.remoteEntities) {
|
||||
if (currentTime - remoteState.lastUpdateTime > maxAge) {
|
||||
this.remoteEntities.delete(instanceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 清理历史记录
|
||||
remoteState.history = remoteState.history.filter(
|
||||
(record) => currentTime - record.timestamp < maxAge
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
private updateStats(): void {
|
||||
this.stats.remoteEntities = this.remoteEntities.size;
|
||||
this.stats.predictedEntities = this.predictions.size;
|
||||
this.stats.interpolatingEntities = this.interpolations.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间(包含服务端偏移)
|
||||
*/
|
||||
private getCurrentTime(): number {
|
||||
return Date.now() + this.serverTimeOffset;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 客户端系统导出
|
||||
*/
|
||||
|
||||
export * from './ClientSyncSystem';
|
||||
@@ -1,410 +0,0 @@
|
||||
/**
|
||||
* 重连管理器
|
||||
* 负责管理客户端的自动重连逻辑
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/**
|
||||
* 网络层专用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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 网络客户端工具类
|
||||
*/
|
||||
export * from './NetworkTimer';
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": ["ES2020", "DOM", "WebWorker"],
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"importHelpers": false,
|
||||
"downlevelIteration": true,
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../network-shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user