From 6e21ff08d596097d79b29240507850693009e876 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Sat, 9 Aug 2025 18:56:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90tsrpc=E4=BB=A3=E6=9B=BFprotob?= =?UTF-8?q?uf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 130 +++- packages/network/package.json | 9 +- .../src/Config/NetworkConfigManager.ts | 478 ++++++++++++ packages/network/src/Core/HeartbeatManager.ts | 477 ++++++++++++ packages/network/src/Core/NetworkClient.ts | 3 +- .../network/src/Core/NetworkConnection.ts | 177 ++++- .../src/Core/NetworkPerformanceMonitor.ts | 340 ++++++++- packages/network/src/Core/NetworkServer.ts | 3 +- .../network/src/Error/NetworkErrorHandler.ts | 409 ++++++++++ .../network/src/Messaging/MessageHandler.ts | 20 +- .../network/src/Messaging/MessageTypes.ts | 2 + .../network/src/Messaging/NetworkMessage.ts | 363 ++++++++- packages/network/src/NetworkComponent.ts | 81 +- .../src/Serialization/ProtobufDecorators.ts | 470 ------------ .../src/Serialization/ProtobufSerializer.ts | 479 ------------ .../src/Serialization/SerializationTypes.ts | 15 +- .../src/Serialization/TsrpcDecorators.ts | 181 +++++ .../src/Serialization/TsrpcSerializer.ts | 179 +++++ .../network/src/Serialization/TsrpcTypes.ts | 218 ++++++ packages/network/src/Serialization/index.ts | 68 +- .../network/src/Snapshot/SnapshotManager.ts | 58 +- .../src/SyncVar/ComponentIdGenerator.ts | 219 ++++++ .../network/src/SyncVar/SyncVarAuthority.ts | 318 ++++++++ .../network/src/SyncVar/SyncVarManager.ts | 109 +-- .../src/SyncVar/SyncVarSyncScheduler.ts | 207 ++++++ packages/network/src/SyncVar/index.ts | 3 +- packages/network/src/TSRPC/TsrpcClient.ts | 383 ++++++++++ packages/network/src/TSRPC/TsrpcManager.ts | 129 ++++ packages/network/src/TSRPC/TsrpcServer.ts | 698 ++++++++++++++++++ packages/network/src/TSRPC/index.ts | 14 + .../src/TSRPC/protocols/MsgComponentUpdate.ts | 9 + .../src/TSRPC/protocols/PtlJoinRoom.ts | 20 + .../src/TSRPC/protocols/PtlSyncComponent.ts | 18 + .../src/TSRPC/protocols/serviceProto.ts | 78 ++ .../network/src/constants/NetworkConstants.ts | 211 ++++++ packages/network/src/index.ts | 5 +- packages/network/src/types/CoreTypes.ts | 151 ++++ packages/network/src/types/MessageTypes.ts | 207 ++++++ packages/network/src/types/NetworkTypes.ts | 14 +- packages/network/src/types/index.ts | 33 + .../tests/Serialization/Performance.test.ts | 445 ----------- .../Serialization/ProtobufDecorators.test.ts | 295 -------- .../Serialization/ProtobufSerializer.test.ts | 295 -------- .../ProtobufSerializerEdgeCases.test.ts | 438 ----------- .../Serialization/RealPerformance.test.ts | 393 ---------- .../SnapshotManagerIntegration.test.ts | 370 ---------- .../Serialization/TsrpcSerializer.test.ts | 113 +++ .../network/tests/Serialization/index.test.ts | 5 +- 48 files changed, 5942 insertions(+), 3398 deletions(-) create mode 100644 packages/network/src/Config/NetworkConfigManager.ts create mode 100644 packages/network/src/Core/HeartbeatManager.ts create mode 100644 packages/network/src/Error/NetworkErrorHandler.ts delete mode 100644 packages/network/src/Serialization/ProtobufDecorators.ts delete mode 100644 packages/network/src/Serialization/ProtobufSerializer.ts create mode 100644 packages/network/src/Serialization/TsrpcDecorators.ts create mode 100644 packages/network/src/Serialization/TsrpcSerializer.ts create mode 100644 packages/network/src/Serialization/TsrpcTypes.ts create mode 100644 packages/network/src/SyncVar/ComponentIdGenerator.ts create mode 100644 packages/network/src/SyncVar/SyncVarAuthority.ts create mode 100644 packages/network/src/TSRPC/TsrpcClient.ts create mode 100644 packages/network/src/TSRPC/TsrpcManager.ts create mode 100644 packages/network/src/TSRPC/TsrpcServer.ts create mode 100644 packages/network/src/TSRPC/index.ts create mode 100644 packages/network/src/TSRPC/protocols/MsgComponentUpdate.ts create mode 100644 packages/network/src/TSRPC/protocols/PtlJoinRoom.ts create mode 100644 packages/network/src/TSRPC/protocols/PtlSyncComponent.ts create mode 100644 packages/network/src/TSRPC/protocols/serviceProto.ts create mode 100644 packages/network/src/constants/NetworkConstants.ts create mode 100644 packages/network/src/types/CoreTypes.ts create mode 100644 packages/network/src/types/MessageTypes.ts create mode 100644 packages/network/src/types/index.ts delete mode 100644 packages/network/tests/Serialization/Performance.test.ts delete mode 100644 packages/network/tests/Serialization/ProtobufDecorators.test.ts delete mode 100644 packages/network/tests/Serialization/ProtobufSerializer.test.ts delete mode 100644 packages/network/tests/Serialization/ProtobufSerializerEdgeCases.test.ts delete mode 100644 packages/network/tests/Serialization/RealPerformance.test.ts delete mode 100644 packages/network/tests/Serialization/SnapshotManagerIntegration.test.ts create mode 100644 packages/network/tests/Serialization/TsrpcSerializer.test.ts diff --git a/package-lock.json b/package-lock.json index a3287ab0..44104bc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3132,7 +3132,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3661,7 +3660,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3835,7 +3833,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3848,7 +3845,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/color-support": { @@ -5490,7 +5486,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -7123,6 +7118,31 @@ "dev": true, "license": "MIT" }, + "node_modules/k8w-extend-native": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/k8w-extend-native/-/k8w-extend-native-1.4.6.tgz", + "integrity": "sha512-AHTCyFshldMme0s9FKD+QKG+QZdBkHXzl+8kYfNhsSDhcdQ5TYWQwphjecSJjxNdGd78TIbO0fHiOvM+Ei22YA==", + "dependencies": { + "k8w-linq-array": "*", + "k8w-super-date": "*", + "k8w-super-object": "*" + } + }, + "node_modules/k8w-linq-array": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/k8w-linq-array/-/k8w-linq-array-0.2.8.tgz", + "integrity": "sha512-4IAkQN8UJdk804tQi++wuwSZvFWk/Wcl1uG5PR/0c0YvB5hUd2f8tJm3OgOMOxjV9UVByNLvnPYGIwrFQPpjlA==" + }, + "node_modules/k8w-super-date": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/k8w-super-date/-/k8w-super-date-0.1.3.tgz", + "integrity": "sha512-IBqKOAMAXR/bgzu+rYI30tEMP/Y6Q8HQuqJiTkE2mLJg11yok9guoi8uZTynTahviVBndcfBpOgi1H/zhihv7w==" + }, + "node_modules/k8w-super-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/k8w-super-object/-/k8w-super-object-0.3.0.tgz", + "integrity": "sha512-u2jfh4goYXKZmSucaLaOTaNbLRatjv0CSRpzE0KU0732+9XtYZFd5vrdw/mzJfK5fPHb/zyikOSHDX5mJrav+g==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10339,7 +10359,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10745,6 +10764,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tsbuffer": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/tsbuffer/-/tsbuffer-2.2.10.tgz", + "integrity": "sha512-3+lICDlKm2lLxmFPzvh4hu+aHA//a0D7OWyOP2BX5JMvlOBCaFbsVfyvyb14XIG3iL5voYQ2Qrc2qAc+ec5tbA==", + "dependencies": { + "k8w-extend-native": "^1.4.6", + "tsbuffer-validator": "^2.1.2", + "tslib": "*" + } + }, + "node_modules/tsbuffer-schema": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tsbuffer-schema/-/tsbuffer-schema-2.2.0.tgz", + "integrity": "sha512-I4+5Xfk7G+D++kXdNnYTeY26WQTaf14C84XQwPKteNmrwxRY3CQCkMqASRiCUqtpOuDn43qmoxuXpT+Vo8Wltg==" + }, + "node_modules/tsbuffer-validator": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tsbuffer-validator/-/tsbuffer-validator-2.1.2.tgz", + "integrity": "sha512-PrqIYy7aANY7ssr92HJN8ZM+eGc4Qmpvu7nNBv+T2DOAb+eqblKjlDZEhNnzxjs/ddqu9PqPe4Aa+fqYdzo98g==", + "dependencies": { + "k8w-extend-native": "^1.4.6", + "tsbuffer-schema": "^2.2.0", + "tslib": "*" + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -10774,9 +10818,78 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, + "node_modules/tsrpc": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tsrpc/-/tsrpc-3.4.19.tgz", + "integrity": "sha512-VkTOzaCEQsXCZf6z+VSYwG2NRZcmBVH7AtkWxafwhy5E4cYieH7ApUFCKssx8tdiHbFUQ5JGSPoZVvYznbpgdw==", + "dependencies": { + "@types/ws": "^7.4.7", + "chalk": "^4.1.2", + "tsbuffer": "^2.2.10", + "tsrpc-base-client": "^2.1.15", + "tsrpc-proto": "^1.4.3", + "uuid": "^8.3.2", + "ws": "^7.5.9" + } + }, + "node_modules/tsrpc-base-client": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/tsrpc-base-client/-/tsrpc-base-client-2.1.15.tgz", + "integrity": "sha512-ejIsGKF1MtcS2Mqpv1JYjoOmFbkOMaubb0FYglA52Sfl0glnq2UAqbCu5embQISzuIF9DiDeg1Rui9EyOc2hdA==", + "dependencies": { + "k8w-extend-native": "^1.4.6", + "tsbuffer": "^2.2.9", + "tslib": "*", + "tsrpc-proto": "^1.4.3" + } + }, + "node_modules/tsrpc-proto": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/tsrpc-proto/-/tsrpc-proto-1.4.3.tgz", + "integrity": "sha512-qtkk5i34m9/K1258EdyXAEikU/ADPELHCCXN/oFJ4XwH+kN3kXnKYmwCDblUuMA73V2+A/EwkgUGyAgPa335Hw==", + "dependencies": { + "tsbuffer-schema": "^2.2.0", + "tslib": "*" + } + }, + "node_modules/tsrpc/node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/tsrpc/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/tsrpc/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/tuf-js": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", @@ -11421,8 +11534,9 @@ "license": "MIT", "dependencies": { "isomorphic-ws": "^5.0.0", - "protobufjs": "^7.5.3", "reflect-metadata": "^0.2.2", + "tsbuffer": "^2.2.10", + "tsrpc": "^3.4.19", "uuid": "^10.0.0", "ws": "^8.18.0" }, diff --git a/packages/network/package.json b/packages/network/package.json index 79641457..b858cf2f 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -1,7 +1,7 @@ { "name": "@esengine/ecs-framework-network", "version": "1.0.0", - "description": "ECS框架网络插件 - 提供protobuf序列化、帧同步和快照功能", + "description": "ECS框架网络插件 - 提供TSRPC网络通信、帧同步和快照功能", "type": "module", "main": "bin/index.js", "types": "bin/index.d.ts", @@ -14,7 +14,7 @@ "ecs", "networking", "frame-sync", - "protobuf", + "tsrpc", "serialization", "multiplayer", "game-engine", @@ -37,11 +37,12 @@ "author": "yhh", "license": "MIT", "dependencies": { - "protobufjs": "^7.5.3", "reflect-metadata": "^0.2.2", "ws": "^8.18.0", "isomorphic-ws": "^5.0.0", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "tsrpc": "^3.4.19", + "tsbuffer": "^2.2.10" }, "peerDependencies": { "@esengine/ecs-framework": ">=2.1.29" diff --git a/packages/network/src/Config/NetworkConfigManager.ts b/packages/network/src/Config/NetworkConfigManager.ts new file mode 100644 index 00000000..bea4eb28 --- /dev/null +++ b/packages/network/src/Config/NetworkConfigManager.ts @@ -0,0 +1,478 @@ +/** + * 网络配置管理器 + */ + +import { + NETWORK_CONFIG, + SYNCVAR_CONFIG, + MESSAGE_CONFIG, + SERIALIZATION_CONFIG, + TSRPC_CONFIG, + AUTHORITY_CONFIG, + PERFORMANCE_CONFIG +} from '../constants/NetworkConstants'; + +/** + * 网络配置接口 + */ +export interface INetworkConfig { + [key: string]: unknown; + /** 连接配置 */ + connection: { + timeout: number; + maxReconnectAttempts: number; + reconnectDelay: number; + }; + + /** 心跳配置 */ + heartbeat: { + interval: number; + timeout: number; + maxConsecutiveLoss: number; + packetSize: number; + enableAdaptiveInterval: boolean; + rttHistorySize: number; + }; + + /** SyncVar配置 */ + syncVar: { + cacheTimeout: number; + defaultThrottleMs: number; + maxFieldNumber: number; + minFieldNumber: number; + }; + + /** 消息配置 */ + message: { + maxSequenceNumber: number; + maxHeaderSize: number; + maxPayloadSize: number; + defaultTimeout: number; + maxBatchSize: number; + }; + + /** 序列化配置 */ + serialization: { + defaultCompressionLevel: number; + minCompressionSize: number; + initialBufferSize: number; + maxBufferSize: number; + }; + + /** TSRPC配置 */ + tsrpc: { + defaultServerUrl: string; + defaultTimeout: number; + heartbeatInterval: number; + heartbeatTimeout: number; + poolConfig: { + minConnections: number; + maxConnections: number; + idleTimeout: number; + }; + }; + + /** 权限配置 */ + authority: { + minPriority: number; + maxPriority: number; + defaultRulePriority: number; + }; + + /** 性能监控配置 */ + performance: { + statsCollectionInterval: number; + statsRetentionTime: number; + warningThresholds: { + rtt: number; + packetLoss: number; + jitter: number; + cpuUsage: number; + memoryUsage: number; + }; + }; +} + +/** + * 配置更新事件接口 + */ +export interface IConfigUpdateEvent { + path: string; + oldValue: T; + newValue: T; + timestamp: number; +} + +/** + * 配置管理器 + * + * 提供类型安全的配置管理,支持配置更新监听和验证 + */ +export class NetworkConfigManager { + private static _instance: NetworkConfigManager | null = null; + private _config: INetworkConfig; + private _updateListeners: Map void>> = new Map(); + + private constructor() { + this._config = this.createDefaultConfig(); + } + + public static get Instance(): NetworkConfigManager { + if (!NetworkConfigManager._instance) { + NetworkConfigManager._instance = new NetworkConfigManager(); + } + return NetworkConfigManager._instance; + } + + /** + * 创建默认配置 + */ + private createDefaultConfig(): INetworkConfig { + return { + connection: { + timeout: NETWORK_CONFIG.DEFAULT_CONNECTION_TIMEOUT, + maxReconnectAttempts: NETWORK_CONFIG.DEFAULT_MAX_RECONNECT_ATTEMPTS, + reconnectDelay: NETWORK_CONFIG.DEFAULT_RECONNECT_DELAY + }, + heartbeat: { + interval: NETWORK_CONFIG.DEFAULT_HEARTBEAT_INTERVAL, + timeout: NETWORK_CONFIG.DEFAULT_HEARTBEAT_TIMEOUT, + maxConsecutiveLoss: NETWORK_CONFIG.DEFAULT_MAX_CONSECUTIVE_LOSS, + packetSize: NETWORK_CONFIG.DEFAULT_HEARTBEAT_PACKET_SIZE, + enableAdaptiveInterval: true, + rttHistorySize: NETWORK_CONFIG.DEFAULT_RTT_HISTORY_SIZE + }, + syncVar: { + cacheTimeout: SYNCVAR_CONFIG.DEFAULT_CACHE_TIMEOUT, + defaultThrottleMs: SYNCVAR_CONFIG.DEFAULT_THROTTLE_MS, + maxFieldNumber: SYNCVAR_CONFIG.MAX_FIELD_NUMBER, + minFieldNumber: SYNCVAR_CONFIG.MIN_FIELD_NUMBER + }, + message: { + maxSequenceNumber: MESSAGE_CONFIG.MAX_SEQUENCE_NUMBER, + maxHeaderSize: MESSAGE_CONFIG.MAX_HEADER_SIZE, + maxPayloadSize: MESSAGE_CONFIG.MAX_PAYLOAD_SIZE, + defaultTimeout: MESSAGE_CONFIG.DEFAULT_MESSAGE_TIMEOUT, + maxBatchSize: MESSAGE_CONFIG.MAX_BATCH_SIZE + }, + serialization: { + defaultCompressionLevel: SERIALIZATION_CONFIG.DEFAULT_COMPRESSION_LEVEL, + minCompressionSize: SERIALIZATION_CONFIG.MIN_COMPRESSION_SIZE, + initialBufferSize: SERIALIZATION_CONFIG.INITIAL_BUFFER_SIZE, + maxBufferSize: SERIALIZATION_CONFIG.MAX_BUFFER_SIZE + }, + tsrpc: { + defaultServerUrl: TSRPC_CONFIG.DEFAULT_SERVER_URL, + defaultTimeout: TSRPC_CONFIG.DEFAULT_TIMEOUT, + heartbeatInterval: TSRPC_CONFIG.DEFAULT_HEARTBEAT.interval, + heartbeatTimeout: TSRPC_CONFIG.DEFAULT_HEARTBEAT.timeout, + poolConfig: { + minConnections: TSRPC_CONFIG.DEFAULT_POOL_CONFIG.minConnections, + maxConnections: TSRPC_CONFIG.DEFAULT_POOL_CONFIG.maxConnections, + idleTimeout: TSRPC_CONFIG.DEFAULT_POOL_CONFIG.idleTimeout + } + }, + authority: { + minPriority: AUTHORITY_CONFIG.MIN_PRIORITY, + maxPriority: AUTHORITY_CONFIG.MAX_PRIORITY, + defaultRulePriority: AUTHORITY_CONFIG.DEFAULT_RULE_PRIORITY + }, + performance: { + statsCollectionInterval: PERFORMANCE_CONFIG.STATS_COLLECTION_INTERVAL, + statsRetentionTime: PERFORMANCE_CONFIG.STATS_RETENTION_TIME, + warningThresholds: { + rtt: PERFORMANCE_CONFIG.WARNING_THRESHOLDS.RTT, + packetLoss: PERFORMANCE_CONFIG.WARNING_THRESHOLDS.PACKET_LOSS, + jitter: PERFORMANCE_CONFIG.WARNING_THRESHOLDS.JITTER, + cpuUsage: PERFORMANCE_CONFIG.WARNING_THRESHOLDS.CPU_USAGE, + memoryUsage: PERFORMANCE_CONFIG.WARNING_THRESHOLDS.MEMORY_USAGE + } + } + }; + } + + /** + * 获取配置 + */ + public getConfig(): Readonly { + return this._config; + } + + /** + * 获取指定路径的配置值 + * + * @param path - 配置路径,使用点号分隔 + * @returns 配置值 + */ + public get(path: string): T { + const keys = path.split('.'); + let current: unknown = this._config; + + for (const key of keys) { + if (typeof current !== 'object' || current === null || !(key in current)) { + throw new Error(`配置路径不存在: ${path}`); + } + current = (current as Record)[key]; + } + + return current as T; + } + + /** + * 设置指定路径的配置值 + * + * @param path - 配置路径 + * @param value - 新值 + */ + public set(path: string, value: T): void { + const keys = path.split('.'); + const lastKey = keys.pop()!; + let current = this._config as Record; + + // 导航到父对象 + for (const key of keys) { + if (typeof current[key] !== 'object' || current[key] === null) { + throw new Error(`配置路径无效: ${path}`); + } + current = current[key] as Record; + } + + const oldValue = current[lastKey]; + + // 验证新值 + this.validateConfigValue(path, value); + + // 设置新值 + current[lastKey] = value; + + // 触发更新事件 + this.emitConfigUpdate({ + path, + oldValue, + newValue: value, + timestamp: Date.now() + }); + } + + /** + * 批量更新配置 + * + * @param updates - 配置更新对象 + */ + public update(updates: Partial): void { + const flatUpdates = this.flattenObject(updates as unknown as Record); + + for (const [path, value] of Object.entries(flatUpdates)) { + this.set(path, value); + } + } + + /** + * 重置配置为默认值 + */ + public reset(): void { + const oldConfig = { ...this._config }; + this._config = this.createDefaultConfig(); + + this.emitConfigUpdate({ + path: '', + oldValue: oldConfig, + newValue: this._config, + timestamp: Date.now() + }); + } + + /** + * 添加配置更新监听器 + * + * @param path - 监听的配置路径(空字符串监听所有) + * @param listener - 监听器函数 + */ + public addUpdateListener(path: string, listener: (event: IConfigUpdateEvent) => void): void { + if (!this._updateListeners.has(path)) { + this._updateListeners.set(path, []); + } + this._updateListeners.get(path)!.push(listener); + } + + /** + * 移除配置更新监听器 + * + * @param path - 配置路径 + * @param listener - 监听器函数 + */ + public removeUpdateListener(path: string, listener: (event: IConfigUpdateEvent) => void): void { + const listeners = this._updateListeners.get(path); + if (listeners) { + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + } + } + + /** + * 从环境变量加载配置 + */ + public loadFromEnv(): void { + const envConfig = this.parseEnvConfig(); + if (Object.keys(envConfig).length > 0) { + this.update(envConfig); + } + } + + /** + * 从JSON对象加载配置 + * + * @param config - 配置对象 + */ + public loadFromObject(config: Partial): void { + this.validateConfig(config); + this.update(config); + } + + /** + * 导出配置为JSON + */ + public exportConfig(): INetworkConfig { + return JSON.parse(JSON.stringify(this._config)); + } + + /** + * 验证配置值 + */ + private validateConfigValue(path: string, value: unknown): void { + // 基本类型检查 + if (path.includes('timeout') || path.includes('interval') || path.includes('delay')) { + if (typeof value !== 'number' || value < 0) { + throw new Error(`配置值必须是非负数: ${path}`); + } + } + + if (path.includes('size') && typeof value === 'number' && value <= 0) { + throw new Error(`大小配置必须是正数: ${path}`); + } + + if (path.includes('url') && typeof value !== 'string') { + throw new Error(`URL配置必须是字符串: ${path}`); + } + + // 特定路径验证 + if (path === 'syncVar.maxFieldNumber' && typeof value === 'number' && value > SYNCVAR_CONFIG.MAX_FIELD_NUMBER) { + throw new Error(`字段编号不能超过 ${SYNCVAR_CONFIG.MAX_FIELD_NUMBER}`); + } + + if (path === 'syncVar.minFieldNumber' && typeof value === 'number' && value < SYNCVAR_CONFIG.MIN_FIELD_NUMBER) { + throw new Error(`字段编号不能小于 ${SYNCVAR_CONFIG.MIN_FIELD_NUMBER}`); + } + } + + /** + * 验证整个配置对象 + */ + private validateConfig(config: Partial): void { + // 递归验证配置结构 + const flatConfig = this.flattenObject(config); + for (const [path, value] of Object.entries(flatConfig)) { + this.validateConfigValue(path, value); + } + } + + /** + * 扁平化对象 + */ + private flattenObject(obj: Record, prefix = ''): Record { + const flattened: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + const newKey = prefix ? `${prefix}.${key}` : key; + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + Object.assign(flattened, this.flattenObject(value as Record, newKey)); + } else { + flattened[newKey] = value; + } + } + + return flattened; + } + + /** + * 解析环境变量配置 + */ + private parseEnvConfig(): Partial { + const config: Partial = {}; + + // 连接配置 + if (process.env.NETWORK_CONNECTION_TIMEOUT) { + config.connection = { + timeout: parseInt(process.env.NETWORK_CONNECTION_TIMEOUT, 10), + maxReconnectAttempts: NETWORK_CONFIG.DEFAULT_MAX_RECONNECT_ATTEMPTS, + reconnectDelay: NETWORK_CONFIG.DEFAULT_RECONNECT_DELAY + }; + } + + // 心跳配置 + if (process.env.NETWORK_HEARTBEAT_INTERVAL) { + config.heartbeat = { + interval: parseInt(process.env.NETWORK_HEARTBEAT_INTERVAL, 10), + timeout: NETWORK_CONFIG.DEFAULT_HEARTBEAT_TIMEOUT, + maxConsecutiveLoss: NETWORK_CONFIG.DEFAULT_MAX_CONSECUTIVE_LOSS, + packetSize: NETWORK_CONFIG.DEFAULT_HEARTBEAT_PACKET_SIZE, + enableAdaptiveInterval: true, + rttHistorySize: NETWORK_CONFIG.DEFAULT_RTT_HISTORY_SIZE + }; + } + + // TSRPC配置 + if (process.env.TSRPC_SERVER_URL) { + config.tsrpc = { + defaultServerUrl: process.env.TSRPC_SERVER_URL, + defaultTimeout: TSRPC_CONFIG.DEFAULT_TIMEOUT, + heartbeatInterval: TSRPC_CONFIG.DEFAULT_HEARTBEAT.interval, + heartbeatTimeout: TSRPC_CONFIG.DEFAULT_HEARTBEAT.timeout, + poolConfig: { + minConnections: TSRPC_CONFIG.DEFAULT_POOL_CONFIG.minConnections, + maxConnections: TSRPC_CONFIG.DEFAULT_POOL_CONFIG.maxConnections, + idleTimeout: TSRPC_CONFIG.DEFAULT_POOL_CONFIG.idleTimeout + } + }; + } + + return config; + } + + /** + * 触发配置更新事件 + */ + private emitConfigUpdate(event: IConfigUpdateEvent): void { + // 触发具体路径的监听器 + const listeners = this._updateListeners.get(event.path); + if (listeners) { + listeners.forEach(listener => { + try { + listener(event); + } catch (error) { + console.error(`配置更新监听器错误 (${event.path}):`, error); + } + }); + } + + // 触发全局监听器 + const globalListeners = this._updateListeners.get(''); + if (globalListeners) { + globalListeners.forEach(listener => { + try { + listener(event); + } catch (error) { + console.error('全局配置更新监听器错误:', error); + } + }); + } + } +} + +/** + * 配置管理器单例实例 + */ +export const NetworkConfig = NetworkConfigManager.Instance; \ No newline at end of file diff --git a/packages/network/src/Core/HeartbeatManager.ts b/packages/network/src/Core/HeartbeatManager.ts new file mode 100644 index 00000000..6324eb95 --- /dev/null +++ b/packages/network/src/Core/HeartbeatManager.ts @@ -0,0 +1,477 @@ +import { NetworkConnection } from './NetworkConnection'; +import { MessageData } from '../types/NetworkTypes'; +import { NETWORK_CONFIG } from '../constants/NetworkConstants'; + +// Logger akan diimport dari framework logger +const logger = { + info: console.log, + warn: console.warn, + error: console.error, + debug: console.debug +}; + +/** + * 心跳包数据 + */ +export interface HeartbeatPacket { + id: string; + timestamp: number; + sequenceNumber: number; + type: 'ping' | 'pong'; + payload?: MessageData; +} + +/** + * 心跳统计信息 + */ +export interface HeartbeatStats { + /** 总发送数 */ + totalSent: number; + /** 总接收数 */ + totalReceived: number; + /** 丢包数 */ + lostPackets: number; + /** 丢包率 */ + packetLossRate: number; + /** 平均RTT */ + averageRtt: number; + /** 最小RTT */ + minRtt: number; + /** 最大RTT */ + maxRtt: number; + /** 抖动 */ + jitter: number; + /** 连续丢包数 */ + consecutiveLoss: number; + /** 最后心跳时间 */ + lastHeartbeat: number; + /** 连接状态 */ + isAlive: boolean; +} + +/** + * 心跳配置 + */ +export interface HeartbeatConfig { + /** 心跳间隔(毫秒) */ + interval: number; + /** 超时时间(毫秒) */ + timeout: number; + /** 最大连续丢包数 */ + maxConsecutiveLoss: number; + /** 心跳包大小(字节) */ + packetSize: number; + /** 是否启用自适应间隔 */ + enableAdaptiveInterval: boolean; + /** RTT历史记录数量 */ + rttHistorySize: number; +} + +/** + * 心跳管理器 + * + * 提供精确的包丢失检测和连接质量监控 + */ +export class HeartbeatManager { + private static readonly DEFAULT_CONFIG: HeartbeatConfig = { + interval: NETWORK_CONFIG.DEFAULT_HEARTBEAT_INTERVAL, + timeout: NETWORK_CONFIG.DEFAULT_HEARTBEAT_TIMEOUT, + maxConsecutiveLoss: NETWORK_CONFIG.DEFAULT_MAX_CONSECUTIVE_LOSS, + packetSize: NETWORK_CONFIG.DEFAULT_HEARTBEAT_PACKET_SIZE, + enableAdaptiveInterval: true, + rttHistorySize: NETWORK_CONFIG.DEFAULT_RTT_HISTORY_SIZE + }; + + private _config: HeartbeatConfig; + private _connection: NetworkConnection; + private _isRunning: boolean = false; + private _intervalId: NodeJS.Timeout | null = null; + private _sequenceNumber: number = 0; + + // 统计数据 + private _stats: HeartbeatStats = { + totalSent: 0, + totalReceived: 0, + lostPackets: 0, + packetLossRate: 0, + averageRtt: 0, + minRtt: Number.MAX_VALUE, + maxRtt: 0, + jitter: 0, + consecutiveLoss: 0, + lastHeartbeat: 0, + isAlive: true + }; + + // 心跳记录 + private _pendingPings: Map = new Map(); + private _rttHistory: number[] = []; + private _lastRtt: number = 0; + + constructor(connection: NetworkConnection, config?: Partial) { + this._connection = connection; + this._config = { ...HeartbeatManager.DEFAULT_CONFIG, ...config }; + this.setupConnectionHandlers(); + } + + /** + * 设置连接处理器 + */ + private setupConnectionHandlers(): void { + this._connection.on('message', (data) => { + this.handleMessage(data); + }); + + this._connection.on('disconnected', () => { + this.stop(); + }); + } + + /** + * 开始心跳监控 + */ + public start(): void { + if (this._isRunning) { + return; + } + + this._isRunning = true; + this.scheduleNextHeartbeat(); + logger.info('心跳管理器已启动'); + } + + /** + * 停止心跳监控 + */ + public stop(): void { + if (!this._isRunning) { + return; + } + + this._isRunning = false; + + if (this._intervalId) { + clearTimeout(this._intervalId); + this._intervalId = null; + } + + this._pendingPings.clear(); + logger.info('心跳管理器已停止'); + } + + /** + * 安排下次心跳 + */ + private scheduleNextHeartbeat(): void { + if (!this._isRunning) { + return; + } + + let interval = this._config.interval; + + // 自适应间隔调整 + if (this._config.enableAdaptiveInterval) { + interval = this.calculateAdaptiveInterval(); + } + + this._intervalId = setTimeout(() => { + this.sendHeartbeat(); + this.scheduleNextHeartbeat(); + }, interval); + } + + /** + * 计算自适应间隔 + */ + private calculateAdaptiveInterval(): number { + const baseInterval = this._config.interval; + + // 根据网络质量调整间隔 + if (this._stats.packetLossRate > 0.05) { + // 丢包率高时增加心跳频率 + return Math.max(baseInterval * 0.5, 1000); + } else if (this._stats.packetLossRate < 0.01 && this._stats.averageRtt < 50) { + // 网络质量好时减少心跳频率 + return Math.min(baseInterval * 1.5, 15000); + } + + return baseInterval; + } + + /** + * 发送心跳包 + */ + private sendHeartbeat(): void { + const packet: HeartbeatPacket = { + id: this.generatePacketId(), + timestamp: Date.now(), + sequenceNumber: ++this._sequenceNumber, + type: 'ping', + payload: this.generateHeartbeatPayload() + }; + + const data = this.serializePacket(packet); + const success = this._connection.send(data); + + if (success) { + this._pendingPings.set(packet.id, { + packet, + sentAt: Date.now() + }); + this._stats.totalSent++; + + // 清理超时的心跳包 + this.cleanupTimeoutPings(); + } else { + logger.warn('心跳包发送失败'); + this._stats.consecutiveLoss++; + this.updateConnectionStatus(); + } + } + + /** + * 处理接收到的消息 + */ + private handleMessage(data: Uint8Array): void { + try { + const packet = this.deserializePacket(data); + if (!packet || !this.isHeartbeatPacket(packet)) { + return; + } + + if (packet.type === 'ping') { + this.handlePingPacket(packet); + } else if (packet.type === 'pong') { + this.handlePongPacket(packet); + } + } catch (error) { + logger.debug('处理心跳包时出错:', error); + } + } + + /** + * 处理Ping包 + */ + private handlePingPacket(packet: HeartbeatPacket): void { + const pongPacket: HeartbeatPacket = { + id: packet.id, + timestamp: Date.now(), + sequenceNumber: packet.sequenceNumber, + type: 'pong', + payload: packet.payload + }; + + const data = this.serializePacket(pongPacket); + this._connection.send(data); + } + + /** + * 处理Pong包 + */ + private handlePongPacket(packet: HeartbeatPacket): void { + const pendingPing = this._pendingPings.get(packet.id); + if (!pendingPing) { + return; // 可能是超时的包 + } + + // 计算RTT + const now = Date.now(); + const rtt = now - pendingPing.sentAt; + + // 更新统计信息 + this._stats.totalReceived++; + this._stats.lastHeartbeat = now; + this._stats.consecutiveLoss = 0; + this._stats.isAlive = true; + + // 更新RTT统计 + this.updateRttStats(rtt); + + // 移除已确认的ping + this._pendingPings.delete(packet.id); + + // 更新丢包统计 + this.updatePacketLossStats(); + + logger.debug(`心跳RTT: ${rtt}ms`); + } + + /** + * 更新RTT统计 + */ + private updateRttStats(rtt: number): void { + this._rttHistory.push(rtt); + if (this._rttHistory.length > this._config.rttHistorySize) { + this._rttHistory.shift(); + } + + // 计算平均RTT + this._stats.averageRtt = this._rttHistory.reduce((sum, r) => sum + r, 0) / this._rttHistory.length; + + // 更新最小/最大RTT + this._stats.minRtt = Math.min(this._stats.minRtt, rtt); + this._stats.maxRtt = Math.max(this._stats.maxRtt, rtt); + + // 计算抖动 + if (this._lastRtt > 0) { + const jitterSample = Math.abs(rtt - this._lastRtt); + this._stats.jitter = (this._stats.jitter * 0.9) + (jitterSample * 0.1); + } + this._lastRtt = rtt; + } + + /** + * 更新丢包统计 + */ + private updatePacketLossStats(): void { + const now = Date.now(); + let lostPackets = 0; + + // 计算超时的包数量 + for (const [id, pingInfo] of this._pendingPings) { + if (now - pingInfo.sentAt > this._config.timeout) { + lostPackets++; + this._pendingPings.delete(id); + } + } + + this._stats.lostPackets += lostPackets; + + if (this._stats.totalSent > 0) { + this._stats.packetLossRate = this._stats.lostPackets / this._stats.totalSent; + } + } + + /** + * 清理超时的ping包 + */ + private cleanupTimeoutPings(): void { + const now = Date.now(); + const timeoutIds: string[] = []; + + for (const [id, pingInfo] of this._pendingPings) { + if (now - pingInfo.sentAt > this._config.timeout) { + timeoutIds.push(id); + } + } + + if (timeoutIds.length > 0) { + this._stats.consecutiveLoss += timeoutIds.length; + this._stats.lostPackets += timeoutIds.length; + + for (const id of timeoutIds) { + this._pendingPings.delete(id); + } + + this.updateConnectionStatus(); + logger.debug(`清理了 ${timeoutIds.length} 个超时心跳包`); + } + } + + /** + * 更新连接状态 + */ + private updateConnectionStatus(): void { + const now = Date.now(); + const timeSinceLastHeartbeat = now - this._stats.lastHeartbeat; + + // 检查连接是否还活着 + this._stats.isAlive = + this._stats.consecutiveLoss < this._config.maxConsecutiveLoss && + timeSinceLastHeartbeat < this._config.timeout * 2; + + if (!this._stats.isAlive) { + logger.warn('连接被判定为不活跃', { + consecutiveLoss: this._stats.consecutiveLoss, + timeSinceLastHeartbeat + }); + } + } + + /** + * 生成包ID + */ + private generatePacketId(): string { + return `heartbeat_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + /** + * 生成心跳载荷 + */ + private generateHeartbeatPayload(): MessageData { + const payloadSize = this._config.packetSize - 50; // 减去头部大小 + return { + data: 'x'.repeat(Math.max(0, payloadSize)), + timestamp: Date.now() + }; + } + + /** + * 序列化包 + */ + private serializePacket(packet: HeartbeatPacket): Uint8Array { + const jsonString = JSON.stringify(packet); + return new TextEncoder().encode(jsonString); + } + + /** + * 反序列化包 + */ + private deserializePacket(data: Uint8Array): HeartbeatPacket | null { + try { + const jsonString = new TextDecoder().decode(data); + return JSON.parse(jsonString) as HeartbeatPacket; + } catch { + return null; + } + } + + /** + * 检查是否为心跳包 + */ + private isHeartbeatPacket(packet: unknown): packet is HeartbeatPacket { + return typeof packet === 'object' && + packet !== null && + typeof (packet as Record).id === 'string' && + typeof (packet as Record).timestamp === 'number' && + typeof (packet as Record).sequenceNumber === 'number' && + ((packet as Record).type === 'ping' || (packet as Record).type === 'pong'); + } + + /** + * 获取统计信息 + */ + public getStats(): HeartbeatStats { + return { ...this._stats }; + } + + /** + * 重置统计信息 + */ + public resetStats(): void { + this._stats = { + totalSent: 0, + totalReceived: 0, + lostPackets: 0, + packetLossRate: 0, + averageRtt: 0, + minRtt: Number.MAX_VALUE, + maxRtt: 0, + jitter: 0, + consecutiveLoss: 0, + lastHeartbeat: 0, + isAlive: true + }; + this._rttHistory = []; + this._pendingPings.clear(); + logger.debug('心跳统计信息已重置'); + } + + /** + * 更新配置 + */ + public updateConfig(newConfig: Partial): void { + this._config = { ...this._config, ...newConfig }; + logger.debug('心跳配置已更新'); + } +} \ No newline at end of file diff --git a/packages/network/src/Core/NetworkClient.ts b/packages/network/src/Core/NetworkClient.ts index b34a1cdc..87dd57f9 100644 --- a/packages/network/src/Core/NetworkClient.ts +++ b/packages/network/src/Core/NetworkClient.ts @@ -150,8 +150,7 @@ export class NetworkClient { this.emit('message', data); // 自动处理消息 - const { MessageHandler } = require('../Messaging/MessageHandler'); - await MessageHandler.Instance.handleRawMessage(data); + await this._messageHandler.handleRawMessage(data); }); this._connection.on('error', (error) => { diff --git a/packages/network/src/Core/NetworkConnection.ts b/packages/network/src/Core/NetworkConnection.ts index b644d3a9..f90af929 100644 --- a/packages/network/src/Core/NetworkConnection.ts +++ b/packages/network/src/Core/NetworkConnection.ts @@ -16,9 +16,12 @@ export enum ConnectionState { */ export interface NetworkConnectionEvents { connected: () => void; - disconnected: (reason?: string) => void; + disconnected: (reason?: string, code?: number) => void; message: (data: Uint8Array) => void; error: (error: Error) => void; + reconnecting: (attempt: number) => void; + reconnected: () => void; + reconnectFailed: (error: Error) => void; } /** @@ -38,23 +41,55 @@ export class NetworkConnection { private _pingInterval: NodeJS.Timeout | null = null; private _eventHandlers: Map = new Map(); + // 重连相关属性 + private _reconnectAttempts: number = 0; + private _maxReconnectAttempts: number = 5; + private _reconnectInterval: number = 1000; // 1秒 + private _maxReconnectInterval: number = 30000; // 30秒 + private _reconnectTimer: NodeJS.Timeout | null = null; + private _enableAutoReconnect: boolean = true; + private _originalUrl: string = ''; + // 心跳配置 private static readonly PING_INTERVAL = 30000; // 30秒 private static readonly PING_TIMEOUT = 5000; // 5秒超时 + // 错误分类 + private static readonly RECOVERABLE_CODES = [1006, 1011, 1012, 1013, 1014]; + /** * 构造函数 * * @param ws - WebSocket实例 * @param connectionId - 连接ID * @param address - 连接地址 + * @param options - 连接选项 */ - constructor(ws: WebSocket, connectionId: string, address: string = '') { + constructor( + ws: WebSocket, + connectionId: string, + address: string = '', + options?: { + enableAutoReconnect?: boolean; + maxReconnectAttempts?: number; + reconnectInterval?: number; + maxReconnectInterval?: number; + } + ) { this._ws = ws; this._connectionId = connectionId; this._address = address; + this._originalUrl = address; this._connectedTime = Date.now(); + // 设置重连选项 + if (options) { + this._enableAutoReconnect = options.enableAutoReconnect ?? true; + this._maxReconnectAttempts = options.maxReconnectAttempts ?? 5; + this._reconnectInterval = options.reconnectInterval ?? 1000; + this._maxReconnectInterval = options.maxReconnectInterval ?? 30000; + } + this.setupWebSocket(); this.startPingInterval(); } @@ -67,13 +102,31 @@ export class NetworkConnection { this._ws.onopen = () => { this._state = ConnectionState.Connected; + + // 重连成功,重置重连状态 + if (this._reconnectAttempts > 0) { + NetworkConnection.logger.info(`重连成功 (第 ${this._reconnectAttempts} 次尝试)`); + this.resetReconnectState(); + this.emit('reconnected'); + } + this.emit('connected'); }; this._ws.onclose = (event) => { this._state = ConnectionState.Disconnected; this.stopPingInterval(); - this.emit('disconnected', event.reason); + + const reason = event.reason || '连接已关闭'; + const code = event.code; + + NetworkConnection.logger.warn(`连接断开: ${reason} (code: ${code})`); + this.emit('disconnected', reason, code); + + // 判断是否需要自动重连 + if (this._enableAutoReconnect && this.shouldReconnect(code)) { + this.scheduleReconnect(); + } }; this._ws.onerror = (event) => { @@ -164,6 +217,119 @@ export class NetworkConnection { } } + /** + * 判断是否应该重连 + */ + private shouldReconnect(code: number): boolean { + // 正常关闭不需要重连 + if (code === 1000) { + return false; + } + + // 检查是否是可恢复的错误代码 + return NetworkConnection.RECOVERABLE_CODES.includes(code) || code === 1006; + } + + /** + * 安排重连 + */ + private scheduleReconnect(): void { + if (this._reconnectAttempts >= this._maxReconnectAttempts) { + NetworkConnection.logger.error(`重连失败,已达到最大重试次数 ${this._maxReconnectAttempts}`); + this.emit('reconnectFailed', new Error(`重连失败,已达到最大重试次数`)); + return; + } + + this._reconnectAttempts++; + const delay = Math.min( + this._reconnectInterval * Math.pow(2, this._reconnectAttempts - 1), + this._maxReconnectInterval + ); + + NetworkConnection.logger.info(`将在 ${delay}ms 后尝试第 ${this._reconnectAttempts} 次重连`); + this.emit('reconnecting', this._reconnectAttempts); + + this._reconnectTimer = setTimeout(() => { + this.attemptReconnect(); + }, delay); + } + + /** + * 尝试重连 + */ + private attemptReconnect(): void { + if (this._state === ConnectionState.Connected) { + return; + } + + try { + NetworkConnection.logger.info(`正在尝试重连到 ${this._originalUrl}`); + + // 创建新的WebSocket连接 + this._ws = new WebSocket(this._originalUrl); + this._state = ConnectionState.Connecting; + + // 重新设置事件处理 + this.setupWebSocket(); + + } catch (error) { + NetworkConnection.logger.error(`重连失败:`, error); + + // 继续下一次重连尝试 + setTimeout(() => { + this.scheduleReconnect(); + }, 1000); + } + } + + /** + * 重置重连状态 + */ + private resetReconnectState(): void { + this._reconnectAttempts = 0; + + if (this._reconnectTimer) { + clearTimeout(this._reconnectTimer); + this._reconnectTimer = null; + } + } + + /** + * 设置重连配置 + */ + public setReconnectOptions(options: { + enableAutoReconnect?: boolean; + maxReconnectAttempts?: number; + reconnectInterval?: number; + maxReconnectInterval?: number; + }): void { + if (options.enableAutoReconnect !== undefined) { + this._enableAutoReconnect = options.enableAutoReconnect; + } + if (options.maxReconnectAttempts !== undefined) { + this._maxReconnectAttempts = options.maxReconnectAttempts; + } + if (options.reconnectInterval !== undefined) { + this._reconnectInterval = options.reconnectInterval; + } + if (options.maxReconnectInterval !== undefined) { + this._maxReconnectInterval = options.maxReconnectInterval; + } + } + + /** + * 手动重连 + */ + public reconnect(): void { + if (this._state === ConnectionState.Connected) { + NetworkConnection.logger.warn('连接已存在,无需重连'); + return; + } + + this.resetReconnectState(); + this.attemptReconnect(); + } + /** * 关闭连接 * @@ -175,6 +341,11 @@ export class NetworkConnection { } this._state = ConnectionState.Disconnecting; + + // 主动关闭时禁用自动重连 + this._enableAutoReconnect = false; + this.resetReconnectState(); + this.stopPingInterval(); if (this._ws) { diff --git a/packages/network/src/Core/NetworkPerformanceMonitor.ts b/packages/network/src/Core/NetworkPerformanceMonitor.ts index 0c349a85..91f7b167 100644 --- a/packages/network/src/Core/NetworkPerformanceMonitor.ts +++ b/packages/network/src/Core/NetworkPerformanceMonitor.ts @@ -4,6 +4,8 @@ * 监控网络连接的性能指标,包括延迟、吞吐量、包丢失率等 */ import { createLogger } from '@esengine/ecs-framework'; +import { HeartbeatManager, HeartbeatStats } from './HeartbeatManager'; +import { NetworkConnection } from './NetworkConnection'; export interface NetworkMetrics { /** 往返时延 (ms) */ @@ -103,6 +105,9 @@ export class NetworkPerformanceMonitor { /** 事件监听器 */ private _eventListeners: Map = new Map(); + /** 心跳管理器映射 */ + private _heartbeatManagers: Map = new Map(); + public static get Instance(): NetworkPerformanceMonitor { if (!NetworkPerformanceMonitor._instance) { NetworkPerformanceMonitor._instance = new NetworkPerformanceMonitor(); @@ -200,8 +205,8 @@ export class NetworkPerformanceMonitor { // 计算带宽 const { uploadBandwidth, downloadBandwidth } = this.calculateBandwidth(); - // 模拟包丢失率(实际应用中需要通过心跳包检测) - const packetLoss = this.estimatePacketLoss(); + // 获取真实的包丢失率(优先使用心跳管理器数据) + const packetLoss = this.getAccuratePacketLoss(); // 计算连接质量评分 const connectionQuality = this.calculateConnectionQuality(avgRtt, jitter, packetLoss); @@ -257,19 +262,43 @@ export class NetworkPerformanceMonitor { /** * 估算包丢失率 + * 使用多种指标进行更精确的丢包检测 */ private estimatePacketLoss(): number { - // 实际实现中应该通过心跳包或消息确认机制来检测包丢失 - // 这里提供一个简单的估算 - const recentRtt = this._rttHistory.slice(-5); - if (recentRtt.length === 0) return 0; + const recentRtt = this._rttHistory.slice(-10); // 增加样本数量 + if (recentRtt.length < 3) return 0; // 需要足够的样本 + // 1. 基于RTT标准差的检测 const avgRtt = recentRtt.reduce((a, b) => a + b, 0) / recentRtt.length; - const maxRtt = Math.max(...recentRtt); + const variance = recentRtt.reduce((sum, rtt) => sum + Math.pow(rtt - avgRtt, 2), 0) / recentRtt.length; + const stdDev = Math.sqrt(variance); + const coefficientOfVariation = stdDev / avgRtt; - // 基于RTT变化估算丢包率 - const rttVariation = maxRtt / avgRtt - 1; - return Math.min(rttVariation * 0.1, 0.1); // 最大10%丢包率 + // 2. 异常RTT值检测(可能是重传导致) + const threshold = avgRtt + 2 * stdDev; + const abnormalRttCount = recentRtt.filter(rtt => rtt > threshold).length; + const abnormalRttRatio = abnormalRttCount / recentRtt.length; + + // 3. 基于连续超时的检测 + let consecutiveHighRtt = 0; + let maxConsecutive = 0; + for (const rtt of recentRtt) { + if (rtt > avgRtt * 1.5) { + consecutiveHighRtt++; + maxConsecutive = Math.max(maxConsecutive, consecutiveHighRtt); + } else { + consecutiveHighRtt = 0; + } + } + const consecutiveImpact = Math.min(maxConsecutive / recentRtt.length * 2, 1); + + // 综合评估丢包率 + const basePacketLoss = Math.min(coefficientOfVariation * 0.3, 0.15); + const abnormalAdjustment = abnormalRttRatio * 0.1; + const consecutiveAdjustment = consecutiveImpact * 0.05; + + const totalPacketLoss = basePacketLoss + abnormalAdjustment + consecutiveAdjustment; + return Math.min(totalPacketLoss, 0.2); // 最大20%丢包率 } /** @@ -296,22 +325,87 @@ export class NetworkPerformanceMonitor { /** * 获取SyncVar统计信息 + * 改进异常处理,提供更详细的错误信息和降级策略 + * 使用懒加载避免循环依赖问题 */ private getSyncVarStatistics(): PerformanceSnapshot['syncVarStats'] { try { - const { SyncVarSyncScheduler } = require('../SyncVar/SyncVarSyncScheduler'); - const scheduler = SyncVarSyncScheduler.Instance; + // 使用懒加载获取SyncVarSyncScheduler,避免循环依赖 + const scheduler = this.getSyncVarScheduler(); + + if (!scheduler) { + NetworkPerformanceMonitor.logger.debug('SyncVarSyncScheduler实例不存在,可能尚未初始化'); + return { + syncedComponents: 0, + syncedFields: 0, + averageSyncRate: 0, + syncDataSize: 0 + }; + } + + // 检查getStats方法是否存在 + if (typeof scheduler.getStats !== 'function') { + NetworkPerformanceMonitor.logger.warn('SyncVarSyncScheduler缺少getStats方法'); + return { + syncedComponents: 0, + syncedFields: 0, + averageSyncRate: 0, + syncDataSize: 0 + }; + } + const stats = scheduler.getStats(); - return { - syncedComponents: stats.totalComponents || 0, - syncedFields: stats.totalFields || 0, - averageSyncRate: stats.averageFrequency || 0, - syncDataSize: stats.totalDataSize || 0 + // 验证统计数据的有效性 + const validatedStats = { + syncedComponents: (typeof stats.totalComponents === 'number') ? Math.max(0, stats.totalComponents) : 0, + syncedFields: (typeof stats.totalFields === 'number') ? Math.max(0, stats.totalFields) : 0, + averageSyncRate: (typeof stats.averageFrequency === 'number') ? Math.max(0, stats.averageFrequency) : 0, + syncDataSize: (typeof stats.totalDataSize === 'number') ? Math.max(0, stats.totalDataSize) : 0 }; + + return validatedStats; + } catch (error) { - NetworkPerformanceMonitor.logger.warn('获取SyncVar统计失败:', error); - return undefined; + const errorMsg = error instanceof Error ? error.message : String(error); + NetworkPerformanceMonitor.logger.warn(`获取SyncVar统计失败: ${errorMsg}, 返回默认统计数据`); + + // 返回安全的默认值而不是undefined + return { + syncedComponents: 0, + syncedFields: 0, + averageSyncRate: 0, + syncDataSize: 0 + }; + } + } + + /** + * 懒加载获取SyncVarSyncScheduler实例,避免循环依赖 + */ + private getSyncVarScheduler(): any | null { + try { + // 检查全局对象中是否已有实例 + const globalObj = (globalThis as any); + if (globalObj.SyncVarSyncScheduler && globalObj.SyncVarSyncScheduler.Instance) { + return globalObj.SyncVarSyncScheduler.Instance; + } + + // 尝试动态导入(仅在必要时) + try { + const SyncVarModule = require('../SyncVar/SyncVarSyncScheduler'); + if (SyncVarModule.SyncVarSyncScheduler && SyncVarModule.SyncVarSyncScheduler.Instance) { + return SyncVarModule.SyncVarSyncScheduler.Instance; + } + } catch (requireError) { + // 如果动态require失败,返回null而不是抛出错误 + NetworkPerformanceMonitor.logger.debug('无法动态加载SyncVarSyncScheduler模块'); + } + + return null; + } catch (error) { + NetworkPerformanceMonitor.logger.debug('获取SyncVarScheduler实例失败'); + return null; } } @@ -553,4 +647,212 @@ export class NetworkPerformanceMonitor { bandwidthWindowSize: this._bandwidthWindow.length }; } + + /** + * 网络质量自适应机制 + * 根据网络状况动态调整同步策略 + */ + public adaptNetworkStrategy(): { + suggestedSyncInterval: number; + suggestedBatchSize: number; + suggestedCompressionLevel: number; + prioritizeUpdate: boolean; + } { + const metrics = this.getCurrentMetrics(); + + // 基础设置 + let syncInterval = 50; // 默认50ms + let batchSize = 10; // 默认批大小 + let compressionLevel = 1; // 默认压缩级别 + let prioritizeUpdate = false; + + // 根据连接质量调整 + if (metrics.connectionQuality >= 80) { + // 高质量网络: 高频更新,小批次 + syncInterval = 33; // 30fps + batchSize = 5; + compressionLevel = 0; // 不压缩 + } else if (metrics.connectionQuality >= 60) { + // 中等质量网络: 标准设置 + syncInterval = 50; // 20fps + batchSize = 10; + compressionLevel = 1; + } else if (metrics.connectionQuality >= 40) { + // 低质量网络: 降低频率,增加批处理 + syncInterval = 100; // 10fps + batchSize = 20; + compressionLevel = 2; + prioritizeUpdate = true; + } else { + // 极低质量网络: 最保守设置 + syncInterval = 200; // 5fps + batchSize = 50; + compressionLevel = 3; + prioritizeUpdate = true; + } + + // 根据RTT进一步调整 + if (metrics.rtt > 300) { + syncInterval = Math.max(syncInterval * 1.5, 200); + batchSize = Math.min(batchSize * 2, 100); + } + + // 根据丢包率调整 + if (metrics.packetLoss > 0.1) { + syncInterval = Math.max(syncInterval * 1.2, 150); + compressionLevel = Math.min(compressionLevel + 1, 3); + prioritizeUpdate = true; + } + + return { + suggestedSyncInterval: Math.round(syncInterval), + suggestedBatchSize: Math.round(batchSize), + suggestedCompressionLevel: compressionLevel, + prioritizeUpdate + }; + } + + /** + * 检测网络拥塞状态 + */ + public detectNetworkCongestion(): { + isCongested: boolean; + congestionLevel: 'none' | 'light' | 'moderate' | 'severe'; + suggestedAction: string; + } { + const recentSnapshots = this._snapshots.slice(-5); + if (recentSnapshots.length < 3) { + return { + isCongested: false, + congestionLevel: 'none', + suggestedAction: '数据不足,继续监控' + }; + } + + // 计算趋势 + const rttTrend = this.calculateTrend(recentSnapshots.map(s => s.metrics.rtt)); + const packetLossTrend = this.calculateTrend(recentSnapshots.map(s => s.metrics.packetLoss)); + const qualityTrend = this.calculateTrend(recentSnapshots.map(s => s.metrics.connectionQuality)); + + // 检测拥塞指标 + const avgRtt = recentSnapshots.reduce((sum, s) => sum + s.metrics.rtt, 0) / recentSnapshots.length; + const avgPacketLoss = recentSnapshots.reduce((sum, s) => sum + s.metrics.packetLoss, 0) / recentSnapshots.length; + const avgQuality = recentSnapshots.reduce((sum, s) => sum + s.metrics.connectionQuality, 0) / recentSnapshots.length; + + // 拥塞判定 + let congestionLevel: 'none' | 'light' | 'moderate' | 'severe' = 'none'; + let suggestedAction = '网络状况良好'; + + if (avgRtt > 500 || avgPacketLoss > 0.15 || avgQuality < 30) { + congestionLevel = 'severe'; + suggestedAction = '严重拥塞,建议降低同步频率至最低,启用高压缩'; + } else if (avgRtt > 300 || avgPacketLoss > 0.08 || avgQuality < 50) { + congestionLevel = 'moderate'; + suggestedAction = '中等拥塞,建议减少同步频率,启用压缩'; + } else if (avgRtt > 150 || avgPacketLoss > 0.03 || avgQuality < 70) { + congestionLevel = 'light'; + suggestedAction = '轻微拥塞,建议适度降低同步频率'; + } + + const isCongested = congestionLevel !== 'none'; + + return { + isCongested, + congestionLevel, + suggestedAction + }; + } + + /** + * 计算数据趋势(斜率) + */ + private calculateTrend(values: number[]): number { + if (values.length < 2) return 0; + + const n = values.length; + let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + + for (let i = 0; i < n; i++) { + sumX += i; + sumY += values[i]; + sumXY += i * values[i]; + sumX2 += i * i; + } + + const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); + return slope; + } + + /** + * 为连接添加心跳监控 + */ + public addHeartbeatMonitoring(connectionId: string, connection: NetworkConnection): void { + if (!this._heartbeatManagers.has(connectionId)) { + const heartbeatManager = new HeartbeatManager(connection); + this._heartbeatManagers.set(connectionId, heartbeatManager); + heartbeatManager.start(); + NetworkPerformanceMonitor.logger.info(`为连接 ${connectionId} 启动心跳监控`); + } + } + + /** + * 移除连接的心跳监控 + */ + public removeHeartbeatMonitoring(connectionId: string): void { + const heartbeatManager = this._heartbeatManagers.get(connectionId); + if (heartbeatManager) { + heartbeatManager.stop(); + this._heartbeatManagers.delete(connectionId); + NetworkPerformanceMonitor.logger.info(`移除连接 ${connectionId} 的心跳监控`); + } + } + + /** + * 获取精确的包丢失率(优先使用心跳数据) + */ + private getAccuratePacketLoss(): number { + let totalPacketLoss = 0; + let count = 0; + + // 从心跳管理器获取真实丢包率 + for (const heartbeatManager of this._heartbeatManagers.values()) { + const stats = heartbeatManager.getStats(); + totalPacketLoss += stats.packetLossRate; + count++; + } + + if (count > 0) { + return totalPacketLoss / count; + } + + // 回退到估算方法 + return this.estimatePacketLoss(); + } + + /** + * 获取心跳统计信息 + */ + public getHeartbeatStats(): Map { + const stats = new Map(); + + for (const [connectionId, manager] of this._heartbeatManagers) { + stats.set(connectionId, manager.getStats()); + } + + return stats; + } + + /** + * 获取所有连接的健康状态 + */ + public getConnectionHealth(): Map { + const health = new Map(); + + for (const [connectionId, manager] of this._heartbeatManagers) { + const stats = manager.getStats(); + health.set(connectionId, stats.isAlive); + } + + return health; + } } \ No newline at end of file diff --git a/packages/network/src/Core/NetworkServer.ts b/packages/network/src/Core/NetworkServer.ts index 5621aa29..bf493ee6 100644 --- a/packages/network/src/Core/NetworkServer.ts +++ b/packages/network/src/Core/NetworkServer.ts @@ -202,8 +202,7 @@ export class NetworkServer { this.emit('clientMessage', connection, data); // 自动处理消息 - const { MessageHandler } = require('../Messaging/MessageHandler'); - await MessageHandler.Instance.handleRawMessage(data, connection); + await this._messageHandler.handleRawMessage(data, connection); }); connection.on('error', (error) => { diff --git a/packages/network/src/Error/NetworkErrorHandler.ts b/packages/network/src/Error/NetworkErrorHandler.ts new file mode 100644 index 00000000..f724a6a6 --- /dev/null +++ b/packages/network/src/Error/NetworkErrorHandler.ts @@ -0,0 +1,409 @@ +/** + * 网络错误处理系统 + */ + +import { ERROR_CODES, LOG_LEVELS } from '../constants/NetworkConstants'; +import { NetworkErrorType, INetworkError, createNetworkError } from '../types/NetworkTypes'; + +/** + * 错误处理器接口 + */ +export interface IErrorHandler { + /** 处理器名称 */ + readonly name: string; + /** 是否可以处理该错误 */ + canHandle(error: INetworkError): boolean; + /** 处理错误 */ + handle(error: INetworkError): Promise | boolean; + /** 优先级(数值越大优先级越高) */ + readonly priority: number; +} + +/** + * 错误处理选项 + */ +export interface ErrorHandlingOptions { + /** 是否自动重试 */ + autoRetry?: boolean; + /** 最大重试次数 */ + maxRetries?: number; + /** 重试延迟(毫秒) */ + retryDelay?: number; + /** 是否记录错误日志 */ + logError?: boolean; + /** 日志级别 */ + logLevel?: 'error' | 'warn' | 'info' | 'debug'; + /** 是否通知用户 */ + notifyUser?: boolean; + /** 自定义处理函数 */ + customHandler?: ((error: INetworkError) => Promise | boolean) | undefined; +} + +/** + * 错误统计信息 + */ +export interface ErrorStats { + /** 总错误数 */ + totalErrors: number; + /** 按类型分组的错误数 */ + errorsByType: Map; + /** 按错误代码分组的错误数 */ + errorsByCode: Map; + /** 最近的错误 */ + recentErrors: INetworkError[]; + /** 错误趋势(每小时) */ + hourlyTrend: number[]; +} + +/** + * 网络错误处理器 + * + * 提供统一的错误处理、重试机制和错误统计功能 + */ +export class NetworkErrorHandler { + private static _instance: NetworkErrorHandler | null = null; + private _handlers: IErrorHandler[] = []; + private _stats: ErrorStats; + private _options: Omit, 'customHandler'> & { customHandler?: (error: INetworkError) => Promise | boolean }; + + private readonly logger = { + info: console.log, + warn: console.warn, + error: console.error, + debug: console.debug + }; + + private constructor() { + this._stats = { + totalErrors: 0, + errorsByType: new Map(), + errorsByCode: new Map(), + recentErrors: [], + hourlyTrend: new Array(24).fill(0) + }; + + this._options = { + autoRetry: true, + maxRetries: 3, + retryDelay: 1000, + logError: true, + logLevel: 'error' as const, + notifyUser: false, + customHandler: undefined + }; + + this.initializeDefaultHandlers(); + } + + public static get Instance(): NetworkErrorHandler { + if (!NetworkErrorHandler._instance) { + NetworkErrorHandler._instance = new NetworkErrorHandler(); + } + return NetworkErrorHandler._instance; + } + + /** + * 处理网络错误 + */ + public async handleError( + error: Error | INetworkError, + options?: Partial + ): Promise { + const networkError = this.ensureNetworkError(error); + const handlingOptions = { ...this._options, ...options }; + + // 更新统计信息 + this.updateStats(networkError); + + // 记录错误日志 + if (handlingOptions.logError) { + this.logError(networkError, handlingOptions.logLevel); + } + + // 执行自定义处理器 + if (handlingOptions.customHandler) { + try { + const handled = await handlingOptions.customHandler(networkError); + if (handled) { + return true; + } + } catch (customError) { + this.logger.error('自定义错误处理器失败:', customError); + } + } + + // 查找合适的处理器 + const handlers = this._handlers + .filter(handler => handler.canHandle(networkError)) + .sort((a, b) => b.priority - a.priority); + + for (const handler of handlers) { + try { + const handled = await handler.handle(networkError); + if (handled) { + this.logger.debug(`错误已被处理器 "${handler.name}" 处理`); + return true; + } + } catch (handlerError) { + this.logger.error(`处理器 "${handler.name}" 执行失败:`, handlerError); + } + } + + // 自动重试机制 + if (handlingOptions.autoRetry && this.shouldRetry(networkError)) { + return this.attemptRetry(networkError, handlingOptions); + } + + // 通知用户 + if (handlingOptions.notifyUser) { + this.notifyUser(networkError); + } + + return false; + } + + /** + * 添加错误处理器 + */ + public addHandler(handler: IErrorHandler): void { + this._handlers.push(handler); + this._handlers.sort((a, b) => b.priority - a.priority); + this.logger.debug(`添加错误处理器: ${handler.name} (优先级: ${handler.priority})`); + } + + /** + * 移除错误处理器 + */ + public removeHandler(name: string): boolean { + const index = this._handlers.findIndex(handler => handler.name === name); + if (index !== -1) { + this._handlers.splice(index, 1); + this.logger.debug(`移除错误处理器: ${name}`); + return true; + } + return false; + } + + /** + * 设置默认处理选项 + */ + public setDefaultOptions(options: Partial): void { + this._options = { ...this._options, ...options }; + } + + /** + * 获取错误统计信息 + */ + public getStats(): ErrorStats { + return { + totalErrors: this._stats.totalErrors, + errorsByType: new Map(this._stats.errorsByType), + errorsByCode: new Map(this._stats.errorsByCode), + recentErrors: [...this._stats.recentErrors], + hourlyTrend: [...this._stats.hourlyTrend] + }; + } + + /** + * 重置统计信息 + */ + public resetStats(): void { + this._stats = { + totalErrors: 0, + errorsByType: new Map(), + errorsByCode: new Map(), + recentErrors: [], + hourlyTrend: new Array(24).fill(0) + }; + } + + /** + * 创建网络错误 + */ + public createError( + type: NetworkErrorType, + message: string, + context?: Record + ): INetworkError { + return createNetworkError(type, message, context); + } + + /** + * 初始化默认错误处理器 + */ + private initializeDefaultHandlers(): void { + // 连接错误处理器 + this.addHandler({ + name: 'connection-error-handler', + priority: 80, + canHandle: (error) => error.type === NetworkErrorType.CONNECTION_FAILED, + handle: (error) => { + this.logger.warn('连接失败,尝试重新连接:', error.message); + return false; // 让重试机制处理 + } + }); + + // 序列化错误处理器 + this.addHandler({ + name: 'serialization-error-handler', + priority: 70, + canHandle: (error) => + error.type === NetworkErrorType.SERIALIZATION_FAILED || + error.type === NetworkErrorType.DESERIALIZATION_FAILED, + handle: (error) => { + this.logger.error('序列化/反序列化失败:', error.message); + // 序列化错误通常不需要重试 + return true; + } + }); + + // 权限错误处理器 + this.addHandler({ + name: 'permission-error-handler', + priority: 60, + canHandle: (error) => error.type === NetworkErrorType.PERMISSION_DENIED, + handle: (error) => { + this.logger.warn('权限被拒绝:', error.message); + // 权限错误不应该重试 + return true; + } + }); + + // 默认错误处理器 + this.addHandler({ + name: 'default-error-handler', + priority: 0, + canHandle: () => true, + handle: (error) => { + this.logger.error('未处理的网络错误:', error.message, error.context); + return false; + } + }); + } + + /** + * 确保错误是NetworkError类型 + */ + private ensureNetworkError(error: Error | INetworkError): INetworkError { + if ('type' in error && 'timestamp' in error) { + return error as INetworkError; + } + + // 转换普通错误为网络错误 + return createNetworkError( + NetworkErrorType.INVALID_DATA, + error.message, + { originalError: error.name } + ); + } + + /** + * 更新错误统计 + */ + private updateStats(error: INetworkError): void { + this._stats.totalErrors++; + + // 按类型统计 + const typeCount = this._stats.errorsByType.get(error.type) || 0; + this._stats.errorsByType.set(error.type, typeCount + 1); + + // 按错误代码统计 + if (error.code !== undefined) { + const codeCount = this._stats.errorsByCode.get(error.code) || 0; + this._stats.errorsByCode.set(error.code, codeCount + 1); + } + + // 记录最近错误(保留最近100个) + this._stats.recentErrors.push(error); + if (this._stats.recentErrors.length > 100) { + this._stats.recentErrors.shift(); + } + + // 更新小时趋势 + const currentHour = new Date().getHours(); + this._stats.hourlyTrend[currentHour]++; + } + + /** + * 记录错误日志 + */ + private logError(error: INetworkError, level: 'error' | 'warn' | 'info' | 'debug'): void { + const logMessage = `[${error.type}] ${error.message}`; + const logData = { + error: error.message, + type: error.type, + code: error.code, + context: error.context, + timestamp: new Date(error.timestamp).toISOString() + }; + + switch (level) { + case 'error': + this.logger.error(logMessage, logData); + break; + case 'warn': + this.logger.warn(logMessage, logData); + break; + case 'info': + this.logger.info(logMessage, logData); + break; + case 'debug': + this.logger.debug(logMessage, logData); + break; + } + } + + /** + * 判断是否应该重试 + */ + private shouldRetry(error: INetworkError): boolean { + // 这些错误类型不应该重试 + const noRetryTypes = [ + NetworkErrorType.PERMISSION_DENIED, + NetworkErrorType.SERIALIZATION_FAILED, + NetworkErrorType.DESERIALIZATION_FAILED + ]; + + return !noRetryTypes.includes(error.type); + } + + /** + * 尝试重试 + */ + private async attemptRetry( + error: INetworkError, + options: Omit, 'customHandler'> & { customHandler?: (error: INetworkError) => Promise | boolean } + ): Promise { + for (let attempt = 1; attempt <= options.maxRetries; attempt++) { + this.logger.info(`重试处理错误 (${attempt}/${options.maxRetries}):`, error.message); + + // 等待重试延迟 + await new Promise(resolve => setTimeout(resolve, options.retryDelay * attempt)); + + try { + // 这里应该有具体的重试逻辑,取决于错误类型 + // 目前返回false表示重试失败 + return false; + } catch (retryError) { + this.logger.error(`重试失败 (${attempt}):`, retryError); + } + } + + this.logger.error(`重试${options.maxRetries}次后仍然失败:`, error.message); + return false; + } + + /** + * 通知用户 + */ + private notifyUser(error: INetworkError): void { + // 这里可以实现用户通知逻辑 + // 例如显示弹窗、发送邮件等 + console.warn(`用户通知: ${error.message}`); + } +} + +/** + * 错误处理器单例实例 + */ +export const ErrorHandler = NetworkErrorHandler.Instance; \ No newline at end of file diff --git a/packages/network/src/Messaging/MessageHandler.ts b/packages/network/src/Messaging/MessageHandler.ts index c41e81df..aab1b2ba 100644 --- a/packages/network/src/Messaging/MessageHandler.ts +++ b/packages/network/src/Messaging/MessageHandler.ts @@ -1,12 +1,12 @@ import { NetworkMessage } from './NetworkMessage'; import { NetworkConnection } from '../Core/NetworkConnection'; -import { INetworkMessage, MessageData } from '../types/NetworkTypes'; +import { IBasicNetworkMessage, MessageData } from '../types/NetworkTypes'; import { createLogger } from '@esengine/ecs-framework'; /** * 消息处理器接口 */ -export interface IMessageHandler { +export interface IMessageHandler { /** * 处理消息 * @@ -20,8 +20,8 @@ export interface IMessageHandler { * 消息处理器注册信息 */ interface MessageHandlerInfo { - handler: IMessageHandler>; - messageClass: new (...args: any[]) => INetworkMessage; + handler: IMessageHandler>; + messageClass: new (...args: any[]) => IBasicNetworkMessage; priority: number; } @@ -35,7 +35,7 @@ export class MessageHandler { private static readonly logger = createLogger('MessageHandler'); private static _instance: MessageHandler | null = null; private _handlers: Map = new Map(); - private _messageClasses: Map INetworkMessage> = new Map(); + private _messageClasses: Map IBasicNetworkMessage> = new Map(); /** * 获取消息处理器单例 @@ -57,7 +57,7 @@ export class MessageHandler { * @param handler - 消息处理器 * @param priority - 处理优先级(数字越小优先级越高) */ - public registerHandler>( + public registerHandler>( messageType: number, messageClass: new (...args: any[]) => T, handler: IMessageHandler, @@ -81,8 +81,8 @@ export class MessageHandler { } else { // 添加新处理器 handlers.push({ - handler: handler as IMessageHandler, - messageClass: messageClass as new (...args: any[]) => INetworkMessage, + handler: handler as IMessageHandler, + messageClass: messageClass as new (...args: any[]) => IBasicNetworkMessage, priority }); } @@ -161,7 +161,7 @@ export class MessageHandler { * @param connection - 发送消息的连接(服务端有效) * @returns 是否成功处理 */ - public async handleMessage(message: INetworkMessage, connection?: NetworkConnection): Promise { + public async handleMessage(message: IBasicNetworkMessage, connection?: NetworkConnection): Promise { const messageType = message.messageType; const handlers = this._handlers.get(messageType); @@ -279,7 +279,7 @@ export class MessageHandler { * @param messageClass - 消息类构造函数 * @param priority - 处理优先级 */ -export function MessageHandlerDecorator>( +export function MessageHandlerDecorator>( messageType: number, messageClass: new (...args: any[]) => T, priority: number = 0 diff --git a/packages/network/src/Messaging/MessageTypes.ts b/packages/network/src/Messaging/MessageTypes.ts index 869caf73..d76fac4f 100644 --- a/packages/network/src/Messaging/MessageTypes.ts +++ b/packages/network/src/Messaging/MessageTypes.ts @@ -1,4 +1,6 @@ import { NetworkMessage, JsonMessage } from './NetworkMessage'; +import { MessageType as CoreMessageType } from '../types/MessageTypes'; +import { MESSAGE_CONFIG } from '../constants/NetworkConstants'; /** * 内置消息类型枚举 diff --git a/packages/network/src/Messaging/NetworkMessage.ts b/packages/network/src/Messaging/NetworkMessage.ts index 61138b54..e74eb04b 100644 --- a/packages/network/src/Messaging/NetworkMessage.ts +++ b/packages/network/src/Messaging/NetworkMessage.ts @@ -1,4 +1,4 @@ -import { INetworkMessage, MessageData } from '../types/NetworkTypes'; +import { IBasicNetworkMessage, MessageData } from '../types/NetworkTypes'; /** * 网络消息基类 @@ -6,7 +6,7 @@ import { INetworkMessage, MessageData } from '../types/NetworkTypes'; * 所有网络消息都应该继承此类 * 提供消息的序列化和反序列化功能 */ -export abstract class NetworkMessage implements INetworkMessage { +export abstract class NetworkMessage implements IBasicNetworkMessage { /** * 消息类型ID * 每个消息类型都应该有唯一的ID @@ -313,4 +313,363 @@ export class ProtobufMessage extends NetworkMessage { // 读取protobuf数据 this._data = data.subarray(offset); } +} + +/** + * 心跳消息 + * 用于维持连接和检测网络延迟 + */ +export class HeartbeatMessage extends NetworkMessage<{ ping: boolean; timestamp: number }> { + public readonly messageType: number = 4; + private _isPing: boolean; + private _pingTimestamp: number; + + public get data(): { ping: boolean; timestamp: number } { + return { + ping: this._isPing, + timestamp: this._pingTimestamp + }; + } + + constructor(isPing: boolean = true, timestamp: number = Date.now(), senderId?: string, sequence?: number) { + super(senderId, sequence); + this._isPing = isPing; + this._pingTimestamp = timestamp; + } + + public serialize(): Uint8Array { + const buffer = new ArrayBuffer(13); // 4(type) + 1(ping) + 8(timestamp) + const view = new DataView(buffer); + + view.setUint32(0, this.messageType, true); + view.setUint8(4, this._isPing ? 1 : 0); + // 使用BigUint64将timestamp存储为64位整数 + view.setBigUint64(5, BigInt(this._pingTimestamp), true); + + return new Uint8Array(buffer); + } + + public deserialize(data: Uint8Array): void { + if (data.length < 13) { + throw new Error('心跳消息数据长度不足'); + } + + const view = new DataView(data.buffer, data.byteOffset); + this._isPing = view.getUint8(4) === 1; + this._pingTimestamp = Number(view.getBigUint64(5, true)); + } + + /** + * 创建ping消息 + */ + public static createPing(senderId?: string): HeartbeatMessage { + return new HeartbeatMessage(true, Date.now(), senderId); + } + + /** + * 创建pong消息 + */ + public static createPong(originalTimestamp: number, senderId?: string): HeartbeatMessage { + return new HeartbeatMessage(false, originalTimestamp, senderId); + } +} + +/** + * 错误消息 + * 用于传递错误信息 + */ +export class ErrorMessage extends NetworkMessage<{ code: number; message: string; details?: any }> { + public readonly messageType: number = 5; + private _errorCode: number; + private _errorMessage: string; + private _details?: any; + + public get data(): { code: number; message: string; details?: any } { + return { + code: this._errorCode, + message: this._errorMessage, + details: this._details + }; + } + + constructor(code: number, message: string, details?: any, senderId?: string, sequence?: number) { + super(senderId, sequence); + this._errorCode = code; + this._errorMessage = message; + this._details = details; + } + + public serialize(): Uint8Array { + const messageBytes = new TextEncoder().encode(this._errorMessage); + const detailsBytes = this._details ? new TextEncoder().encode(JSON.stringify(this._details)) : new Uint8Array(0); + + const buffer = new ArrayBuffer(4 + 4 + 4 + messageBytes.length + 4 + detailsBytes.length); + const view = new DataView(buffer); + + let offset = 0; + view.setUint32(offset, this.messageType, true); + offset += 4; + + view.setUint32(offset, this._errorCode, true); + offset += 4; + + view.setUint32(offset, messageBytes.length, true); + offset += 4; + + new Uint8Array(buffer, offset, messageBytes.length).set(messageBytes); + offset += messageBytes.length; + + view.setUint32(offset, detailsBytes.length, true); + offset += 4; + + if (detailsBytes.length > 0) { + new Uint8Array(buffer, offset, detailsBytes.length).set(detailsBytes); + } + + return new Uint8Array(buffer); + } + + public deserialize(data: Uint8Array): void { + if (data.length < 16) { // 至少4+4+4+4字节 + throw new Error('错误消息数据长度不足'); + } + + const view = new DataView(data.buffer, data.byteOffset); + let offset = 4; // 跳过消息类型 + + this._errorCode = view.getUint32(offset, true); + offset += 4; + + const messageLength = view.getUint32(offset, true); + offset += 4; + + if (data.length < offset + messageLength + 4) { + throw new Error('错误消息数据不足'); + } + + const messageBytes = data.subarray(offset, offset + messageLength); + this._errorMessage = new TextDecoder().decode(messageBytes); + offset += messageLength; + + const detailsLength = view.getUint32(offset, true); + offset += 4; + + if (detailsLength > 0) { + if (data.length < offset + detailsLength) { + throw new Error('错误消息详情数据不足'); + } + const detailsBytes = data.subarray(offset, offset + detailsLength); + try { + this._details = JSON.parse(new TextDecoder().decode(detailsBytes)); + } catch { + this._details = new TextDecoder().decode(detailsBytes); + } + } + } +} + +/** + * 状态同步消息 + * 用于同步游戏对象状态 + */ +export interface StateData extends Record { + entityId: number; + position?: { x: number; y: number; z?: number }; + rotation?: { x: number; y: number; z: number; w: number }; + velocity?: { x: number; y: number; z?: number }; + health?: number; + customData?: Record; +} + +export class StateSyncMessage extends NetworkMessage { + public readonly messageType: number = 6; + private _stateData: StateData; + + public get data(): StateData { + return this._stateData; + } + + constructor(stateData: StateData, senderId?: string, sequence?: number) { + super(senderId, sequence); + this._stateData = stateData; + } + + public serialize(): Uint8Array { + // 使用JSON序列化(可以替换为更高效的二进制协议) + const jsonString = JSON.stringify(this._stateData); + const jsonBytes = new TextEncoder().encode(jsonString); + + const buffer = new ArrayBuffer(8 + jsonBytes.length); + const view = new DataView(buffer); + + view.setUint32(0, this.messageType, true); + view.setUint32(4, jsonBytes.length, true); + + new Uint8Array(buffer, 8, jsonBytes.length).set(jsonBytes); + + return new Uint8Array(buffer); + } + + public deserialize(data: Uint8Array): void { + if (data.length < 8) { + throw new Error('状态同步消息数据长度不足'); + } + + const view = new DataView(data.buffer, data.byteOffset); + const jsonLength = view.getUint32(4, true); + + if (data.length < 8 + jsonLength) { + throw new Error('状态同步消息JSON数据不足'); + } + + const jsonBytes = data.subarray(8, 8 + jsonLength); + const jsonString = new TextDecoder().decode(jsonBytes); + + try { + this._stateData = JSON.parse(jsonString); + } catch (error) { + throw new Error(`状态同步消息JSON解析失败: ${error}`); + } + } +} + +/** + * 房间消息 + * 用于房间相关的操作和通知 + */ +export interface RoomMessageData extends Record { + action: 'join' | 'leave' | 'kick' | 'message' | 'update'; + roomId: string; + playerId?: number; + playerName?: string; + message?: string; + metadata?: Record; +} + +export class RoomMessage extends NetworkMessage { + public readonly messageType: number = 7; + private _roomData: RoomMessageData; + + public get data(): RoomMessageData { + return this._roomData; + } + + constructor(roomData: RoomMessageData, senderId?: string, sequence?: number) { + super(senderId, sequence); + this._roomData = roomData; + } + + public serialize(): Uint8Array { + const jsonString = JSON.stringify(this._roomData); + const jsonBytes = new TextEncoder().encode(jsonString); + + const buffer = new ArrayBuffer(8 + jsonBytes.length); + const view = new DataView(buffer); + + view.setUint32(0, this.messageType, true); + view.setUint32(4, jsonBytes.length, true); + + new Uint8Array(buffer, 8, jsonBytes.length).set(jsonBytes); + + return new Uint8Array(buffer); + } + + public deserialize(data: Uint8Array): void { + if (data.length < 8) { + throw new Error('房间消息数据长度不足'); + } + + const view = new DataView(data.buffer, data.byteOffset); + const jsonLength = view.getUint32(4, true); + + if (data.length < 8 + jsonLength) { + throw new Error('房间消息JSON数据不足'); + } + + const jsonBytes = data.subarray(8, 8 + jsonLength); + const jsonString = new TextDecoder().decode(jsonBytes); + + try { + this._roomData = JSON.parse(jsonString); + } catch (error) { + throw new Error(`房间消息JSON解析失败: ${error}`); + } + } +} + +/** + * 消息工厂 + * 根据消息类型创建相应的消息实例 + */ +export class NetworkMessageFactory { + private static messageConstructors = new Map NetworkMessage>([ + [0, RawMessage], + [1, JsonMessage], + [2, ProtobufMessage], + [4, HeartbeatMessage], + [5, ErrorMessage], + [6, StateSyncMessage], + [7, RoomMessage] + ]); + + /** + * 根据消息类型创建消息实例 + */ + public static createMessage(messageType: number): NetworkMessage | null { + const Constructor = this.messageConstructors.get(messageType); + if (!Constructor) { + return null; + } + // 为不同消息类型提供默认参数 + try { + switch (messageType) { + case 4: // HeartbeatMessage + return new Constructor(); + case 5: // ErrorMessage + return new Constructor(0, ''); + case 6: // StateSyncMessage + return new Constructor({ entityId: 0 }); + case 7: // RoomMessage + return new Constructor({ action: 'message', roomId: '' }); + default: + return new Constructor(); + } + } catch { + return null; + } + } + + /** + * 从二进制数据反序列化消息 + */ + public static deserializeMessage(data: Uint8Array): NetworkMessage | null { + if (data.length < 4) { + return null; + } + + const view = new DataView(data.buffer, data.byteOffset); + const messageType = view.getUint32(0, true); + + const message = this.createMessage(messageType); + if (!message) { + return null; + } + + try { + message.deserialize(data); + return message; + } catch { + return null; + } + } + + /** + * 注册新的消息类型 + */ + public static registerMessageType( + messageType: number, + constructor: new (...args: any[]) => T + ): void { + this.messageConstructors.set(messageType, constructor as new (...args: any[]) => NetworkMessage); + } } \ No newline at end of file diff --git a/packages/network/src/NetworkComponent.ts b/packages/network/src/NetworkComponent.ts index bfd0a2f3..61f6d356 100644 --- a/packages/network/src/NetworkComponent.ts +++ b/packages/network/src/NetworkComponent.ts @@ -2,10 +2,11 @@ import { Component } from '@esengine/ecs-framework'; import { INetworkSyncable } from './INetworkSyncable'; import { NetworkRole } from './NetworkRole'; import { NetworkEnvironment } from './Core/NetworkEnvironment'; -import { createSyncVarProxy, isSyncVarProxied, destroySyncVarProxy } from './SyncVar/SyncVarProxy'; import { SyncVarManager } from './SyncVar/SyncVarManager'; import { getSyncVarMetadata } from './SyncVar/SyncVarDecorator'; -import { createLogger } from '@esengine/ecs-framework'; +import { ComponentRegistry } from '@esengine/ecs-framework'; +import { isTsrpcSerializable } from './Serialization/TsrpcDecorators'; +import { TsrpcSerializer } from './Serialization/TsrpcSerializer'; /** * 网络组件基类 @@ -16,14 +17,14 @@ import { createLogger } from '@esengine/ecs-framework'; * @example * ```typescript * import { NetworkComponent } from '@esengine/ecs-framework-network'; - * import { ProtoSerializable, ProtoFloat } from '@esengine/ecs-framework-network'; + * import { TsrpcSerializable, SyncField } from '@esengine/ecs-framework-network'; * - * @ProtoSerializable('Position') + * @TsrpcSerializable() * class PositionComponent extends NetworkComponent { - * @ProtoFloat(1) + * @SyncField() * public x: number = 0; * - * @ProtoFloat(2) + * @SyncField() * public y: number = 0; * * constructor(x: number = 0, y: number = 0) { @@ -57,6 +58,13 @@ import { createLogger } from '@esengine/ecs-framework'; * ``` */ export abstract class NetworkComponent extends Component implements INetworkSyncable { + /** SyncVar内部ID */ + _syncVarId?: string; + /** SyncVar监听禁用标志 */ + _syncVarDisabled?: boolean; + + /** 允许通过字符串键访问属性 */ + [propertyKey: string]: unknown; /** * 脏字段标记集合 * @@ -87,17 +95,16 @@ export abstract class NetworkComponent extends Component implements INetworkSync */ private ensureComponentRegistered(): void { try { - const { ComponentRegistry } = require('@esengine/ecs-framework'); // 检查当前组件类型是否已注册 - if (!ComponentRegistry.isRegistered(this.constructor)) { + if (!ComponentRegistry.isRegistered(this.constructor as any)) { // 如果未注册,自动注册 - ComponentRegistry.register(this.constructor); - const logger = createLogger('NetworkComponent'); + ComponentRegistry.register(this.constructor as any); + const logger = { info: console.log, warn: console.warn, error: console.error, debug: console.debug }; logger.debug(`自动注册组件类型: ${this.constructor.name}`); } } catch (error) { - const logger = createLogger('NetworkComponent'); + const logger = { info: console.log, warn: console.warn, error: console.error, debug: console.debug }; logger.warn(`无法注册组件类型 ${this.constructor.name}:`, error); } } @@ -110,7 +117,7 @@ export abstract class NetworkComponent extends Component implements INetworkSync private initializeSyncVar(): void { const metadata = getSyncVarMetadata(this.constructor); if (metadata.length > 0) { - const logger = createLogger('NetworkComponent'); + const logger = { info: console.log, warn: console.warn, error: console.error, debug: console.debug }; logger.debug(`${this.constructor.name} 发现 ${metadata.length} 个SyncVar字段,将启用代理监听`); } } @@ -150,19 +157,20 @@ export abstract class NetworkComponent extends Component implements INetworkSync * @returns 序列化的网络状态数据 */ public getNetworkState(): Uint8Array { - const { isProtoSerializable } = require('./Serialization/ProtobufDecorators'); - const { ProtobufSerializer } = require('./Serialization/ProtobufSerializer'); - - if (!isProtoSerializable(this)) { - throw new Error(`组件 ${this.constructor.name} 不支持网络同步,请添加@ProtoSerializable装饰器`); + if (!isTsrpcSerializable(this)) { + throw new Error(`组件 ${this.constructor.name} 不支持网络同步,请添加@TsrpcSerializable装饰器`); } try { - const serializer = ProtobufSerializer.getInstance(); + const serializer = TsrpcSerializer.getInstance(); const serializedData = serializer.serialize(this); + if (!serializedData) { + throw new Error(`序列化失败: 组件=${this.constructor.name}`); + } return serializedData.data; } catch (error) { - throw new Error(`获取网络状态失败: ${error}`); + const errorMsg = error instanceof Error ? error.message : String(error); + throw new Error(`获取网络状态失败: 组件=${this.constructor.name}, 错误=${errorMsg}, 可能原因: 序列化字段格式错误或网络连接问题`); } } @@ -173,27 +181,44 @@ export abstract class NetworkComponent extends Component implements INetworkSync * @param data - 网络状态数据 */ public applyNetworkState(data: Uint8Array): void { - const { isProtoSerializable } = require('./Serialization/ProtobufDecorators'); - const { ProtobufSerializer } = require('./Serialization/ProtobufSerializer'); - - if (!isProtoSerializable(this)) { - throw new Error(`组件 ${this.constructor.name} 不支持网络同步,请添加@ProtoSerializable装饰器`); + if (!isTsrpcSerializable(this)) { + throw new Error(`组件 ${this.constructor.name} 不支持网络同步,请添加@TsrpcSerializable装饰器`); } try { - const serializer = ProtobufSerializer.getInstance(); + const serializer = TsrpcSerializer.getInstance(); const serializedData = { - type: 'protobuf' as const, + type: 'tsrpc' as const, componentType: this.constructor.name, data: data, size: data.length }; - serializer.deserialize(this, serializedData); + + // 反序列化并应用到当前组件实例 + const deserializedComponent = serializer.deserialize(serializedData, this.constructor as any); + if (!deserializedComponent) { + throw new Error(`反序列化失败: 组件=${this.constructor.name}`); + } + + // 将反序列化的数据应用到当前实例 + Object.assign(this, deserializedComponent); // 应用后清理脏字段标记 this.markClean(); } catch (error) { - throw new Error(`应用网络状态失败: ${error}`); + const errorMsg = error instanceof Error ? error.message : String(error); + // 记录错误但不完全阻止组件运行 + console.warn(`应用网络状态失败: 组件=${this.constructor.name}, 错误=${errorMsg}, 尝试恢复到安全状态`); + + // 尝试恢复到安全状态 + try { + this.markClean(); // 至少清理脏字段标记 + } catch (cleanupError) { + // 如果连清理都失败,则抛出原始错误 + throw new Error(`应用网络状态失败: 组件=${this.constructor.name}, 原始错误=${errorMsg}, 清理失败=${cleanupError}, 组件可能处于不一致状态`); + } + + throw new Error(`应用网络状态失败: 组件=${this.constructor.name}, 错误=${errorMsg}, 已恢复到安全状态`); } } diff --git a/packages/network/src/Serialization/ProtobufDecorators.ts b/packages/network/src/Serialization/ProtobufDecorators.ts deleted file mode 100644 index 994e683a..00000000 --- a/packages/network/src/Serialization/ProtobufDecorators.ts +++ /dev/null @@ -1,470 +0,0 @@ -/** - * Protobuf序列化装饰器 - * - * 提供装饰器语法来标记组件和字段进行protobuf序列化 - */ - -import 'reflect-metadata'; -import { Component } from '@esengine/ecs-framework'; -import * as protobuf from 'protobufjs'; - -/** - * 使用protobufjs官方字段类型定义 - */ -export type ProtoFieldType = keyof typeof protobuf.types.basic | keyof typeof protobuf.types.defaults | 'message' | 'enum'; - -/** - * protobufjs官方字段类型常量 - */ -export const ProtoTypes = { - // 基本数值类型 - DOUBLE: 'double' as ProtoFieldType, - FLOAT: 'float' as ProtoFieldType, - INT32: 'int32' as ProtoFieldType, - INT64: 'int64' as ProtoFieldType, - UINT32: 'uint32' as ProtoFieldType, - UINT64: 'uint64' as ProtoFieldType, - SINT32: 'sint32' as ProtoFieldType, - SINT64: 'sint64' as ProtoFieldType, - FIXED32: 'fixed32' as ProtoFieldType, - FIXED64: 'fixed64' as ProtoFieldType, - SFIXED32: 'sfixed32' as ProtoFieldType, - SFIXED64: 'sfixed64' as ProtoFieldType, - BOOL: 'bool' as ProtoFieldType, - STRING: 'string' as ProtoFieldType, - BYTES: 'bytes' as ProtoFieldType, - // 复合类型 - MESSAGE: 'message' as ProtoFieldType, - ENUM: 'enum' as ProtoFieldType -} as const; - -/** - * protobufjs官方类型映射 - */ -export const ProtobufTypes = protobuf.types; - -/** - * 字段同步优先级 - */ -export enum FieldSyncPriority { - /** 关键字段 - 每帧必须同步 */ - CRITICAL = 'critical', - /** 高优先级 - 高频同步 */ - HIGH = 'high', - /** 中等优先级 - 中频同步 */ - MEDIUM = 'medium', - /** 低优先级 - 低频同步 */ - LOW = 'low' -} - -/** - * Protobuf字段定义接口 - */ -export interface ProtoFieldDefinition { - /** 字段编号 */ - fieldNumber: number; - /** 字段类型 */ - type: ProtoFieldType; - /** 是否为数组 */ - repeated?: boolean; - /** 是否可选 */ - optional?: boolean; - /** 字段名称 */ - name: string; - /** 自定义类型名称 */ - customTypeName?: string; - /** 枚举值映射 */ - enumValues?: Record; - /** 默认值 */ - defaultValue?: any; - - // 帧同步特定选项 - /** 同步优先级 */ - syncPriority?: FieldSyncPriority; - /** 数值精度(用于量化压缩) */ - precision?: number; - /** 是否支持插值 */ - interpolation?: boolean; - /** 量化位数 */ - quantizationBits?: number; - /** 变化阈值(小于此值不同步) */ - changeThreshold?: number; -} - -/** - * 组件同步模式 - */ -export enum ComponentSyncMode { - /** 完整同步 - 传输所有字段 */ - FULL = 'full', - /** 增量同步 - 只传输变化字段 */ - DELTA = 'delta', - /** 自适应 - 根据变化量自动选择 */ - ADAPTIVE = 'adaptive' -} - -/** - * Protobuf组件定义接口 - */ -export interface ProtoComponentDefinition { - /** 组件名称 */ - name: string; - /** 字段定义列表 */ - fields: Map; - /** 构造函数 */ - constructor: any; - - // 帧同步特定选项 - /** 同步模式 */ - syncMode?: ComponentSyncMode; - /** 同步频率(每秒同步次数) */ - syncFrequency?: number; - /** 是否启用压缩 */ - enableCompression?: boolean; - /** 网络优先级 */ - networkPriority?: number; -} - -/** - * Protobuf注册表 - */ -export class ProtobufRegistry { - private static instance: ProtobufRegistry; - private components = new Map(); - - public static getInstance(): ProtobufRegistry { - if (!ProtobufRegistry.instance) { - ProtobufRegistry.instance = new ProtobufRegistry(); - } - return ProtobufRegistry.instance; - } - - /** - * 注册组件定义 - */ - public registerComponent(componentName: string, definition: ProtoComponentDefinition): void { - this.components.set(componentName, definition); - } - - /** - * 获取组件定义 - */ - public getComponentDefinition(componentName: string): ProtoComponentDefinition | undefined { - return this.components.get(componentName); - } - - /** - * 检查组件是否支持protobuf - */ - public hasProtoDefinition(componentName: string): boolean { - return this.components.has(componentName); - } - - /** - * 获取所有注册的组件 - */ - public getAllComponents(): Map { - return new Map(this.components); - } - - /** - * 生成proto文件定义 - */ - public generateProtoDefinition(): string { - let protoContent = 'syntax = "proto3";\n\n'; - protoContent += 'package ecs;\n\n'; - - // 生成消息定义 - for (const [name, definition] of this.components) { - protoContent += `message ${name} {\n`; - - // 按字段编号排序 - const sortedFields = Array.from(definition.fields.values()) - .sort((a, b) => a.fieldNumber - b.fieldNumber); - - for (const field of sortedFields) { - let fieldDef = ' '; - - if (field.repeated) { - fieldDef += 'repeated '; - } else if (field.optional) { - fieldDef += 'optional '; - } - - fieldDef += `${field.type} ${field.name} = ${field.fieldNumber};\n`; - protoContent += fieldDef; - } - - protoContent += '}\n\n'; - } - - return protoContent; - } -} - -/** - * 组件同步选项接口 - */ -export interface ComponentSyncOptions { - /** 同步模式 */ - syncMode?: ComponentSyncMode; - /** 同步频率(每秒同步次数) */ - syncFrequency?: number; - /** 是否启用压缩 */ - enableCompression?: boolean; - /** 网络优先级(1-10,数字越大优先级越高) */ - networkPriority?: number; -} - -/** - * ProtoSerializable 组件装饰器 - * - * 标记组件支持protobuf序列化,专为帧同步框架优化 - * @param protoName protobuf消息名称,默认使用类名 - * @param options 同步选项 - * @example - * ```typescript - * @ProtoSerializable('Position', { - * syncMode: ComponentSyncMode.DELTA, - * syncFrequency: 30, - * networkPriority: 8 - * }) - * class PositionComponent extends Component { - * // 组件实现 - * } - * ``` - */ -export function ProtoSerializable(protoName?: string, options?: ComponentSyncOptions) { - return function (constructor: T) { - const componentName = protoName || constructor.name; - const registry = ProtobufRegistry.getInstance(); - - // 获取字段定义 - const fields = (constructor.prototype._protoFields as Map) - || new Map(); - - // 注册组件定义 - registry.registerComponent(componentName, { - name: componentName, - fields: fields, - constructor: constructor, - syncMode: options?.syncMode || ComponentSyncMode.FULL, - syncFrequency: options?.syncFrequency || 30, - enableCompression: options?.enableCompression || true, - networkPriority: options?.networkPriority || 5 - }); - - // 标记组件支持protobuf - (constructor.prototype._isProtoSerializable = true); - (constructor.prototype._protoName = componentName); - - return constructor; - }; -} - -/** - * 字段同步选项接口 - */ -export interface FieldSyncOptions { - /** 是否为数组 */ - repeated?: boolean; - /** 是否可选 */ - optional?: boolean; - /** 自定义类型名称 */ - customTypeName?: string; - /** 枚举值映射 */ - enumValues?: Record; - /** 默认值 */ - defaultValue?: any; - - // 帧同步特定选项 - /** 同步优先级 */ - syncPriority?: FieldSyncPriority; - /** 数值精度(用于量化压缩) */ - precision?: number; - /** 是否支持插值 */ - interpolation?: boolean; - /** 量化位数 */ - quantizationBits?: number; - /** 变化阈值(小于此值不同步) */ - changeThreshold?: number; -} - -/** - * ProtoField 字段装饰器 - * - * 标记字段参与protobuf序列化,针对帧同步进行优化 - * @param fieldNumber protobuf字段编号,必须唯一且大于0 - * @param type 字段类型,默认自动推断 - * @param options 字段选项 - * @example - * ```typescript - * class PositionComponent extends Component { - * @ProtoField(1, ProtoFieldType.FLOAT, { - * syncPriority: FieldSyncPriority.CRITICAL, - * precision: 0.01, - * interpolation: true - * }) - * public x: number = 0; - * - * @ProtoField(2, ProtoFieldType.FLOAT, { - * syncPriority: FieldSyncPriority.CRITICAL, - * precision: 0.01, - * interpolation: true - * }) - * public y: number = 0; - * } - * ``` - */ -export function ProtoField( - fieldNumber: number, - type?: ProtoFieldType, - options?: FieldSyncOptions -) { - return function (target: any, propertyKey: string) { - // 验证字段编号 - if (fieldNumber <= 0) { - throw new Error(`ProtoField: 字段编号必须大于0,当前值: ${fieldNumber}`); - } - - // 初始化字段集合 - if (!target._protoFields) { - target._protoFields = new Map(); - } - - // 自动推断类型 - let inferredType = type; - if (!inferredType) { - const designType = Reflect.getMetadata?.('design:type', target, propertyKey); - inferredType = inferProtoType(designType); - } - - // 检查字段编号冲突 - for (const [key, field] of target._protoFields) { - if (field.fieldNumber === fieldNumber && key !== propertyKey) { - throw new Error(`ProtoField: 字段编号 ${fieldNumber} 已被字段 ${key} 使用`); - } - } - - // 添加字段定义 - target._protoFields.set(propertyKey, { - fieldNumber, - type: inferredType || ProtoTypes.STRING, - repeated: options?.repeated || false, - optional: options?.optional || false, - name: propertyKey, - customTypeName: options?.customTypeName, - enumValues: options?.enumValues, - defaultValue: options?.defaultValue, - // 帧同步特定选项 - syncPriority: options?.syncPriority || FieldSyncPriority.MEDIUM, - precision: options?.precision, - interpolation: options?.interpolation || false, - quantizationBits: options?.quantizationBits, - changeThreshold: options?.changeThreshold || 0 - }); - }; -} - -/** - * 自动推断protobuf类型 - */ -function inferProtoType(jsType: any): ProtoFieldType { - if (!jsType) return ProtoTypes.STRING; - - switch (jsType) { - case Number: - return ProtoTypes.DOUBLE; - case Boolean: - return ProtoTypes.BOOL; - case String: - return ProtoTypes.STRING; - case Date: - // 对于Date类型,使用int64存储时间戳或者使用message类型 - return ProtoTypes.INT64; - case Array: - return ProtoTypes.STRING; - case Uint8Array: - case ArrayBuffer: - return ProtoTypes.BYTES; - case Object: - return ProtoTypes.MESSAGE; - default: - return ProtoTypes.STRING; - } -} - -/** - * 便捷装饰器 - 常用类型 - */ -export const ProtoInt32 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.INT32, options); - -export const ProtoFloat = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.FLOAT, options); - -export const ProtoString = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.STRING, options); - -export const ProtoBool = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.BOOL, options); - -// 扩展的便捷装饰器 -export const ProtoDouble = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.DOUBLE, options); - -export const ProtoInt64 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.INT64, options); - -export const ProtoUint32 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.UINT32, options); - -export const ProtoUint64 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.UINT64, options); - -export const ProtoBytes = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.BYTES, options); - -// 对于时间戳,使用int64存储毫秒时间戳 -export const ProtoTimestamp = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.INT64, options); - -// 对于持续时间,使用int32存储毫秒数 -export const ProtoDuration = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.INT32, options); - -// 对于结构体,使用message类型 -export const ProtoStruct = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.MESSAGE, options); - -/** - * 自定义消息类型装饰器 - * @param fieldNumber 字段编号 - * @param customTypeName 自定义类型名称 - * @param options 额外选项 - */ -export const ProtoMessage = (fieldNumber: number, customTypeName: string, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.MESSAGE, { ...options, customTypeName }); - -/** - * 枚举类型装饰器 - * @param fieldNumber 字段编号 - * @param enumValues 枚举值映射 - * @param options 额外选项 - */ -export const ProtoEnum = (fieldNumber: number, enumValues: Record, options?: { repeated?: boolean; optional?: boolean }) => - ProtoField(fieldNumber, ProtoTypes.ENUM, { ...options, enumValues }); - -/** - * 检查组件是否支持protobuf序列化 - */ -export function isProtoSerializable(component: Component): boolean { - return !!(component as any)._isProtoSerializable; -} - -/** - * 获取组件的protobuf名称 - */ -export function getProtoName(component: Component): string | undefined { - return (component as any)._protoName; -} \ No newline at end of file diff --git a/packages/network/src/Serialization/ProtobufSerializer.ts b/packages/network/src/Serialization/ProtobufSerializer.ts deleted file mode 100644 index 0a84f8fc..00000000 --- a/packages/network/src/Serialization/ProtobufSerializer.ts +++ /dev/null @@ -1,479 +0,0 @@ -/** - * Protobuf序列化器 - * - * 处理组件的protobuf序列化和反序列化 - */ - -import { Component } from '@esengine/ecs-framework'; -import * as protobuf from 'protobufjs'; -import { - ProtobufRegistry, - isProtoSerializable, - getProtoName -} from './ProtobufDecorators'; -import { SerializedData } from './SerializationTypes'; -import { createLogger } from '@esengine/ecs-framework'; - -/** - * 可序列化组件接口 - */ -interface SerializableComponent extends Component { - readonly constructor: { name: string }; -} - - - -/** - * Protobuf序列化器 - */ -export class ProtobufSerializer { - private registry: ProtobufRegistry; - private static instance: ProtobufSerializer; - private static readonly logger = createLogger('ProtobufSerializer'); - - /** protobuf.js根对象 */ - private root: protobuf.Root | null = null; - - /** MessageType缓存映射表 */ - private messageTypeCache: Map = new Map(); - - /** 组件序列化数据缓存 */ - private componentDataCache: Map> = new Map(); - - /** 缓存访问计数器 */ - private cacheAccessCount: Map = new Map(); - - /** 最大缓存大小 */ - private maxCacheSize: number = 1000; - - /** 是否启用数据验证 */ - private enableValidation: boolean = process.env.NODE_ENV === 'development'; - - /** 是否启用组件数据缓存 */ - private enableComponentDataCache: boolean = true; - - private constructor() { - this.registry = ProtobufRegistry.getInstance(); - this.initializeProtobuf(); - } - - /** - * 设置性能选项 - * @param options 性能配置选项 - * @param options.enableValidation 是否启用数据验证 - * @param options.enableComponentDataCache 是否启用组件数据缓存 - * @param options.maxCacheSize 最大缓存大小 - * @param options.clearCache 是否清空消息类型缓存 - * @param options.clearAllCaches 是否清空所有缓存 - */ - public setPerformanceOptions(options: { - enableValidation?: boolean; - enableComponentDataCache?: boolean; - maxCacheSize?: number; - clearCache?: boolean; - clearAllCaches?: boolean; - }): void { - if (options.enableValidation !== undefined) { - this.enableValidation = options.enableValidation; - } - if (options.enableComponentDataCache !== undefined) { - this.enableComponentDataCache = options.enableComponentDataCache; - } - if (options.maxCacheSize !== undefined && options.maxCacheSize > 0) { - this.maxCacheSize = options.maxCacheSize; - } - if (options.clearCache) { - this.messageTypeCache.clear(); - this.cacheAccessCount.clear(); - } - if (options.clearAllCaches) { - this.clearAllCaches(); - } - } - - /** - * 清空所有缓存 - */ - public clearAllCaches(): void { - this.messageTypeCache.clear(); - this.componentDataCache.clear(); - this.cacheAccessCount.clear(); - } - - /** - * 自动初始化protobuf支持 - */ - private async initializeProtobuf(): Promise { - try { - this.buildProtoDefinitions(); - ProtobufSerializer.logger.info('Protobuf支持已启用'); - } catch (error) { - throw new Error('[ProtobufSerializer] 初始化protobuf失败: ' + error); - } - } - - public static getInstance(): ProtobufSerializer { - if (!ProtobufSerializer.instance) { - ProtobufSerializer.instance = new ProtobufSerializer(); - } - return ProtobufSerializer.instance; - } - - /** - * 手动初始化protobuf.js库 - * @param protobufRoot protobuf根对象 - */ - public initialize(protobufRoot?: protobuf.Root): void { - if (protobufRoot) { - this.root = protobufRoot; - } else { - this.buildProtoDefinitions(); - } - ProtobufSerializer.logger.info('Protobuf支持已手动启用'); - } - - /** - * 序列化组件 - * @param component 要序列化的组件 - * @returns 序列化数据 - */ - public serialize(component: SerializableComponent): SerializedData { - const componentType = component.constructor.name; - - // 检查是否支持protobuf序列化 - if (!isProtoSerializable(component)) { - throw new Error(`组件 ${componentType} 不支持protobuf序列化,请添加@ProtoSerializable装饰器`); - } - - const protoName = getProtoName(component); - if (!protoName) { - throw new Error(`组件 ${componentType} 未设置protobuf名称`); - } - - // 获取protobuf消息类型 - const MessageType = this.getMessageType(protoName); - if (!MessageType) { - throw new Error(`未找到消息类型: ${protoName}`); - } - - // 数据验证(可选) - if (this.enableValidation && MessageType.verify) { - const error = MessageType.verify(component); - if (error) { - throw new Error(`数据验证失败: ${error}`); - } - } - - // 直接让protobufjs处理序列化 - const message = MessageType.create(component); - const buffer = MessageType.encode(message).finish(); - - return { - type: 'protobuf', - componentType: componentType, - data: buffer, - size: buffer.length - }; - } - - /** - * 反序列化组件 - * @param component 目标组件实例 - * @param serializedData 序列化数据 - */ - public deserialize(component: SerializableComponent, serializedData: SerializedData): void { - if (serializedData.type !== 'protobuf') { - throw new Error(`不支持的序列化类型: ${serializedData.type}`); - } - - const protoName = getProtoName(component); - if (!protoName) { - throw new Error(`组件 ${component.constructor.name} 未设置protobuf名称`); - } - - const MessageType = this.getMessageType(protoName); - if (!MessageType) { - throw new Error(`未找到消息类型: ${protoName}`); - } - - // 解码消息并直接应用到组件 - const message = MessageType.decode(serializedData.data); - const decoded = MessageType.toObject(message); - - // 直接应用解码后的数据到组件 - Object.assign(component, decoded); - } - - /** - * 检查组件是否支持protobuf序列化 - */ - public canSerialize(component: SerializableComponent): boolean { - if (!this.root) return false; - return isProtoSerializable(component); - } - - /** - * 批量序列化组件 - * @param components 要序列化的组件数组 - * @param options 批量序列化选项 - * @param options.continueOnError 遇到错误时是否继续处理 - * @param options.maxBatchSize 最大批次大小 - * @returns 序列化结果数组 - */ - public serializeBatch( - components: SerializableComponent[], - options?: { - continueOnError?: boolean; - maxBatchSize?: number; - } - ): SerializedData[] { - const results: SerializedData[] = []; - const errors: Error[] = []; - - const continueOnError = options?.continueOnError ?? false; - const maxBatchSize = options?.maxBatchSize ?? 1000; - - // 分批处理大量组件 - const batches = this.splitIntoBatches(components, maxBatchSize); - - for (const batch of batches) { - const batchResults = this.serializeBatchSerial(batch, continueOnError); - results.push(...batchResults.results); - errors.push(...batchResults.errors); - } - - // 如果有错误且不继续执行,抛出第一个错误 - if (errors.length > 0 && !continueOnError) { - throw errors[0]; - } - - // 记录错误统计 - if (errors.length > 0) { - ProtobufSerializer.logger.warn(`批量序列化完成,${results.length} 成功,${errors.length} 失败`); - } - - return results; - } - - /** - * 串行批量序列化 - */ - private serializeBatchSerial( - components: Component[], - continueOnError: boolean - ): { results: SerializedData[], errors: Error[] } { - const results: SerializedData[] = []; - const errors: Error[] = []; - - // 按组件类型分组,减少重复查找 - const componentGroups = this.groupComponentsByType(components, continueOnError, errors); - - // 按组分别序列化 - for (const [protoName, groupComponents] of componentGroups) { - const definition = this.registry.getComponentDefinition(protoName); - const MessageType = this.getMessageType(protoName); - - if (!definition || !MessageType) { - const error = new Error(`[ProtobufSerializer] 组件类型 ${protoName} 未正确注册`); - if (continueOnError) { - errors.push(error); - continue; - } else { - throw error; - } - } - - for (const component of groupComponents) { - try { - const result = this.serializeSingleComponent(component, MessageType); - results.push(result); - } catch (error) { - if (continueOnError) { - errors.push(error instanceof Error ? error : new Error(String(error))); - } else { - throw error; - } - } - } - } - - return { results, errors }; - } - - - /** - * 序列化单个组件 - */ - private serializeSingleComponent( - component: SerializableComponent, - MessageType: protobuf.Type - ): SerializedData { - // 数据验证 - if (this.enableValidation && MessageType.verify) { - const error = MessageType.verify(component); - if (error) { - throw new Error(`[ProtobufSerializer] 数据验证失败: ${error}`); - } - } - - const message = MessageType.create(component); - const buffer = MessageType.encode(message).finish(); - - return { - type: 'protobuf', - componentType: component.constructor.name, - data: buffer, - size: buffer.length - }; - } - - /** - * 按类型分组组件 - */ - private groupComponentsByType( - components: SerializableComponent[], - continueOnError: boolean, - errors: Error[] - ): Map { - const componentGroups = new Map(); - - for (const component of components) { - try { - if (!isProtoSerializable(component)) { - throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 不支持protobuf序列化`); - } - - const protoName = getProtoName(component); - if (!protoName) { - throw new Error(`[ProtobufSerializer] 组件 ${component.constructor.name} 未设置protobuf名称`); - } - - if (!componentGroups.has(protoName)) { - componentGroups.set(protoName, []); - } - componentGroups.get(protoName)!.push(component); - } catch (error) { - if (continueOnError) { - errors.push(error instanceof Error ? error : new Error(String(error))); - } else { - throw error; - } - } - } - - return componentGroups; - } - - - - /** - * 根据需要清理缓存 - */ - private cleanupCacheIfNeeded(): void { - if (this.messageTypeCache.size <= this.maxCacheSize) { - return; - } - - const entries = Array.from(this.cacheAccessCount.entries()) - .sort((a, b) => a[1] - b[1]) - .slice(0, Math.floor(this.maxCacheSize * 0.2)); - - for (const [key] of entries) { - this.messageTypeCache.delete(key); - this.cacheAccessCount.delete(key); - this.componentDataCache.delete(key); - } - - ProtobufSerializer.logger.debug(`清理了 ${entries.length} 个缓存项`); - } - - /** - * 将数组分割成批次 - */ - private splitIntoBatches(items: T[], batchSize: number): T[][] { - const batches: T[][] = []; - for (let i = 0; i < items.length; i += batchSize) { - batches.push(items.slice(i, i + batchSize)); - } - return batches; - } - - /** - * 获取序列化统计信息 - */ - public getStats(): { - registeredComponents: number; - protobufAvailable: boolean; - messageTypeCacheSize: number; - componentDataCacheSize: number; - enableComponentDataCache: boolean; - maxCacheSize: number; - } { - return { - registeredComponents: this.registry.getAllComponents().size, - protobufAvailable: !!this.root, - messageTypeCacheSize: this.messageTypeCache.size, - componentDataCacheSize: this.componentDataCache.size, - enableComponentDataCache: this.enableComponentDataCache, - maxCacheSize: this.maxCacheSize - }; - } - - - - - - - - /** - * 构建protobuf定义 - */ - private buildProtoDefinitions(): void { - try { - const protoDefinition = this.registry.generateProtoDefinition(); - this.root = protobuf.parse(protoDefinition).root; - // 清空缓存,schema已更新 - this.messageTypeCache.clear(); - this.cacheAccessCount.clear(); - } catch (error) { - ProtobufSerializer.logger.error('构建protobuf定义失败:', error); - } - } - - /** - * 获取消息类型并缓存结果 - */ - private getMessageType(typeName: string): protobuf.Type | null { - if (!this.root) return null; - - // 检查缓存 - const fullTypeName = `ecs.${typeName}`; - if (this.messageTypeCache.has(fullTypeName)) { - this.cacheAccessCount.set(fullTypeName, (this.cacheAccessCount.get(fullTypeName) || 0) + 1); - return this.messageTypeCache.get(fullTypeName)!; - } - - try { - const messageType = this.root.lookupType(fullTypeName); - if (messageType) { - // 缓存MessageType - this.messageTypeCache.set(fullTypeName, messageType); - this.cacheAccessCount.set(fullTypeName, 1); - - this.cleanupCacheIfNeeded(); - return messageType; - } - return null; - } catch (error) { - ProtobufSerializer.logger.warn(`未找到消息类型: ${fullTypeName}`); - return null; - } - } - - - - - - - -} \ No newline at end of file diff --git a/packages/network/src/Serialization/SerializationTypes.ts b/packages/network/src/Serialization/SerializationTypes.ts index 61c4b617..8295cc18 100644 --- a/packages/network/src/Serialization/SerializationTypes.ts +++ b/packages/network/src/Serialization/SerializationTypes.ts @@ -1,17 +1,8 @@ -/** - * 序列化类型定义 - */ - -/** - * 序列化数据接口 - */ export interface SerializedData { - /** 序列化类型 */ - type: 'protobuf' | 'json'; - /** 组件类型名称 */ + type: 'tsrpc' | 'json'; componentType: string; - /** 序列化后的数据 */ data: Uint8Array | any; - /** 数据大小(字节) */ size: number; + schema?: string; + version?: number; } \ No newline at end of file diff --git a/packages/network/src/Serialization/TsrpcDecorators.ts b/packages/network/src/Serialization/TsrpcDecorators.ts new file mode 100644 index 00000000..3e675b3b --- /dev/null +++ b/packages/network/src/Serialization/TsrpcDecorators.ts @@ -0,0 +1,181 @@ +import 'reflect-metadata'; +import { Component, createLogger } from '@esengine/ecs-framework'; +import { + SyncFieldOptions, + TsrpcSerializableOptions, + TsrpcFieldMetadata, + TsrpcComponentMetadata, + TsrpcSupportedTypes +} from './TsrpcTypes'; + +const logger = createLogger('TsrpcDecorators'); + +const TSRPC_COMPONENT_KEY = Symbol('tsrpc:component'); +const TSRPC_FIELDS_KEY = Symbol('tsrpc:fields'); +const TSRPC_SERIALIZABLE_KEY = Symbol('tsrpc:serializable'); + +export class TsrpcRegistry { + private static _instance: TsrpcRegistry | null = null; + private _components: Map = new Map(); + private _constructors: Map = new Map(); + + public static getInstance(): TsrpcRegistry { + if (!TsrpcRegistry._instance) { + TsrpcRegistry._instance = new TsrpcRegistry(); + } + return TsrpcRegistry._instance; + } + + public register(metadata: TsrpcComponentMetadata): void { + this._components.set(metadata.componentType, metadata); + this._constructors.set(metadata.constructor, metadata); + } + + public getByName(componentType: string): TsrpcComponentMetadata | undefined { + return this._components.get(componentType); + } + + public getByConstructor(constructor: Function): TsrpcComponentMetadata | undefined { + return this._constructors.get(constructor); + } + + public getAllComponents(): TsrpcComponentMetadata[] { + return Array.from(this._components.values()); + } + + public isRegistered(constructor: Function): boolean { + return this._constructors.has(constructor); + } + + public clear(): void { + this._components.clear(); + this._constructors.clear(); + } +} + +function getTypeInfo(target: any, propertyKey: string) { + const designType = Reflect.getMetadata('design:type', target, propertyKey); + + if (!designType) { + return { typeName: 'unknown', isArray: false, isOptional: false, isUnion: false }; + } + + const typeName = designType.name || designType.toString(); + + return { + typeName: typeName.toLowerCase(), + isArray: designType === Array || typeName === 'Array', + isOptional: false, + isUnion: false, + unionTypes: [], + genericTypes: [] + }; +} + +function createTypeChecker(typeInfo: any): (value: any) => boolean { + return (value: any): boolean => { + if (value === null || value === undefined) { + return typeInfo.isOptional; + } + + switch (typeInfo.typeName) { + case 'boolean': return typeof value === 'boolean'; + case 'number': return typeof value === 'number' && !isNaN(value); + case 'string': return typeof value === 'string'; + case 'array': return Array.isArray(value); + case 'object': return typeof value === 'object' && !Array.isArray(value); + case 'date': return value instanceof Date; + default: return true; + } + }; +} + +export function SyncField(options: SyncFieldOptions = {}): PropertyDecorator { + return (target: any, propertyKey: string | symbol) => { + if (typeof propertyKey !== 'string') { + throw new Error('SyncField只支持字符串属性名'); + } + + const existingFields: Map = + Reflect.getMetadata(TSRPC_FIELDS_KEY, target.constructor) || new Map(); + + const typeInfo = getTypeInfo(target, propertyKey); + + const fieldMetadata: TsrpcFieldMetadata = { + propertyKey, + options: { priority: 'normal', authorityOnly: false, throttle: 0, delta: false, ...options }, + typeInfo, + typeChecker: createTypeChecker(typeInfo), + fieldIndex: existingFields.size + }; + + existingFields.set(propertyKey, fieldMetadata); + Reflect.defineMetadata(TSRPC_FIELDS_KEY, existingFields, target.constructor); + }; +} + +export function TsrpcSerializable(options: TsrpcSerializableOptions = {}): ClassDecorator { + return (constructor: any) => { + Reflect.defineMetadata(TSRPC_SERIALIZABLE_KEY, true, constructor); + + const fields: Map = + Reflect.getMetadata(TSRPC_FIELDS_KEY, constructor) || new Map(); + + const componentMetadata: TsrpcComponentMetadata = { + componentType: options.name || constructor.name, + options: { version: 1, validation: false, compression: false, strategy: 'auto', ...options }, + fields, + constructor, + version: options.version || 1, + createdAt: Date.now() + }; + + Reflect.defineMetadata(TSRPC_COMPONENT_KEY, componentMetadata, constructor); + + const registry = TsrpcRegistry.getInstance(); + registry.register(componentMetadata); + }; +} + +export function isTsrpcSerializable(component: any): boolean { + return Reflect.getMetadata(TSRPC_SERIALIZABLE_KEY, component.constructor) === true; +} + +export function getTsrpcMetadata(constructor: Function): TsrpcComponentMetadata | undefined { + return Reflect.getMetadata(TSRPC_COMPONENT_KEY, constructor); +} + +export function getTsrpcFields(constructor: Function): Map { + return Reflect.getMetadata(TSRPC_FIELDS_KEY, constructor) || new Map(); +} + +export function getTsrpcName(component: any): string | undefined { + const metadata = getTsrpcMetadata(component.constructor); + return metadata?.componentType; +} + +export const TsrpcString = (options: SyncFieldOptions = {}) => SyncField(options); +export const TsrpcNumber = (options: SyncFieldOptions = {}) => SyncField(options); +export const TsrpcBoolean = (options: SyncFieldOptions = {}) => SyncField(options); +export const TsrpcDate = (options: SyncFieldOptions = {}) => SyncField(options); +export const TsrpcArray = (options: SyncFieldOptions = {}) => SyncField({ ...options, delta: true }); +export const TsrpcObject = (options: SyncFieldOptions = {}) => SyncField({ ...options, delta: true }); +export const TsrpcCritical = (options: SyncFieldOptions = {}) => SyncField({ ...options, priority: 'critical' }); +export const TsrpcAuthority = (options: SyncFieldOptions = {}) => SyncField({ ...options, authorityOnly: true }); + +export function validateTsrpcComponent(component: Component): boolean { + const metadata = getTsrpcMetadata(component.constructor); + if (!metadata) return false; + + for (const [fieldName, fieldMetadata] of metadata.fields) { + const value = (component as any)[fieldName]; + if (fieldMetadata.typeChecker && !fieldMetadata.typeChecker(value)) { + return false; + } + } + return true; +} + +export function AutoSync(target: any): any { + return target; +} \ No newline at end of file diff --git a/packages/network/src/Serialization/TsrpcSerializer.ts b/packages/network/src/Serialization/TsrpcSerializer.ts new file mode 100644 index 00000000..e5f34591 --- /dev/null +++ b/packages/network/src/Serialization/TsrpcSerializer.ts @@ -0,0 +1,179 @@ +import { Component, createLogger } from '@esengine/ecs-framework'; +import { SerializedData } from './SerializationTypes'; +import { + TsrpcComponentMetadata, + TsrpcFieldMetadata, + TsrpcSerializationStats, + TsrpcSerializable +} from './TsrpcTypes'; +import { + TsrpcRegistry, + isTsrpcSerializable, + getTsrpcMetadata, + validateTsrpcComponent +} from './TsrpcDecorators'; + +const logger = createLogger('TsrpcSerializer'); +export class TsrpcSerializer { + private static _instance: TsrpcSerializer | null = null; + private _registry: TsrpcRegistry; + private _stats: TsrpcSerializationStats; + + constructor() { + this._registry = TsrpcRegistry.getInstance(); + this._stats = { + serializeCount: 0, + deserializeCount: 0, + totalSerializeTime: 0, + totalDeserializeTime: 0, + averageSerializedSize: 0, + errorCount: 0, + cacheHits: 0, + cacheMisses: 0 + }; + } + + public static getInstance(): TsrpcSerializer { + if (!TsrpcSerializer._instance) { + TsrpcSerializer._instance = new TsrpcSerializer(); + } + return TsrpcSerializer._instance; + } + + public serialize(component: Component): SerializedData | null { + if (!isTsrpcSerializable(component)) return null; + + const metadata = getTsrpcMetadata(component.constructor); + if (!metadata) return null; + + try { + const data = this.extractSerializableData(component, metadata); + const jsonString = JSON.stringify(data); + const serialized = new TextEncoder().encode(jsonString); + + this._stats.serializeCount++; + this._stats.averageSerializedSize = + (this._stats.averageSerializedSize * (this._stats.serializeCount - 1) + serialized.length) + / this._stats.serializeCount; + + return { + type: 'tsrpc', + componentType: metadata.componentType, + data: serialized, + size: serialized.length, + schema: metadata.componentType, + version: metadata.version + }; + } catch (error) { + this._stats.errorCount++; + return null; + } + } + + public deserialize( + serializedData: SerializedData, + ComponentClass?: new (...args: any[]) => T + ): T | null { + if (serializedData.type !== 'tsrpc') return null; + + let metadata: TsrpcComponentMetadata | undefined; + if (ComponentClass) { + metadata = getTsrpcMetadata(ComponentClass); + } else { + metadata = this._registry.getByName(serializedData.componentType); + } + + if (!metadata) return null; + + try { + const jsonString = new TextDecoder().decode(serializedData.data as Uint8Array); + const data = JSON.parse(jsonString); + + const component = new metadata.constructor(); + this.applySerializableData(component as T, data, metadata); + this._stats.deserializeCount++; + + return component as T; + } catch (error) { + this._stats.errorCount++; + return null; + } + } + + + private extractSerializableData(component: Component, metadata: TsrpcComponentMetadata): any { + const data: any = {}; + for (const [fieldName, fieldMetadata] of metadata.fields) { + const value = (component as any)[fieldName]; + if (value !== undefined || fieldMetadata.typeInfo.isOptional) { + data[fieldName] = this.processFieldValue(value, fieldMetadata); + } + } + return data; + } + + private applySerializableData(component: Component, data: any, metadata: TsrpcComponentMetadata): void { + for (const [fieldName, fieldMetadata] of metadata.fields) { + if (fieldName in data) { + const value = this.processFieldValue(data[fieldName], fieldMetadata, true); + (component as any)[fieldName] = value; + } + } + + if (this.isTsrpcSerializableInstance(component)) { + component.applyTsrpcData(data); + } + } + + private processFieldValue(value: any, fieldMetadata: TsrpcFieldMetadata, isDeserializing = false): any { + if (value === null || value === undefined) return value; + + const { typeInfo } = fieldMetadata; + + if (['boolean', 'number', 'string'].includes(typeInfo.typeName)) { + return value; + } + + if (typeInfo.typeName === 'date') { + return isDeserializing ? new Date(value) : value.toISOString(); + } + + if (typeInfo.isArray && Array.isArray(value)) { + return value.map(item => this.processFieldValue(item, fieldMetadata, isDeserializing)); + } + + if (typeInfo.typeName === 'object' && typeof value === 'object') { + return isDeserializing ? structuredClone(value) : value; + } + + return value; + } + + private isTsrpcSerializableInstance(component: any): component is TsrpcSerializable { + return typeof component.getTsrpcData === 'function' && + typeof component.applyTsrpcData === 'function'; + } + + public getStats(): TsrpcSerializationStats { + return { ...this._stats }; + } + + public resetStats(): void { + this._stats = { + serializeCount: 0, + deserializeCount: 0, + totalSerializeTime: 0, + totalDeserializeTime: 0, + averageSerializedSize: 0, + errorCount: 0, + cacheHits: 0, + cacheMisses: 0 + }; + } + + public getSupportedTypes(): string[] { + return this._registry.getAllComponents().map(comp => comp.componentType); + } +} + +export const tsrpcSerializer = TsrpcSerializer.getInstance(); \ No newline at end of file diff --git a/packages/network/src/Serialization/TsrpcTypes.ts b/packages/network/src/Serialization/TsrpcTypes.ts new file mode 100644 index 00000000..f663fe8a --- /dev/null +++ b/packages/network/src/Serialization/TsrpcTypes.ts @@ -0,0 +1,218 @@ +/** + * TSRPC序列化类型定义 + */ + +import { Component } from '@esengine/ecs-framework'; + +/** + * TSRPC同步字段配置选项 + */ +export interface SyncFieldOptions { + /** + * 同步优先级 + * - 'critical': 关键字段,每帧必须同步 + * - 'high': 高优先级,频繁同步 + * - 'normal': 普通优先级,正常同步频率 + * - 'low': 低优先级,较少同步 + */ + priority?: 'critical' | 'high' | 'normal' | 'low'; + + /** + * 值变化时的回调函数名 + * 回调函数签名: (oldValue: T, newValue: T) => void + */ + hook?: string; + + /** + * 是否只有拥有权限的客户端才能修改 + */ + authorityOnly?: boolean; + + /** + * 同步频率限制(毫秒) + * 防止过于频繁的网络同步,默认为0(不限制) + */ + throttle?: number; + + /** + * 是否启用增量同步 + * 对于对象和数组类型,启用后只同步变化的部分 + */ + delta?: boolean; + + /** + * 自定义比较函数 + * 用于判断值是否发生变化,默认使用深度比较 + */ + compare?: (oldValue: any, newValue: any) => boolean; +} + +/** + * TSRPC序列化组件配置选项 + */ +export interface TsrpcSerializableOptions { + /** + * 组件版本号,用于兼容性检查 + */ + version?: number; + + /** + * 是否启用类型验证 + * 在开发模式下默认启用,生产模式下默认关闭 + */ + validation?: boolean; + + /** + * 是否启用压缩 + */ + compression?: boolean; + + /** + * 自定义序列化名称,默认使用类名 + */ + name?: string; + + /** + * 序列化策略 + * - 'full': 完整序列化所有字段 + * - 'partial': 只序列化标记为@SyncField的字段 + * - 'auto': 自动检测,推荐使用 + */ + strategy?: 'full' | 'partial' | 'auto'; +} + +/** + * TSRPC字段元数据 + */ +export interface TsrpcFieldMetadata { + /** 属性名称 */ + propertyKey: string; + + /** 字段选项 */ + options: SyncFieldOptions; + + /** TypeScript类型信息 */ + typeInfo: { + /** 基本类型名 */ + typeName: string; + /** 是否为数组 */ + isArray: boolean; + /** 是否可选 */ + isOptional: boolean; + /** 是否为联合类型 */ + isUnion: boolean; + /** 联合类型成员(如果是联合类型) */ + unionTypes?: string[]; + /** 泛型参数(如果有) */ + genericTypes?: string[]; + }; + + /** 运行时类型检查函数 */ + typeChecker?: (value: any) => boolean; + + /** 字段索引(用于二进制序列化) */ + fieldIndex: number; +} + +/** + * TSRPC组件元数据 + */ +export interface TsrpcComponentMetadata { + /** 组件类型名称 */ + componentType: string; + + /** 组件配置选项 */ + options: TsrpcSerializableOptions; + + /** 字段元数据映射 */ + fields: Map; + + /** 组件构造函数 */ + constructor: new (...args: any[]) => Component; + + /** TSBuffer schema */ + schema?: any; + + /** 序列化版本 */ + version: number; + + /** 创建时间戳 */ + createdAt: number; +} + +/** + * TSRPC可序列化组件接口 + */ +export interface TsrpcSerializable { + /** 获取TSRPC序列化数据 */ + getTsrpcData(): Record; + + /** 应用TSRPC序列化数据 */ + applyTsrpcData(data: Record): void; + + /** 获取变化的字段 */ + getDirtyFields(): string[]; + + /** 标记字段为clean状态 */ + markClean(fieldNames?: string[]): void; +} + +/** + * 支持的TypeScript基本类型 + */ +export const TsrpcSupportedTypes = { + // 基本类型 + BOOLEAN: 'boolean', + NUMBER: 'number', + BIGINT: 'bigint', + STRING: 'string', + + // 对象类型 + OBJECT: 'object', + ARRAY: 'array', + DATE: 'Date', + REGEXP: 'RegExp', + + // 特殊类型 + UNDEFINED: 'undefined', + NULL: 'null', + + // 二进制类型 + UINT8ARRAY: 'Uint8Array', + BUFFER: 'Buffer', + ARRAYBUFFER: 'ArrayBuffer', + + // 联合类型和字面量类型 + UNION: 'union', + LITERAL: 'literal', + ENUM: 'enum' +} as const; + +/** + * TSRPC序列化统计信息 + */ +export interface TsrpcSerializationStats { + /** 序列化次数 */ + serializeCount: number; + + /** 反序列化次数 */ + deserializeCount: number; + + /** 总序列化时间 */ + totalSerializeTime: number; + + /** 总反序列化时间 */ + totalDeserializeTime: number; + + /** 平均序列化大小 */ + averageSerializedSize: number; + + /** 错误次数 */ + errorCount: number; + + /** 缓存命中次数 */ + cacheHits: number; + + /** 缓存未命中次数 */ + cacheMisses: number; +} \ No newline at end of file diff --git a/packages/network/src/Serialization/index.ts b/packages/network/src/Serialization/index.ts index 8dc9c017..2202694f 100644 --- a/packages/network/src/Serialization/index.ts +++ b/packages/network/src/Serialization/index.ts @@ -1,41 +1,37 @@ -/** - * 网络插件序列化系统导出 - * - * 统一导出所有protobuf序列化相关的类型、装饰器和工具函数 - */ - -// Protobuf序列化系统(用于帧同步框架) -export { - ProtobufSerializer -} from './ProtobufSerializer'; +export { SerializedData } from './SerializationTypes'; export { - ProtoSerializable, - ProtoField, - ProtoFloat, - ProtoInt32, - ProtoString, - ProtoBool, - ProtoBytes, - ProtoTimestamp, - ProtoDouble, - ProtoInt64, - ProtoStruct, - ProtoMessage, - ProtoEnum, - isProtoSerializable, - getProtoName, - ProtobufRegistry, - ProtoComponentDefinition, - ProtoFieldDefinition, - ProtoFieldType, - FieldSyncPriority, - ComponentSyncMode, - ComponentSyncOptions, - FieldSyncOptions -} from './ProtobufDecorators'; + SyncFieldOptions, + TsrpcSerializableOptions, + TsrpcFieldMetadata, + TsrpcComponentMetadata, + TsrpcSerializable, + TsrpcSupportedTypes, + TsrpcSerializationStats +} from './TsrpcTypes'; export { - SerializedData -} from './SerializationTypes'; + TsrpcRegistry, + SyncField, + TsrpcSerializable as TsrpcSerializableDecorator, + isTsrpcSerializable, + getTsrpcMetadata, + getTsrpcFields, + getTsrpcName, + validateTsrpcComponent, + TsrpcString, + TsrpcNumber, + TsrpcBoolean, + TsrpcDate, + TsrpcArray, + TsrpcObject, + TsrpcCritical, + TsrpcAuthority, + AutoSync +} from './TsrpcDecorators'; + +export { + TsrpcSerializer, + tsrpcSerializer +} from './TsrpcSerializer'; diff --git a/packages/network/src/Snapshot/SnapshotManager.ts b/packages/network/src/Snapshot/SnapshotManager.ts index fb27e7ce..8e10f576 100644 --- a/packages/network/src/Snapshot/SnapshotManager.ts +++ b/packages/network/src/Snapshot/SnapshotManager.ts @@ -1,8 +1,8 @@ import { Entity, Component, ComponentType, createLogger } from '@esengine/ecs-framework'; import { ISnapshotable, SceneSnapshot, EntitySnapshot, ComponentSnapshot, SnapshotConfig } from './ISnapshotable'; -import { ProtobufSerializer } from '../Serialization/ProtobufSerializer'; +import { TsrpcSerializer } from '../Serialization/TsrpcSerializer'; import { SerializedData } from '../Serialization/SerializationTypes'; -import { isProtoSerializable } from '../Serialization/ProtobufDecorators'; +import { isTsrpcSerializable } from '../Serialization/TsrpcDecorators'; import { NetworkComponentType, IComponentFactory, @@ -125,8 +125,8 @@ export class SnapshotManager { /** 最大缓存数量 */ private maxCacheSize: number = 10; - /** Protobuf序列化器 */ - private protobufSerializer: ProtobufSerializer; + /** TSRPC序列化器 */ + private tsrpcSerializer: TsrpcSerializer; /** 组件类型注册表 */ private componentRegistry: ComponentTypeRegistry; @@ -135,7 +135,7 @@ export class SnapshotManager { * 构造函数 */ constructor() { - this.protobufSerializer = ProtobufSerializer.getInstance(); + this.tsrpcSerializer = TsrpcSerializer.getInstance(); this.componentRegistry = ComponentTypeRegistry.Instance; } @@ -268,11 +268,11 @@ export class SnapshotManager { // 恢复组件数据 const serializedData = componentSnapshot.data as SerializedData; - if (!isProtoSerializable(component)) { - throw new Error(`[SnapshotManager] 组件 ${component.constructor.name} 不支持protobuf反序列化`); + if (!isTsrpcSerializable(component)) { + throw new Error(`[SnapshotManager] 组件 ${component.constructor.name} 不支持TSRPC反序列化`); } - this.protobufSerializer.deserialize(component, serializedData); + this.tsrpcSerializer.deserialize(serializedData); } catch (error) { SnapshotManager.logger.error(`创建组件失败: ${componentSnapshot.type}`, error); } @@ -392,33 +392,22 @@ export class SnapshotManager { */ public getCacheStats(): { snapshotCacheSize: number; - protobufStats?: { + tsrpcStats?: { registeredComponents: number; - protobufAvailable: boolean; + tsrpcAvailable: boolean; }; } { const stats: any = { snapshotCacheSize: this.snapshotCache.size }; - if (this.protobufSerializer) { - stats.protobufStats = this.protobufSerializer.getStats(); + if (this.tsrpcSerializer) { + stats.tsrpcStats = this.tsrpcSerializer.getStats(); } return stats; } - /** - * 手动初始化protobuf支持(可选,通常会自动初始化) - * - * @param protobufJs - protobuf.js库实例 - */ - public initializeProtobuf(protobufJs: any): void { - if (this.protobufSerializer) { - this.protobufSerializer.initialize(protobufJs); - SnapshotManager.logger.info('Protobuf支持已手动启用'); - } - } /** * 注册组件类型 @@ -487,7 +476,7 @@ export class SnapshotManager { /** * 创建组件快照 * - * 优先使用protobuf序列化,fallback到JSON序列化 + * 优先使用TSRPC序列化,fallback到JSON序列化 */ private createComponentSnapshot(component: Component): ComponentSnapshot | null { if (!this.isComponentSnapshotable(component)) { @@ -496,12 +485,17 @@ export class SnapshotManager { let serializedData: SerializedData; - // 优先使用protobuf序列化 - if (isProtoSerializable(component)) { + // 优先使用TSRPC序列化 + if (isTsrpcSerializable(component)) { try { - serializedData = this.protobufSerializer.serialize(component); + const tsrpcResult = this.tsrpcSerializer.serialize(component); + if (tsrpcResult) { + serializedData = tsrpcResult; + } else { + throw new Error('TSRPC序列化返回null'); + } } catch (error) { - SnapshotManager.logger.warn(`[SnapshotManager] Protobuf序列化失败,fallback到JSON: ${error}`); + SnapshotManager.logger.warn(`[SnapshotManager] TSRPC序列化失败,fallback到JSON: ${error}`); serializedData = this.createJsonSerializedData(component); } } else { @@ -626,7 +620,7 @@ export class SnapshotManager { /** * 从快照恢复组件 * - * 使用protobuf反序列化 + * 使用TSRPC反序列化 */ private restoreComponentFromSnapshot(entity: Entity, componentSnapshot: ComponentSnapshot): void { // 查找现有组件 @@ -650,9 +644,9 @@ export class SnapshotManager { // 恢复组件数据 const serializedData = componentSnapshot.data as SerializedData; - if (serializedData.type === 'protobuf' && isProtoSerializable(component)) { - // 使用protobuf反序列化 - this.protobufSerializer.deserialize(component, serializedData); + if (serializedData.type === 'tsrpc' && isTsrpcSerializable(component)) { + // 使用TSRPC反序列化 + this.tsrpcSerializer.deserialize(serializedData); } else if (serializedData.type === 'json') { // 使用JSON反序列化 this.deserializeFromJson(component, serializedData); diff --git a/packages/network/src/SyncVar/ComponentIdGenerator.ts b/packages/network/src/SyncVar/ComponentIdGenerator.ts new file mode 100644 index 00000000..96256169 --- /dev/null +++ b/packages/network/src/SyncVar/ComponentIdGenerator.ts @@ -0,0 +1,219 @@ +import { Entity, createLogger } from '@esengine/ecs-framework'; +import { NetworkEnvironment } from '../Core/NetworkEnvironment'; +import { NetworkIdentity } from '../Core/NetworkIdentity'; + +const logger = createLogger('ComponentIdGenerator'); + +/** + * 组件ID生成器配置 + */ +export interface ComponentIdGeneratorConfig { + useNetworkId: boolean; + useEntityId: boolean; + useTimestamp: boolean; + useSequence: boolean; + customPrefix?: string; +} + +/** + * 组件唯一ID生成器 + * + * 提供多种策略生成组件的唯一标识符 + * 可以集成网络ID系统、实体ID系统等 + */ +export class ComponentIdGenerator { + private static readonly DEFAULT_CONFIG: ComponentIdGeneratorConfig = { + useNetworkId: true, + useEntityId: true, + useTimestamp: true, + useSequence: true + }; + + private _config: ComponentIdGeneratorConfig; + private _sequenceCounter: number = 0; + private _generatedIds: Set = new Set(); + + constructor(config?: Partial) { + this._config = { ...ComponentIdGenerator.DEFAULT_CONFIG, ...config }; + } + + /** + * 为组件生成唯一ID + * + * @param component - 组件实例 + * @returns 唯一ID字符串 + */ + public generateId(component: any): string { + const parts: string[] = []; + + // 添加自定义前缀 + if (this._config.customPrefix) { + parts.push(this._config.customPrefix); + } + + // 添加组件类型名称 + parts.push(component.constructor.name); + + // 尝试使用网络ID + if (this._config.useNetworkId) { + const networkId = this.extractNetworkId(component); + if (networkId) { + parts.push(`net_${networkId}`); + } + } + + // 尝试使用实体ID + if (this._config.useEntityId) { + const entityId = this.extractEntityId(component); + if (entityId !== null) { + parts.push(`ent_${entityId}`); + } + } + + // 添加环境前缀 + const env = NetworkEnvironment.isServer ? 's' : 'c'; + parts.push(env); + + // 添加时间戳 + if (this._config.useTimestamp) { + parts.push(Date.now().toString(36)); + } + + // 添加序列号 + if (this._config.useSequence) { + parts.push((++this._sequenceCounter).toString(36)); + } + + // 生成基础ID + let baseId = parts.join('_'); + + // 确保ID唯一性 + let finalId = baseId; + let counter = 0; + while (this._generatedIds.has(finalId)) { + finalId = `${baseId}_${counter}`; + counter++; + } + + // 记录生成的ID + this._generatedIds.add(finalId); + + logger.debug(`为组件 ${component.constructor.name} 生成ID: ${finalId}`); + return finalId; + } + + /** + * 从组件中提取网络ID + * + * @param component - 组件实例 + * @returns 网络ID或null + */ + private extractNetworkId(component: any): string | null { + try { + // 检查组件是否有网络身份 + if (component.networkIdentity && component.networkIdentity instanceof NetworkIdentity) { + return component.networkIdentity.networkId; + } + + // 检查组件的实体是否有网络身份 + if (component.entity) { + const networkIdentity = component.entity.getComponent(NetworkIdentity); + if (networkIdentity) { + return networkIdentity.networkId; + } + } + + // 检查组件本身是否有networkId属性 + if (component.networkId && typeof component.networkId === 'string') { + return component.networkId; + } + + return null; + } catch (error) { + logger.debug('提取网络ID时出错:', error); + return null; + } + } + + /** + * 从组件中提取实体ID + * + * @param component - 组件实例 + * @returns 实体ID或null + */ + private extractEntityId(component: any): number | null { + try { + // 检查组件是否有实体引用 + if (component.entity && component.entity instanceof Entity) { + return component.entity.id; + } + + // 检查组件本身是否有entityId属性 + if (typeof component.entityId === 'number') { + return component.entityId; + } + + return null; + } catch (error) { + logger.debug('提取实体ID时出错:', error); + return null; + } + } + + /** + * 检查ID是否已经生成过 + * + * @param id - 要检查的ID + * @returns 是否已存在 + */ + public hasGenerated(id: string): boolean { + return this._generatedIds.has(id); + } + + /** + * 清理已生成的ID记录 + * + * @param maxAge - 最大保留时间(毫秒),默认1小时 + */ + public cleanup(maxAge: number = 3600000): void { + // 避免未使用参数警告 + void maxAge; + // 简单实现:清空所有记录 + // 在实际应用中,可以根据时间戳进行更精细的清理 + this._generatedIds.clear(); + this._sequenceCounter = 0; + logger.debug('已清理ID生成器缓存'); + } + + /** + * 获取统计信息 + */ + public getStats(): { + generatedCount: number; + sequenceCounter: number; + config: ComponentIdGeneratorConfig; + } { + return { + generatedCount: this._generatedIds.size, + sequenceCounter: this._sequenceCounter, + config: { ...this._config } + }; + } + + /** + * 重置生成器状态 + */ + public reset(): void { + this._generatedIds.clear(); + this._sequenceCounter = 0; + logger.debug('ID生成器已重置'); + } + + /** + * 更新配置 + */ + public updateConfig(newConfig: Partial): void { + this._config = { ...this._config, ...newConfig }; + logger.debug('ID生成器配置已更新'); + } +} \ No newline at end of file diff --git a/packages/network/src/SyncVar/SyncVarAuthority.ts b/packages/network/src/SyncVar/SyncVarAuthority.ts new file mode 100644 index 00000000..98359718 --- /dev/null +++ b/packages/network/src/SyncVar/SyncVarAuthority.ts @@ -0,0 +1,318 @@ +import { AUTHORITY_CONFIG, SYNCVAR_CONFIG } from '../constants/NetworkConstants'; +import { NetworkEnvironment } from '../Core/NetworkEnvironment'; +import { NetworkIdentity } from '../Core/NetworkIdentity'; +import { INetworkComponent, INetworkEntity, AuthorityContext as CoreAuthorityContext, AuthorityType as CoreAuthorityType, NetworkEnvironmentType } from '../types/CoreTypes'; + +const logger = { + info: console.log, + warn: console.warn, + error: console.error, + debug: console.debug +}; + +/** 重新导出核心权限类型 */ +export { AuthorityType } from '../types/CoreTypes'; + +/** 重新导出核心权限上下文 */ +export { AuthorityContext } from '../types/CoreTypes'; + +/** + * 权限规则 + */ +export interface AuthorityRule { + /** 规则名称 */ + name: string; + /** 规则检查函数 */ + check: (component: T, context: CoreAuthorityContext) => boolean; + /** 优先级(数值越大优先级越高) */ + priority: number; + /** 是否启用 */ + enabled: boolean; +} + +/** + * SyncVar权限管理器 + * + * 提供灵活的权限检查机制,支持多种权限策略 + */ +export class SyncVarAuthorityManager { + private static _instance: SyncVarAuthorityManager | null = null; + private _rules: AuthorityRule[] = []; + private _cache: Map = new Map(); + private _cacheTimeout: number = SYNCVAR_CONFIG.DEFAULT_CACHE_TIMEOUT; + + private constructor() { + this.initializeDefaultRules(); + } + + public static get Instance(): SyncVarAuthorityManager { + if (!SyncVarAuthorityManager._instance) { + SyncVarAuthorityManager._instance = new SyncVarAuthorityManager(); + } + return SyncVarAuthorityManager._instance; + } + + /** + * 初始化默认权限规则 + */ + private initializeDefaultRules(): void { + // 服务端权限规则 + this.addRule({ + name: 'server-full-authority', + check: (component, context) => context.environment === 'server', + priority: AUTHORITY_CONFIG.SERVER_AUTHORITY_PRIORITY, + enabled: true + }); + + // 网络身份权限规则 + this.addRule({ + name: 'network-identity-authority', + check: (component, context) => { + if (context.environment !== 'client') return false; + + const networkIdentity = this.extractNetworkIdentity(component); + if (!networkIdentity) return false; + + // 客户端只能控制属于自己的网络对象 + return networkIdentity.hasAuthority && + networkIdentity.ownerId === context.clientId; + }, + priority: AUTHORITY_CONFIG.NETWORK_IDENTITY_PRIORITY, + enabled: true + }); + + // 组件自定义权限规则 + this.addRule({ + name: 'component-custom-authority', + check: (component: any, context) => { + if (typeof component.hasAuthority === 'function') { + return component.hasAuthority(context); + } + if (typeof component.checkAuthority === 'function') { + return component.checkAuthority(context); + } + return false; + }, + priority: AUTHORITY_CONFIG.COMPONENT_CUSTOM_PRIORITY, + enabled: true + }); + + // 实体所有者权限规则 + this.addRule({ + name: 'entity-owner-authority', + check: (component, context) => { + if (context.environment !== 'client') return false; + + const entity = component.entity; + if (!entity) return false; + + // 检查实体的所有者信息 + if ((entity as INetworkEntity).ownerId && (entity as INetworkEntity).ownerId === context.clientId) { + return true; + } + + return false; + }, + priority: AUTHORITY_CONFIG.ENTITY_OWNER_PRIORITY, + enabled: true + }); + + // 默认拒绝规则 + this.addRule({ + name: 'default-deny', + check: () => false, + priority: AUTHORITY_CONFIG.DEFAULT_DENY_PRIORITY, + enabled: true + }); + } + + /** + * 检查组件权限 + * + * @param component - 组件实例 + * @param clientId - 客户端ID(可选) + * @returns 是否有权限 + */ + public hasAuthority(component: T, clientId?: string): boolean { + const context = this.createContext(component, clientId); + const cacheKey = this.generateCacheKey(component, context); + + // 检查缓存 + const cached = this._cache.get(cacheKey); + if (cached && (Date.now() - cached.timestamp) < this._cacheTimeout) { + return cached.result; + } + + // 执行权限检查 + const result = this.checkAuthority(component, context); + + // 缓存结果 + this._cache.set(cacheKey, { + result, + timestamp: Date.now() + }); + + logger.debug(`权限检查结果: ${component.constructor.name} -> ${result}`); + return result; + } + + /** + * 执行权限检查 + */ + private checkAuthority(component: T, context: CoreAuthorityContext): boolean { + const enabledRules = this._rules + .filter(rule => rule.enabled) + .sort((a, b) => b.priority - a.priority); + + for (const rule of enabledRules) { + try { + const result = rule.check(component, context); + if (result) { + logger.debug(`权限规则 "${rule.name}" 通过`); + return true; + } + } catch (error) { + logger.warn(`权限规则 "${rule.name}" 执行失败:`, error); + } + } + + logger.debug('所有权限规则都不匹配,拒绝权限'); + return false; + } + + /** + * 创建权限上下文 + */ + private createContext(component: T, clientId?: string): CoreAuthorityContext { + const networkIdentity = this.extractNetworkIdentity(component); + const entity = component.entity; + + return { + environment: NetworkEnvironment.isServer ? 'server' : 'client' as NetworkEnvironmentType, + networkId: networkIdentity?.networkId, + entityId: entity?.id, + clientId, + level: CoreAuthorityType.ReadWrite, + timestamp: Date.now(), + metadata: {} + }; + } + + /** + * 提取网络身份 + */ + private extractNetworkIdentity(component: T): NetworkIdentity | null { + try { + // 直接检查组件的网络身份 + if ((component as any).networkIdentity instanceof NetworkIdentity) { + return (component as any).networkIdentity; + } + + // 检查组件实体的网络身份 + if (component.entity) { + const networkIdentity = (component.entity as any).getComponent?.(NetworkIdentity); + if (networkIdentity) { + return networkIdentity; + } + } + + return null; + } catch (error) { + logger.debug('提取网络身份时出错:', error); + return null; + } + } + + /** + * 生成缓存键 + */ + private generateCacheKey(component: T, context: CoreAuthorityContext): string { + const parts = [ + component.constructor.name, + context.environment, + context.networkId || 'no-net-id', + context.clientId || 'no-client-id' + ]; + return parts.join('|'); + } + + /** + * 添加权限规则 + */ + public addRule(rule: AuthorityRule): void { + this._rules.push(rule); + this._rules.sort((a, b) => b.priority - a.priority); + this.clearCache(); + logger.debug(`添加权限规则: ${rule.name} (优先级: ${rule.priority})`); + } + + /** + * 移除权限规则 + */ + public removeRule(name: string): boolean { + const index = this._rules.findIndex(rule => rule.name === name); + if (index !== -1) { + this._rules.splice(index, 1); + this.clearCache(); + logger.debug(`移除权限规则: ${name}`); + return true; + } + return false; + } + + /** + * 启用/禁用权限规则 + */ + public setRuleEnabled(name: string, enabled: boolean): boolean { + const rule = this._rules.find(rule => rule.name === name); + if (rule) { + rule.enabled = enabled; + this.clearCache(); + logger.debug(`${enabled ? '启用' : '禁用'}权限规则: ${name}`); + return true; + } + return false; + } + + /** + * 获取所有权限规则 + */ + public getRules(): AuthorityRule[] { + return [...this._rules]; + } + + /** + * 清除权限缓存 + */ + public clearCache(): void { + this._cache.clear(); + logger.debug('权限缓存已清除'); + } + + /** + * 设置缓存超时时间 + */ + public setCacheTimeout(timeout: number): void { + this._cacheTimeout = timeout; + logger.debug(`权限缓存超时时间设为: ${timeout}ms`); + } + + /** + * 获取权限检查统计 + */ + public getStats(): { + rulesCount: number; + cacheSize: number; + cacheTimeout: number; + enabledRules: string[]; + } { + return { + rulesCount: this._rules.length, + cacheSize: this._cache.size, + cacheTimeout: this._cacheTimeout, + enabledRules: this._rules + .filter(rule => rule.enabled) + .map(rule => rule.name) + }; + } +} \ No newline at end of file diff --git a/packages/network/src/SyncVar/SyncVarManager.ts b/packages/network/src/SyncVar/SyncVarManager.ts index 76113a87..66e62864 100644 --- a/packages/network/src/SyncVar/SyncVarManager.ts +++ b/packages/network/src/SyncVar/SyncVarManager.ts @@ -12,7 +12,10 @@ import { NetworkComponentType, TypeGuards } from '../types/NetworkTypes'; -import { createLogger } from '@esengine/ecs-framework'; +import { SYNCVAR_CONFIG } from '../constants/NetworkConstants'; +import { INetworkComponent } from '../types/CoreTypes'; +import { ComponentIdGenerator } from './ComponentIdGenerator'; +import { SyncVarAuthorityManager } from './SyncVarAuthority'; /** * SyncVar变化记录 @@ -84,7 +87,12 @@ export interface SyncVarSyncData { */ export class SyncVarManager { private static _instance: SyncVarManager | null = null; - private static readonly logger = createLogger('SyncVarManager'); + private static readonly logger = { + info: console.log, + warn: console.warn, + error: console.error, + debug: console.debug + }; /** * 组件实例的SyncVar变化监听器 @@ -116,7 +124,7 @@ export class SyncVarManager { * @param component - 网络组件实例 * @returns 是否成功初始化 */ - public initializeComponent(component: INetworkSyncable): boolean { + public initializeComponent(component: T): boolean { const componentId = this.getComponentId(component); const metadata = getSyncVarMetadata(component.constructor as NetworkComponentType); @@ -152,7 +160,7 @@ export class SyncVarManager { * * @param component - 网络组件实例 */ - public cleanupComponent(component: INetworkSyncable): void { + public cleanupComponent(component: T): void { const componentId = this.getComponentId(component); this._componentChanges.delete(componentId); this._lastSyncTimes.delete(componentId); @@ -166,8 +174,8 @@ export class SyncVarManager { * @param oldValue - 旧值 * @param newValue - 新值 */ - public recordChange( - component: INetworkSyncable, + public recordChange( + component: T, propertyKey: string, oldValue: SyncVarValue, newValue: SyncVarValue @@ -243,7 +251,7 @@ export class SyncVarManager { * @param component - 组件实例 * @returns 待同步的变化数组 */ - public getPendingChanges(component: any): SyncVarChange[] { + public getPendingChanges(component: T): SyncVarChange[] { const componentId = this.getComponentId(component); const changes = this._componentChanges.get(componentId) || []; return changes.filter(change => change.needsSync); @@ -255,7 +263,7 @@ export class SyncVarManager { * @param component - 组件实例 * @param propertyKeys - 要清除的属性名数组,如果不提供则清除所有 */ - public clearChanges(component: any, propertyKeys?: string[]): void { + public clearChanges(component: T, propertyKeys?: string[]): void { const componentId = this.getComponentId(component); const changes = this._componentChanges.get(componentId); @@ -279,7 +287,7 @@ export class SyncVarManager { * @param component - 组件实例 * @returns 同步数据 */ - public createSyncData(component: any): SyncVarSyncData | null { + public createSyncData(component: T): SyncVarSyncData | null { const pendingChanges = this.getPendingChanges(component); if (pendingChanges.length === 0) { @@ -317,7 +325,7 @@ export class SyncVarManager { * @param component - 组件实例 * @param syncData - 同步数据 */ - public applySyncData(component: any, syncData: SyncVarSyncData): void { + public applySyncData(component: T, syncData: SyncVarSyncData): void { const metadata = getSyncVarMetadata(component.constructor); const metadataMap = new Map(metadata.map(m => [m.fieldNumber, m])); @@ -345,16 +353,31 @@ export class SyncVarManager { } } + /** + * ID生成器实例 + */ + private static _idGenerator: ComponentIdGenerator | null = null; + + /** + * 获取ID生成器实例 + */ + private static getIdGenerator(): ComponentIdGenerator { + if (!SyncVarManager._idGenerator) { + SyncVarManager._idGenerator = new ComponentIdGenerator(); + } + return SyncVarManager._idGenerator; + } + /** * 生成组件的唯一ID * * @param component - 组件实例 * @returns 唯一ID */ - private getComponentId(component: any): string { - // 简单实现,将来可以集成网络ID系统 + private getComponentId(component: T): string { if (!component._syncVarId) { - component._syncVarId = `${component.constructor.name}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const idGenerator = SyncVarManager.getIdGenerator(); + component._syncVarId = idGenerator.generateId(component); } return component._syncVarId; } @@ -366,7 +389,7 @@ export class SyncVarManager { * @param b - 值B * @returns 是否相等 */ - private isValueEqual(a: any, b: any): boolean { + private isValueEqual(a: unknown, b: unknown): boolean { // 基础类型比较 if (typeof a !== typeof b) { return false; @@ -378,39 +401,33 @@ export class SyncVarManager { // 对象比较(浅比较) if (typeof a === 'object' && a !== null && b !== null) { - const keysA = Object.keys(a); - const keysB = Object.keys(b); + const keysA = Object.keys(a as Record); + const keysB = Object.keys(b as Record); if (keysA.length !== keysB.length) { return false; } - return keysA.every(key => a[key] === b[key]); + return keysA.every(key => (a as Record)[key] === (b as Record)[key]); } return false; } + /** + * 权限管理器实例 + */ + private _authorityManager: SyncVarAuthorityManager = SyncVarAuthorityManager.Instance; + /** * 检查组件是否有修改权限 * * @param component - 组件实例 + * @param clientId - 客户端ID(可选) * @returns 是否有权限 */ - private hasAuthority(component: any): boolean { - // 简单实现:服务端始终有权限 - if (NetworkEnvironment.isServer) { - return true; - } - - // 客户端检查组件的权限设置 - // 如果组件有hasAuthority方法,使用它;否则默认客户端没有权限 - if (typeof component.hasAuthority === 'function') { - return component.hasAuthority(); - } - - // 默认情况下,客户端对权威字段没有权限 - return false; + private hasAuthority(component: T, clientId?: string): boolean { + return this._authorityManager.hasAuthority(component as unknown as INetworkComponent, clientId); } /** @@ -420,7 +437,7 @@ export class SyncVarManager { * @param metadata - SyncVar元数据 * @returns 是否应该同步 */ - private shouldSync(component: any, metadata: SyncVarMetadata): boolean { + private shouldSync(component: T, metadata: SyncVarMetadata): boolean { // 权限检查:权威字段只有在有权限时才同步 if (metadata.options.authorityOnly && !this.hasAuthority(component)) { SyncVarManager.logger.debug(`字段 ${metadata.propertyKey} 是权威字段,但当前没有权限,跳过同步`); @@ -449,7 +466,7 @@ export class SyncVarManager { * @param oldValue - 旧值 * @param newValue - 新值 */ - private triggerHook(component: any, metadata: SyncVarMetadata, oldValue: any, newValue: any): void { + private triggerHook(component: T, metadata: SyncVarMetadata, oldValue: unknown, newValue: unknown): void { if (!metadata.options.hook) { return; } @@ -472,7 +489,7 @@ export class SyncVarManager { * @param value - 值 * @returns 序列化数据 */ - private serializeValue(component: any, propertyKey: string, value: any): Uint8Array { + private serializeValue(component: T, propertyKey: string, value: unknown): Uint8Array { const metadata = getSyncVarMetadataForProperty(component, propertyKey); if (metadata?.options.serializer) { @@ -490,7 +507,7 @@ export class SyncVarManager { * @param data - 序列化数据 * @returns 反序列化的值 */ - private deserializeValue(component: any, propertyKey: string, data: Uint8Array): any { + private deserializeValue(component: T, propertyKey: string, data: Uint8Array): unknown { const metadata = getSyncVarMetadataForProperty(component, propertyKey); if (metadata?.options.deserializer) { @@ -503,7 +520,7 @@ export class SyncVarManager { /** * 将值序列化为二进制数据 */ - private serializeValueToBinary(value: any): Uint8Array { + private serializeValueToBinary(value: unknown): Uint8Array { if (value === null || value === undefined) { return new Uint8Array([0]); } @@ -543,7 +560,7 @@ export class SyncVarManager { /** * 从二进制数据反序列化值 */ - private deserializeValueFromBinary(data: Uint8Array): any { + private deserializeValueFromBinary(data: Uint8Array): unknown { if (data.length === 0) return null; const view = new DataView(data.buffer, data.byteOffset); @@ -574,13 +591,13 @@ export class SyncVarManager { * @param propertyKey - 属性名 * @param value - 值 */ - private setValueDirectly(component: any, propertyKey: string, value: any): void { + private setValueDirectly(component: T, propertyKey: string, value: unknown): void { // 临时禁用代理监听 const originalValue = component._syncVarDisabled; component._syncVarDisabled = true; try { - component[propertyKey] = value; + (component as Record)[propertyKey] = value; } finally { component._syncVarDisabled = originalValue; } @@ -596,8 +613,8 @@ export class SyncVarManager { * @param isFullSync - 是否是完整同步 * @returns SyncVar更新消息,如果没有待同步的变化则返回null */ - public createSyncVarUpdateMessage( - component: any, + public createSyncVarUpdateMessage( + component: T, networkId: string = '', senderId: string = '', syncSequence: number = 0, @@ -653,7 +670,7 @@ export class SyncVarManager { * @param component - 组件实例 * @param message - SyncVar更新消息 */ - public applySyncVarUpdateMessage(component: any, message: SyncVarUpdateMessage): void { + public applySyncVarUpdateMessage(component: T, message: SyncVarUpdateMessage): void { if (message.componentType !== component.constructor.name) { SyncVarManager.logger.warn(`组件类型不匹配: 期望 ${component.constructor.name}, 收到 ${message.componentType}`); return; @@ -705,8 +722,8 @@ export class SyncVarManager { * @param syncSequence - 同步序号 * @returns SyncVar更新消息数组 */ - public createBatchSyncVarUpdateMessages( - components: any[], + public createBatchSyncVarUpdateMessages( + components: T[], networkIds: string[] = [], senderId: string = '', syncSequence: number = 0 @@ -738,7 +755,7 @@ export class SyncVarManager { * @param components - 组件数组 * @returns 有待同步变化的组件数组 */ - public filterComponentsWithChanges(components: any[]): any[] { + public filterComponentsWithChanges(components: T[]): T[] { return components.filter(component => { const pendingChanges = this.getPendingChanges(component); return pendingChanges.length > 0; @@ -751,7 +768,7 @@ export class SyncVarManager { * @param component - 组件实例 * @returns 变化统计信息 */ - public getComponentChangeStats(component: any): { + public getComponentChangeStats(component: T): { totalChanges: number; pendingChanges: number; lastChangeTime: number; diff --git a/packages/network/src/SyncVar/SyncVarSyncScheduler.ts b/packages/network/src/SyncVar/SyncVarSyncScheduler.ts index a38e7ca8..949d0f8c 100644 --- a/packages/network/src/SyncVar/SyncVarSyncScheduler.ts +++ b/packages/network/src/SyncVar/SyncVarSyncScheduler.ts @@ -475,4 +475,211 @@ export class SyncVarSyncScheduler { public get isRunning(): boolean { return this._isRunning; } + + /** + * 智能调度配置 + * 根据网络状况动态调整同步策略 + */ + public applyNetworkAdaptiveConfig(adaptiveConfig: { + suggestedSyncInterval: number; + suggestedBatchSize: number; + prioritizeUpdate: boolean; + }): void { + const oldConfig = { ...this._config }; + + // 应用建议的同步间隔,但不超出安全范围 + this._config.syncInterval = Math.max( + Math.min(adaptiveConfig.suggestedSyncInterval, 1000), // 最大1秒 + this._config.minSyncInterval // 最小间隔 + ); + + // 应用建议的批处理大小 + this._config.maxBatchSize = Math.max( + Math.min(adaptiveConfig.suggestedBatchSize, 100), // 最大100 + 1 // 最小1 + ); + + // 根据优先级更新标志调整其他参数 + if (adaptiveConfig.prioritizeUpdate) { + // 高优先级模式:减少每帧处理对象数,确保重要更新及时处理 + this._config.maxObjectsPerFrame = Math.max( + Math.floor(this._config.maxObjectsPerFrame * 0.7), + 10 + ); + this._config.enablePrioritySort = true; + } else { + // 正常模式:恢复标准设置 + this._config.maxObjectsPerFrame = 50; + } + + SyncVarSyncScheduler.logger.info('应用网络自适应配置:', { + oldInterval: oldConfig.syncInterval, + newInterval: this._config.syncInterval, + oldBatchSize: oldConfig.maxBatchSize, + newBatchSize: this._config.maxBatchSize, + prioritizeMode: adaptiveConfig.prioritizeUpdate + }); + + // 重启调度器以应用新配置 + if (this._isRunning) { + this.stop(); + this.start(); + } + } + + /** + * 网络拥塞控制 + * 根据拥塞状态调整同步行为 + */ + public applyCongestionControl(congestionInfo: { + isCongested: boolean; + congestionLevel: 'none' | 'light' | 'moderate' | 'severe'; + }): void { + if (!congestionInfo.isCongested) { + // 无拥塞:可以恢复正常配置 + this.resetToOptimalConfig(); + return; + } + + const emergencyConfig = { ...this._config }; + + switch (congestionInfo.congestionLevel) { + case 'light': + // 轻微拥塞:适度降低频率 + emergencyConfig.syncInterval = Math.max(this._config.syncInterval * 1.2, 80); + emergencyConfig.maxBatchSize = Math.min(this._config.maxBatchSize * 1.5, 15); + break; + + case 'moderate': + // 中度拥塞:显著降低频率,增加批处理 + emergencyConfig.syncInterval = Math.max(this._config.syncInterval * 1.5, 120); + emergencyConfig.maxBatchSize = Math.min(this._config.maxBatchSize * 2, 25); + emergencyConfig.maxObjectsPerFrame = Math.max( + Math.floor(this._config.maxObjectsPerFrame * 0.6), + 15 + ); + break; + + case 'severe': + // 严重拥塞:最保守策略 + emergencyConfig.syncInterval = Math.max(this._config.syncInterval * 2, 200); + emergencyConfig.maxBatchSize = Math.min(this._config.maxBatchSize * 3, 50); + emergencyConfig.maxObjectsPerFrame = Math.max( + Math.floor(this._config.maxObjectsPerFrame * 0.4), + 10 + ); + emergencyConfig.enablePrioritySort = true; // 强制启用优先级排序 + break; + } + + this._config = emergencyConfig; + + SyncVarSyncScheduler.logger.warn(`应用拥塞控制策略 [${congestionInfo.congestionLevel}]:`, { + syncInterval: this._config.syncInterval, + maxBatchSize: this._config.maxBatchSize, + maxObjectsPerFrame: this._config.maxObjectsPerFrame + }); + + // 重启调度器以应用拥塞控制配置 + if (this._isRunning) { + this.stop(); + this.start(); + } + } + + /** + * 重置到优化配置 + * 当网络状况改善时调用 + */ + private resetToOptimalConfig(): void { + const optimalConfig: SyncVarSyncConfig = { + syncInterval: 50, // 20fps + maxBatchSize: 10, + maxObjectsPerFrame: 50, + enablePrioritySort: true, + minSyncInterval: 16, // 最小16ms (60fps) + enableIncrementalSync: true + }; + + const configChanged = ( + this._config.syncInterval !== optimalConfig.syncInterval || + this._config.maxBatchSize !== optimalConfig.maxBatchSize || + this._config.maxObjectsPerFrame !== optimalConfig.maxObjectsPerFrame + ); + + if (configChanged) { + this._config = optimalConfig; + SyncVarSyncScheduler.logger.info('恢复到优化配置'); + + if (this._isRunning) { + this.stop(); + this.start(); + } + } + } + + /** + * 智能批处理优化 + * 基于网络状况和组件变化频率动态调整批处理策略 + */ + public optimizeBatching(): void { + // 分析最近的同步统计 + const recentSuccessRate = this.calculateRecentSuccessRate(); + const avgProcessingTime = this._stats.averageCycleTime; + + // 根据成功率调整批处理大小 + if (recentSuccessRate < 0.8) { + // 成功率低,减少批处理大小 + this._config.maxBatchSize = Math.max( + Math.floor(this._config.maxBatchSize * 0.8), + 2 + ); + SyncVarSyncScheduler.logger.debug(`批处理优化: 降低批大小到 ${this._config.maxBatchSize} (成功率: ${recentSuccessRate.toFixed(2)})`); + } else if (recentSuccessRate > 0.95 && avgProcessingTime < this._config.syncInterval * 0.5) { + // 成功率高且处理时间充足,可以增加批处理大小 + this._config.maxBatchSize = Math.min( + Math.floor(this._config.maxBatchSize * 1.1), + 30 + ); + SyncVarSyncScheduler.logger.debug(`批处理优化: 提升批大小到 ${this._config.maxBatchSize}`); + } + } + + /** + * 计算最近的成功率 + * 这里简化实现,实际可以基于更复杂的统计 + */ + private calculateRecentSuccessRate(): number { + // 简化实现:基于错误率计算成功率 + const totalOperations = this._stats.totalSyncCycles; + if (totalOperations === 0) return 1.0; + + const errorRate = this._stats.errors / totalOperations; + return Math.max(0, 1 - errorRate); + } + + /** + * 动态优先级调整 + * 根据组件活跃度和网络状况调整优先级计算 + */ + public updatePriorityStrategy(networkQuality: number): void { + if (networkQuality >= 80) { + // 高质量网络:可以使用复杂的优先级计算 + this._priorityCalculator = new DefaultSyncPriorityCalculator(); + } else { + // 低质量网络:使用简化的优先级计算,减少CPU开销 + this._priorityCalculator = new SimplifiedPriorityCalculator(); + } + } +} + +/** + * 简化优先级计算器 + * 在网络状况不佳时使用,减少计算开销 + */ +class SimplifiedPriorityCalculator implements ISyncPriorityCalculator { + public calculatePriority(component: any, identity: NetworkIdentity): number { + // 简化的优先级计算:仅基于权威性 + return identity.hasAuthority ? 10 : 5; + } } \ No newline at end of file diff --git a/packages/network/src/SyncVar/index.ts b/packages/network/src/SyncVar/index.ts index 05e1aab5..a45156d8 100644 --- a/packages/network/src/SyncVar/index.ts +++ b/packages/network/src/SyncVar/index.ts @@ -38,4 +38,5 @@ export { } from './SyncVarOptimizer'; export type { SyncVarOptimizationConfig -} from './SyncVarOptimizer'; \ No newline at end of file +} from './SyncVarOptimizer'; + diff --git a/packages/network/src/TSRPC/TsrpcClient.ts b/packages/network/src/TSRPC/TsrpcClient.ts new file mode 100644 index 00000000..3b333c9e --- /dev/null +++ b/packages/network/src/TSRPC/TsrpcClient.ts @@ -0,0 +1,383 @@ +/** + * TSRPC 客户端 + */ +import { WsClient } from 'tsrpc'; +import { ServiceType, serviceProto } from './protocols/serviceProto'; +import { Component } from '@esengine/ecs-framework'; +import { getTsrpcMetadata } from '../Serialization/TsrpcDecorators'; +import { INetworkSyncable, MessageData } from '../types/NetworkTypes'; +import { ConnectionState, ITypedEventEmitter, NetworkEventHandlers } from '../types/CoreTypes'; +import { TSRPC_CONFIG, NETWORK_CONFIG } from '../constants/NetworkConstants'; + +const logger = { + info: console.log, + warn: console.warn, + error: console.error, + debug: console.debug +}; + +export class TsrpcNetworkClient { + private _client: WsClient; + private _playerId?: number; + private _roomId?: string; + private _componentUpdateCallbacks = new Map void>(); + private _connectionState: ConnectionState = 'disconnected'; + private _reconnectAttempts: number = 0; + private _maxReconnectAttempts: number = NETWORK_CONFIG.DEFAULT_MAX_RECONNECT_ATTEMPTS; + private _reconnectDelay: number = NETWORK_CONFIG.DEFAULT_RECONNECT_DELAY; + private _lastPingTime: number = 0; + private _rtt: number = 0; + private _eventHandlers = new Map(); + + constructor(serverUrl: string = TSRPC_CONFIG.DEFAULT_SERVER_URL) { + this._client = new WsClient(serviceProto, { + server: serverUrl, + // JSON兼容模式 + json: false, + // 自动重连 + heartbeat: { + interval: TSRPC_CONFIG.DEFAULT_HEARTBEAT.interval, + timeout: TSRPC_CONFIG.DEFAULT_HEARTBEAT.timeout + } + }); + + this.setupMessageHandlers(); + this.setupEvents(); + } + + private setupMessageHandlers() { + // 监听组件更新消息 + this._client.listenMsg('ComponentUpdate', (msg) => { + const { entityId, componentType, componentData, timestamp } = msg; + + logger.debug(`收到组件更新: Entity ${entityId}, Component ${componentType}`); + + const callback = this._componentUpdateCallbacks.get(componentType); + if (callback) { + callback(entityId, componentData); + } + }); + } + + private setupEvents() { + // 连接成功 + this._client.flows.postConnectFlow.push((conn) => { + this._connectionState = 'connected'; + this._reconnectAttempts = 0; + logger.info('已连接到TSRPC服务器'); + this.emit('connected'); + return conn; + }); + + // 连接断开 + this._client.flows.postDisconnectFlow.push((data) => { + this._connectionState = 'disconnected'; + logger.info('与TSRPC服务器断开连接'); + this.emit('disconnected', data?.reason || 'Unknown disconnect reason'); + + // 自动重连 + if (this._reconnectAttempts < this._maxReconnectAttempts) { + this.attemptReconnect(); + } + + return data; + }); + } + + /** + * 连接到服务器 + */ + async connect(): Promise { + this._connectionState = 'connecting'; + this.emit('connecting'); + + const result = await this._client.connect(); + if (result.isSucc) { + this._connectionState = 'connected'; + return true; + } else { + this._connectionState = 'disconnected'; + this.emit('connectError', new Error(result.errMsg || 'Connection failed')); + return false; + } + } + + /** + * 断开连接 + */ + disconnect() { + this._client.disconnect(); + } + + /** + * 加入房间 + */ + async joinRoom(roomId: string, playerName?: string): Promise { + try { + const result = await this._client.callApi('JoinRoom', { + roomId, + playerName + }); + + if (result.isSucc && result.res.success) { + this._playerId = result.res.playerId; + this._roomId = roomId; + logger.info(`成功加入房间 ${roomId}, 玩家ID: ${this._playerId}`); + return true; + } else { + logger.error('加入房间失败:', result.isSucc ? result.res.errorMsg : result.err); + return false; + } + } catch (error) { + logger.error('加入房间异常:', error); + return false; + } + } + + /** + * 同步组件到服务器 + */ + async syncComponent(entityId: number, component: T): Promise { + try { + // 获取组件的TSRPC元数据 + const metadata = getTsrpcMetadata(component.constructor); + if (!metadata) { + logger.error(`组件 ${component.constructor.name} 不支持TSRPC同步`); + return false; + } + + // 提取组件数据 + const componentData: Record = {}; + for (const [fieldName] of metadata.fields) { + componentData[fieldName] = component[fieldName]; + } + + const result = await this._client.callApi('SyncComponent', { + entityId, + componentType: metadata.componentType, + componentData, + timestamp: Date.now() + }); + + if (result.isSucc && result.res.success) { + logger.debug(`组件同步成功: Entity ${entityId}, Component ${metadata.componentType}`); + return true; + } else { + logger.error('组件同步失败:', result.isSucc ? result.res.errorMsg : result.err); + return false; + } + } catch (error) { + logger.error('同步组件异常:', error); + return false; + } + } + + /** + * 注册组件更新回调 + */ + onComponentUpdate(componentType: string, callback: (entityId: number, data: MessageData) => void) { + this._componentUpdateCallbacks.set(componentType, callback); + } + + /** + * 取消组件更新回调 + */ + offComponentUpdate(componentType: string) { + this._componentUpdateCallbacks.delete(componentType); + } + + /** + * 获取客户端状态 + */ + get isConnected(): boolean { + return this._client.isConnected; + } + + /** + * 获取玩家ID + */ + get playerId(): number | undefined { + return this._playerId; + } + + /** + * 获取房间ID + */ + get roomId(): string | undefined { + return this._roomId; + } + + /** + * 获取客户端实例 + */ + get client(): WsClient { + return this._client; + } + + /** + * 尝试重连 + */ + private async attemptReconnect(): Promise { + this._reconnectAttempts++; + const delay = Math.min(this._reconnectDelay * Math.pow(2, this._reconnectAttempts - 1), 30000); + + logger.info(`尝试重连 (${this._reconnectAttempts}/${this._maxReconnectAttempts}),${delay}ms后重试`); + this.emit('reconnecting', this._reconnectAttempts); + + setTimeout(async () => { + try { + const success = await this.connect(); + if (success) { + logger.info('重连成功'); + this.emit('reconnected'); + } else if (this._reconnectAttempts < this._maxReconnectAttempts) { + await this.attemptReconnect(); + } else { + logger.error('重连失败,已达到最大重试次数'); + this.emit('reconnectFailed'); + } + } catch (error) { + logger.error('重连异常:', error); + if (this._reconnectAttempts < this._maxReconnectAttempts) { + await this.attemptReconnect(); + } + } + }, delay); + } + + /** + * Ping服务器测试连接 + */ + public async ping(): Promise { + try { + this._lastPingTime = Date.now(); + const result = await this._client.callApi('Ping', { + timestamp: this._lastPingTime + }); + + if (result.isSucc) { + this._rtt = Date.now() - this._lastPingTime; + return this._rtt; + } else { + throw new Error(result.err?.message || 'Ping失败'); + } + } catch (error) { + logger.error('Ping服务器失败:', error); + throw error; + } + } + + /** + * 获取RTT + */ + public getRtt(): number { + return this._rtt; + } + + /** + * 获取连接状态 + */ + public getConnectionState(): ConnectionState { + return this._connectionState; + } + + /** + * 设置重连配置 + */ + public setReconnectConfig(maxAttempts: number, delay: number): void { + this._maxReconnectAttempts = maxAttempts; + this._reconnectDelay = delay; + } + + /** + * 添加事件监听器 + */ + public on(event: K, handler: NetworkEventHandlers[K]): void { + if (!this._eventHandlers.has(event)) { + this._eventHandlers.set(event, []); + } + this._eventHandlers.get(event)!.push(handler); + } + + /** + * 移除事件监听器 + */ + public off(event: K, handler: NetworkEventHandlers[K]): void { + const handlers = this._eventHandlers.get(event); + if (handlers) { + const index = handlers.indexOf(handler); + if (index !== -1) { + handlers.splice(index, 1); + } + } + } + + /** + * 触发事件 + */ + private emit(event: K, ...args: Parameters): void { + const handlers = this._eventHandlers.get(event); + if (handlers) { + handlers.forEach(handler => { + try { + (handler as (...args: Parameters) => void)(...args); + } catch (error) { + logger.error(`事件处理器错误 (${event}):`, error); + } + }); + } + } + + /** + * 批量同步组件 + */ + public async syncComponents(updates: Array<{ + entityId: number; + component: T; + }>): Promise<{ success: number; failed: number }> { + let successCount = 0; + let failedCount = 0; + + const promises = updates.map(async ({ entityId, component }) => { + try { + const success = await this.syncComponent(entityId, component); + if (success) { + successCount++; + } else { + failedCount++; + } + } catch (error) { + logger.error(`批量同步组件失败 Entity ${entityId}:`, error); + failedCount++; + } + }); + + await Promise.all(promises); + + logger.debug(`批量同步完成: 成功 ${successCount}, 失败 ${failedCount}`); + return { success: successCount, failed: failedCount }; + } + + /** + * 获取客户端统计信息 + */ + public getStats(): { + connectionState: string; + playerId?: number; + roomId?: string; + rtt: number; + reconnectAttempts: number; + maxReconnectAttempts: number; + componentCallbacks: number; + } { + return { + connectionState: this._connectionState, + playerId: this._playerId, + roomId: this._roomId, + rtt: this._rtt, + reconnectAttempts: this._reconnectAttempts, + maxReconnectAttempts: this._maxReconnectAttempts, + componentCallbacks: this._componentUpdateCallbacks.size + }; + } +} \ No newline at end of file diff --git a/packages/network/src/TSRPC/TsrpcManager.ts b/packages/network/src/TSRPC/TsrpcManager.ts new file mode 100644 index 00000000..30c2b40c --- /dev/null +++ b/packages/network/src/TSRPC/TsrpcManager.ts @@ -0,0 +1,129 @@ +/** + * TSRPC 网络管理器 + */ +import { createLogger } from '@esengine/ecs-framework'; +import { TsrpcNetworkServer } from './TsrpcServer'; +import { TsrpcNetworkClient } from './TsrpcClient'; + +const logger = createLogger('TsrpcManager'); + +export enum NetworkMode { + Server = 'server', + Client = 'client' +} + +export class TsrpcManager { + private static _instance: TsrpcManager | null = null; + private _server: TsrpcNetworkServer | null = null; + private _client: TsrpcNetworkClient | null = null; + private _mode: NetworkMode | null = null; + + private constructor() {} + + public static getInstance(): TsrpcManager { + if (!TsrpcManager._instance) { + TsrpcManager._instance = new TsrpcManager(); + } + return TsrpcManager._instance; + } + + /** + * 初始化为服务器模式 + */ + async initAsServer(port: number = 3000): Promise { + if (this._mode !== null) { + throw new Error('TSRPC管理器已经初始化'); + } + + this._mode = NetworkMode.Server; + this._server = new TsrpcNetworkServer(port); + await this._server.start(); + + logger.info(`TSRPC管理器已初始化为服务器模式,端口: ${port}`); + } + + /** + * 初始化为客户端模式 + */ + async initAsClient(serverUrl: string = 'ws://localhost:3000'): Promise { + if (this._mode !== null) { + throw new Error('TSRPC管理器已经初始化'); + } + + this._mode = NetworkMode.Client; + this._client = new TsrpcNetworkClient(serverUrl); + const connected = await this._client.connect(); + + if (connected) { + logger.info(`TSRPC管理器已初始化为客户端模式,服务器: ${serverUrl}`); + } else { + throw new Error('无法连接到TSRPC服务器'); + } + } + + /** + * 关闭网络连接 + */ + async shutdown(): Promise { + if (this._server) { + await this._server.stop(); + this._server = null; + } + + if (this._client) { + this._client.disconnect(); + this._client = null; + } + + this._mode = null; + logger.info('TSRPC管理器已关闭'); + } + + /** + * 获取服务器实例(仅服务器模式) + */ + get server(): TsrpcNetworkServer { + if (!this._server) { + throw new Error('未初始化为服务器模式或服务器未启动'); + } + return this._server; + } + + /** + * 获取客户端实例(仅客户端模式) + */ + get client(): TsrpcNetworkClient { + if (!this._client) { + throw new Error('未初始化为客户端模式或客户端未连接'); + } + return this._client; + } + + /** + * 获取当前模式 + */ + get mode(): NetworkMode | null { + return this._mode; + } + + /** + * 是否为服务器模式 + */ + get isServer(): boolean { + return this._mode === NetworkMode.Server; + } + + /** + * 是否为客户端模式 + */ + get isClient(): boolean { + return this._mode === NetworkMode.Client; + } + + /** + * 是否已初始化 + */ + get isInitialized(): boolean { + return this._mode !== null; + } +} \ No newline at end of file diff --git a/packages/network/src/TSRPC/TsrpcServer.ts b/packages/network/src/TSRPC/TsrpcServer.ts new file mode 100644 index 00000000..b28347ad --- /dev/null +++ b/packages/network/src/TSRPC/TsrpcServer.ts @@ -0,0 +1,698 @@ +/** + * TSRPC 服务器 + */ +import { WsServer } from 'tsrpc'; +import { ServiceType, serviceProto } from './protocols/serviceProto'; +import { createLogger } from '@esengine/ecs-framework'; + +const logger = createLogger('TsrpcServer'); + +export class TsrpcNetworkServer { + private _server: WsServer; + private _connectedClients = new Map(); + private _isRunning: boolean = false; + private _startTime: number = 0; + private _totalConnections: number = 0; + private _maxConcurrentConnections: number = 0; + private _eventHandlers: Map = new Map(); + private _performanceStats = { + totalMessages: 0, + totalBytesTransferred: 0, + averageResponseTime: 0, + peakConnections: 0, + uptime: 0 + }; + + constructor(port: number = 3000) { + this._server = new WsServer(serviceProto, { + port: port, + // JSON兼容模式 + json: false, + // 连接日志 + logConnect: true + }); + + this.setupApi(); + this.setupEvents(); + } + + private setupApi() { + // 实现同步组件API + this._server.implementApi('SyncComponent', async (call) => { + try { + const { entityId, componentType, componentData, timestamp } = call.req; + + logger.debug(`收到组件同步: Entity ${entityId}, Component ${componentType}`); + + // 广播组件更新给其他客户端 + this.broadcastComponentUpdate(entityId, componentType, componentData, timestamp, call.conn.id); + + call.succ({ + success: true, + entityId + }); + } catch (error) { + call.error('同步组件失败', { error: (error as Error).message }); + } + }); + + // 实现加入房间API + this._server.implementApi('JoinRoom', async (call) => { + try { + const { roomId, playerName, password } = call.req; + + // 检查房间是否存在,如果不存在则自动创建 + let room = this._rooms.get(roomId); + if (!room) { + this.createRoom(roomId, { maxPlayers: 10 }); + room = this._rooms.get(roomId)!; + } + + // 检查房间是否已满 + if (room.currentPlayers >= room.maxPlayers) { + call.succ({ + success: false, + errorMsg: '房间已满' + }); + return; + } + + // 检查密码 + if (room.password && room.password !== password) { + call.succ({ + success: false, + errorMsg: '密码错误' + }); + return; + } + + const playerId = Date.now() + Math.floor(Math.random() * 1000); // 更好的ID生成 + + // 添加玩家到房间 + const playerInfo: PlayerInfo = { + playerId, + playerName, + joinedAt: Date.now() + }; + room.players.set(call.conn.id, playerInfo); + room.currentPlayers++; + + // 记录连接 + this._connectedClients.set(call.conn.id, { + playerId, + roomId, + playerName, + connId: call.conn.id + }); + + logger.info(`玩家 ${playerName} (ID: ${playerId}) 加入房间 ${roomId} (${room.currentPlayers}/${room.maxPlayers})`); + + // 通知房间内其他玩家 + this.broadcastToRoom(roomId, 'PlayerJoined', { + playerId, + playerName, + timestamp: Date.now() + }, call.conn.id); + + call.succ({ + success: true, + playerId, + roomInfo: { + roomId, + playerCount: room.currentPlayers, + maxPlayers: room.maxPlayers, + metadata: room.metadata + } + }); + } catch (error) { + call.error('加入房间失败', { error: (error as Error).message }); + } + }); + } + + private setupEvents() { + // 连接建立 + this._server.flows.postConnectFlow.push((conn) => { + logger.info(`客户端连接: ${conn.id}`); + return conn; + }); + + // 连接断开 + this._server.flows.postDisconnectFlow.push((data) => { + const client = this._connectedClients.get(data.conn.id); + if (client) { + logger.info(`客户端断开: ${client.playerName} (${data.conn.id})`); + + // 从房间中移除玩家 + const room = this._rooms.get(client.roomId); + if (room) { + room.players.delete(data.conn.id); + room.currentPlayers--; + + // 通知房间内其他玩家 + this.broadcastToRoom(client.roomId, 'PlayerLeft', { + playerId: client.playerId, + playerName: client.playerName, + timestamp: Date.now() + }); + + logger.info(`玩家 ${client.playerName} 离开房间 ${client.roomId} (${room.currentPlayers}/${room.maxPlayers})`); + + // 如果房间空了,可选择性删除房间 + if (room.currentPlayers === 0) { + logger.info(`房间 ${client.roomId} 已空,保留等待新玩家`); + // 可以选择删除空房间: this._rooms.delete(client.roomId); + } + } + + this._connectedClients.delete(data.conn.id); + } + return data; + }); + } + + /** + * 广播组件更新给其他客户端 + */ + private broadcastComponentUpdate(entityId: number, componentType: string, componentData: any, timestamp: number, excludeConnId?: string) { + const updateMsg = { + entityId, + componentType, + componentData, + timestamp + }; + + // 给所有其他客户端发送更新消息 + const targetConns = this._server.connections.filter(conn => conn.id !== excludeConnId); + this._server.broadcastMsg('ComponentUpdate', updateMsg, targetConns); + } + + /** + * 启动服务器 + */ + async start(): Promise { + await this._server.start(); + logger.info(`TSRPC服务器已启动,端口: ${this._server.options.port}`); + } + + /** + * 停止服务器 + */ + async stop(): Promise { + await this._server.stop(); + logger.info('TSRPC服务器已停止'); + } + + /** + * 获取连接的客户端数量 + */ + get clientCount(): number { + return this._connectedClients.size; + } + + /** + * 获取服务器实例 + */ + get server(): WsServer { + return this._server; + } + + /** + * 高级房间管理功能 + */ + + /** + * 创建房间 + */ + public createRoom(roomId: string, options?: { + maxPlayers?: number; + password?: string; + metadata?: Record; + }): boolean { + if (this._rooms.has(roomId)) { + return false; // 房间已存在 + } + + const room: RoomInfo = { + roomId, + maxPlayers: options?.maxPlayers || 10, + currentPlayers: 0, + players: new Map(), + created: Date.now(), + password: options?.password, + metadata: options?.metadata || {} + }; + + this._rooms.set(roomId, room); + logger.info(`创建房间: ${roomId}, 最大玩家数: ${room.maxPlayers}`); + return true; + } + + /** + * 删除房间 + */ + public removeRoom(roomId: string): boolean { + const room = this._rooms.get(roomId); + if (!room) { + return false; + } + + // 通知房间内的所有玩家 + for (const [connId, playerInfo] of room.players) { + const conn = this._server.connections.find(c => c.id === connId); + if (conn) { + this._server.broadcastMsg('RoomClosed', { roomId, reason: '房间已关闭' }, [conn]); + // 从已连接客户端列表中移除 + this._connectedClients.delete(connId); + } + } + + this._rooms.delete(roomId); + logger.info(`删除房间: ${roomId}`); + return true; + } + + /** + * 获取房间列表 + */ + public getRoomList(includePassword: boolean = false): Array<{ + roomId: string; + currentPlayers: number; + maxPlayers: number; + hasPassword: boolean; + metadata: Record; + }> { + const roomList: Array<{ + roomId: string; + currentPlayers: number; + maxPlayers: number; + hasPassword: boolean; + metadata: Record; + }> = []; + + for (const room of this._rooms.values()) { + roomList.push({ + roomId: room.roomId, + currentPlayers: room.currentPlayers, + maxPlayers: room.maxPlayers, + hasPassword: !!room.password, + metadata: room.metadata + }); + } + + return roomList; + } + + /** + * 获取房间详情 + */ + public getRoomInfo(roomId: string): RoomInfo | null { + return this._rooms.get(roomId) || null; + } + + /** + * 房间内广播消息 + */ + public broadcastToRoom(roomId: string, message: string, data: any, excludeConnId?: string): void { + const room = this._rooms.get(roomId); + if (!room) { + logger.warn(`房间不存在: ${roomId}`); + return; + } + + const targetConns = this._server.connections.filter(conn => { + return room.players.has(conn.id) && conn.id !== excludeConnId; + }); + + if (targetConns.length > 0) { + // 使用对应的房间消息类型 + switch (message) { + case 'PlayerJoined': + this._server.broadcastMsg('PlayerJoined', data, targetConns); + break; + case 'PlayerLeft': + this._server.broadcastMsg('PlayerLeft', data, targetConns); + break; + case 'PlayerKicked': + this._server.broadcastMsg('PlayerKicked', data, targetConns); + break; + default: + // 其他消息使用ComponentUpdate作为通用消息 + this._server.broadcastMsg('ComponentUpdate', { + entityId: 0, + componentType: message, + componentData: data, + timestamp: Date.now() + }, targetConns); + break; + } + } + } + + /** + * 踢出玩家 + */ + public kickPlayer(roomId: string, playerId: number, reason?: string): boolean { + const room = this._rooms.get(roomId); + if (!room) { + return false; + } + + let targetConnId: string | null = null; + for (const [connId, playerInfo] of room.players) { + if (playerInfo.playerId === playerId) { + targetConnId = connId; + break; + } + } + + if (!targetConnId) { + return false; + } + + // 从房间中移除玩家 + room.players.delete(targetConnId); + room.currentPlayers--; + + // 从已连接客户端列表中移除 + this._connectedClients.delete(targetConnId); + + // 通知被踢出的玩家 + const conn = this._server.connections.find(c => c.id === targetConnId); + if (conn) { + this.broadcastToRoom(roomId, 'PlayerKicked', { + playerId, + reason: reason || '被踢出房间' + }); + + // 断开连接 + conn.close(reason); + } + + logger.info(`踢出玩家: ${playerId} 从房间 ${roomId}, 原因: ${reason || '未指定'}`); + return true; + } + + /** + * 设置房间元数据 + */ + public setRoomMetadata(roomId: string, metadata: Record): boolean { + const room = this._rooms.get(roomId); + if (!room) { + return false; + } + + room.metadata = { ...room.metadata, ...metadata }; + logger.debug(`更新房间元数据: ${roomId}`, metadata); + return true; + } + + /** + * 房间状态统计 + */ + public getRoomStats(): { + totalRooms: number; + totalPlayers: number; + averagePlayersPerRoom: number; + roomDetails: Array<{ + roomId: string; + players: number; + maxPlayers: number; + uptime: number; + }>; + } { + let totalPlayers = 0; + const roomDetails: Array<{ + roomId: string; + players: number; + maxPlayers: number; + uptime: number; + }> = []; + + for (const room of this._rooms.values()) { + totalPlayers += room.currentPlayers; + roomDetails.push({ + roomId: room.roomId, + players: room.currentPlayers, + maxPlayers: room.maxPlayers, + uptime: Date.now() - room.created + }); + } + + return { + totalRooms: this._rooms.size, + totalPlayers, + averagePlayersPerRoom: this._rooms.size > 0 ? totalPlayers / this._rooms.size : 0, + roomDetails + }; + } + + // 添加房间映射和房间信息接口 + private _rooms = new Map(); + + /** + * 启动服务器 + */ + public async start(): Promise { + try { + await this._server.start(); + this._isRunning = true; + this._startTime = Date.now(); + logger.info(`TSRPC服务器已启动,端口: ${this._server.options.port}`); + this.emit('started'); + } catch (error) { + logger.error('启动TSRPC服务器失败:', error); + this.emit('startError', error); + throw error; + } + } + + /** + * 停止服务器 + */ + public async stop(): Promise { + try { + if (this._isRunning) { + await this._server.stop(); + this._isRunning = false; + this._startTime = 0; + this._connectedClients.clear(); + this._rooms.clear(); + logger.info('TSRPC服务器已停止'); + this.emit('stopped'); + } + } catch (error) { + logger.error('停止TSRPC服务器失败:', error); + this.emit('stopError', error); + throw error; + } + } + + /** + * 获取服务器统计信息 + */ + public getStats(): { + isRunning: boolean; + uptime: number; + connectedClients: number; + totalConnections: number; + maxConcurrentConnections: number; + rooms: number; + performance: typeof this._performanceStats; + } { + this._performanceStats.uptime = this._isRunning ? Date.now() - this._startTime : 0; + this._performanceStats.peakConnections = Math.max( + this._maxConcurrentConnections, + this._connectedClients.size + ); + + return { + isRunning: this._isRunning, + uptime: this._performanceStats.uptime, + connectedClients: this._connectedClients.size, + totalConnections: this._totalConnections, + maxConcurrentConnections: this._maxConcurrentConnections, + rooms: this._rooms.size, + performance: { ...this._performanceStats } + }; + } + + /** + * 广播消息给所有客户端 + */ + public broadcastToAll(msgName: string, msg: any, excludeConnId?: string): void { + for (const [connId, clientInfo] of this._connectedClients) { + if (excludeConnId && connId === excludeConnId) { + continue; + } + try { + this._server.sendMsg(clientInfo.conn, msgName, msg); + this._performanceStats.totalMessages++; + } catch (error) { + logger.error(`向客户端 ${connId} 广播消息失败:`, error); + } + } + } + + /** + * 向房间内广播消息 + */ + public broadcastToRoom(roomId: string, msgName: string, msg: any, excludeConnId?: string): void { + const room = this._rooms.get(roomId); + if (!room) { + logger.warn(`房间 ${roomId} 不存在`); + return; + } + + for (const [connId] of room.players) { + if (excludeConnId && connId === excludeConnId) { + continue; + } + + const clientInfo = this._connectedClients.get(connId); + if (clientInfo) { + try { + this._server.sendMsg(clientInfo.conn, msgName, msg); + this._performanceStats.totalMessages++; + } catch (error) { + logger.error(`向房间 ${roomId} 客户端 ${connId} 广播消息失败:`, error); + } + } + } + } + + /** + * 添加事件监听器 + */ + public on(event: string, handler: Function): void { + if (!this._eventHandlers.has(event)) { + this._eventHandlers.set(event, []); + } + this._eventHandlers.get(event)!.push(handler); + } + + /** + * 移除事件监听器 + */ + public off(event: string, handler: Function): void { + const handlers = this._eventHandlers.get(event); + if (handlers) { + const index = handlers.indexOf(handler); + if (index !== -1) { + handlers.splice(index, 1); + } + } + } + + /** + * 触发事件 + */ + private emit(event: string, ...args: any[]): void { + const handlers = this._eventHandlers.get(event); + if (handlers) { + handlers.forEach(handler => { + try { + handler(...args); + } catch (error) { + logger.error(`事件处理器错误 (${event}):`, error); + } + }); + } + } + + /** + * 获取房间列表 + */ + public getRooms(): Array<{ + roomId: string; + currentPlayers: number; + maxPlayers: number; + created: number; + hasPassword: boolean; + }> { + return Array.from(this._rooms.values()).map(room => ({ + roomId: room.roomId, + currentPlayers: room.currentPlayers, + maxPlayers: room.maxPlayers, + created: room.created, + hasPassword: !!room.password + })); + } + + /** + * 强制踢出玩家 + */ + public kickPlayer(connId: string, reason?: string): boolean { + const clientInfo = this._connectedClients.get(connId); + if (clientInfo) { + try { + // 发送踢出通知 + this._server.sendMsg(clientInfo.conn, 'PlayerKicked', { + reason: reason || '被服务器踢出' + }); + + // 断开连接 + clientInfo.conn.close(); + logger.info(`踢出玩家 ${connId}: ${reason || '未指定原因'}`); + return true; + } catch (error) { + logger.error(`踢出玩家 ${connId} 失败:`, error); + return false; + } + } + return false; + } + + /** + * 获取在线玩家列表 + */ + public getOnlinePlayers(): Array<{ + connId: string; + playerId?: number; + playerName?: string; + joinedAt: number; + roomId?: string; + }> { + const players: Array<{ + connId: string; + playerId?: number; + playerName?: string; + joinedAt: number; + roomId?: string; + }> = []; + + for (const [connId, clientInfo] of this._connectedClients) { + players.push({ + connId, + playerId: clientInfo.playerId, + playerName: clientInfo.playerName, + joinedAt: clientInfo.joinedAt, + roomId: clientInfo.roomId + }); + } + + return players; + } +} + +/** + * 房间信息接口 + */ +interface RoomInfo { + roomId: string; + maxPlayers: number; + currentPlayers: number; + players: Map; // connId -> PlayerInfo + created: number; + password?: string; + metadata: Record; +} + +/** + * 玩家信息接口 + */ +interface PlayerInfo { + playerId: number; + playerName?: string; + joinedAt: number; +} \ No newline at end of file diff --git a/packages/network/src/TSRPC/index.ts b/packages/network/src/TSRPC/index.ts new file mode 100644 index 00000000..da234eda --- /dev/null +++ b/packages/network/src/TSRPC/index.ts @@ -0,0 +1,14 @@ +/** + * TSRPC 网络通信系统导出 + */ + +// 协议定义 +export * from './protocols/serviceProto'; +export * from './protocols/PtlSyncComponent'; +export * from './protocols/PtlJoinRoom'; +export * from './protocols/MsgComponentUpdate'; + +// 客户端和服务器 +export { TsrpcNetworkServer } from './TsrpcServer'; +export { TsrpcNetworkClient } from './TsrpcClient'; +export { TsrpcManager, NetworkMode } from './TsrpcManager'; \ No newline at end of file diff --git a/packages/network/src/TSRPC/protocols/MsgComponentUpdate.ts b/packages/network/src/TSRPC/protocols/MsgComponentUpdate.ts new file mode 100644 index 00000000..cc793e1b --- /dev/null +++ b/packages/network/src/TSRPC/protocols/MsgComponentUpdate.ts @@ -0,0 +1,9 @@ +/** + * 组件更新消息(服务器广播给客户端) + */ +export interface MsgComponentUpdate { + entityId: number; + componentType: string; + componentData: any; + timestamp: number; +} \ No newline at end of file diff --git a/packages/network/src/TSRPC/protocols/PtlJoinRoom.ts b/packages/network/src/TSRPC/protocols/PtlJoinRoom.ts new file mode 100644 index 00000000..21084351 --- /dev/null +++ b/packages/network/src/TSRPC/protocols/PtlJoinRoom.ts @@ -0,0 +1,20 @@ +/** + * 加入房间协议 + */ +export interface ReqJoinRoom { + roomId: string; + playerName?: string; + password?: string; +} + +export interface ResJoinRoom { + success: boolean; + playerId?: number; + roomInfo?: { + roomId: string; + playerCount: number; + maxPlayers: number; + metadata?: Record; + }; + errorMsg?: string; +} \ No newline at end of file diff --git a/packages/network/src/TSRPC/protocols/PtlSyncComponent.ts b/packages/network/src/TSRPC/protocols/PtlSyncComponent.ts new file mode 100644 index 00000000..5378aa50 --- /dev/null +++ b/packages/network/src/TSRPC/protocols/PtlSyncComponent.ts @@ -0,0 +1,18 @@ +/** + * 组件同步协议定义 + */ + +// 同步组件数据请求 +export interface ReqSyncComponent { + entityId: number; + componentType: string; + componentData: any; + timestamp: number; +} + +// 同步组件数据响应 +export interface ResSyncComponent { + success: boolean; + entityId: number; + errorMsg?: string; +} \ No newline at end of file diff --git a/packages/network/src/TSRPC/protocols/serviceProto.ts b/packages/network/src/TSRPC/protocols/serviceProto.ts new file mode 100644 index 00000000..11396f46 --- /dev/null +++ b/packages/network/src/TSRPC/protocols/serviceProto.ts @@ -0,0 +1,78 @@ +/** + * TSRPC 服务协议定义 + */ +import { ServiceProto } from 'tsrpc-proto'; + +// API 协议 +export interface ServiceType { + api: { + 'SyncComponent': { + req: import('./PtlSyncComponent').ReqSyncComponent; + res: import('./PtlSyncComponent').ResSyncComponent; + }; + 'JoinRoom': { + req: import('./PtlJoinRoom').ReqJoinRoom; + res: import('./PtlJoinRoom').ResJoinRoom; + }; + 'Ping': { + req: { timestamp: number }; + res: { timestamp: number; serverTime: number }; + }; + }; + msg: { + 'ComponentUpdate': import('./MsgComponentUpdate').MsgComponentUpdate; + 'RoomClosed': { roomId: string; reason: string }; + 'PlayerJoined': { playerId: number; playerName?: string; timestamp: number }; + 'PlayerLeft': { playerId: number; playerName?: string; timestamp: number }; + 'PlayerKicked': { playerId: number; reason: string }; + }; +} + +export const serviceProto: ServiceProto = { + "version": 1, + "services": [ + // API 服务 + { + "id": 0, + "name": "SyncComponent", + "type": "api" + }, + { + "id": 1, + "name": "JoinRoom", + "type": "api" + }, + { + "id": 2, + "name": "Ping", + "type": "api" + }, + // 消息服务 + { + "id": 3, + "name": "ComponentUpdate", + "type": "msg" + }, + { + "id": 4, + "name": "RoomClosed", + "type": "msg" + }, + { + "id": 5, + "name": "PlayerJoined", + "type": "msg" + }, + { + "id": 6, + "name": "PlayerLeft", + "type": "msg" + }, + { + "id": 7, + "name": "PlayerKicked", + "type": "msg" + } + ], + "types": {} +}; \ No newline at end of file diff --git a/packages/network/src/constants/NetworkConstants.ts b/packages/network/src/constants/NetworkConstants.ts new file mode 100644 index 00000000..7e7db2f7 --- /dev/null +++ b/packages/network/src/constants/NetworkConstants.ts @@ -0,0 +1,211 @@ +/** + * 网络库常量定义 + */ + +/** + * 网络配置常量 + */ +export const NETWORK_CONFIG = { + /** 默认连接超时时间 (ms) */ + DEFAULT_CONNECTION_TIMEOUT: 10000, + /** 默认心跳间隔 (ms) */ + DEFAULT_HEARTBEAT_INTERVAL: 5000, + /** 默认心跳超时时间 (ms) */ + DEFAULT_HEARTBEAT_TIMEOUT: 10000, + /** 默认最大重连次数 */ + DEFAULT_MAX_RECONNECT_ATTEMPTS: 5, + /** 默认重连延迟 (ms) */ + DEFAULT_RECONNECT_DELAY: 1000, + /** 默认最大连续丢包数 */ + DEFAULT_MAX_CONSECUTIVE_LOSS: 3, + /** 默认心跳包大小 (bytes) */ + DEFAULT_HEARTBEAT_PACKET_SIZE: 64, + /** 默认RTT历史记录大小 */ + DEFAULT_RTT_HISTORY_SIZE: 50 +} as const; + +/** + * SyncVar配置常量 + */ +export const SYNCVAR_CONFIG = { + /** 默认SyncVar缓存超时 (ms) */ + DEFAULT_CACHE_TIMEOUT: 5000, + /** 默认同步频率限制 (ms) */ + DEFAULT_THROTTLE_MS: 50, + /** 最大字段编号 */ + MAX_FIELD_NUMBER: 65535, + /** 最小字段编号 */ + MIN_FIELD_NUMBER: 1 +} as const; + +/** + * 消息配置常量 + */ +export const MESSAGE_CONFIG = { + /** 默认消息序列号范围 */ + MAX_SEQUENCE_NUMBER: 4294967295, // 2^32 - 1 + /** 消息头部最大大小 (bytes) */ + MAX_HEADER_SIZE: 256, + /** 消息体最大大小 (bytes) */ + MAX_PAYLOAD_SIZE: 1048576, // 1MB + /** 默认消息超时时间 (ms) */ + DEFAULT_MESSAGE_TIMEOUT: 30000, + /** 批处理消息最大数量 */ + MAX_BATCH_SIZE: 100 +} as const; + +/** + * 序列化配置常量 + */ +export const SERIALIZATION_CONFIG = { + /** 默认压缩级别 */ + DEFAULT_COMPRESSION_LEVEL: 6, + /** 启用压缩的最小数据大小 (bytes) */ + MIN_COMPRESSION_SIZE: 1024, + /** 序列化缓冲区初始大小 (bytes) */ + INITIAL_BUFFER_SIZE: 4096, + /** 序列化缓冲区最大大小 (bytes) */ + MAX_BUFFER_SIZE: 16777216 // 16MB +} as const; + +/** + * TSRPC配置常量 + */ +export const TSRPC_CONFIG = { + /** 默认服务器URL */ + DEFAULT_SERVER_URL: 'ws://localhost:3000', + /** 默认超时时间 (ms) */ + DEFAULT_TIMEOUT: 30000, + /** 默认心跳配置 */ + DEFAULT_HEARTBEAT: { + interval: 10000, + timeout: 5000 + }, + /** 默认连接池配置 */ + DEFAULT_POOL_CONFIG: { + minConnections: 1, + maxConnections: 10, + idleTimeout: 300000 // 5分钟 + } +} as const; + +/** + * 权限配置常量 + */ +export const AUTHORITY_CONFIG = { + /** 权限优先级范围 */ + MIN_PRIORITY: 0, + MAX_PRIORITY: 1000, + /** 默认权限规则优先级 */ + DEFAULT_RULE_PRIORITY: 50, + /** 服务端权限优先级 */ + SERVER_AUTHORITY_PRIORITY: 100, + /** 网络身份权限优先级 */ + NETWORK_IDENTITY_PRIORITY: 80, + /** 组件自定义权限优先级 */ + COMPONENT_CUSTOM_PRIORITY: 60, + /** 实体所有者权限优先级 */ + ENTITY_OWNER_PRIORITY: 50, + /** 默认拒绝规则优先级 */ + DEFAULT_DENY_PRIORITY: 0 +} as const; + +/** + * 性能监控配置常量 + */ +export const PERFORMANCE_CONFIG = { + /** 性能统计收集间隔 (ms) */ + STATS_COLLECTION_INTERVAL: 1000, + /** 性能数据保留时间 (ms) */ + STATS_RETENTION_TIME: 300000, // 5分钟 + /** 警告阈值 */ + WARNING_THRESHOLDS: { + /** RTT警告阈值 (ms) */ + RTT: 200, + /** 丢包率警告阈值 */ + PACKET_LOSS: 0.05, // 5% + /** 抖动警告阈值 (ms) */ + JITTER: 50, + /** CPU使用率警告阈值 */ + CPU_USAGE: 0.8, // 80% + /** 内存使用率警告阈值 */ + MEMORY_USAGE: 0.8 // 80% + } +} as const; + +/** + * 错误代码常量 + */ +export const ERROR_CODES = { + // 连接错误 (1000-1099) + CONNECTION_FAILED: 1000, + CONNECTION_TIMEOUT: 1001, + CONNECTION_REFUSED: 1002, + CONNECTION_LOST: 1003, + + // 序列化错误 (1100-1199) + SERIALIZATION_FAILED: 1100, + DESERIALIZATION_FAILED: 1101, + INVALID_DATA_FORMAT: 1102, + + // SyncVar错误 (1200-1299) + SYNCVAR_INIT_FAILED: 1200, + SYNCVAR_METADATA_MISSING: 1201, + SYNCVAR_TYPE_MISMATCH: 1202, + SYNCVAR_AUTHORITY_DENIED: 1203, + + // 消息错误 (1300-1399) + MESSAGE_TIMEOUT: 1300, + MESSAGE_TOO_LARGE: 1301, + MESSAGE_INVALID_TYPE: 1302, + MESSAGE_SEQUENCE_ERROR: 1303, + + // TSRPC错误 (1400-1499) + TSRPC_CALL_FAILED: 1400, + TSRPC_METHOD_NOT_FOUND: 1401, + TSRPC_INVALID_PARAMS: 1402, + TSRPC_SERVER_ERROR: 1403, + + // 权限错误 (1500-1599) + PERMISSION_DENIED: 1500, + INVALID_AUTHORITY: 1501, + AUTHORITY_CHECK_FAILED: 1502 +} as const; + +/** + * 日志级别常量 + */ +export const LOG_LEVELS = { + ERROR: 0, + WARN: 1, + INFO: 2, + DEBUG: 3, + TRACE: 4 +} as const; + +/** + * 环境类型常量 + */ +export const ENVIRONMENTS = { + SERVER: 'server', + CLIENT: 'client', + HYBRID: 'hybrid' +} as const; + +/** + * 网络事件名称常量 + */ +export const NETWORK_EVENTS = { + CONNECTED: 'connected', + DISCONNECTED: 'disconnected', + CONNECTING: 'connecting', + RECONNECTING: 'reconnecting', + RECONNECTED: 'reconnected', + CONNECT_ERROR: 'connectError', + RECONNECT_FAILED: 'reconnectFailed', + MESSAGE: 'message', + ERROR: 'error', + HEARTBEAT: 'heartbeat', + PERFORMANCE_WARNING: 'performanceWarning' +} as const; + diff --git a/packages/network/src/index.ts b/packages/network/src/index.ts index f637202b..95449984 100644 --- a/packages/network/src/index.ts +++ b/packages/network/src/index.ts @@ -19,7 +19,10 @@ export { NetworkComponent } from './NetworkComponent'; export { INetworkSyncable } from './INetworkSyncable'; export { NetworkRole } from './NetworkRole'; -// Protobuf序列化系统 +// TSRPC网络通信系统 +export * from './TSRPC'; + +// 装饰器序列化系统(用于组件同步) export * from './Serialization'; // 快照系统(帧同步) diff --git a/packages/network/src/types/CoreTypes.ts b/packages/network/src/types/CoreTypes.ts new file mode 100644 index 00000000..dfae6ac3 --- /dev/null +++ b/packages/network/src/types/CoreTypes.ts @@ -0,0 +1,151 @@ +import { Component, Entity } from '@esengine/ecs-framework'; + +/** + * 网络环境类型 + */ +export type NetworkEnvironmentType = 'server' | 'client' | 'hybrid'; + +/** + * 连接状态类型 + */ +export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'; + +/** + * 权限类型 + */ +export enum AuthorityType { + None = 'none', + ReadOnly = 'readonly', + ReadWrite = 'readwrite', + Full = 'full' +} + +/** + * 网络组件基接口 + */ +export interface INetworkComponent extends Component { + networkId?: string; + hasNetworkAuthority?(): boolean; + getNetworkState?(): Uint8Array; + applyNetworkState?(data: Uint8Array): void; + + /** 允许通过字符串键访问属性 */ + [propertyKey: string]: unknown; +} + +/** + * 具有权限检查能力的组件 + */ +export interface IAuthorizedComponent extends INetworkComponent { + hasAuthority(context?: AuthorityContext): boolean; + checkAuthority?(context?: AuthorityContext): boolean; +} + +/** + * 权限上下文 + */ +export interface AuthorityContext { + environment: NetworkEnvironmentType; + networkId?: string; + entityId?: number; + clientId?: string; + level: AuthorityType; + timestamp: number; + metadata?: Record; +} + +/** + * 网络实体接口 + */ +export interface INetworkEntity extends Entity { + ownerId?: string; + hasNetworkAuthority?(): boolean; +} + +/** + * 组件构造器类型 + */ +export type ComponentConstructor = new (...args: unknown[]) => T; + +/** + * 网络组件构造器类型 + */ +export type NetworkComponentConstructor = new (...args: unknown[]) => T; + +/** + * 事件处理器类型映射 + */ +export interface NetworkEventHandlers { + connected: () => void; + disconnected: (reason?: string) => void; + connecting: () => void; + reconnecting: (attempt: number) => void; + reconnected: () => void; + connectError: (error: Error) => void; + reconnectFailed: () => void; + message: (data: Uint8Array) => void; + error: (error: Error) => void; +} + +/** + * 类型安全的事件发射器接口 + */ +export interface ITypedEventEmitter void>> { + on(event: K, handler: TEvents[K]): void; + off(event: K, handler: TEvents[K]): void; + emit(event: K, ...args: Parameters): void; +} + +/** + * 网络统计接口 + */ +export interface NetworkStats { + connectionCount: number; + totalConnections: number; + uptime: number; + bytesTransferred: number; + messagesCount: number; + errors: number; +} + +/** + * 性能指标接口 + */ +export interface PerformanceMetrics { + rtt: number; + latency: number; + jitter: number; + packetLoss: number; + bandwidth: number; + connectionQuality: number; +} + +/** + * 类型守卫工具类 + */ +export class TypeGuards { + static isNetworkComponent(obj: unknown): obj is INetworkComponent { + return obj !== null && + typeof obj === 'object' && + obj instanceof Component; + } + + static isAuthorizedComponent(obj: unknown): obj is IAuthorizedComponent { + return TypeGuards.isNetworkComponent(obj) && + typeof (obj as IAuthorizedComponent).hasAuthority === 'function'; + } + + static isNetworkEntity(obj: unknown): obj is INetworkEntity { + return obj !== null && + typeof obj === 'object' && + obj instanceof Entity; + } + + static isValidNetworkEnvironment(env: string): env is NetworkEnvironmentType { + return ['server', 'client', 'hybrid'].includes(env); + } + + static isValidConnectionState(state: string): state is ConnectionState { + return ['disconnected', 'connecting', 'connected', 'reconnecting'].includes(state); + } +} \ No newline at end of file diff --git a/packages/network/src/types/MessageTypes.ts b/packages/network/src/types/MessageTypes.ts new file mode 100644 index 00000000..09a73a93 --- /dev/null +++ b/packages/network/src/types/MessageTypes.ts @@ -0,0 +1,207 @@ +/** + * 消息类型枚举 + */ +export enum MessageType { + // 基础消息类型 (0-99) + HEARTBEAT = 0, + PING = 1, + PONG = 2, + ERROR = 3, + + // 连接管理 (100-199) + CONNECT = 100, + DISCONNECT = 101, + RECONNECT = 102, + AUTH = 103, + + // SyncVar消息 (200-299) + SYNC_VAR_UPDATE = 200, + SYNC_VAR_BATCH = 201, + SYNC_VAR_REQUEST = 202, + + // 快照消息 (300-399) + SNAPSHOT_FULL = 300, + SNAPSHOT_INCREMENTAL = 301, + SNAPSHOT_REQUEST = 302, + + // TSRPC消息 (400-499) + TSRPC_CALL = 400, + TSRPC_RESPONSE = 401, + TSRPC_NOTIFICATION = 402, + + // 自定义消息 (500+) + CUSTOM = 500 +} + +/** + * 消息优先级 + */ +export enum MessagePriority { + CRITICAL = 0, // 关键消息(连接、认证等) + HIGH = 1, // 高优先级(实时游戏数据) + NORMAL = 2, // 普通优先级(常规同步) + LOW = 3, // 低优先级(统计、日志等) + BACKGROUND = 4 // 后台消息(清理、维护等) +} + +/** + * 网络消息基接口 + */ +export interface INetworkMessage { + readonly type: MessageType; + readonly timestamp: number; + readonly priority: MessagePriority; + readonly sequenceNumber?: number; + serialize(): Uint8Array; +} + +/** + * 消息头接口 + */ +export interface MessageHeader { + type: MessageType; + size: number; + timestamp: number; + priority: MessagePriority; + sequenceNumber?: number; + checksum?: number; +} + +/** + * 心跳消息接口 + */ +export interface IHeartbeatMessage extends INetworkMessage { + readonly pingId: string; + readonly payload?: Record; +} + +/** + * SyncVar更新消息接口 + */ +export interface ISyncVarMessage extends INetworkMessage { + readonly networkId: string; + readonly componentType: string; + readonly fieldUpdates: SyncVarFieldUpdate[]; + readonly isFullSync: boolean; + readonly senderId?: string; +} + +/** + * SyncVar字段更新 + */ +export interface SyncVarFieldUpdate { + readonly fieldNumber: number; + readonly propertyKey: string; + readonly newValue: unknown; + readonly oldValue?: unknown; + readonly timestamp: number; + readonly authorityOnly: boolean; +} + +/** + * 错误消息接口 + */ +export interface IErrorMessage extends INetworkMessage { + readonly errorCode: string; + readonly errorMessage: string; + readonly errorData?: Record; + readonly originalMessageType?: MessageType; +} + +/** + * 快照消息接口 + */ +export interface ISnapshotMessage extends INetworkMessage { + readonly snapshotId: string; + readonly snapshotType: 'full' | 'incremental'; + readonly entityData: EntitySnapshot[]; + readonly compressionType?: string; +} + +/** + * 实体快照数据 + */ +export interface EntitySnapshot { + readonly entityId: number; + readonly components: ComponentSnapshot[]; + readonly timestamp: number; +} + +/** + * 组件快照数据 + */ +export interface ComponentSnapshot { + readonly componentType: string; + readonly data: Uint8Array; + readonly version?: string; +} + +/** + * TSRPC消息接口 + */ +export interface ITsrpcMessage extends INetworkMessage { + readonly method: string; + readonly requestId?: string; + readonly params?: Record; + readonly result?: unknown; + readonly error?: TsrpcError; +} + +/** + * TSRPC错误 + */ +export interface TsrpcError { + readonly code: number; + readonly message: string; + readonly data?: unknown; +} + +/** + * 消息工厂接口 + */ +export interface IMessageFactory { + createMessage( + type: MessageType, + data: Record + ): T; + + deserializeMessage(data: Uint8Array): INetworkMessage | null; + + registerMessageType( + type: MessageType, + constructor: new (...args: unknown[]) => T + ): void; +} + +/** + * 消息处理器接口 + */ +export interface IMessageHandler { + readonly messageType: MessageType; + readonly priority: number; + + canHandle(message: INetworkMessage): message is T; + handle(message: T, context?: MessageHandlerContext): Promise | void; +} + +/** + * 消息处理上下文 + */ +export interface MessageHandlerContext { + readonly connectionId?: string; + readonly senderId?: string; + readonly timestamp: number; + readonly metadata?: Record; +} + +/** + * 消息统计接口 + */ +export interface MessageStats { + readonly totalSent: number; + readonly totalReceived: number; + readonly totalDropped: number; + readonly averageSize: number; + readonly messagesByType: Map; + readonly messagesByPriority: Map; +} \ No newline at end of file diff --git a/packages/network/src/types/NetworkTypes.ts b/packages/network/src/types/NetworkTypes.ts index 749face8..03aa1ff8 100644 --- a/packages/network/src/types/NetworkTypes.ts +++ b/packages/network/src/types/NetworkTypes.ts @@ -12,6 +12,11 @@ import { SerializedData } from '../Serialization/SerializationTypes'; * 扩展核心组件接口,添加网络同步功能 */ export interface INetworkSyncable extends IComponent { + /** 内部SyncVar ID */ + _syncVarId?: string; + /** 是否禁用SyncVar监听 */ + _syncVarDisabled?: boolean; + /** * 获取网络同步状态 */ @@ -36,6 +41,9 @@ export interface INetworkSyncable extends IComponent { * 标记字段为脏状态 */ markFieldDirty(fieldNumber: number): void; + + /** 允许通过字符串键访问属性 */ + [propertyKey: string]: unknown; } /** @@ -110,10 +118,10 @@ export type MessageData = | null; /** - * 网络消息基接口 - * 为所有网络消息提供类型安全的基础 + * 基础网络消息接口 + * 为SyncVar等网络同步功能提供消息接口 */ -export interface INetworkMessage { +export interface IBasicNetworkMessage { /** 消息类型 */ readonly messageType: number; /** 消息数据 */ diff --git a/packages/network/src/types/index.ts b/packages/network/src/types/index.ts new file mode 100644 index 00000000..9792ed70 --- /dev/null +++ b/packages/network/src/types/index.ts @@ -0,0 +1,33 @@ +/** + * 网络库类型导出 + */ + +// 核心类型 +export * from './CoreTypes'; +export { + INetworkSyncable, + IBasicNetworkMessage, + NetworkComponentType, + SyncVarValue, + MessageData, + TypeGuards as NetworkTypeGuards +} from './NetworkTypes'; +export { + MessageType, + MessagePriority, + INetworkMessage, + IHeartbeatMessage, + ISyncVarMessage, + IErrorMessage, + ISnapshotMessage, + ITsrpcMessage +} from './MessageTypes'; + +// 常量 +export * from '../constants/NetworkConstants'; + +// 配置 +export * from '../Config/NetworkConfigManager'; + +// 错误处理 +export * from '../Error/NetworkErrorHandler'; \ No newline at end of file diff --git a/packages/network/tests/Serialization/Performance.test.ts b/packages/network/tests/Serialization/Performance.test.ts deleted file mode 100644 index a50bff4d..00000000 --- a/packages/network/tests/Serialization/Performance.test.ts +++ /dev/null @@ -1,445 +0,0 @@ -/** - * Protobuf序列化性能测试 - */ - -import { Component, Entity, Scene } from '@esengine/ecs-framework'; -import { SnapshotManager } from '../../src/Snapshot/SnapshotManager'; -import { ProtobufSerializer } from '../../src/Serialization/ProtobufSerializer'; -import { - ProtoSerializable, - ProtoFloat, - ProtoInt32, - ProtoString, - ProtoBool -} from '../../src/Serialization/ProtobufDecorators'; - -// 性能测试组件 -@ProtoSerializable('PerfPosition') -class PerfPositionComponent extends Component { - @ProtoFloat(1) public x: number = 0; - @ProtoFloat(2) public y: number = 0; - @ProtoFloat(3) public z: number = 0; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.x = x; - this.y = y; - this.z = z; - } -} - -@ProtoSerializable('PerfVelocity') -class PerfVelocityComponent extends Component { - @ProtoFloat(1) public vx: number = 0; - @ProtoFloat(2) public vy: number = 0; - @ProtoFloat(3) public vz: number = 0; - - constructor(vx: number = 0, vy: number = 0, vz: number = 0) { - super(); - this.vx = vx; - this.vy = vy; - this.vz = vz; - } -} - -@ProtoSerializable('PerfHealth') -class PerfHealthComponent extends Component { - @ProtoInt32(1) public maxHealth: number = 100; - @ProtoInt32(2) public currentHealth: number = 100; - @ProtoBool(3) public isDead: boolean = false; - @ProtoFloat(4) public regenerationRate: number = 0.5; - - constructor(maxHealth: number = 100) { - super(); - this.maxHealth = maxHealth; - this.currentHealth = maxHealth; - } -} - -@ProtoSerializable('PerfPlayer') -class PerfPlayerComponent extends Component { - @ProtoString(1) public name: string = ''; - @ProtoInt32(2) public level: number = 1; - @ProtoInt32(3) public experience: number = 0; - @ProtoInt32(4) public score: number = 0; - @ProtoBool(5) public isOnline: boolean = true; - - constructor(name: string = 'Player', level: number = 1) { - super(); - this.name = name; - this.level = level; - } -} - -// 传统JSON序列化组件(用于对比) -class JsonPositionComponent extends Component { - public x: number = 0; - public y: number = 0; - public z: number = 0; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.x = x; - this.y = y; - this.z = z; - } -} - -class JsonPlayerComponent extends Component { - public name: string = ''; - public level: number = 1; - public experience: number = 0; - public score: number = 0; - public isOnline: boolean = true; - - constructor(name: string = 'Player', level: number = 1) { - super(); - this.name = name; - this.level = level; - } -} - -// Mock protobuf.js for performance testing -const createMockProtobuf = () => { - const mockEncodedData = new Uint8Array(32); - mockEncodedData.fill(1); - - return { - parse: jest.fn().mockReturnValue({ - root: { - lookupType: jest.fn().mockImplementation((typeName: string) => { - // 根据类型名返回相应的数据 - const mockData: Record = { - 'ecs.PerfPosition': { x: 10, y: 20, z: 30 }, - 'ecs.PerfVelocity': { vx: 1, vy: 2, vz: 3 }, - 'ecs.PerfHealth': { maxHealth: 100, currentHealth: 80, isDead: false, regenerationRate: 0.5 }, - 'ecs.PerfPlayer': { name: 'TestPlayer', level: 5, experience: 1000, score: 5000, isOnline: true } - }; - - return { - verify: jest.fn().mockReturnValue(null), - create: jest.fn().mockImplementation((data) => data), - encode: jest.fn().mockReturnValue({ - finish: jest.fn().mockReturnValue(mockEncodedData) - }), - decode: jest.fn().mockReturnValue(mockData[typeName] || {}), - toObject: jest.fn().mockImplementation((message) => message) - }; - }) - } - }) - }; -}; - -describe('Protobuf序列化性能测试', () => { - let protobufSerializer: ProtobufSerializer; - let snapshotManager: SnapshotManager; - let scene: Scene; - - beforeEach(() => { - const mockProtobuf = createMockProtobuf(); - protobufSerializer = ProtobufSerializer.getInstance(); - protobufSerializer.initialize(mockProtobuf.parse().root as any); - - snapshotManager = new SnapshotManager(); - snapshotManager.initializeProtobuf(mockProtobuf.parse().root as any); - - scene = new Scene(); - jest.clearAllMocks(); - }); - - describe('单组件序列化性能', () => { - const iterations = 1000; - - it('应该比较protobuf和JSON序列化速度', () => { - const protobufComponents: PerfPositionComponent[] = []; - const jsonComponents: JsonPositionComponent[] = []; - - // 准备测试数据 - for (let i = 0; i < iterations; i++) { - protobufComponents.push(new PerfPositionComponent( - Math.random() * 1000, - Math.random() * 1000, - Math.random() * 100 - )); - - jsonComponents.push(new JsonPositionComponent( - Math.random() * 1000, - Math.random() * 1000, - Math.random() * 100 - )); - } - - // 测试Protobuf序列化 - const protobufStartTime = performance.now(); - let protobufTotalSize = 0; - - for (const component of protobufComponents) { - const result = protobufSerializer.serialize(component); - protobufTotalSize += result.size; - } - - const protobufEndTime = performance.now(); - const protobufTime = protobufEndTime - protobufStartTime; - - // 测试JSON序列化 - const jsonStartTime = performance.now(); - let jsonTotalSize = 0; - - for (const component of jsonComponents) { - const jsonString = JSON.stringify({ - x: component.x, - y: component.y, - z: component.z - }); - jsonTotalSize += new Blob([jsonString]).size; - } - - const jsonEndTime = performance.now(); - const jsonTime = jsonEndTime - jsonStartTime; - - // 性能断言 - console.log(`\\n=== 单组件序列化性能对比 (${iterations} 次迭代) ===`); - console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`); - console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`); - console.log(`Protobuf总大小: ${protobufTotalSize} bytes`); - console.log(`JSON总大小: ${jsonTotalSize} bytes`); - - if (jsonTime > 0) { - const speedImprovement = ((jsonTime - protobufTime) / jsonTime * 100); - console.log(`速度提升: ${speedImprovement.toFixed(1)}%`); - } - - if (jsonTotalSize > 0) { - const sizeReduction = ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100); - console.log(`大小减少: ${sizeReduction.toFixed(1)}%`); - } - - // 基本性能验证 - expect(protobufTime).toBeLessThan(1000); // 不应该超过1秒 - expect(jsonTime).toBeLessThan(1000); - expect(protobufTotalSize).toBeGreaterThan(0); - expect(jsonTotalSize).toBeGreaterThan(0); - }); - - it('应该测试复杂组件的序列化性能', () => { - const protobufPlayers: PerfPlayerComponent[] = []; - const jsonPlayers: JsonPlayerComponent[] = []; - - // 创建测试数据 - for (let i = 0; i < iterations; i++) { - protobufPlayers.push(new PerfPlayerComponent( - `Player${i}`, - Math.floor(Math.random() * 100) + 1 - )); - - jsonPlayers.push(new JsonPlayerComponent( - `Player${i}`, - Math.floor(Math.random() * 100) + 1 - )); - } - - // Protobuf序列化测试 - const protobufStart = performance.now(); - for (const player of protobufPlayers) { - protobufSerializer.serialize(player); - } - const protobufTime = performance.now() - protobufStart; - - // JSON序列化测试 - const jsonStart = performance.now(); - for (const player of jsonPlayers) { - JSON.stringify({ - name: player.name, - level: player.level, - experience: player.experience, - score: player.score, - isOnline: player.isOnline - }); - } - const jsonTime = performance.now() - jsonStart; - - console.log(`\\n=== 复杂组件序列化性能 (${iterations} 次迭代) ===`); - console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`); - console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`); - - expect(protobufTime).toBeLessThan(1000); - expect(jsonTime).toBeLessThan(1000); - }); - }); - - describe('批量实体序列化性能', () => { - it('应该测试大量实体的快照创建性能', () => { - const entityCount = 100; - const entities: Entity[] = []; - - // 创建测试实体 - for (let i = 0; i < entityCount; i++) { - const entity = scene.createEntity(`Entity${i}`); - entity.addComponent(new PerfPositionComponent( - Math.random() * 1000, - Math.random() * 1000, - Math.random() * 100 - )); - entity.addComponent(new PerfVelocityComponent( - Math.random() * 10 - 5, - Math.random() * 10 - 5, - Math.random() * 2 - 1 - )); - entity.addComponent(new PerfHealthComponent(100 + Math.floor(Math.random() * 50))); - entity.addComponent(new PerfPlayerComponent(`Player${i}`, Math.floor(Math.random() * 50) + 1)); - - entities.push(entity); - } - - // 测试快照创建性能 - const snapshotStart = performance.now(); - const snapshot = snapshotManager.createSceneSnapshot(entities); - const snapshotTime = performance.now() - snapshotStart; - - console.log(`\\n=== 批量实体序列化性能 ===`); - console.log(`实体数量: ${entityCount}`); - console.log(`每个实体组件数: 4`); - console.log(`总组件数: ${entityCount * 4}`); - console.log(`快照创建时间: ${snapshotTime.toFixed(2)}ms`); - console.log(`平均每组件时间: ${(snapshotTime / (entityCount * 4)).toFixed(3)}ms`); - - expect(snapshot.entities).toHaveLength(entityCount); - expect(snapshotTime).toBeLessThan(5000); // 不应该超过5秒 - - // 计算快照大小 - let totalSnapshotSize = 0; - for (const entitySnapshot of snapshot.entities) { - for (const componentSnapshot of entitySnapshot.components) { - if (componentSnapshot.data && typeof componentSnapshot.data === 'object' && 'size' in componentSnapshot.data) { - totalSnapshotSize += (componentSnapshot.data as any).size; - } - } - } - - console.log(`快照总大小: ${totalSnapshotSize} bytes`); - console.log(`平均每实体大小: ${(totalSnapshotSize / entityCount).toFixed(1)} bytes`); - - expect(totalSnapshotSize).toBeGreaterThan(0); - }); - }); - - describe('反序列化性能', () => { - it('应该测试快照恢复性能', () => { - const entityCount = 50; - const originalEntities: Entity[] = []; - - // 创建原始实体 - for (let i = 0; i < entityCount; i++) { - const entity = scene.createEntity(`Original${i}`); - entity.addComponent(new PerfPositionComponent(i * 10, i * 20, i)); - entity.addComponent(new PerfHealthComponent(100 + i)); - originalEntities.push(entity); - } - - // 创建快照 - const snapshotStart = performance.now(); - const snapshot = snapshotManager.createSceneSnapshot(originalEntities); - const snapshotTime = performance.now() - snapshotStart; - - // 创建目标实体 - const targetEntities: Entity[] = []; - for (let i = 0; i < entityCount; i++) { - const entity = scene.createEntity(`Target${i}`); - entity.addComponent(new PerfPositionComponent()); - entity.addComponent(new PerfHealthComponent()); - targetEntities.push(entity); - } - - // 测试恢复性能 - const restoreStart = performance.now(); - snapshotManager.restoreFromSnapshot(snapshot, targetEntities); - const restoreTime = performance.now() - restoreStart; - - console.log(`\\n=== 反序列化性能测试 ===`); - console.log(`实体数量: ${entityCount}`); - console.log(`序列化时间: ${snapshotTime.toFixed(2)}ms`); - console.log(`反序列化时间: ${restoreTime.toFixed(2)}ms`); - console.log(`总往返时间: ${(snapshotTime + restoreTime).toFixed(2)}ms`); - console.log(`平均每实体往返时间: ${((snapshotTime + restoreTime) / entityCount).toFixed(3)}ms`); - - expect(restoreTime).toBeLessThan(2000); // 不应该超过2秒 - expect(snapshotTime + restoreTime).toBeLessThan(3000); // 总时间不超过3秒 - }); - }); - - describe('内存使用', () => { - it('应该监控序列化过程中的内存使用', () => { - const entityCount = 200; - const entities: Entity[] = []; - - // 创建大量实体 - for (let i = 0; i < entityCount; i++) { - const entity = scene.createEntity(`MemoryTest${i}`); - entity.addComponent(new PerfPositionComponent( - Math.random() * 1000, - Math.random() * 1000, - Math.random() * 100 - )); - entity.addComponent(new PerfVelocityComponent( - Math.random() * 10, - Math.random() * 10, - Math.random() * 2 - )); - entity.addComponent(new PerfHealthComponent(Math.floor(Math.random() * 200) + 50)); - entities.push(entity); - } - - // 记录初始内存(如果可用) - const initialMemory = (performance as any).memory?.usedJSHeapSize || 0; - - // 执行序列化 - const snapshot = snapshotManager.createSceneSnapshot(entities); - - // 记录序列化后内存 - const afterMemory = (performance as any).memory?.usedJSHeapSize || 0; - const memoryIncrease = afterMemory - initialMemory; - - if (initialMemory > 0) { - console.log(`\\n=== 内存使用测试 ===`); - console.log(`实体数量: ${entityCount}`); - console.log(`初始内存: ${(initialMemory / 1024 / 1024).toFixed(2)} MB`); - console.log(`序列化后内存: ${(afterMemory / 1024 / 1024).toFixed(2)} MB`); - console.log(`内存增加: ${(memoryIncrease / 1024).toFixed(2)} KB`); - console.log(`平均每实体内存: ${(memoryIncrease / entityCount).toFixed(1)} bytes`); - } - - expect(snapshot.entities).toHaveLength(entityCount); - - // 清理 - entities.length = 0; - }); - }); - - describe('极端情况性能', () => { - it('应该处理大量小组件的性能', () => { - const componentCount = 5000; - const components: PerfPositionComponent[] = []; - - // 创建大量小组件 - for (let i = 0; i < componentCount; i++) { - components.push(new PerfPositionComponent(i, i * 2, i * 3)); - } - - const start = performance.now(); - for (const component of components) { - protobufSerializer.serialize(component); - } - const time = performance.now() - start; - - console.log(`\\n=== 大量小组件性能测试 ===`); - console.log(`组件数量: ${componentCount}`); - console.log(`总时间: ${time.toFixed(2)}ms`); - console.log(`平均每组件: ${(time / componentCount).toFixed(4)}ms`); - console.log(`每秒处理: ${Math.floor(componentCount / (time / 1000))} 个组件`); - - expect(time).toBeLessThan(10000); // 不超过10秒 - expect(time / componentCount).toBeLessThan(2); // 每个组件不超过2ms - }); - }); -}); \ No newline at end of file diff --git a/packages/network/tests/Serialization/ProtobufDecorators.test.ts b/packages/network/tests/Serialization/ProtobufDecorators.test.ts deleted file mode 100644 index a0add7e7..00000000 --- a/packages/network/tests/Serialization/ProtobufDecorators.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * Protobuf装饰器测试 - */ - -import { Component } from '@esengine/ecs-framework'; -import { - ProtoSerializable, - ProtoField, - ProtoTypes, - ProtoFloat, - ProtoInt32, - ProtoString, - ProtoBool, - ProtobufRegistry, - isProtoSerializable, - getProtoName -} from '../../src/Serialization/ProtobufDecorators'; - -// 测试组件 -@ProtoSerializable('TestPosition') -class TestPositionComponent extends Component { - @ProtoFloat(1) - public x: number = 0; - - @ProtoFloat(2) - public y: number = 0; - - @ProtoFloat(3) - public z: number = 0; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.x = x; - this.y = y; - this.z = z; - } -} - -@ProtoSerializable('TestPlayer') -class TestPlayerComponent extends Component { - @ProtoString(1) - public name: string = ''; - - @ProtoInt32(2) - public level: number = 1; - - @ProtoInt32(3) - public health: number = 100; - - @ProtoBool(4) - public isAlive: boolean = true; - - constructor(name: string = '', level: number = 1) { - super(); - this.name = name; - this.level = level; - } -} - -// 没有装饰器的组件 -class PlainComponent extends Component { - public data: string = 'test'; -} - -// 测试字段编号冲突的组件 -const createConflictingComponent = () => { - try { - @ProtoSerializable('Conflict') - class ConflictComponent extends Component { - @ProtoFloat(1) - public x: number = 0; - - @ProtoFloat(1) // 故意使用相同的字段编号 - public y: number = 0; - } - return ConflictComponent; - } catch (error) { - return error; - } -}; - -describe('ProtobufDecorators', () => { - let registry: ProtobufRegistry; - - beforeEach(() => { - // 获取注册表实例 - registry = ProtobufRegistry.getInstance(); - // 不清理状态,因为装饰器在模块加载时已执行 - }); - - describe('@ProtoSerializable装饰器', () => { - it('应该正确标记组件为可序列化', () => { - const component = new TestPositionComponent(10, 20, 30); - - expect(isProtoSerializable(component)).toBe(true); - expect(getProtoName(component)).toBe('TestPosition'); - }); - - it('应该在注册表中注册组件定义', () => { - expect(registry.hasProtoDefinition('TestPosition')).toBe(true); - expect(registry.hasProtoDefinition('TestPlayer')).toBe(true); - }); - - it('应该正确处理没有装饰器的组件', () => { - const component = new PlainComponent(); - - expect(isProtoSerializable(component)).toBe(false); - expect(getProtoName(component)).toBeUndefined(); - }); - }); - - describe('@ProtoField装饰器', () => { - it('应该正确定义字段', () => { - const definition = registry.getComponentDefinition('TestPosition'); - - expect(definition).toBeDefined(); - expect(definition!.fields.size).toBeGreaterThanOrEqual(2); - - const xField = definition!.fields.get('x'); - expect(xField).toEqual({ - fieldNumber: 1, - type: ProtoTypes.FLOAT, - repeated: false, - optional: false, - name: 'x', - customTypeName: undefined, - enumValues: undefined, - defaultValue: undefined, - syncPriority: 'medium', - precision: undefined, - interpolation: false, - quantizationBits: undefined, - changeThreshold: 0 - }); - - const yField = definition!.fields.get('y'); - expect(yField).toEqual({ - fieldNumber: 2, - type: ProtoTypes.FLOAT, - repeated: false, - optional: false, - name: 'y', - customTypeName: undefined, - enumValues: undefined, - defaultValue: undefined, - syncPriority: 'medium', - precision: undefined, - interpolation: false, - quantizationBits: undefined, - changeThreshold: 0 - }); - }); - - it('应该支持不同的字段类型', () => { - const definition = registry.getComponentDefinition('TestPlayer'); - - expect(definition).toBeDefined(); - expect(definition!.fields.size).toBeGreaterThanOrEqual(4); - - const nameField = definition!.fields.get('name'); - expect(nameField!.type).toBe(ProtoTypes.STRING); - - const levelField = definition!.fields.get('level'); - expect(levelField!.type).toBe(ProtoTypes.INT32); - - const healthField = definition!.fields.get('health'); - expect(healthField!.type).toBe(ProtoTypes.INT32); - - const isAliveField = definition!.fields.get('isAlive'); - expect(isAliveField!.type).toBe(ProtoTypes.BOOL); - }); - - it('应该检测字段编号冲突', () => { - const result = createConflictingComponent(); - expect(result).toBeInstanceOf(Error); - expect((result as Error).message).toContain('字段编号 1 已被字段'); - }); - - it('应该验证字段编号有效性', () => { - expect(() => { - class InvalidFieldComponent extends Component { - @ProtoField(0) // 无效的字段编号 - public invalid: number = 0; - } - }).toThrow('字段编号必须大于0'); - - expect(() => { - class InvalidFieldComponent extends Component { - @ProtoField(-1) // 无效的字段编号 - public invalid: number = 0; - } - }).toThrow('字段编号必须大于0'); - }); - }); - - describe('便捷装饰器', () => { - it('ProtoFloat应该设置正确的字段类型', () => { - @ProtoSerializable('FloatTest') - class FloatTestComponent extends Component { - @ProtoFloat(1) - public value: number = 0; - } - - const definition = registry.getComponentDefinition('FloatTest'); - const field = definition!.fields.get('value'); - expect(field!.type).toBe(ProtoTypes.FLOAT); - }); - - it('ProtoInt32应该设置正确的字段类型', () => { - @ProtoSerializable('Int32Test') - class Int32TestComponent extends Component { - @ProtoInt32(1) - public value: number = 0; - } - - const definition = registry.getComponentDefinition('Int32Test'); - const field = definition!.fields.get('value'); - expect(field!.type).toBe(ProtoTypes.INT32); - }); - - it('ProtoString应该设置正确的字段类型', () => { - @ProtoSerializable('StringTest') - class StringTestComponent extends Component { - @ProtoString(1) - public value: string = ''; - } - - const definition = registry.getComponentDefinition('StringTest'); - const field = definition!.fields.get('value'); - expect(field!.type).toBe(ProtoTypes.STRING); - }); - - it('ProtoBool应该设置正确的字段类型', () => { - @ProtoSerializable('BoolTest') - class BoolTestComponent extends Component { - @ProtoBool(1) - public value: boolean = false; - } - - const definition = registry.getComponentDefinition('BoolTest'); - const field = definition!.fields.get('value'); - expect(field!.type).toBe(ProtoTypes.BOOL); - }); - }); - - describe('ProtobufRegistry', () => { - it('应该正确生成proto定义', () => { - const protoDefinition = registry.generateProtoDefinition(); - - expect(protoDefinition).toContain('syntax = "proto3";'); - expect(protoDefinition).toContain('package ecs;'); - expect(protoDefinition).toContain('message TestPosition'); - expect(protoDefinition).toContain('message TestPlayer'); - expect(protoDefinition).toContain('float x = 1;'); - expect(protoDefinition).toContain('float y = 2;'); - expect(protoDefinition).toContain('string name = 1;'); - expect(protoDefinition).toContain('int32 level = 2;'); - expect(protoDefinition).toContain('bool isAlive = 4;'); - }); - - it('应该正确管理组件注册', () => { - const allComponents = registry.getAllComponents(); - - expect(allComponents.size).toBeGreaterThanOrEqual(1); - // 由于测试执行顺序不确定,只检查有组件注册即可 - expect(allComponents.size).toBeGreaterThan(0); - }); - }); - - describe('字段选项', () => { - it('应该支持repeated字段', () => { - @ProtoSerializable('RepeatedTest') - class RepeatedTestComponent extends Component { - @ProtoField(1, ProtoTypes.INT32, { repeated: true }) - public values: number[] = []; - } - - const definition = registry.getComponentDefinition('RepeatedTest'); - const field = definition!.fields.get('values'); - expect(field!.repeated).toBe(true); - }); - - it('应该支持optional字段', () => { - @ProtoSerializable('OptionalTest') - class OptionalTestComponent extends Component { - @ProtoField(1, ProtoTypes.STRING, { optional: true }) - public optionalValue?: string; - } - - const definition = registry.getComponentDefinition('OptionalTest'); - const field = definition!.fields.get('optionalValue'); - expect(field!.optional).toBe(true); - }); - }); -}); \ No newline at end of file diff --git a/packages/network/tests/Serialization/ProtobufSerializer.test.ts b/packages/network/tests/Serialization/ProtobufSerializer.test.ts deleted file mode 100644 index fc5879af..00000000 --- a/packages/network/tests/Serialization/ProtobufSerializer.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * Protobuf序列化器测试 - */ - -import { Component } from '@esengine/ecs-framework'; -import { ProtobufSerializer } from '../../src/Serialization/ProtobufSerializer'; -import { SerializedData } from '../../src/Serialization/SerializationTypes'; -import { - ProtoSerializable, - ProtoFloat, - ProtoInt32, - ProtoString, - ProtoBool, - ProtobufRegistry -} from '../../src/Serialization/ProtobufDecorators'; - -// 测试组件 -@ProtoSerializable('Position') -class PositionComponent extends Component { - @ProtoFloat(1) - public x: number = 0; - - @ProtoFloat(2) - public y: number = 0; - - @ProtoFloat(3) - public z: number = 0; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.x = x; - this.y = y; - this.z = z; - } -} - -@ProtoSerializable('Health') -class HealthComponent extends Component { - @ProtoInt32(1) - public maxHealth: number = 100; - - @ProtoInt32(2) - public currentHealth: number = 100; - - @ProtoBool(3) - public isDead: boolean = false; - - constructor(maxHealth: number = 100) { - super(); - this.maxHealth = maxHealth; - this.currentHealth = maxHealth; - } - - takeDamage(damage: number): void { - this.currentHealth = Math.max(0, this.currentHealth - damage); - this.isDead = this.currentHealth <= 0; - } -} - -@ProtoSerializable('Player') -class PlayerComponent extends Component { - @ProtoString(1) - public playerName: string = ''; - - @ProtoInt32(2) - public playerId: number = 0; - - @ProtoInt32(3) - public level: number = 1; - - constructor(playerId: number = 0, playerName: string = '') { - super(); - this.playerId = playerId; - this.playerName = playerName; - } -} - -// 没有protobuf装饰器的组件 -class CustomComponent extends Component { - public customData = { - settings: { volume: 0.8 }, - achievements: ['first_kill', 'level_up'], - inventory: new Map([['sword', 1], ['potion', 3]]) - }; - - // 自定义序列化方法 - serialize(): any { - return { - customData: { - settings: this.customData.settings, - achievements: this.customData.achievements, - inventory: Array.from(this.customData.inventory.entries()) - } - }; - } - - deserialize(data: any): void { - if (data.customData) { - this.customData.settings = data.customData.settings || this.customData.settings; - this.customData.achievements = data.customData.achievements || this.customData.achievements; - if (data.customData.inventory) { - this.customData.inventory = new Map(data.customData.inventory); - } - } - } -} - -// Mock protobuf.js Root -const mockProtobufRoot = { - lookupType: jest.fn().mockImplementation((typeName: string) => { - // 模拟protobuf消息类型 - return { - verify: jest.fn().mockReturnValue(null), // 验证通过 - create: jest.fn().mockImplementation((data) => data), - encode: jest.fn().mockReturnValue({ - finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) // 模拟编码结果 - }), - decode: jest.fn().mockImplementation(() => ({ - x: 10, y: 20, z: 30, - maxHealth: 100, currentHealth: 80, isDead: false, - playerName: 'TestPlayer', playerId: 1001, level: 5 - })), - toObject: jest.fn().mockImplementation((message) => message), - fromObject: jest.fn().mockImplementation((obj) => obj) - }; - }) -} as any; - -describe('ProtobufSerializer', () => { - let serializer: ProtobufSerializer; - - beforeEach(() => { - serializer = ProtobufSerializer.getInstance(); - // 重置mock - jest.clearAllMocks(); - }); - - describe('初始化', () => { - it('应该正确初始化protobuf支持', () => { - serializer.initialize(mockProtobufRoot); - - expect(serializer.canSerialize(new PositionComponent())).toBe(true); - }); - - it('自动初始化后应该能够序列化protobuf组件', () => { - const newSerializer = new (ProtobufSerializer as any)(); - expect(newSerializer.canSerialize(new PositionComponent())).toBe(true); - }); - }); - - describe('序列化', () => { - beforeEach(() => { - serializer.initialize(mockProtobufRoot); - }); - - it('应该正确序列化protobuf组件', () => { - const position = new PositionComponent(10, 20, 30); - const result = serializer.serialize(position); - - expect(result.type).toBe('protobuf'); - expect(result.componentType).toBe('PositionComponent'); - expect(result.data).toBeInstanceOf(Uint8Array); - expect(result.size).toBeGreaterThan(0); - }); - - it('应该正确序列化复杂protobuf组件', () => { - const health = new HealthComponent(150); - health.takeDamage(50); - - const result = serializer.serialize(health); - - expect(result.type).toBe('protobuf'); - expect(result.componentType).toBe('HealthComponent'); - expect(result.data).toBeInstanceOf(Uint8Array); - }); - - it('应该拒绝非protobuf组件并抛出错误', () => { - const custom = new CustomComponent(); - - expect(() => { - serializer.serialize(custom); - }).toThrow('组件 CustomComponent 不支持protobuf序列化,请添加@ProtoSerializable装饰器'); - }); - - it.skip('protobuf验证失败时应该抛出错误(跳过mock测试)', () => { - // 此测试跳过,因为mock验证在重构后需要更复杂的设置 - }); - }); - - describe('反序列化', () => { - beforeEach(() => { - serializer.initialize(mockProtobufRoot); - }); - - it('应该正确反序列化protobuf数据', () => { - const position = new PositionComponent(); - const serializedData: SerializedData = { - type: 'protobuf', - componentType: 'PositionComponent', - data: new Uint8Array([1, 2, 3, 4]), - size: 4 - }; - - expect(() => { - serializer.deserialize(position, serializedData); - }).not.toThrow(); - }); - - it('应该拒绝非protobuf数据并抛出错误', () => { - const custom = new CustomComponent(); - - const serializedData: SerializedData = { - type: 'json', - componentType: 'CustomComponent', - data: {}, - size: 100 - }; - - expect(() => { - serializer.deserialize(custom, serializedData); - }).toThrow('不支持的序列化类型: json'); - }); - - it('应该处理反序列化错误', () => { - const position = new PositionComponent(); - const invalidData: SerializedData = { - type: 'protobuf', - componentType: 'PositionComponent', - data: new Uint8Array([255, 255, 255, 255]), // 无效数据 - size: 4 - }; - - // 模拟解码失败 - const mockType = mockProtobufRoot.lookupType('ecs.Position'); - mockType.decode.mockImplementation(() => { - throw new Error('解码失败'); - }); - - // 应该不抛出异常 - expect(() => { - serializer.deserialize(position, invalidData); - }).not.toThrow(); - }); - }); - - describe('统计信息', () => { - it('应该返回正确的统计信息', () => { - serializer.initialize(mockProtobufRoot); - const stats = serializer.getStats(); - - expect(stats.protobufAvailable).toBe(true); - expect(stats.registeredComponents).toBeGreaterThan(0); - }); - - it('自动初始化后应该返回正确的状态', () => { - const newSerializer = new (ProtobufSerializer as any)(); - const stats = newSerializer.getStats(); - - expect(stats.protobufAvailable).toBe(true); - }); - }); - - describe('边界情况', () => { - beforeEach(() => { - serializer.initialize(mockProtobufRoot); - }); - - it('应该处理空值和undefined', () => { - const position = new PositionComponent(); - // 设置一些undefined值 - (position as any).undefinedProp = undefined; - (position as any).nullProp = null; - - const result = serializer.serialize(position); - expect(result).toBeDefined(); - }); - - it('应该拒绝非protobuf组件', () => { - const custom = new CustomComponent(); - // 创建循环引用 - (custom as any).circular = custom; - - expect(() => { - serializer.serialize(custom); - }).toThrow('组件 CustomComponent 不支持protobuf序列化'); - }); - - it('应该处理非常大的数值', () => { - const position = new PositionComponent(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, 0); - - const result = serializer.serialize(position); - expect(result).toBeDefined(); - }); - }); -}); \ No newline at end of file diff --git a/packages/network/tests/Serialization/ProtobufSerializerEdgeCases.test.ts b/packages/network/tests/Serialization/ProtobufSerializerEdgeCases.test.ts deleted file mode 100644 index c858cfdd..00000000 --- a/packages/network/tests/Serialization/ProtobufSerializerEdgeCases.test.ts +++ /dev/null @@ -1,438 +0,0 @@ -/** - * Protobuf序列化器边界情况测试 - */ - -import { Component, BigIntFactory } from '@esengine/ecs-framework'; -import { ProtobufSerializer } from '../../src/Serialization/ProtobufSerializer'; -import { - ProtoSerializable, - ProtoFloat, - ProtoInt32, - ProtoString, - ProtoBool, - ProtoBytes, - ProtoTimestamp, - ProtoDouble, - ProtoInt64, - ProtoStruct -} from '../../src/Serialization/ProtobufDecorators'; - -// 边界测试组件 -@ProtoSerializable('EdgeCaseComponent') -class EdgeCaseComponent extends Component { - @ProtoFloat(1) - public floatValue: number = 0; - - @ProtoDouble(2) - public doubleValue: number = 0; - - @ProtoInt32(3) - public intValue: number = 0; - - @ProtoInt64(4) - public bigIntValue: any = BigIntFactory.zero(); - - @ProtoString(5) - public stringValue: string = ''; - - @ProtoBool(6) - public boolValue: boolean = false; - - @ProtoBytes(7) - public bytesValue: Uint8Array = new Uint8Array(); - - @ProtoTimestamp(8) - public timestampValue: Date = new Date(); - - @ProtoStruct(9) - public structValue: any = {}; - - @ProtoFloat(10, { repeated: true }) - public arrayValue: number[] = []; - - constructor() { - super(); - } -} - -// 不完整的组件(缺少字段) -@ProtoSerializable('IncompleteComponent') -class IncompleteComponent extends Component { - @ProtoString(1) - public name: string = ''; - - // 故意添加没有装饰器的字段 - public undecoratedField: number = 42; - - constructor(name: string = '') { - super(); - this.name = name; - } -} - -// 有循环引用的组件 -@ProtoSerializable('CircularComponent') -class CircularComponent extends Component { - @ProtoString(1) - public name: string = ''; - - @ProtoStruct(2) - public circular: any = null; - - constructor(name: string = '') { - super(); - this.name = name; - // 创建循环引用 - this.circular = this; - } -} - -// 没有protobuf装饰器的组件 -class NonSerializableComponent extends Component { - public data: string = 'test'; - - serialize(): any { - return { data: this.data }; - } - - deserialize(data: any): void { - this.data = data.data || this.data; - } -} - -// Mock protobuf.js -const mockProtobuf = { - Root: jest.fn(), - Type: jest.fn(), - Field: jest.fn(), - parse: jest.fn().mockReturnValue({ - root: { - lookupType: jest.fn().mockImplementation((typeName: string) => { - return { - verify: jest.fn().mockReturnValue(null), - create: jest.fn().mockImplementation((data) => data), - encode: jest.fn().mockReturnValue({ - finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) - }), - decode: jest.fn().mockImplementation(() => ({ - floatValue: 3.14, - doubleValue: 2.718, - intValue: 42, - bigIntValue: BigIntFactory.create(999), - stringValue: 'test', - boolValue: true, - bytesValue: new Uint8Array([65, 66, 67]), - timestampValue: { seconds: 1609459200, nanos: 0 }, - structValue: { fields: { key: { stringValue: 'value' } } }, - arrayValue: [1.1, 2.2, 3.3], - name: 'TestComponent' - })), - toObject: jest.fn().mockImplementation((message) => message), - fromObject: jest.fn().mockImplementation((obj) => obj) - }; - }), - lookupTypeOrEnum: jest.fn().mockImplementation((typeName: string) => { - if (typeName === 'google.protobuf.Timestamp') { - return { - verify: jest.fn().mockReturnValue(null), - create: jest.fn().mockImplementation((data) => data), - encode: jest.fn().mockReturnValue({ - finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) - }), - decode: jest.fn().mockImplementation(() => ({ - seconds: 1609459200, - nanos: 0 - })), - toObject: jest.fn().mockImplementation((message) => message), - fromObject: jest.fn().mockImplementation((obj) => obj) - }; - } - return null; - }) - } - }) -}; - -describe('ProtobufSerializer边界情况测试', () => { - let serializer: ProtobufSerializer; - - beforeEach(() => { - serializer = ProtobufSerializer.getInstance(); - serializer.initialize(mockProtobuf.parse().root as any); - jest.clearAllMocks(); - }); - - describe('极值测试', () => { - it('应该处理极大值', () => { - const component = new EdgeCaseComponent(); - component.floatValue = Number.MAX_VALUE; - component.doubleValue = Number.MAX_VALUE; - component.intValue = Number.MAX_SAFE_INTEGER; - component.bigIntValue = BigIntFactory.create(Number.MAX_SAFE_INTEGER); - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - expect(result.size).toBeGreaterThan(0); - }); - - it('应该处理极小值', () => { - const component = new EdgeCaseComponent(); - component.floatValue = Number.MIN_VALUE; - component.doubleValue = Number.MIN_VALUE; - component.intValue = Number.MIN_SAFE_INTEGER; - component.bigIntValue = BigIntFactory.create(Number.MIN_SAFE_INTEGER); - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - - it('应该处理特殊数值', () => { - const component = new EdgeCaseComponent(); - component.floatValue = NaN; - component.doubleValue = Infinity; - component.intValue = 0; - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - }); - - describe('空值和undefined测试', () => { - it('应该处理null值', () => { - const component = new EdgeCaseComponent(); - (component as any).stringValue = null; - (component as any).structValue = null; - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - - it('应该处理undefined值', () => { - const component = new EdgeCaseComponent(); - (component as any).stringValue = undefined; - (component as any).floatValue = undefined; - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - - it('应该处理空数组', () => { - const component = new EdgeCaseComponent(); - component.arrayValue = []; - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - }); - - describe('复杂数据类型测试', () => { - it('应该处理复杂对象结构', () => { - const component = new EdgeCaseComponent(); - component.structValue = { - nested: { - array: [1, 2, 3], - object: { key: 'value' }, - date: new Date(), - null: null, - undefined: undefined - } - }; - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - - it('应该处理Date对象', () => { - const component = new EdgeCaseComponent(); - component.timestampValue = new Date('2021-01-01T00:00:00Z'); - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - - it('应该处理Uint8Array', () => { - const component = new EdgeCaseComponent(); - component.bytesValue = new Uint8Array([0, 255, 128, 64]); - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - }); - - describe('循环引用测试', () => { - it('应该处理循环引用对象', () => { - const component = new CircularComponent('circular'); - - // 循环引用应该被妥善处理 - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - expect(result.componentType).toBe('CircularComponent'); - }); - }); - - describe('不完整组件测试', () => { - it('应该处理缺少装饰器的字段', () => { - const component = new IncompleteComponent('test'); - - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - expect(result.componentType).toBe('IncompleteComponent'); - }); - }); - - describe('非序列化组件测试', () => { - it('应该拒绝非protobuf组件并抛出错误', () => { - const component = new NonSerializableComponent(); - - // 没有protobuf装饰器的组件应该抛出错误,不再回退到JSON序列化 - expect(() => { - serializer.serialize(component); - }).toThrow(); - }); - }); - - describe('批量序列化边界测试', () => { - it('应该处理空数组', () => { - const results = serializer.serializeBatch([]); - expect(results).toEqual([]); - }); - - it('应该处理混合组件类型', () => { - const components = [ - new EdgeCaseComponent(), - new NonSerializableComponent(), - new IncompleteComponent('mixed'), - ]; - - // continueOnError: true 时,只有可序列化的组件能成功,其他会被跳过 - const results = serializer.serializeBatch(components, { continueOnError: true }); - expect(results.length).toBeGreaterThanOrEqual(1); - expect(results.every(r => r.type === 'protobuf')).toBe(true); - - // continueOnError: false 时应该抛出错误 - expect(() => { - serializer.serializeBatch(components, { continueOnError: false }); - }).toThrow(); - }); - - it('应该处理批量数据', () => { - const components = Array.from({ length: 50 }, () => new EdgeCaseComponent()); - - const results = serializer.serializeBatch(components); - expect(results).toHaveLength(50); - expect(results.every(r => r.type === 'protobuf')).toBe(true); - }); - - it('应该处理序列化错误', () => { - // 创建会导致序列化失败的组件 - const components = [new NonSerializableComponent()]; - - // continueOnError = false 应该抛出异常 - expect(() => { - serializer.serializeBatch(components, { continueOnError: false }); - }).toThrow(); - - // continueOnError = true 应该返回空数组(跳过失败的组件) - const results = serializer.serializeBatch(components, { continueOnError: true }); - expect(results).toHaveLength(0); - }); - }); - - describe('反序列化边界测试', () => { - it('应该拒绝JSON类型的反序列化并抛出错误', () => { - const component = new NonSerializableComponent(); - const serializedData = { - type: 'json' as const, - componentType: 'NonSerializableComponent', - data: { data: 'deserialized' }, - size: 100 - }; - - // JSON类型的数据应该被拒绝,抛出错误 - expect(() => { - serializer.deserialize(component, serializedData); - }).toThrow(); - }); - - it('应该优雅处理反序列化错误', () => { - const component = new EdgeCaseComponent(); - const invalidData = { - type: 'protobuf' as const, - componentType: 'EdgeCaseComponent', - data: new Uint8Array([255, 255, 255, 255]), - size: 4 - }; - - // 模拟解码失败 - const mockType = mockProtobuf.parse().root.lookupType('ecs.EdgeCaseComponent'); - mockType.decode.mockImplementation(() => { - throw new Error('Decode failed'); - }); - - // 不应该抛出异常 - expect(() => { - serializer.deserialize(component, invalidData); - }).not.toThrow(); - }); - - it('应该处理缺失的proto定义', () => { - const component = new EdgeCaseComponent(); - // 清除proto名称以模拟缺失情况 - (component as any)._protoName = undefined; - - const serializedData = { - type: 'protobuf' as const, - componentType: 'EdgeCaseComponent', - data: new Uint8Array([1, 2, 3, 4]), - size: 4 - }; - - // 应该抛出未设置protobuf名称的错误 - expect(() => { - serializer.deserialize(component, serializedData); - }).toThrow('组件 EdgeCaseComponent 未设置protobuf名称'); - }); - }); - - describe('缓存测试', () => { - it('应该能清空所有缓存', () => { - serializer.clearAllCaches(); - const stats = serializer.getStats(); - expect(stats.messageTypeCacheSize).toBe(0); - expect(stats.componentDataCacheSize).toBe(0); - }); - }); - - describe('性能选项测试', () => { - it('应该能禁用数据验证', () => { - serializer.setPerformanceOptions({ enableValidation: false }); - - const component = new EdgeCaseComponent(); - const result = serializer.serialize(component); - expect(result.type).toBe('protobuf'); - }); - - it('应该能禁用组件数据缓存', () => { - serializer.setPerformanceOptions({ enableComponentDataCache: false }); - - const component = new EdgeCaseComponent(); - serializer.serialize(component); - - const stats = serializer.getStats(); - expect(stats.componentDataCacheSize).toBe(0); - }); - }); - - describe('统计信息测试', () => { - it('应该返回正确的统计信息', () => { - const stats = serializer.getStats(); - - expect(typeof stats.registeredComponents).toBe('number'); - expect(typeof stats.protobufAvailable).toBe('boolean'); - expect(typeof stats.messageTypeCacheSize).toBe('number'); - expect(typeof stats.componentDataCacheSize).toBe('number'); - expect(typeof stats.enableComponentDataCache).toBe('boolean'); - expect(typeof stats.maxCacheSize).toBe('number'); - }); - }); -}); \ No newline at end of file diff --git a/packages/network/tests/Serialization/RealPerformance.test.ts b/packages/network/tests/Serialization/RealPerformance.test.ts deleted file mode 100644 index 516bd2e7..00000000 --- a/packages/network/tests/Serialization/RealPerformance.test.ts +++ /dev/null @@ -1,393 +0,0 @@ -/** - * 真实 Protobuf 序列化性能测试 - * 使用实际的 protobufjs 库进行性能对比 - */ - -import 'reflect-metadata'; -import { Component } from '@esengine/ecs-framework'; -import { - ProtoSerializable, - ProtoFloat, - ProtoInt32, - ProtoString, - ProtoBool, - ProtobufRegistry -} from '../../src/Serialization/ProtobufDecorators'; - -// 测试组件 -@ProtoSerializable('Position') -class PositionComponent extends Component { - @ProtoFloat(1) public x: number = 0; - @ProtoFloat(2) public y: number = 0; - @ProtoFloat(3) public z: number = 0; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.x = x; - this.y = y; - this.z = z; - } -} - -@ProtoSerializable('Player') -class PlayerComponent extends Component { - @ProtoString(1) public name: string = ''; - @ProtoInt32(2) public level: number = 1; - @ProtoInt32(3) public experience: number = 0; - @ProtoInt32(4) public score: number = 0; - @ProtoBool(5) public isOnline: boolean = true; - @ProtoFloat(6) public health: number = 100.0; - - constructor(name: string = 'Player', level: number = 1) { - super(); - this.name = name; - this.level = level; - this.experience = level * 1000; - this.score = level * 500; - this.health = 100.0; - } -} - -// JSON 对比组件 -class JsonPositionComponent extends Component { - public x: number = 0; - public y: number = 0; - public z: number = 0; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.x = x; - this.y = y; - this.z = z; - } -} - -class JsonPlayerComponent extends Component { - public name: string = ''; - public level: number = 1; - public experience: number = 0; - public score: number = 0; - public isOnline: boolean = true; - public health: number = 100.0; - - constructor(name: string = 'Player', level: number = 1) { - super(); - this.name = name; - this.level = level; - this.experience = level * 1000; - this.score = level * 500; - this.health = 100.0; - } -} - -describe('真实 Protobuf 性能测试', () => { - let protobuf: any; - let root: any; - let PositionType: any; - let PlayerType: any; - - beforeAll(async () => { - try { - // 尝试加载真实的 protobufjs - protobuf = require('protobufjs'); - - // 生成 proto 定义 - const registry = ProtobufRegistry.getInstance(); - const protoDefinition = registry.generateProtoDefinition(); - - console.log('Generated proto definition:'); - console.log(protoDefinition); - - // 解析 proto 定义 - root = protobuf.parse(protoDefinition).root; - PositionType = root.lookupType('ecs.Position'); - PlayerType = root.lookupType('ecs.Player'); - - } catch (error) { - console.warn('Protobuf not available, skipping real performance tests:', error); - } - }); - - const skipIfNoProtobuf = () => { - if (!protobuf || !root) { - console.log('Skipping test: protobufjs not available'); - return true; - } - return false; - }; - - describe('简单组件性能对比', () => { - it('Position 组件序列化性能', () => { - if (skipIfNoProtobuf()) return; - - const iterations = 1000; - const protobufComponents: PositionComponent[] = []; - const jsonComponents: JsonPositionComponent[] = []; - - // 准备测试数据 - for (let i = 0; i < iterations; i++) { - const x = Math.random() * 1000; - const y = Math.random() * 1000; - const z = Math.random() * 100; - - protobufComponents.push(new PositionComponent(x, y, z)); - jsonComponents.push(new JsonPositionComponent(x, y, z)); - } - - // Protobuf 序列化测试 - const protobufStartTime = performance.now(); - let protobufTotalSize = 0; - const protobufResults: Uint8Array[] = []; - - for (const component of protobufComponents) { - const message = PositionType.create({ - x: component.x, - y: component.y, - z: component.z - }); - const buffer = PositionType.encode(message).finish(); - protobufResults.push(buffer); - protobufTotalSize += buffer.length; - } - - const protobufEndTime = performance.now(); - const protobufTime = protobufEndTime - protobufStartTime; - - // JSON 序列化测试 - const jsonStartTime = performance.now(); - let jsonTotalSize = 0; - const jsonResults: string[] = []; - - for (const component of jsonComponents) { - const jsonString = JSON.stringify({ - x: component.x, - y: component.y, - z: component.z - }); - jsonResults.push(jsonString); - jsonTotalSize += new Blob([jsonString]).size; - } - - const jsonEndTime = performance.now(); - const jsonTime = jsonEndTime - jsonStartTime; - - // 计算性能指标 - const speedImprovement = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0; - const sizeReduction = jsonTotalSize > 0 ? ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100) : 0; - - console.log(`\\n=== Position 组件性能对比 (${iterations} 次迭代) ===`); - console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`); - console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`); - console.log(`速度变化: ${speedImprovement > 0 ? '+' : ''}${speedImprovement.toFixed(1)}%`); - console.log(''); - console.log(`Protobuf 总大小: ${protobufTotalSize} bytes`); - console.log(`JSON 总大小: ${jsonTotalSize} bytes`); - console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`); - console.log(`平均 Protobuf 大小: ${(protobufTotalSize / iterations).toFixed(1)} bytes`); - console.log(`平均 JSON 大小: ${(jsonTotalSize / iterations).toFixed(1)} bytes`); - - // 验证反序列化 - let deserializeTime = performance.now(); - for (const buffer of protobufResults.slice(0, 10)) { // 只测试前10个 - const decoded = PositionType.decode(buffer); - expect(typeof decoded.x).toBe('number'); - expect(typeof decoded.y).toBe('number'); - expect(typeof decoded.z).toBe('number'); - } - deserializeTime = performance.now() - deserializeTime; - console.log(`Protobuf 反序列化 10 个: ${deserializeTime.toFixed(2)}ms`); - - // 基本验证 - expect(protobufTime).toBeGreaterThan(0); - expect(jsonTime).toBeGreaterThan(0); - expect(protobufTotalSize).toBeGreaterThan(0); - expect(jsonTotalSize).toBeGreaterThan(0); - }); - - it('复杂 Player 组件序列化性能', () => { - if (skipIfNoProtobuf()) return; - - const iterations = 500; - const protobufPlayers: PlayerComponent[] = []; - const jsonPlayers: JsonPlayerComponent[] = []; - - // 创建测试数据 - for (let i = 0; i < iterations; i++) { - const name = `Player_${i}_${'x'.repeat(10 + Math.floor(Math.random() * 20))}`; - const level = Math.floor(Math.random() * 100) + 1; - - protobufPlayers.push(new PlayerComponent(name, level)); - jsonPlayers.push(new JsonPlayerComponent(name, level)); - } - - // Protobuf 序列化测试 - const protobufStart = performance.now(); - let protobufSize = 0; - - for (const player of protobufPlayers) { - const message = PlayerType.create({ - name: player.name, - level: player.level, - experience: player.experience, - score: player.score, - isOnline: player.isOnline, - health: player.health - }); - const buffer = PlayerType.encode(message).finish(); - protobufSize += buffer.length; - } - - const protobufTime = performance.now() - protobufStart; - - // JSON 序列化测试 - const jsonStart = performance.now(); - let jsonSize = 0; - - for (const player of jsonPlayers) { - const jsonString = JSON.stringify({ - name: player.name, - level: player.level, - experience: player.experience, - score: player.score, - isOnline: player.isOnline, - health: player.health - }); - jsonSize += new Blob([jsonString]).size; - } - - const jsonTime = performance.now() - jsonStart; - - const speedChange = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0; - const sizeReduction = jsonSize > 0 ? ((jsonSize - protobufSize) / jsonSize * 100) : 0; - - console.log(`\\n=== Player 组件性能对比 (${iterations} 次迭代) ===`); - console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`); - console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`); - console.log(`速度变化: ${speedChange > 0 ? '+' : ''}${speedChange.toFixed(1)}%`); - console.log(''); - console.log(`Protobuf 总大小: ${protobufSize} bytes`); - console.log(`JSON 总大小: ${jsonSize} bytes`); - console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`); - console.log(`平均 Protobuf 大小: ${(protobufSize / iterations).toFixed(1)} bytes`); - console.log(`平均 JSON 大小: ${(jsonSize / iterations).toFixed(1)} bytes`); - - expect(protobufTime).toBeGreaterThan(0); - expect(jsonTime).toBeGreaterThan(0); - }); - }); - - describe('批量数据性能测试', () => { - it('大量小对象序列化', () => { - if (skipIfNoProtobuf()) return; - - const count = 5000; - console.log(`\\n=== 大量小对象测试 (${count} 个 Position) ===`); - - // 准备数据 - const positions = Array.from({ length: count }, (_, i) => ({ - x: i * 0.1, - y: i * 0.2, - z: i * 0.05 - })); - - // Protobuf 批量序列化 - const protobufStart = performance.now(); - let protobufSize = 0; - - for (const pos of positions) { - const message = PositionType.create(pos); - const buffer = PositionType.encode(message).finish(); - protobufSize += buffer.length; - } - - const protobufTime = performance.now() - protobufStart; - - // JSON 批量序列化 - const jsonStart = performance.now(); - let jsonSize = 0; - - for (const pos of positions) { - const jsonString = JSON.stringify(pos); - jsonSize += jsonString.length; - } - - const jsonTime = performance.now() - jsonStart; - - console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufSize} bytes`); - console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonSize} bytes`); - console.log(`速度: ${protobufTime < jsonTime ? 'Protobuf 更快' : 'JSON 更快'} (${Math.abs(protobufTime - jsonTime).toFixed(2)}ms 差异)`); - console.log(`大小: Protobuf ${protobufSize < jsonSize ? '更小' : '更大'} (${Math.abs(protobufSize - jsonSize)} bytes 差异)`); - console.log(`处理速度: Protobuf ${Math.floor(count / (protobufTime / 1000))} ops/s, JSON ${Math.floor(count / (jsonTime / 1000))} ops/s`); - }); - }); - - describe('真实网络场景模拟', () => { - it('游戏状态同步场景', () => { - if (skipIfNoProtobuf()) return; - - console.log(`\\n=== 游戏状态同步场景 ===`); - - // 模拟 100 个玩家的位置更新 - const playerCount = 100; - const updateData = Array.from({ length: playerCount }, (_, i) => ({ - playerId: i, - x: Math.random() * 1000, - y: Math.random() * 1000, - z: Math.random() * 100, - health: Math.floor(Math.random() * 100), - isMoving: Math.random() > 0.5 - })); - - // 创建组合消息类型(模拟) - const GameUpdateType = root.lookupType('ecs.Position'); // 简化使用 Position - - // Protobuf 序列化所有更新 - const protobufStart = performance.now(); - let protobufTotalSize = 0; - - for (const update of updateData) { - const message = GameUpdateType.create({ - x: update.x, - y: update.y, - z: update.z - }); - const buffer = GameUpdateType.encode(message).finish(); - protobufTotalSize += buffer.length; - } - - const protobufTime = performance.now() - protobufStart; - - // JSON 序列化所有更新 - const jsonStart = performance.now(); - let jsonTotalSize = 0; - - for (const update of updateData) { - const jsonString = JSON.stringify({ - playerId: update.playerId, - x: update.x, - y: update.y, - z: update.z, - health: update.health, - isMoving: update.isMoving - }); - jsonTotalSize += jsonString.length; - } - - const jsonTime = performance.now() - jsonStart; - - console.log(`${playerCount} 个玩家位置更新:`); - console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufTotalSize} bytes`); - console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonTotalSize} bytes`); - - // 计算网络传输节省 - const sizeSaving = jsonTotalSize - protobufTotalSize; - const percentSaving = (sizeSaving / jsonTotalSize * 100); - - console.log(`数据大小节省: ${sizeSaving} bytes (${percentSaving.toFixed(1)}%)`); - console.log(`每秒 60 次更新的带宽节省: ${(sizeSaving * 60 / 1024).toFixed(2)} KB/s`); - - expect(protobufTotalSize).toBeGreaterThan(0); - expect(jsonTotalSize).toBeGreaterThan(0); - }); - }); -}); \ No newline at end of file diff --git a/packages/network/tests/Serialization/SnapshotManagerIntegration.test.ts b/packages/network/tests/Serialization/SnapshotManagerIntegration.test.ts deleted file mode 100644 index c1820946..00000000 --- a/packages/network/tests/Serialization/SnapshotManagerIntegration.test.ts +++ /dev/null @@ -1,370 +0,0 @@ -/** - * SnapshotManager与Protobuf序列化集成测试 - */ - -import { Entity, Scene, Component } from '@esengine/ecs-framework'; -import { SnapshotManager } from '../../src/Snapshot/SnapshotManager'; -import { - ProtoSerializable, - ProtoFloat, - ProtoInt32, - ProtoString, - ProtoBool -} from '../../src/Serialization/ProtobufDecorators'; - -// 测试组件 -@ProtoSerializable('TestPosition') -class TestPositionComponent extends Component { - @ProtoFloat(1) - public x: number = 0; - - @ProtoFloat(2) - public y: number = 0; - - constructor(x: number = 0, y: number = 0) { - super(); - this.x = x; - this.y = y; - } -} - -@ProtoSerializable('TestVelocity') -class TestVelocityComponent extends Component { - @ProtoFloat(1) - public vx: number = 0; - - @ProtoFloat(2) - public vy: number = 0; - - constructor(vx: number = 0, vy: number = 0) { - super(); - this.vx = vx; - this.vy = vy; - } -} - -@ProtoSerializable('TestHealth') -class TestHealthComponent extends Component { - @ProtoInt32(1) - public maxHealth: number = 100; - - @ProtoInt32(2) - public currentHealth: number = 100; - - @ProtoBool(3) - public isDead: boolean = false; - - constructor(maxHealth: number = 100) { - super(); - this.maxHealth = maxHealth; - this.currentHealth = maxHealth; - } -} - -// 传统JSON序列化组件 -class TraditionalComponent extends Component { - public customData = { - name: 'traditional', - values: [1, 2, 3], - settings: { enabled: true } - }; - - serialize(): any { - return { - customData: this.customData - }; - } - - deserialize(data: any): void { - if (data.customData) { - this.customData = data.customData; - } - } -} - -// 简单组件(使用默认序列化) -class SimpleComponent extends Component { - public value: number = 42; - public text: string = 'simple'; - public flag: boolean = true; -} - -// Mock protobuf.js -const mockProtobuf = { - parse: jest.fn().mockReturnValue({ - root: { - lookupType: jest.fn().mockImplementation((typeName: string) => { - const mockData: Record = { - 'ecs.TestPosition': { x: 10, y: 20 }, - 'ecs.TestVelocity': { vx: 5, vy: 3 }, - 'ecs.TestHealth': { maxHealth: 100, currentHealth: 80, isDead: false } - }; - - return { - verify: jest.fn().mockReturnValue(null), - create: jest.fn().mockImplementation((data: any) => data), - encode: jest.fn().mockReturnValue({ - finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) - }), - decode: jest.fn().mockReturnValue(mockData[typeName] || {}), - toObject: jest.fn().mockImplementation((message: any) => message) - }; - }) - } - }) -}; - -describe('SnapshotManager Protobuf集成', () => { - let snapshotManager: SnapshotManager; - let scene: Scene; - - beforeEach(() => { - snapshotManager = new SnapshotManager(); - snapshotManager.initializeProtobuf(mockProtobuf.parse().root as any); - scene = new Scene(); - jest.clearAllMocks(); - }); - - describe('混合序列化快照', () => { - it('应该正确创建包含protobuf和JSON组件的快照', () => { - // 创建实体 - const player = scene.createEntity('Player'); - player.addComponent(new TestPositionComponent(100, 200)); - player.addComponent(new TestVelocityComponent(5, 3)); - player.addComponent(new TestHealthComponent(120)); - player.addComponent(new TraditionalComponent()); - player.addComponent(new SimpleComponent()); - - // 创建快照 - const snapshot = snapshotManager.createSceneSnapshot([player]); - - expect(snapshot).toBeDefined(); - expect(snapshot.entities).toHaveLength(1); - expect(snapshot.entities[0].components).toHaveLength(5); - - // 验证快照包含所有组件 - const componentTypes = snapshot.entities[0].components.map(c => c.type); - expect(componentTypes).toContain('TestPositionComponent'); - expect(componentTypes).toContain('TestVelocityComponent'); - expect(componentTypes).toContain('TestHealthComponent'); - expect(componentTypes).toContain('TraditionalComponent'); - expect(componentTypes).toContain('SimpleComponent'); - }); - - it('应该根据组件类型使用相应的序列化方式', () => { - const entity = scene.createEntity('TestEntity'); - const position = new TestPositionComponent(50, 75); - const traditional = new TraditionalComponent(); - - entity.addComponent(position); - entity.addComponent(traditional); - - const snapshot = snapshotManager.createSceneSnapshot([entity]); - const components = snapshot.entities[0].components; - - // 检查序列化数据格式 - const positionSnapshot = components.find(c => c.type === 'TestPositionComponent'); - const traditionalSnapshot = components.find(c => c.type === 'TraditionalComponent'); - - expect(positionSnapshot).toBeDefined(); - expect(traditionalSnapshot).toBeDefined(); - - // Protobuf组件应该有SerializedData格式 - expect(positionSnapshot!.data).toHaveProperty('type'); - expect(positionSnapshot!.data).toHaveProperty('componentType'); - expect(positionSnapshot!.data).toHaveProperty('data'); - expect(positionSnapshot!.data).toHaveProperty('size'); - }); - }); - - describe('快照恢复', () => { - it('应该正确恢复protobuf序列化的组件', () => { - // 创建原始实体 - const originalEntity = scene.createEntity('Original'); - const originalPosition = new TestPositionComponent(100, 200); - const originalHealth = new TestHealthComponent(150); - originalHealth.currentHealth = 120; - - originalEntity.addComponent(originalPosition); - originalEntity.addComponent(originalHealth); - - // 创建快照 - const snapshot = snapshotManager.createSceneSnapshot([originalEntity]); - - // 创建新实体进行恢复 - const newEntity = scene.createEntity('New'); - newEntity.addComponent(new TestPositionComponent()); - newEntity.addComponent(new TestHealthComponent()); - - // 恢复快照 - snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); - - // 验证数据被正确恢复(注意:由于使用mock,实际值来自mock数据) - const restoredPosition = newEntity.getComponent(TestPositionComponent as any); - const restoredHealth = newEntity.getComponent(TestHealthComponent as any); - - expect(restoredPosition).toBeDefined(); - expect(restoredHealth).toBeDefined(); - - // 验证快照恢复成功(有组件数据被恢复) - expect((restoredPosition as TestPositionComponent)?.x).toBeDefined(); - expect((restoredHealth as TestHealthComponent)?.maxHealth).toBeDefined(); - }); - - it('应该正确恢复传统JSON序列化的组件', () => { - const originalEntity = scene.createEntity('Original'); - const originalTraditional = new TraditionalComponent(); - originalTraditional.customData.name = 'modified'; - originalTraditional.customData.values = [4, 5, 6]; - - originalEntity.addComponent(originalTraditional); - - const snapshot = snapshotManager.createSceneSnapshot([originalEntity]); - - const newEntity = scene.createEntity('New'); - const newTraditional = new TraditionalComponent(); - newEntity.addComponent(newTraditional); - - snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); - - // 验证JSON数据被正确恢复(由于使用mock,验证组件被恢复即可) - expect(newTraditional.customData).toBeDefined(); - expect(newTraditional.customData.name).toBe('traditional'); - expect(newTraditional.customData.values).toEqual([1, 2, 3]); - }); - - it('应该处理混合序列化的实体恢复', () => { - const originalEntity = scene.createEntity('Mixed'); - const position = new TestPositionComponent(30, 40); - const traditional = new TraditionalComponent(); - const simple = new SimpleComponent(); - - traditional.customData.name = 'mixed_test'; - simple.value = 99; - simple.text = 'updated'; - - originalEntity.addComponent(position); - originalEntity.addComponent(traditional); - originalEntity.addComponent(simple); - - const snapshot = snapshotManager.createSceneSnapshot([originalEntity]); - - const newEntity = scene.createEntity('NewMixed'); - newEntity.addComponent(new TestPositionComponent()); - newEntity.addComponent(new TraditionalComponent()); - newEntity.addComponent(new SimpleComponent()); - - snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); - - // 验证所有组件都被正确恢复 - const restoredTraditional = newEntity.getComponent(TraditionalComponent); - const restoredSimple = newEntity.getComponent(SimpleComponent); - - expect(restoredTraditional!.customData.name).toBe('traditional'); - expect(restoredSimple!.value).toBe(42); - expect(restoredSimple!.text).toBe('simple'); - }); - }); - - describe('向后兼容性', () => { - it('应该能够处理旧格式的快照数据', () => { - // 模拟旧格式的快照数据 - const legacySnapshot = { - entities: [{ - id: 1, - name: 'LegacyEntity', - enabled: true, - active: true, - tag: 0, - updateOrder: 0, - components: [{ - type: 'SimpleComponent', - id: 1, - data: { value: 123, text: 'legacy', flag: false }, // 直接的JSON数据 - enabled: true, - config: { includeInSnapshot: true, compressionLevel: 0, syncPriority: 5, enableIncremental: true } - }], - children: [], - timestamp: Date.now() - }], - timestamp: Date.now(), - version: '1.0.0', - type: 'full' as const - }; - - const entity = scene.createEntity('TestEntity'); - entity.addComponent(new SimpleComponent()); - - snapshotManager.restoreFromSnapshot(legacySnapshot, [entity]); - - const component = entity.getComponent(SimpleComponent); - expect(component!.value).toBe(42); - expect(component!.text).toBe('simple'); - expect(component!.flag).toBe(true); - }); - }); - - describe('错误处理', () => { - it('应该优雅地处理protobuf序列化失败', () => { - // 模拟protobuf验证失败 - const mockType = mockProtobuf.parse().root.lookupType; - mockType.mockImplementation(() => ({ - verify: jest.fn().mockReturnValue('验证失败'), - create: jest.fn(), - encode: jest.fn(), - decode: jest.fn(), - toObject: jest.fn() - })); - - const entity = scene.createEntity('ErrorTest'); - entity.addComponent(new TestPositionComponent(10, 20)); - - // 应该不抛出异常,而是回退到JSON序列化 - expect(() => { - snapshotManager.createSceneSnapshot([entity]); - }).not.toThrow(); - }); - - it('应该优雅地处理protobuf反序列化失败', () => { - const entity = scene.createEntity('Test'); - const position = new TestPositionComponent(10, 20); - entity.addComponent(position); - - const snapshot = snapshotManager.createSceneSnapshot([entity]); - - // 模拟反序列化失败 - const mockType = mockProtobuf.parse().root.lookupType; - mockType.mockImplementation(() => ({ - verify: jest.fn().mockReturnValue(null), - create: jest.fn(), - encode: jest.fn().mockReturnValue({ - finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) - }), - decode: jest.fn().mockImplementation(() => { - throw new Error('解码失败'); - }), - toObject: jest.fn() - })); - - const newEntity = scene.createEntity('NewTest'); - newEntity.addComponent(new TestPositionComponent()); - - // 应该不抛出异常 - expect(() => { - snapshotManager.restoreFromSnapshot(snapshot, [newEntity]); - }).not.toThrow(); - }); - }); - - describe('统计信息', () => { - it('应该包含protobuf统计信息', () => { - const stats = snapshotManager.getCacheStats(); - - expect(stats).toHaveProperty('snapshotCacheSize'); - expect(stats).toHaveProperty('protobufStats'); - expect(stats.protobufStats).toHaveProperty('registeredComponents'); - expect(stats.protobufStats).toHaveProperty('protobufAvailable'); - expect(stats.protobufStats!.protobufAvailable).toBe(true); - }); - }); -}); \ No newline at end of file diff --git a/packages/network/tests/Serialization/TsrpcSerializer.test.ts b/packages/network/tests/Serialization/TsrpcSerializer.test.ts new file mode 100644 index 00000000..745f069c --- /dev/null +++ b/packages/network/tests/Serialization/TsrpcSerializer.test.ts @@ -0,0 +1,113 @@ +import { Component } from '@esengine/ecs-framework'; +import { TsrpcSerializer, SyncField } from '../../src/Serialization'; +import { TsrpcSerializable } from '../../src/Serialization/TsrpcDecorators'; + +@TsrpcSerializable() +class TestComponent extends Component { + @SyncField() + public health: number = 100; + + @SyncField() + public name: string = 'Test'; + + @SyncField() + public isActive: boolean = true; +} + +describe('TsrpcSerializer', () => { + let serializer: TsrpcSerializer; + let testComponent: TestComponent; + + beforeEach(() => { + serializer = TsrpcSerializer.getInstance(); + testComponent = new TestComponent(); + testComponent.health = 80; + testComponent.name = 'Player'; + testComponent.isActive = false; + }); + + describe('序列化', () => { + it('应该能序列化TSRPC组件', () => { + const result = serializer.serialize(testComponent); + + expect(result).not.toBeNull(); + expect(result?.type).toBe('tsrpc'); + expect(result?.componentType).toBe('TestComponent'); + expect(result?.data).toBeInstanceOf(Uint8Array); + expect(result?.size).toBeGreaterThan(0); + }); + + it('不支持的组件应该返回null', () => { + // 创建一个没有装饰器的组件类 + class UnsupportedComponent extends Component {} + const unsupportedComponent = new UnsupportedComponent(); + const result = serializer.serialize(unsupportedComponent); + + expect(result).toBeNull(); + }); + }); + + describe('反序列化', () => { + it('应该能反序列化TSRPC数据', () => { + // 先序列化 + const serializedData = serializer.serialize(testComponent); + expect(serializedData).not.toBeNull(); + + // 再反序列化 + const deserializedComponent = serializer.deserialize(serializedData!, TestComponent); + + expect(deserializedComponent).not.toBeNull(); + expect(deserializedComponent?.health).toBe(80); + expect(deserializedComponent?.name).toBe('Player'); + expect(deserializedComponent?.isActive).toBe(false); + }); + + it('错误的数据类型应该返回null', () => { + const invalidData = { + type: 'json' as const, + componentType: 'TestComponent', + data: {}, + size: 0 + }; + + const result = serializer.deserialize(invalidData); + expect(result).toBeNull(); + }); + }); + + describe('统计信息', () => { + it('应该正确更新统计信息', () => { + const initialStats = serializer.getStats(); + + // 执行序列化 + serializer.serialize(testComponent); + + const afterSerializeStats = serializer.getStats(); + expect(afterSerializeStats.serializeCount).toBe(initialStats.serializeCount + 1); + + // 执行反序列化 + const serializedData = serializer.serialize(testComponent); + if (serializedData) { + serializer.deserialize(serializedData, TestComponent); + } + + const finalStats = serializer.getStats(); + expect(finalStats.deserializeCount).toBe(initialStats.deserializeCount + 1); + }); + }); + + describe('性能功能', () => { + it('应该正确计算序列化大小', () => { + const initialStats = serializer.getStats(); + + // 执行序列化 + const result = serializer.serialize(testComponent); + + expect(result).not.toBeNull(); + expect(result?.size).toBeGreaterThan(0); + + const finalStats = serializer.getStats(); + expect(finalStats.averageSerializedSize).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/packages/network/tests/Serialization/index.test.ts b/packages/network/tests/Serialization/index.test.ts index e580aff5..6ace3719 100644 --- a/packages/network/tests/Serialization/index.test.ts +++ b/packages/network/tests/Serialization/index.test.ts @@ -3,10 +3,7 @@ */ // 导入所有测试 -import './ProtobufDecorators.test'; -import './ProtobufSerializer.test'; -import './SnapshotManagerIntegration.test'; -import './Performance.test'; +import './TsrpcSerializer.test'; // 这个文件确保所有序列化相关的测试都被包含在测试套件中 describe('序列化模块集成测试', () => {