From 9f76d37a823b490ee0a768d512245e7fb923e78b Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 12 Aug 2025 09:39:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0network=E5=BA=93=E5=8F=8Acore?= =?UTF-8?q?=E5=BA=93=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/electric-world | 2 +- package-lock.json | 236 +++--- package.json | 22 +- packages/core/src/Core.ts | 91 +- .../src/ECS/Core/FluentAPI/ECSFluentAPI.ts | 20 +- .../src/ECS/Core/FluentAPI/EntityBuilder.ts | 6 +- packages/core/src/ECS/IScene.ts | 158 ++++ packages/core/src/ECS/Scene.ts | 49 +- packages/core/src/ECS/index.ts | 1 + packages/core/src/Utils/Debug/DebugManager.ts | 206 +++-- packages/network-client/README.md | 101 +++ packages/network-client/build-rollup.cjs | 117 +++ packages/network-client/jest.config.cjs | 53 ++ packages/network-client/package.json | 86 ++ .../rollup.config.cjs | 26 +- .../src/core/ClientNetworkBehaviour.ts | 179 ++++ .../network-client/src/core/NetworkClient.ts | 638 ++++++++++++++ .../src/core/NetworkIdentity.ts | 378 +++++++++ packages/network-client/src/core/index.ts | 7 + .../src/decorators/ClientRpc.ts | 108 +++ .../src/decorators/ServerRpc.ts | 138 ++++ .../network-client/src/decorators/SyncVar.ts | 146 ++++ .../network-client/src/decorators/index.ts | 7 + packages/network-client/src/index.ts | 23 + .../src/interfaces/NetworkInterfaces.ts | 34 + .../network-client/src/interfaces/index.ts | 5 + .../src/systems/InterpolationSystem.ts | 520 ++++++++++++ .../src/systems/PredictionSystem.ts | 362 ++++++++ packages/network-client/src/systems/index.ts | 6 + .../src/transport/ClientTransport.ts | 445 ++++++++++ .../src/transport/HttpClientTransport.ts | 427 ++++++++++ .../src/transport/WebSocketClientTransport.ts | 282 +++++++ .../network-client/src/transport/index.ts | 7 + .../tests/NetworkClient.integration.test.ts | 384 +++++++++ packages/network-client/tests/setup.ts | 27 + .../tests/transport/ClientTransport.test.ts | 374 +++++++++ .../WebSocketClientTransport.test.ts | 348 ++++++++ .../{network => network-client}/tsconfig.json | 4 +- packages/network-server/README.md | 132 +++ packages/network-server/build-rollup.cjs | 115 +++ packages/network-server/jest.config.cjs | 53 ++ packages/network-server/package.json | 89 ++ packages/network-server/rollup.config.cjs | 102 +++ .../src/auth/AuthenticationManager.ts | 622 ++++++++++++++ .../src/auth/AuthorizationManager.ts | 684 +++++++++++++++ packages/network-server/src/auth/index.ts | 6 + .../src/core/ClientConnection.ts | 478 +++++++++++ .../network-server/src/core/HttpTransport.ts | 602 ++++++++++++++ .../network-server/src/core/NetworkServer.ts | 452 ++++++++++ packages/network-server/src/core/Transport.ts | 224 +++++ .../src/core/WebSocketTransport.ts | 406 +++++++++ packages/network-server/src/core/index.ts | 9 + packages/network-server/src/index.ts | 79 ++ packages/network-server/src/rooms/Room.ts | 637 ++++++++++++++ .../network-server/src/rooms/RoomManager.ts | 499 +++++++++++ packages/network-server/src/rooms/index.ts | 6 + .../network-server/src/systems/RpcSystem.ts | 762 +++++++++++++++++ .../src/systems/SyncVarSystem.ts | 587 +++++++++++++ packages/network-server/src/systems/index.ts | 6 + .../src/validation/MessageValidator.ts | 572 +++++++++++++ .../src/validation/RpcValidator.ts | 776 ++++++++++++++++++ .../network-server/src/validation/index.ts | 6 + packages/network-server/tests/setup.ts | 9 + packages/network-server/tsconfig.json | 45 + packages/network-shared/README.md | 60 ++ .../build-rollup.cjs | 30 +- packages/network-shared/jest.config.cjs | 53 ++ .../{network => network-shared}/package.json | 41 +- packages/network-shared/rollup.config.cjs | 129 +++ .../src/core/NetworkBehaviour.ts | 261 ++++++ .../src/core/NetworkIdentity.ts | 324 ++++++++ packages/network-shared/src/core/index.ts | 6 + .../src/decorators/ClientRpc.ts | 177 ++++ .../src/decorators/NetworkComponent.ts | 138 ++++ .../src/decorators/ServerRpc.ts | 178 ++++ .../network-shared/src/decorators/SyncVar.ts | 249 ++++++ .../network-shared/src/decorators/index.ts | 8 + packages/network-shared/src/index.ts | 43 + .../protocol/analyzer/TypeScriptAnalyzer.ts | 663 +++++++++++++++ .../src/protocol/analyzer/index.ts | 5 + .../compiler/ProtocolInferenceEngine.ts | 576 +++++++++++++ .../src/protocol/compiler/index.ts | 5 + packages/network-shared/src/protocol/index.ts | 8 + .../src/protocol/types/ProtocolTypes.ts | 289 +++++++ .../src/protocol/types/index.ts | 5 + .../src/serialization/NetworkSerializer.ts | 355 ++++++++ .../network-shared/src/serialization/index.ts | 5 + .../network-shared/src/types/NetworkTypes.ts | 375 +++++++++ packages/network-shared/src/types/index.ts | 4 + .../src/types/reflect-metadata.d.ts | 16 + .../network-shared/src/utils/NetworkUtils.ts | 242 ++++++ packages/network-shared/src/utils/index.ts | 5 + packages/network-shared/tests/setup.ts | 9 + packages/network-shared/tsconfig.json | 46 ++ packages/network/jest.config.cjs | 73 -- packages/network/src/Core/NetworkRegistry.ts | 185 ----- packages/network/src/Core/RpcManager.ts | 223 ----- packages/network/src/Core/SyncVarManager.ts | 222 ----- packages/network/src/NetworkBehaviour.ts | 141 ---- packages/network/src/NetworkIdentity.ts | 105 --- packages/network/src/NetworkManager.ts | 449 ---------- packages/network/src/decorators/ClientRpc.ts | 107 --- packages/network/src/decorators/Command.ts | 108 --- packages/network/src/decorators/SyncVar.ts | 116 --- packages/network/src/index.ts | 27 - packages/network/src/transport/TsrpcClient.ts | 431 ---------- packages/network/src/transport/TsrpcServer.ts | 364 -------- .../network/src/transport/TsrpcTransport.ts | 338 -------- packages/network/src/transport/index.ts | 9 - .../transport/protocols/NetworkProtocols.ts | 184 ----- .../src/transport/protocols/serviceProto.ts | 108 --- packages/network/src/types/NetworkTypes.ts | 162 ---- packages/network/tests/NetworkLibrary.test.ts | 258 ------ packages/network/tests/TsrpcTransport.test.ts | 145 ---- packages/network/tests/setup.ts | 28 - packages/network/tsconfig.test.json | 16 - thirdparty/ecs-astar | 2 +- 117 files changed, 17988 insertions(+), 4099 deletions(-) create mode 100644 packages/core/src/ECS/IScene.ts create mode 100644 packages/network-client/README.md create mode 100644 packages/network-client/build-rollup.cjs create mode 100644 packages/network-client/jest.config.cjs create mode 100644 packages/network-client/package.json rename packages/{network => network-client}/rollup.config.cjs (80%) create mode 100644 packages/network-client/src/core/ClientNetworkBehaviour.ts create mode 100644 packages/network-client/src/core/NetworkClient.ts create mode 100644 packages/network-client/src/core/NetworkIdentity.ts create mode 100644 packages/network-client/src/core/index.ts create mode 100644 packages/network-client/src/decorators/ClientRpc.ts create mode 100644 packages/network-client/src/decorators/ServerRpc.ts create mode 100644 packages/network-client/src/decorators/SyncVar.ts create mode 100644 packages/network-client/src/decorators/index.ts create mode 100644 packages/network-client/src/index.ts create mode 100644 packages/network-client/src/interfaces/NetworkInterfaces.ts create mode 100644 packages/network-client/src/interfaces/index.ts create mode 100644 packages/network-client/src/systems/InterpolationSystem.ts create mode 100644 packages/network-client/src/systems/PredictionSystem.ts create mode 100644 packages/network-client/src/systems/index.ts create mode 100644 packages/network-client/src/transport/ClientTransport.ts create mode 100644 packages/network-client/src/transport/HttpClientTransport.ts create mode 100644 packages/network-client/src/transport/WebSocketClientTransport.ts create mode 100644 packages/network-client/src/transport/index.ts create mode 100644 packages/network-client/tests/NetworkClient.integration.test.ts create mode 100644 packages/network-client/tests/setup.ts create mode 100644 packages/network-client/tests/transport/ClientTransport.test.ts create mode 100644 packages/network-client/tests/transport/WebSocketClientTransport.test.ts rename packages/{network => network-client}/tsconfig.json (94%) create mode 100644 packages/network-server/README.md create mode 100644 packages/network-server/build-rollup.cjs create mode 100644 packages/network-server/jest.config.cjs create mode 100644 packages/network-server/package.json create mode 100644 packages/network-server/rollup.config.cjs create mode 100644 packages/network-server/src/auth/AuthenticationManager.ts create mode 100644 packages/network-server/src/auth/AuthorizationManager.ts create mode 100644 packages/network-server/src/auth/index.ts create mode 100644 packages/network-server/src/core/ClientConnection.ts create mode 100644 packages/network-server/src/core/HttpTransport.ts create mode 100644 packages/network-server/src/core/NetworkServer.ts create mode 100644 packages/network-server/src/core/Transport.ts create mode 100644 packages/network-server/src/core/WebSocketTransport.ts create mode 100644 packages/network-server/src/core/index.ts create mode 100644 packages/network-server/src/index.ts create mode 100644 packages/network-server/src/rooms/Room.ts create mode 100644 packages/network-server/src/rooms/RoomManager.ts create mode 100644 packages/network-server/src/rooms/index.ts create mode 100644 packages/network-server/src/systems/RpcSystem.ts create mode 100644 packages/network-server/src/systems/SyncVarSystem.ts create mode 100644 packages/network-server/src/systems/index.ts create mode 100644 packages/network-server/src/validation/MessageValidator.ts create mode 100644 packages/network-server/src/validation/RpcValidator.ts create mode 100644 packages/network-server/src/validation/index.ts create mode 100644 packages/network-server/tests/setup.ts create mode 100644 packages/network-server/tsconfig.json create mode 100644 packages/network-shared/README.md rename packages/{network => network-shared}/build-rollup.cjs (83%) create mode 100644 packages/network-shared/jest.config.cjs rename packages/{network => network-shared}/package.json (76%) create mode 100644 packages/network-shared/rollup.config.cjs create mode 100644 packages/network-shared/src/core/NetworkBehaviour.ts create mode 100644 packages/network-shared/src/core/NetworkIdentity.ts create mode 100644 packages/network-shared/src/core/index.ts create mode 100644 packages/network-shared/src/decorators/ClientRpc.ts create mode 100644 packages/network-shared/src/decorators/NetworkComponent.ts create mode 100644 packages/network-shared/src/decorators/ServerRpc.ts create mode 100644 packages/network-shared/src/decorators/SyncVar.ts create mode 100644 packages/network-shared/src/decorators/index.ts create mode 100644 packages/network-shared/src/index.ts create mode 100644 packages/network-shared/src/protocol/analyzer/TypeScriptAnalyzer.ts create mode 100644 packages/network-shared/src/protocol/analyzer/index.ts create mode 100644 packages/network-shared/src/protocol/compiler/ProtocolInferenceEngine.ts create mode 100644 packages/network-shared/src/protocol/compiler/index.ts create mode 100644 packages/network-shared/src/protocol/index.ts create mode 100644 packages/network-shared/src/protocol/types/ProtocolTypes.ts create mode 100644 packages/network-shared/src/protocol/types/index.ts create mode 100644 packages/network-shared/src/serialization/NetworkSerializer.ts create mode 100644 packages/network-shared/src/serialization/index.ts create mode 100644 packages/network-shared/src/types/NetworkTypes.ts create mode 100644 packages/network-shared/src/types/index.ts create mode 100644 packages/network-shared/src/types/reflect-metadata.d.ts create mode 100644 packages/network-shared/src/utils/NetworkUtils.ts create mode 100644 packages/network-shared/src/utils/index.ts create mode 100644 packages/network-shared/tests/setup.ts create mode 100644 packages/network-shared/tsconfig.json delete mode 100644 packages/network/jest.config.cjs delete mode 100644 packages/network/src/Core/NetworkRegistry.ts delete mode 100644 packages/network/src/Core/RpcManager.ts delete mode 100644 packages/network/src/Core/SyncVarManager.ts delete mode 100644 packages/network/src/NetworkBehaviour.ts delete mode 100644 packages/network/src/NetworkIdentity.ts delete mode 100644 packages/network/src/NetworkManager.ts delete mode 100644 packages/network/src/decorators/ClientRpc.ts delete mode 100644 packages/network/src/decorators/Command.ts delete mode 100644 packages/network/src/decorators/SyncVar.ts delete mode 100644 packages/network/src/index.ts delete mode 100644 packages/network/src/transport/TsrpcClient.ts delete mode 100644 packages/network/src/transport/TsrpcServer.ts delete mode 100644 packages/network/src/transport/TsrpcTransport.ts delete mode 100644 packages/network/src/transport/index.ts delete mode 100644 packages/network/src/transport/protocols/NetworkProtocols.ts delete mode 100644 packages/network/src/transport/protocols/serviceProto.ts delete mode 100644 packages/network/src/types/NetworkTypes.ts delete mode 100644 packages/network/tests/NetworkLibrary.test.ts delete mode 100644 packages/network/tests/TsrpcTransport.test.ts delete mode 100644 packages/network/tests/setup.ts delete mode 100644 packages/network/tsconfig.test.json diff --git a/examples/electric-world b/examples/electric-world index 2b36519b..48f93c47 160000 --- a/examples/electric-world +++ b/examples/electric-world @@ -1 +1 @@ -Subproject commit 2b36519bd964bce71cecdb92337c0a81d7555077 +Subproject commit 48f93c47529481eeb2b46fbc0b1db5da22df52cd diff --git a/package-lock.json b/package-lock.json index 6fad6ef5..bb16b667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -568,8 +568,16 @@ "resolved": "packages/math", "link": true }, - "node_modules/@esengine/ecs-framework-network": { - "resolved": "packages/network", + "node_modules/@esengine/ecs-framework-network-client": { + "resolved": "packages/network-client", + "link": true + }, + "node_modules/@esengine/ecs-framework-network-server": { + "resolved": "packages/network-server", + "link": true + }, + "node_modules/@esengine/ecs-framework-network-shared": { + "resolved": "packages/network-shared", "link": true }, "node_modules/@hutson/parse-repository-url": { @@ -3136,6 +3144,7 @@ "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" @@ -3664,6 +3673,7 @@ "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" @@ -3837,6 +3847,7 @@ "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" @@ -3849,6 +3860,7 @@ "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,6 +5502,7 @@ "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" } @@ -6105,15 +6118,6 @@ "node": ">=0.10.0" } }, - "node_modules/isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -7122,31 +7126,6 @@ "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", @@ -10362,6 +10341,7 @@ "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" }, @@ -10767,31 +10747,6 @@ "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", @@ -10821,78 +10776,9 @@ "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", @@ -11553,6 +11439,7 @@ "packages/network": { "name": "@esengine/ecs-framework-network", "version": "1.0.4", + "extraneous": true, "license": "MIT", "dependencies": { "isomorphic-ws": "^5.0.0", @@ -11582,6 +11469,93 @@ "peerDependencies": { "@esengine/ecs-framework": ">=2.1.29" } + }, + "packages/network-client": { + "name": "@esengine/ecs-framework-network-client", + "version": "1.0.17", + "license": "MIT", + "dependencies": { + "ws": "^8.18.0" + }, + "devDependencies": { + "@esengine/ecs-framework": "*", + "@esengine/ecs-framework-network-shared": "*", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.0", + "@types/ws": "^8.5.13", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "rimraf": "^5.0.0", + "rollup": "^4.42.0", + "rollup-plugin-dts": "^6.2.1", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@esengine/ecs-framework": ">=2.1.29", + "@esengine/ecs-framework-network-shared": ">=1.0.0" + } + }, + "packages/network-server": { + "name": "@esengine/ecs-framework-network-server", + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "uuid": "^10.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@esengine/ecs-framework": "*", + "@esengine/ecs-framework-network-shared": "*", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.0", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.13", + "jest": "^29.7.0", + "jest-environment-node": "^29.7.0", + "rimraf": "^5.0.0", + "rollup": "^4.42.0", + "rollup-plugin-dts": "^6.2.1", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@esengine/ecs-framework": ">=2.1.29", + "@esengine/ecs-framework-network-shared": ">=1.0.0" + } + }, + "packages/network-shared": { + "name": "@esengine/ecs-framework-network-shared", + "version": "1.0.14", + "license": "MIT", + "dependencies": { + "protobufjs": "^7.5.3", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@esengine/ecs-framework": "*", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "rimraf": "^5.0.0", + "rollup": "^4.42.0", + "rollup-plugin-dts": "^6.2.1", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@esengine/ecs-framework": ">=2.1.29" + } } } } diff --git a/package.json b/package.json index c46720c9..8654b7d6 100644 --- a/package.json +++ b/package.json @@ -18,27 +18,35 @@ "scripts": { "bootstrap": "lerna bootstrap", "clean": "lerna run clean", - "build": "npm run build:core && npm run build:math && npm run build:network", + "build": "npm run build:core && npm run build:math && npm run build:network-shared && npm run build:network-client && npm run build:network-server", "build:core": "cd packages/core && npm run build", "build:math": "cd packages/math && npm run build", - "build:network": "cd packages/network && npm run build", - "build:npm": "npm run build:npm:core && npm run build:npm:math && npm run build:npm:network", + "build:network-shared": "cd packages/network-shared && npm run build", + "build:network-client": "cd packages/network-client && npm run build", + "build:network-server": "cd packages/network-server && npm run build", + "build:npm": "npm run build:npm:core && npm run build:npm:math && npm run build:npm:network-shared && npm run build:npm:network-client && npm run build:npm:network-server", "build:npm:core": "cd packages/core && npm run build:npm", "build:npm:math": "cd packages/math && npm run build:npm", - "build:npm:network": "cd packages/network && npm run build:npm", + "build:npm:network-shared": "cd packages/network-shared && npm run build:npm", + "build:npm:network-client": "cd packages/network-client && npm run build:npm", + "build:npm:network-server": "cd packages/network-server && npm run build:npm", "test": "lerna run test", "test:coverage": "lerna run test:coverage", "test:ci": "lerna run test:ci", "prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs", "sync:versions": "node scripts/sync-versions.cjs", "publish:all": "npm run prepare:publish && npm run publish:all:dist", - "publish:all:dist": "npm run publish:core && npm run publish:math && npm run publish:network", + "publish:all:dist": "npm run publish:core && npm run publish:math && npm run publish:network-shared && npm run publish:network-client && npm run publish:network-server", "publish:core": "cd packages/core && npm run publish:npm", "publish:core:patch": "cd packages/core && npm run publish:patch", "publish:math": "cd packages/math && npm run publish:npm", "publish:math:patch": "cd packages/math && npm run publish:patch", - "publish:network": "cd packages/network && npm run publish:npm", - "publish:network:patch": "cd packages/network && npm run publish:patch", + "publish:network-shared": "cd packages/network-shared && npm run publish:npm", + "publish:network-shared:patch": "cd packages/network-shared && npm run publish:patch", + "publish:network-client": "cd packages/network-client && npm run publish:npm", + "publish:network-client:patch": "cd packages/network-client && npm run publish:patch", + "publish:network-server": "cd packages/network-server && npm run publish:npm", + "publish:network-server:patch": "cd packages/network-server && npm run publish:patch", "publish": "lerna publish", "version": "lerna version" }, diff --git a/packages/core/src/Core.ts b/packages/core/src/Core.ts index 4ec3c06d..3b2a7130 100644 --- a/packages/core/src/Core.ts +++ b/packages/core/src/Core.ts @@ -7,6 +7,7 @@ import { PerformanceMonitor } from './Utils/PerformanceMonitor'; import { PoolManager } from './Utils/Pool/PoolManager'; import { ECSFluentAPI, createECSAPI } from './ECS/Core/FluentAPI'; import { Scene } from './ECS/Scene'; +import { IScene } from './ECS/IScene'; import { DebugManager } from './Utils/Debug'; import { ICoreConfig, IECSDebugConfig } from './Types'; import { BigIntFactory, EnvironmentInfo } from './ECS/Utils/BigIntCompatibility'; @@ -75,7 +76,7 @@ export class Core { * * 存储下一帧要切换到的场景实例。 */ - public _nextScene: Scene | null = null; + public _nextScene: IScene | null = null; /** * 全局管理器集合 @@ -115,7 +116,7 @@ export class Core { /** * 当前活动场景 */ - public _scene?: Scene; + public _scene?: IScene; /** * 调试管理器 @@ -197,7 +198,7 @@ export class Core { * * @returns 当前场景实例,如果没有则返回null */ - public static get scene(): Scene | null { + public static get scene(): IScene | null { if (!this._instance) return null; return this._instance._scene || null; @@ -209,24 +210,41 @@ export class Core { * 如果当前没有场景,会立即切换;否则会在下一帧切换。 * * @param value - 要设置的场景实例 - * @throws {Error} 当场景为空时抛出错误 */ - public static set scene(value: Scene | null) { + public static set scene(value: IScene | null) { if (!value) return; - - if (!value) { - throw new Error("场景不能为空"); - } if (this._instance._scene == null) { - this._instance._scene = value; - this._instance.onSceneChanged(); - this._instance._scene.begin(); + this._instance.setSceneInternal(value); } else { this._instance._nextScene = value; } } + /** + * 类型安全的场景设置方法 + * + * @param scene - 要设置的场景实例 + * @returns 设置的场景实例 + */ + public static setScene(scene: T): T { + if (this._instance._scene == null) { + this._instance.setSceneInternal(scene); + } else { + this._instance._nextScene = scene; + } + return scene; + } + + /** + * 类型安全的场景获取方法 + * + * @returns 当前场景实例 + */ + public static getScene(): T | null { + return this._instance?._scene as T || null; + } + /** * 创建Core实例 * @@ -327,8 +345,11 @@ export class Core { * @param onTime - 定时器触发时的回调函数 * @returns 创建的定时器实例 */ - public static schedule(timeInSeconds: number, repeats: boolean = false, context: TContext = null as any, onTime: (timer: ITimer) => void): Timer { - return this._instance._timerManager.schedule(timeInSeconds, repeats, context, onTime); + public static schedule(timeInSeconds: number, repeats: boolean = false, context?: TContext, onTime?: (timer: ITimer) => void): Timer { + if (!onTime) { + throw new Error('onTime callback is required'); + } + return this._instance._timerManager.schedule(timeInSeconds, repeats, context as TContext, onTime); } /** @@ -383,7 +404,7 @@ export class Core { * * @returns 当前调试数据,如果调试未启用则返回null */ - public static getDebugData(): any { + public static getDebugData(): unknown { if (!this._instance?._debugManager) { return null; } @@ -418,6 +439,17 @@ export class Core { return this._instance?._environmentInfo.supportsBigInt || false; } + /** + * 内部场景设置方法 + * + * @param scene - 要设置的场景实例 + */ + private setSceneInternal(scene: IScene): void { + this._scene = scene; + this.onSceneChanged(); + this._scene.begin(); + } + /** * 场景切换回调 * @@ -427,14 +459,27 @@ export class Core { Time.sceneChanged(); // 初始化ECS API(如果场景支持) - if (this._scene && typeof (this._scene as any).querySystem !== 'undefined') { - const scene = this._scene as any; - this._ecsAPI = createECSAPI(scene, scene.querySystem, scene.eventSystem); + if (this._scene && this._scene.querySystem && this._scene.eventSystem) { + this._ecsAPI = createECSAPI(this._scene, this._scene.querySystem, this._scene.eventSystem); } - // 通知调试管理器场景已变更 + // 延迟调试管理器通知,避免在场景初始化过程中干扰属性 if (this._debugManager) { - this._debugManager.onSceneChanged(); + // 使用 requestAnimationFrame 确保在场景完全初始化后再收集数据 + if (typeof requestAnimationFrame !== 'undefined') { + requestAnimationFrame(() => { + if (this._debugManager) { + this._debugManager.onSceneChanged(); + } + }); + } else { + // 兜底:使用 setTimeout + setTimeout(() => { + if (this._debugManager) { + this._debugManager.onSceneChanged(); + } + }, 0); + } } } @@ -480,8 +525,8 @@ export class Core { Time.update(deltaTime); // 更新FPS监控(如果性能监控器支持) - if (typeof (this._performanceMonitor as any).updateFPS === 'function') { - (this._performanceMonitor as any).updateFPS(Time.deltaTime); + if ('updateFPS' in this._performanceMonitor && typeof this._performanceMonitor.updateFPS === 'function') { + this._performanceMonitor.updateFPS(Time.deltaTime); } // 更新全局管理器 @@ -510,7 +555,7 @@ export class Core { if (this._scene != null && this._scene.update) { const sceneStartTime = this._performanceMonitor.startMonitoring('Scene.update'); this._scene.update(); - const entityCount = (this._scene as any).entities?.count || 0; + const entityCount = this._scene.entities?.count || 0; this._performanceMonitor.endMonitoring('Scene.update', sceneStartTime, entityCount); } diff --git a/packages/core/src/ECS/Core/FluentAPI/ECSFluentAPI.ts b/packages/core/src/ECS/Core/FluentAPI/ECSFluentAPI.ts index fb6ea5bb..33833ebb 100644 --- a/packages/core/src/ECS/Core/FluentAPI/ECSFluentAPI.ts +++ b/packages/core/src/ECS/Core/FluentAPI/ECSFluentAPI.ts @@ -1,6 +1,6 @@ import { Entity } from '../../Entity'; import { Component } from '../../Component'; -import { Scene } from '../../Scene'; +import { IScene } from '../../IScene'; import { ComponentType } from '../ComponentStorage'; import { QuerySystem, QueryBuilder } from '../QuerySystem'; import { TypeSafeEventSystem } from '../EventSystem'; @@ -14,11 +14,11 @@ import { EntityBatchOperator } from './EntityBatchOperator'; * 提供统一的流式接口 */ export class ECSFluentAPI { - private scene: Scene; + private scene: IScene; private querySystem: QuerySystem; private eventSystem: TypeSafeEventSystem; - constructor(scene: Scene, querySystem: QuerySystem, eventSystem: TypeSafeEventSystem) { + constructor(scene: IScene, querySystem: QuerySystem, eventSystem: TypeSafeEventSystem) { this.scene = scene; this.querySystem = querySystem; this.eventSystem = eventSystem; @@ -86,7 +86,7 @@ export class ECSFluentAPI { * @returns 实体或null */ public findByName(name: string): Entity | null { - return this.scene.getEntityByName(name); + return this.scene.findEntity(name); } /** @@ -95,7 +95,7 @@ export class ECSFluentAPI { * @returns 实体数组 */ public findByTag(tag: number): Entity[] { - return this.scene.getEntitiesByTag(tag); + return this.scene.findEntitiesByTag(tag); } /** @@ -161,16 +161,16 @@ export class ECSFluentAPI { public getStats(): { entityCount: number; systemCount: number; - componentStats: Map; + componentStats: Map; queryStats: unknown; - eventStats: Map; + eventStats: Map; } { return { entityCount: this.scene.entities.count, systemCount: this.scene.systems.length, componentStats: this.scene.componentStorageManager.getAllStats(), queryStats: this.querySystem.getStats(), - eventStats: this.eventSystem.getStats() as Map + eventStats: this.eventSystem.getStats() as Map }; } } @@ -183,7 +183,7 @@ export class ECSFluentAPI { * @returns ECS流式API实例 */ export function createECSAPI( - scene: Scene, + scene: IScene, querySystem: QuerySystem, eventSystem: TypeSafeEventSystem ): ECSFluentAPI { @@ -202,7 +202,7 @@ export let ECS: ECSFluentAPI; * @param eventSystem 事件系统 */ export function initializeECS( - scene: Scene, + scene: IScene, querySystem: QuerySystem, eventSystem: TypeSafeEventSystem ): void { diff --git a/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts b/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts index 3dc23017..d3ec236e 100644 --- a/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts +++ b/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts @@ -1,6 +1,6 @@ import { Entity } from '../../Entity'; import { Component } from '../../Component'; -import { Scene } from '../../Scene'; +import { IScene } from '../../IScene'; import { ComponentType, ComponentStorageManager } from '../ComponentStorage'; /** @@ -8,10 +8,10 @@ import { ComponentType, ComponentStorageManager } from '../ComponentStorage'; */ export class EntityBuilder { private entity: Entity; - private scene: Scene; + private scene: IScene; private storageManager: ComponentStorageManager; - constructor(scene: Scene, storageManager: ComponentStorageManager) { + constructor(scene: IScene, storageManager: ComponentStorageManager) { this.scene = scene; this.storageManager = storageManager; this.entity = new Entity("", scene.identifierPool.checkOut()); diff --git a/packages/core/src/ECS/IScene.ts b/packages/core/src/ECS/IScene.ts new file mode 100644 index 00000000..798753ea --- /dev/null +++ b/packages/core/src/ECS/IScene.ts @@ -0,0 +1,158 @@ +import { Entity } from './Entity'; +import { EntityList } from './Utils/EntityList'; +import { EntityProcessorList } from './Utils/EntityProcessorList'; +import { IdentifierPool } from './Utils/IdentifierPool'; +import { EntitySystem } from './Systems/EntitySystem'; +import { ComponentStorageManager } from './Core/ComponentStorage'; +import { QuerySystem } from './Core/QuerySystem'; +import { TypeSafeEventSystem } from './Core/EventSystem'; + +/** + * 场景接口定义 + * + * 定义场景应该实现的核心功能和属性,使用接口而非继承提供更灵活的实现方式。 + */ +export interface IScene { + /** + * 场景名称 + */ + readonly name: string; + + /** + * 场景中的实体集合 + */ + readonly entities: EntityList; + + /** + * 实体系统处理器集合 + */ + readonly entityProcessors: EntityProcessorList; + + /** + * 标识符池 + */ + readonly identifierPool: IdentifierPool; + + /** + * 组件存储管理器 + */ + readonly componentStorageManager: ComponentStorageManager; + + /** + * 查询系统 + */ + readonly querySystem: QuerySystem; + + /** + * 事件系统 + */ + readonly eventSystem: TypeSafeEventSystem; + + /** + * 获取系统列表 + */ + readonly systems: EntitySystem[]; + + /** + * 初始化场景 + */ + initialize(): void; + + /** + * 场景开始运行时的回调 + */ + onStart(): void; + + /** + * 场景卸载时的回调 + */ + unload(): void; + + /** + * 开始场景 + */ + begin(): void; + + /** + * 结束场景 + */ + end(): void; + + /** + * 更新场景 + */ + update(): void; + + /** + * 创建实体 + */ + createEntity(name: string): Entity; + + /** + * 添加实体 + */ + addEntity(entity: Entity, deferCacheClear?: boolean): Entity; + + /** + * 批量创建实体 + */ + createEntities(count: number, namePrefix?: string): Entity[]; + + /** + * 销毁所有实体 + */ + destroyAllEntities(): void; + + /** + * 查找实体 + */ + findEntity(name: string): Entity | null; + + /** + * 根据标签查找实体 + */ + findEntitiesByTag(tag: number): Entity[]; + + /** + * 添加实体处理器 + */ + addEntityProcessor(processor: EntitySystem): EntitySystem; + + /** + * 移除实体处理器 + */ + removeEntityProcessor(processor: EntitySystem): void; + + /** + * 获取实体处理器 + */ + getEntityProcessor(type: new (...args: any[]) => T): T | null; +} + +/** + * 场景工厂接口 + */ +export interface ISceneFactory { + /** + * 创建场景实例 + */ + createScene(): T; +} + +/** + * 场景配置接口 + */ +export interface ISceneConfig { + /** + * 场景名称 + */ + name?: string; + /** + * 是否自动开始 + */ + autoStart?: boolean; + /** + * 调试配置 + */ + debug?: boolean; +} \ No newline at end of file diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index c9dda952..6bf59f39 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -7,27 +7,36 @@ import { ComponentStorageManager } from './Core/ComponentStorage'; import { QuerySystem } from './Core/QuerySystem'; import { TypeSafeEventSystem } from './Core/EventSystem'; import { EventBus } from './Core/EventBus'; +import { IScene, ISceneConfig } from './IScene'; /** - * 游戏场景类 + * 游戏场景默认实现类 * - * 管理游戏场景中的所有实体和系统,提供场景生命周期管理。 - * 场景是游戏世界的容器,负责协调实体和系统的运行。 + * 实现IScene接口,提供场景的基础功能。 + * 推荐使用组合而非继承的方式来构建自定义场景。 * * @example * ```typescript + * // 推荐的组合方式 + * class GameScene implements IScene { + * private scene = new Scene(); + * + * public initialize(): void { + * this.scene.initialize(); + * // 自定义初始化逻辑 + * } + * } + * + * // 仍然支持继承方式 * class GameScene extends Scene { * public initialize(): void { - * // 创建游戏实体 - * const player = this.createEntity("Player"); - * - * // 添加系统 - * this.addEntityProcessor(new MovementSystem()); + * super.initialize(); + * // 自定义逻辑 * } * } * ``` */ -export class Scene { +export class Scene implements IScene { /** * 场景名称 * @@ -89,10 +98,15 @@ export class Scene { return this.entityProcessors.processors; } + /** + * 是否已完成基础初始化 + */ + private _isBaseInitialized = false; + /** * 创建场景实例 */ - constructor() { + constructor(config?: ISceneConfig) { this.entities = new EntityList(this); this.entityProcessors = new EntityProcessorList(); this.identifierPool = new IdentifierPool(); @@ -100,17 +114,30 @@ export class Scene { this.querySystem = new QuerySystem(); this.eventSystem = new TypeSafeEventSystem(); + // 应用配置 + if (config?.name) { + this.name = config.name; + } + if (!Entity.eventBus) { Entity.eventBus = new EventBus(false); } if (Entity.eventBus) { - Entity.eventBus.onComponentAdded((data: any) => { + Entity.eventBus.onComponentAdded((data: unknown) => { this.eventSystem.emitSync('component:added', data); }); } + // 标记基础初始化完成 + this._isBaseInitialized = true; + + // 立即调用初始化,但确保在基础设施就绪后 this.initialize(); + + if (config?.autoStart) { + this.begin(); + } } /** diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index 215386dd..bf4244ef 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -4,6 +4,7 @@ export { ECSEventType, EventPriority, EVENT_TYPES, EventTypeValidator } from './ export * from './Systems'; export * from './Utils'; export { Scene } from './Scene'; +export { IScene, ISceneFactory, ISceneConfig } from './IScene'; export { EntityManager, EntityQueryBuilder } from './Core/EntityManager'; export * from './Core/Events'; export * from './Core/Query'; diff --git a/packages/core/src/Utils/Debug/DebugManager.ts b/packages/core/src/Utils/Debug/DebugManager.ts index 0eb8cd71..f248a702 100644 --- a/packages/core/src/Utils/Debug/DebugManager.ts +++ b/packages/core/src/Utils/Debug/DebugManager.ts @@ -6,6 +6,7 @@ import { ComponentDataCollector } from './ComponentDataCollector'; import { SceneDataCollector } from './SceneDataCollector'; import { WebSocketManager } from './WebSocketManager'; import { Core } from '../../Core'; +import { Component } from '../../ECS/Component'; /** * 调试管理器 @@ -181,7 +182,7 @@ export class DebugManager { private handleExpandLazyObjectRequest(message: any): void { try { const { entityId, componentIndex, propertyPath, requestId } = message; - + if (entityId === undefined || componentIndex === undefined || !propertyPath) { this.webSocketManager.send({ type: 'expand_lazy_object_response', @@ -192,7 +193,7 @@ export class DebugManager { } const expandedData = this.entityCollector.expandLazyObject(entityId, componentIndex, propertyPath); - + this.webSocketManager.send({ type: 'expand_lazy_object_response', requestId, @@ -213,7 +214,7 @@ export class DebugManager { private handleGetComponentPropertiesRequest(message: any): void { try { const { entityId, componentIndex, requestId } = message; - + if (entityId === undefined || componentIndex === undefined) { this.webSocketManager.send({ type: 'get_component_properties_response', @@ -224,7 +225,7 @@ export class DebugManager { } const properties = this.entityCollector.getComponentProperties(entityId, componentIndex); - + this.webSocketManager.send({ type: 'get_component_properties_response', requestId, @@ -245,9 +246,9 @@ export class DebugManager { private handleGetRawEntityListRequest(message: any): void { try { const { requestId } = message; - + const rawEntityList = this.entityCollector.getRawEntityList(); - + this.webSocketManager.send({ type: 'get_raw_entity_list_response', requestId, @@ -268,7 +269,7 @@ export class DebugManager { private handleGetEntityDetailsRequest(message: any): void { try { const { entityId, requestId } = message; - + if (entityId === undefined) { this.webSocketManager.send({ type: 'get_entity_details_response', @@ -279,7 +280,7 @@ export class DebugManager { } const entityDetails = this.entityCollector.getEntityDetails(entityId); - + this.webSocketManager.send({ type: 'get_entity_details_response', requestId, @@ -327,7 +328,8 @@ export class DebugManager { // 收集其他内存统计 const baseMemoryInfo = this.collectBaseMemoryInfo(); - const componentMemoryStats = this.collectComponentMemoryStats((Core.scene as any)?.entities); + const scene = Core.scene; + const componentMemoryStats = scene?.entities ? this.collectComponentMemoryStats(scene.entities) : { totalMemory: 0, componentTypes: 0, totalInstances: 0, breakdown: [] }; const systemMemoryStats = this.collectSystemMemoryStats(); const poolMemoryStats = this.collectPoolMemoryStats(); const performanceStats = this.collectPerformanceStats(); @@ -366,18 +368,44 @@ export class DebugManager { /** * 收集基础内存信息 */ - private collectBaseMemoryInfo(): any { - const memoryInfo: any = { + private collectBaseMemoryInfo(): { + totalMemory: number; + usedMemory: number; + freeMemory: number; + gcCollections: number; + heapInfo: { + totalJSHeapSize: number; + usedJSHeapSize: number; + jsHeapSizeLimit: number; + } | null; + detailedMemory?: unknown; + } { + const memoryInfo = { totalMemory: 0, usedMemory: 0, freeMemory: 0, gcCollections: 0, - heapInfo: null + heapInfo: null as { + totalJSHeapSize: number; + usedJSHeapSize: number; + jsHeapSizeLimit: number; + } | null, + detailedMemory: undefined as unknown }; try { - if ((performance as any).memory) { - const perfMemory = (performance as any).memory; + // 类型安全的performance memory访问 + const performanceWithMemory = performance as Performance & { + memory?: { + jsHeapSizeLimit?: number; + usedJSHeapSize?: number; + totalJSHeapSize?: number; + }; + measureUserAgentSpecificMemory?: () => Promise; + }; + + if (performanceWithMemory.memory) { + const perfMemory = performanceWithMemory.memory; memoryInfo.totalMemory = perfMemory.jsHeapSizeLimit || 512 * 1024 * 1024; memoryInfo.usedMemory = perfMemory.usedJSHeapSize || 0; memoryInfo.freeMemory = memoryInfo.totalMemory - memoryInfo.usedMemory; @@ -392,9 +420,8 @@ export class DebugManager { } // 尝试获取GC信息 - if ((performance as any).measureUserAgentSpecificMemory) { - // 这是一个实验性API,可能不可用 - (performance as any).measureUserAgentSpecificMemory().then((result: any) => { + if (performanceWithMemory.measureUserAgentSpecificMemory) { + performanceWithMemory.measureUserAgentSpecificMemory().then((result: unknown) => { memoryInfo.detailedMemory = result; }).catch(() => { // 忽略错误 @@ -412,8 +439,24 @@ export class DebugManager { /** * 收集组件内存统计(仅用于内存快照) */ - private collectComponentMemoryStats(entityList: any): any { - const componentStats = new Map(); + private collectComponentMemoryStats(entityList: { buffer: Array<{ id: number; name?: string; destroyed?: boolean; components?: Component[] }> }): { + totalMemory: number; + componentTypes: number; + totalInstances: number; + breakdown: Array<{ + typeName: string; + instanceCount: number; + totalMemory: number; + averageMemory: number; + percentage: number; + largestInstances: Array<{ + entityId: number; + entityName: string; + memory: number; + }>; + }>; + } { + const componentStats = new Map }>(); let totalComponentMemory = 0; // 首先统计组件类型和数量 @@ -434,12 +477,12 @@ export class DebugManager { totalComponentMemory += totalMemoryForType; // 收集该类型组件的实例信息(用于显示最大的几个实例) - const instances: any[] = []; + const instances: Array<{ entityId: number; entityName: string; memory: number }> = []; let instanceCount = 0; - + for (const entity of entityList.buffer) { if (!entity || entity.destroyed || !entity.components) continue; - + for (const component of entity.components) { if (component.constructor.name === typeName) { instances.push({ @@ -448,7 +491,7 @@ export class DebugManager { memory: detailedMemoryPerInstance // 使用统一的详细计算结果 }); instanceCount++; - + // 限制收集的实例数量,避免过多数据 if (instanceCount >= 100) break; } @@ -480,19 +523,33 @@ export class DebugManager { }; } - private collectSystemMemoryStats(): any { + private collectSystemMemoryStats(): { + totalMemory: number; + systemCount: number; + breakdown: Array<{ + name: string; + memory: number; + enabled: boolean; + updateOrder: number; + }>; + } { const scene = Core.scene; let totalSystemMemory = 0; - const systemBreakdown: any[] = []; + const systemBreakdown: Array<{ + name: string; + memory: number; + enabled: boolean; + updateOrder: number; + }> = []; try { - const entityProcessors = (scene as any).entityProcessors; + const entityProcessors = scene?.entityProcessors; if (entityProcessors && entityProcessors.processors) { const systemTypeMemoryCache = new Map(); - + for (const system of entityProcessors.processors) { const systemTypeName = system.constructor.name; - + let systemMemory: number; if (systemTypeMemoryCache.has(systemTypeName)) { systemMemory = systemTypeMemoryCache.get(systemTypeName)!; @@ -500,7 +557,7 @@ export class DebugManager { systemMemory = this.calculateQuickSystemSize(system); systemTypeMemoryCache.set(systemTypeName, systemMemory); } - + totalSystemMemory += systemMemory; systemBreakdown.push({ @@ -522,20 +579,20 @@ export class DebugManager { }; } - private calculateQuickSystemSize(system: any): number { + private calculateQuickSystemSize(system: unknown): number { if (!system || typeof system !== 'object') return 64; - + let size = 128; - + try { const keys = Object.keys(system); for (let i = 0; i < Math.min(keys.length, 15); i++) { const key = keys[i]; if (key === 'entities' || key === 'scene' || key === 'constructor') continue; - - const value = system[key]; + + const value = (system as Record)[key]; size += key.length * 2; - + if (typeof value === 'string') { size += Math.min(value.length * 2, 100); } else if (typeof value === 'number') { @@ -551,16 +608,34 @@ export class DebugManager { } catch (error) { return 128; } - + return Math.max(size, 64); } /** * 收集对象池内存统计 */ - private collectPoolMemoryStats(): any { + private collectPoolMemoryStats(): { + totalMemory: number; + poolCount: number; + breakdown: Array<{ + typeName: string; + maxSize: number; + currentSize: number; + estimatedMemory: number; + utilization: number; + hitRate?: number; + }>; + } { let totalPoolMemory = 0; - const poolBreakdown: any[] = []; + const poolBreakdown: Array<{ + typeName: string; + maxSize: number; + currentSize: number; + estimatedMemory: number; + utilization: number; + hitRate?: number; + }> = []; try { // 尝试获取组件池统计 @@ -569,7 +644,7 @@ export class DebugManager { const poolStats = poolManager.getPoolStats(); for (const [typeName, stats] of poolStats.entries()) { - const poolData = stats as any; // 类型断言 + const poolData = stats as { maxSize: number; currentSize?: number }; const poolMemory = poolData.maxSize * 32; // 估算每个对象32字节 totalPoolMemory += poolMemory; @@ -591,7 +666,12 @@ export class DebugManager { const poolStats = Pool.getStats(); for (const [typeName, stats] of Object.entries(poolStats)) { - const poolData = stats as any; // 类型断言 + const poolData = stats as { + maxSize: number; + size: number; + estimatedMemoryUsage: number; + hitRate: number; + }; totalPoolMemory += poolData.estimatedMemoryUsage; poolBreakdown.push({ typeName: `Pool_${typeName}`, @@ -616,9 +696,21 @@ export class DebugManager { /** * 收集性能统计信息 */ - private collectPerformanceStats(): any { + private collectPerformanceStats(): { + enabled: boolean; + systemCount?: number; + warnings?: unknown[]; + topSystems?: Array<{ + name: string; + averageTime: number; + maxTime: number; + samples: number; + }>; + error?: string; + } { try { - const performanceMonitor = (Core.Instance as any)._performanceMonitor; + const coreInstance = Core.Instance as Core & { _performanceMonitor?: unknown }; + const performanceMonitor = coreInstance._performanceMonitor; if (!performanceMonitor) { return { enabled: false }; } @@ -626,35 +718,25 @@ export class DebugManager { const stats = performanceMonitor.getAllSystemStats(); const warnings = performanceMonitor.getPerformanceWarnings(); - return { - enabled: performanceMonitor.enabled, + return { + enabled: (performanceMonitor as { enabled?: boolean }).enabled ?? false, systemCount: stats.size, warnings: warnings.slice(0, 10), // 最多10个警告 - topSystems: Array.from(stats.entries() as any).map((entry: any) => { - const [name, stat] = entry; + topSystems: Array.from(stats.entries()).map((entry) => { + const [name, stat] = entry as [string, { averageTime: number; maxTime: number; executionCount: number }]; return { name, averageTime: stat.averageTime, maxTime: stat.maxTime, samples: stat.executionCount }; - }).sort((a: any, b: any) => b.averageTime - a.averageTime).slice(0, 5) + }).sort((a, b) => b.averageTime - a.averageTime).slice(0, 5) }; - } catch (error: any) { - return { enabled: false, error: error.message }; + } catch (error: unknown) { + return { enabled: false, error: error instanceof Error ? error.message : String(error) }; } } - /** - * 获取内存大小分类 - */ - private getMemorySizeCategory(memoryBytes: number): string { - if (memoryBytes < 1024) return '< 1KB'; - if (memoryBytes < 10 * 1024) return '1-10KB'; - if (memoryBytes < 100 * 1024) return '10-100KB'; - if (memoryBytes < 1024 * 1024) return '100KB-1MB'; - return '> 1MB'; - } /** * 获取调试数据 @@ -677,12 +759,14 @@ export class DebugManager { } if (this.config.channels.systems) { - const performanceMonitor = (Core.Instance as any)._performanceMonitor; + const coreInstance = Core.Instance as Core & { _performanceMonitor?: unknown }; + const performanceMonitor = coreInstance._performanceMonitor; debugData.systems = this.systemCollector.collectSystemData(performanceMonitor); } if (this.config.channels.performance) { - const performanceMonitor = (Core.Instance as any)._performanceMonitor; + const coreInstance = Core.Instance as Core & { _performanceMonitor?: unknown }; + const performanceMonitor = coreInstance._performanceMonitor; debugData.performance = this.performanceCollector.collectPerformanceData(performanceMonitor); } diff --git a/packages/network-client/README.md b/packages/network-client/README.md new file mode 100644 index 00000000..8b0069b5 --- /dev/null +++ b/packages/network-client/README.md @@ -0,0 +1,101 @@ +# ECS Framework 网络库 - 客户端 + +该包提供了完整的网络客户端功能,包括连接管理、预测、插值等现代网络游戏必需的特性。 + +## 主要功能 + +- ✅ **传输层支持**: WebSocket 和 HTTP 两种传输方式 +- ✅ **网络客户端**: 完整的连接、认证、房间管理 +- ✅ **网络行为**: ClientNetworkBehaviour 基类和 NetworkIdentity 组件 +- ✅ **装饰器系统**: @SyncVar, @ClientRpc, @ServerRpc 装饰器 +- ✅ **客户端预测**: 减少网络延迟感知的预测系统 +- ✅ **插值系统**: 平滑的网络对象状态同步 +- ✅ **TypeScript**: 完整的类型支持 + +## 安装 + +```bash +npm install @esengine/ecs-framework-network-client +``` + +## 快速开始 + +```typescript +import { + NetworkClient, + WebSocketClientTransport, + ClientNetworkBehaviour, + SyncVar, + ServerRpc +} from '@esengine/ecs-framework-network-client'; + +// 创建网络客户端 +const client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080, + secure: false + } +}); + +// 连接到服务器 +await client.connect(); + +// 认证 +const userInfo = await client.authenticate('username', 'password'); + +// 获取房间列表 +const rooms = await client.getRoomList(); + +// 加入房间 +const roomInfo = await client.joinRoom('room-id'); +``` + +## 网络行为示例 + +```typescript +class PlayerController extends ClientNetworkBehaviour { + @SyncVar({ clientCanModify: true }) + position: { x: number; y: number } = { x: 0, y: 0 }; + + @SyncVar() + health: number = 100; + + @ServerRpc({ requireLocalPlayer: true }) + async move(direction: string): Promise { + // 这个方法会被发送到服务器执行 + } + + @ClientRpc() + onDamaged(damage: number): void { + // 这个方法会被服务器调用 + console.log(`Received damage: ${damage}`); + } +} +``` + +## 预测和插值 + +```typescript +import { PredictionSystem, InterpolationSystem } from '@esengine/ecs-framework-network-client'; + +// 启用预测系统 +const predictionSystem = new PredictionSystem(scene, 64, 500); +scene.addSystem(predictionSystem); + +// 启用插值系统 +const interpolationSystem = new InterpolationSystem(scene, { + delay: 100, + enableExtrapolation: false +}); +scene.addSystem(interpolationSystem); +``` + +## 编译状态 + +✅ **编译成功** - 所有 TypeScript 错误已修复,包生成完成 + +## License + +MIT \ No newline at end of file diff --git a/packages/network-client/build-rollup.cjs b/packages/network-client/build-rollup.cjs new file mode 100644 index 00000000..212e2edf --- /dev/null +++ b/packages/network-client/build-rollup.cjs @@ -0,0 +1,117 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +console.log('🚀 使用 Rollup 构建 network-client 包...'); + +async function main() { + try { + if (fs.existsSync('./dist')) { + console.log('🧹 清理旧的构建文件...'); + execSync('rimraf ./dist', { stdio: 'inherit' }); + } + + console.log('📦 执行 Rollup 构建...'); + execSync('rollup -c rollup.config.cjs', { stdio: 'inherit' }); + + console.log('📋 生成 package.json...'); + generatePackageJson(); + + console.log('📁 复制必要文件...'); + copyFiles(); + + showBuildResults(); + + console.log('✅ network-client 构建完成!'); + console.log('\n🚀 发布命令:'); + console.log('cd dist && npm publish'); + + } catch (error) { + console.error('❌ 构建失败:', error.message); + process.exit(1); + } +} + +function generatePackageJson() { + const sourcePackage = JSON.parse(fs.readFileSync('./package.json', 'utf8')); + + const distPackage = { + name: sourcePackage.name, + version: sourcePackage.version, + description: sourcePackage.description, + main: 'index.cjs', + module: 'index.mjs', + unpkg: 'index.umd.js', + types: 'index.d.ts', + exports: { + '.': { + import: './index.mjs', + require: './index.cjs', + types: './index.d.ts' + } + }, + files: [ + 'index.mjs', + 'index.mjs.map', + 'index.cjs', + 'index.cjs.map', + 'index.umd.js', + 'index.umd.js.map', + 'index.d.ts', + 'README.md', + 'LICENSE' + ], + keywords: [ + 'ecs', + 'networking', + 'client', + 'prediction', + 'interpolation', + 'game-engine', + 'typescript' + ], + author: sourcePackage.author, + license: sourcePackage.license, + repository: sourcePackage.repository, + dependencies: sourcePackage.dependencies, + peerDependencies: sourcePackage.peerDependencies, + engines: { + node: '>=16.0.0' + }, + sideEffects: false + }; + + fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2)); +} + +function copyFiles() { + const filesToCopy = [ + { src: './README.md', dest: './dist/README.md' }, + { src: '../../LICENSE', dest: './dist/LICENSE' } + ]; + + filesToCopy.forEach(({ src, dest }) => { + if (fs.existsSync(src)) { + fs.copyFileSync(src, dest); + console.log(` ✓ 复制: ${path.basename(dest)}`); + } else { + console.log(` ⚠️ 文件不存在: ${src}`); + } + }); +} + +function showBuildResults() { + const distDir = './dist'; + const files = ['index.mjs', 'index.cjs', 'index.umd.js', 'index.d.ts']; + + console.log('\n📊 构建结果:'); + files.forEach(file => { + const filePath = path.join(distDir, file); + if (fs.existsSync(filePath)) { + const size = fs.statSync(filePath).size; + console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`); + } + }); +} + +main().catch(console.error); \ No newline at end of file diff --git a/packages/network-client/jest.config.cjs b/packages/network-client/jest.config.cjs new file mode 100644 index 00000000..74a4925b --- /dev/null +++ b/packages/network-client/jest.config.cjs @@ -0,0 +1,53 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', // 客户端库使用 jsdom 环境 + roots: ['/tests'], + testMatch: ['**/*.test.ts', '**/*.spec.ts'], + testPathIgnorePatterns: ['/node_modules/'], + collectCoverage: false, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/index.ts', + '!src/**/index.ts', + '!**/*.d.ts', + '!src/**/*.test.ts', + '!src/**/*.spec.ts' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + coverageThreshold: { + global: { + branches: 60, + functions: 70, + lines: 70, + statements: 70 + }, + './src/core/': { + branches: 70, + functions: 80, + lines: 80, + statements: 80 + } + }, + verbose: true, + transform: { + '^.+\\.tsx?$': ['ts-jest', { + tsconfig: 'tsconfig.json', + useESM: false, + }], + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + setupFilesAfterEnv: ['/tests/setup.ts'], + testTimeout: 10000, + clearMocks: true, + restoreMocks: true, + modulePathIgnorePatterns: [ + '/bin/', + '/dist/', + '/node_modules/' + ] +}; \ No newline at end of file diff --git a/packages/network-client/package.json b/packages/network-client/package.json new file mode 100644 index 00000000..36081c48 --- /dev/null +++ b/packages/network-client/package.json @@ -0,0 +1,86 @@ +{ + "name": "@esengine/ecs-framework-network-client", + "version": "1.0.17", + "description": "ECS Framework 网络库 - 客户端实现", + "type": "module", + "main": "bin/index.js", + "types": "bin/index.d.ts", + "exports": { + ".": { + "types": "./bin/index.d.ts", + "import": "./bin/index.js", + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + } + }, + "files": [ + "bin/**/*", + "README.md", + "LICENSE" + ], + "keywords": [ + "ecs", + "networking", + "client", + "prediction", + "interpolation", + "game-engine", + "typescript" + ], + "scripts": { + "clean": "rimraf bin dist", + "build:ts": "tsc", + "prebuild": "npm run clean", + "build": "npm run build:ts", + "build:watch": "tsc --watch", + "rebuild": "npm run clean && npm run build", + "build:npm": "npm run build && node build-rollup.cjs", + "publish:npm": "npm run build:npm && cd dist && npm publish", + "publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish", + "publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish", + "publish:major": "npm version major && npm run build:npm && cd dist && npm publish", + "preversion": "npm run rebuild", + "test": "jest --config jest.config.cjs", + "test:watch": "jest --watch --config jest.config.cjs", + "test:coverage": "jest --coverage --config jest.config.cjs", + "test:ci": "jest --ci --coverage --config jest.config.cjs", + "test:clear": "jest --clearCache" + }, + "author": "yhh", + "license": "MIT", + "dependencies": { + "ws": "^8.18.0" + }, + "peerDependencies": { + "@esengine/ecs-framework": ">=2.1.29", + "@esengine/ecs-framework-network-shared": ">=1.0.0" + }, + "devDependencies": { + "@esengine/ecs-framework": "*", + "@esengine/ecs-framework-network-shared": "*", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.0", + "@types/ws": "^8.5.13", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "rimraf": "^5.0.0", + "rollup": "^4.42.0", + "rollup-plugin-dts": "^6.2.1", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/network-client" + } +} diff --git a/packages/network/rollup.config.cjs b/packages/network-client/rollup.config.cjs similarity index 80% rename from packages/network/rollup.config.cjs rename to packages/network-client/rollup.config.cjs index 4a618d10..6de2e3eb 100644 --- a/packages/network/rollup.config.cjs +++ b/packages/network-client/rollup.config.cjs @@ -7,18 +7,17 @@ const { readFileSync } = require('fs'); const pkg = JSON.parse(readFileSync('./package.json', 'utf8')); const banner = `/** - * @esengine/ecs-framework-network v${pkg.version} - * ECS框架网络插件 - protobuf序列化、帧同步和快照功能 + * @esengine/ecs-framework-network-client v${pkg.version} + * ECS Framework 网络库 - 客户端实现 * * @author ${pkg.author} * @license ${pkg.license} */`; -// 外部依赖 - 不打包到bundle中 const external = [ + 'ws', '@esengine/ecs-framework', - 'protobufjs', - 'reflect-metadata' + '@esengine/ecs-framework-network-shared' ]; const commonPlugins = [ @@ -81,21 +80,22 @@ module.exports = [ moduleSideEffects: false } }, - - // UMD构建 - 用于CDN和浏览器直接使用 + + // UMD构建 { input: 'bin/index.js', output: { file: 'dist/index.umd.js', format: 'umd', - name: 'ECSNetwork', + name: 'ECSNetworkClient', banner, sourcemap: true, exports: 'named', globals: { + 'ws': 'WebSocket', + 'uuid': 'uuid', '@esengine/ecs-framework': 'ECS', - 'protobufjs': 'protobuf', - 'reflect-metadata': 'Reflect' + '@esengine/ecs-framework-network-shared': 'ECSNetworkShared' } }, plugins: [ @@ -106,7 +106,7 @@ module.exports = [ } }) ], - external: ['@esengine/ecs-framework', 'protobufjs', 'reflect-metadata'], + external, treeshake: { moduleSideEffects: false } @@ -119,7 +119,7 @@ module.exports = [ file: 'dist/index.d.ts', format: 'es', banner: `/** - * @esengine/ecs-framework-network v${pkg.version} + * @esengine/ecs-framework-network-client v${pkg.version} * TypeScript definitions */` }, @@ -128,6 +128,6 @@ module.exports = [ respectExternal: true }) ], - external: ['@esengine/ecs-framework'] + external } ]; \ No newline at end of file diff --git a/packages/network-client/src/core/ClientNetworkBehaviour.ts b/packages/network-client/src/core/ClientNetworkBehaviour.ts new file mode 100644 index 00000000..90039e93 --- /dev/null +++ b/packages/network-client/src/core/ClientNetworkBehaviour.ts @@ -0,0 +1,179 @@ +/** + * 客户端网络行为基类 + * + * 类似Unity Mirror的NetworkBehaviour,提供网络功能 + */ + +import { Component, Entity } from '@esengine/ecs-framework'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { NetworkClient } from './NetworkClient'; +import { NetworkIdentity } from './NetworkIdentity'; + +/** + * 客户端网络行为基类 + */ +export abstract class ClientNetworkBehaviour extends Component { + /** 网络标识组件 */ + protected networkIdentity: NetworkIdentity | null = null; + /** 网络客户端实例 */ + protected networkClient: NetworkClient | null = null; + + /** + * 组件初始化 + */ + initialize(): void { + + // 获取网络标识组件 + this.networkIdentity = this.entity.getComponent(NetworkIdentity); + if (!this.networkIdentity) { + throw new Error('NetworkBehaviour requires NetworkIdentity component'); + } + + // 从全局获取网络客户端实例 + this.networkClient = this.getNetworkClient(); + } + + /** + * 获取网络客户端实例 + */ + protected getNetworkClient(): NetworkClient | null { + // 这里需要实现从全局管理器获取客户端实例的逻辑 + // 暂时返回null,在实际使用时需要通过单例模式或依赖注入获取 + return null; + } + + /** + * 是否为本地玩家 + */ + get isLocalPlayer(): boolean { + return this.networkIdentity?.isLocalPlayer ?? false; + } + + /** + * 是否为服务器权威 + */ + get hasAuthority(): boolean { + return this.networkIdentity?.hasAuthority ?? false; + } + + /** + * 网络ID + */ + get networkId(): string { + return this.networkIdentity?.networkId ?? ''; + } + + /** + * 是否已连接 + */ + get isConnected(): boolean { + return this.networkClient?.isInRoom() ?? false; + } + + /** + * 发送RPC到服务器 + */ + protected async sendServerRpc(methodName: string, ...args: NetworkValue[]): Promise { + if (!this.networkClient || !this.networkIdentity) { + throw new Error('Network client or identity not available'); + } + + return this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, true); + } + + /** + * 发送不可靠RPC到服务器 + */ + protected async sendServerRpcUnreliable(methodName: string, ...args: NetworkValue[]): Promise { + if (!this.networkClient || !this.networkIdentity) { + throw new Error('Network client or identity not available'); + } + + await this.networkClient.sendRpc(this.networkIdentity.networkId, methodName, args, false); + } + + /** + * 更新SyncVar + */ + protected async updateSyncVar(fieldName: string, value: NetworkValue): Promise { + if (!this.networkClient || !this.networkIdentity) { + throw new Error('Network client or identity not available'); + } + + await this.networkClient.updateSyncVar(this.networkIdentity.networkId, fieldName, value); + } + + /** + * 当收到RPC调用时 + */ + onRpcReceived(methodName: string, args: NetworkValue[]): void { + // 尝试调用对应的方法 + const method = (this as any)[methodName]; + if (typeof method === 'function') { + try { + method.apply(this, args); + } catch (error) { + console.error(`Error calling RPC method ${methodName}:`, error); + } + } else { + console.warn(`RPC method ${methodName} not found on ${this.constructor.name}`); + } + } + + /** + * 当SyncVar更新时 + */ + onSyncVarChanged(fieldName: string, oldValue: NetworkValue, newValue: NetworkValue): void { + // 子类可以重写此方法来处理SyncVar变化 + } + + /** + * 当获得权威时 + */ + onStartAuthority(): void { + // 子类可以重写此方法 + } + + /** + * 当失去权威时 + */ + onStopAuthority(): void { + // 子类可以重写此方法 + } + + /** + * 当成为本地玩家时 + */ + onStartLocalPlayer(): void { + // 子类可以重写此方法 + } + + /** + * 当不再是本地玩家时 + */ + onStopLocalPlayer(): void { + // 子类可以重写此方法 + } + + /** + * 网络启动时调用 + */ + onNetworkStart(): void { + // 子类可以重写此方法 + } + + /** + * 网络停止时调用 + */ + onNetworkStop(): void { + // 子类可以重写此方法 + } + + /** + * 组件销毁 + */ + onDestroy(): void { + this.networkIdentity = null; + this.networkClient = null; + } +} \ No newline at end of file diff --git a/packages/network-client/src/core/NetworkClient.ts b/packages/network-client/src/core/NetworkClient.ts new file mode 100644 index 00000000..03b6f491 --- /dev/null +++ b/packages/network-client/src/core/NetworkClient.ts @@ -0,0 +1,638 @@ +/** + * 网络客户端主类 + * + * 管理连接、认证、房间加入等功能 + */ + +import { Scene, EntityManager, Emitter, ITimer, Core } from '@esengine/ecs-framework'; +import { + NetworkIdentity as SharedNetworkIdentity, + NetworkValue, + RpcMessage, + SyncVarMessage +} from '@esengine/ecs-framework-network-shared'; +import { + ClientTransport, + WebSocketClientTransport, + HttpClientTransport, + ConnectionState, + ClientMessage, + ClientTransportConfig, + WebSocketClientConfig, + HttpClientConfig +} from '../transport'; + +/** + * 网络客户端配置 + */ +export interface NetworkClientConfig { + /** 传输类型 */ + transport: 'websocket' | 'http'; + /** 传输配置 */ + transportConfig: WebSocketClientConfig | HttpClientConfig; + /** 是否启用预测 */ + enablePrediction?: boolean; + /** 预测缓冲区大小 */ + predictionBuffer?: number; + /** 是否启用插值 */ + enableInterpolation?: boolean; + /** 插值延迟(毫秒) */ + interpolationDelay?: number; + /** 网络对象同步间隔(毫秒) */ + syncInterval?: number; + /** 是否启用调试 */ + debug?: boolean; +} + +/** + * 用户信息 + */ +export interface UserInfo { + /** 用户ID */ + userId: string; + /** 用户名 */ + username: string; + /** 用户数据 */ + data?: NetworkValue; +} + +/** + * 房间信息 + */ +export interface RoomInfo { + /** 房间ID */ + roomId: string; + /** 房间名称 */ + name: string; + /** 当前人数 */ + playerCount: number; + /** 最大人数 */ + maxPlayers: number; + /** 房间元数据 */ + metadata?: NetworkValue; + /** 是否私有房间 */ + isPrivate?: boolean; +} + +/** + * 认证消息 + */ +export interface AuthMessage { + action: string; + username: string; + password?: string; + userData?: NetworkValue; +} + +/** + * 房间消息 + */ +export interface RoomMessage { + action: string; + roomId?: string; + name?: string; + maxPlayers?: number; + metadata?: NetworkValue; + isPrivate?: boolean; + password?: string; +} + +/** + * 网络客户端事件 + */ +export interface NetworkClientEvents { + /** 连接建立 */ + 'connected': () => void; + /** 连接断开 */ + 'disconnected': (reason: string) => void; + /** 认证成功 */ + 'authenticated': (userInfo: UserInfo) => void; + /** 加入房间成功 */ + 'joined-room': (roomInfo: RoomInfo) => void; + /** 离开房间 */ + 'left-room': (roomId: string) => void; + /** 房间列表更新 */ + 'room-list-updated': (rooms: RoomInfo[]) => void; + /** 玩家加入房间 */ + 'player-joined': (userId: string, userInfo: UserInfo) => void; + /** 玩家离开房间 */ + 'player-left': (userId: string) => void; + /** 网络对象创建 */ + 'network-object-created': (networkId: string, data: NetworkValue) => void; + /** 网络对象销毁 */ + 'network-object-destroyed': (networkId: string) => void; + /** SyncVar 更新 */ + 'syncvar-updated': (networkId: string, fieldName: string, value: NetworkValue) => void; + /** RPC 调用 */ + 'rpc-received': (networkId: string, methodName: string, args: NetworkValue[]) => void; + /** 错误发生 */ + 'error': (error: Error) => void; +} + +/** + * 网络客户端主类 + */ +export class NetworkClient { + private transport: ClientTransport; + private config: NetworkClientConfig; + private currentUser: UserInfo | null = null; + private currentRoom: RoomInfo | null = null; + private availableRooms: Map = new Map(); + private networkObjects: Map = new Map(); + private pendingRpcs: Map }> = new Map(); + private scene: Scene | null = null; + private eventEmitter: Emitter; + + constructor(config: NetworkClientConfig) { + this.eventEmitter = new Emitter(); + + this.config = { + enablePrediction: true, + predictionBuffer: 64, + enableInterpolation: true, + interpolationDelay: 100, + syncInterval: 50, + debug: false, + ...config + }; + + this.transport = this.createTransport(); + this.setupTransportEvents(); + } + + /** + * 创建传输层 + */ + private createTransport(): ClientTransport { + switch (this.config.transport) { + case 'websocket': + return new WebSocketClientTransport(this.config.transportConfig as WebSocketClientConfig); + case 'http': + return new HttpClientTransport(this.config.transportConfig as HttpClientConfig); + default: + throw new Error(`Unsupported transport type: ${this.config.transport}`); + } + } + + /** + * 设置传输层事件监听 + */ + private setupTransportEvents(): void { + this.transport.on('connected', () => { + this.eventEmitter.emit('connected'); + }); + + this.transport.on('disconnected', (reason) => { + this.handleDisconnected(reason); + }); + + this.transport.on('message', (message) => { + this.handleMessage(message); + }); + + this.transport.on('error', (error) => { + this.eventEmitter.emit('error', error); + }); + } + + /** + * 连接到服务器 + */ + async connect(): Promise { + return this.transport.connect(); + } + + /** + * 断开连接 + */ + async disconnect(): Promise { + await this.transport.disconnect(); + this.cleanup(); + } + + /** + * 用户认证 + */ + async authenticate(username: string, password?: string, userData?: NetworkValue): Promise { + if (!this.transport.isConnected()) { + throw new Error('Not connected to server'); + } + + const authMessage: AuthMessage = { + action: 'login', + username, + password, + userData + }; + + const response = await this.sendRequestWithResponse('system', authMessage as any); + + if (response.success && response.userInfo) { + this.currentUser = response.userInfo as UserInfo; + this.eventEmitter.emit('authenticated', this.currentUser); + return this.currentUser; + } else { + throw new Error(response.error || 'Authentication failed'); + } + } + + /** + * 获取房间列表 + */ + async getRoomList(): Promise { + if (!this.isAuthenticated()) { + throw new Error('Not authenticated'); + } + + const roomMessage: RoomMessage = { + action: 'list-rooms' + }; + + const response = await this.sendRequestWithResponse('system', roomMessage as any); + + if (response.success && response.rooms) { + this.availableRooms.clear(); + response.rooms.forEach((room: RoomInfo) => { + this.availableRooms.set(room.roomId, room); + }); + + this.eventEmitter.emit('room-list-updated', response.rooms); + return response.rooms; + } else { + throw new Error(response.error || 'Failed to get room list'); + } + } + + /** + * 创建房间 + */ + async createRoom(name: string, maxPlayers: number = 8, metadata?: NetworkValue, isPrivate = false): Promise { + if (!this.isAuthenticated()) { + throw new Error('Not authenticated'); + } + + const roomMessage: RoomMessage = { + action: 'create-room', + name, + maxPlayers, + metadata, + isPrivate + }; + + const response = await this.sendRequestWithResponse('system', roomMessage as any); + + if (response.success && response.room) { + this.currentRoom = response.room as RoomInfo; + this.eventEmitter.emit('joined-room', this.currentRoom); + return this.currentRoom; + } else { + throw new Error(response.error || 'Failed to create room'); + } + } + + /** + * 加入房间 + */ + async joinRoom(roomId: string, password?: string): Promise { + if (!this.isAuthenticated()) { + throw new Error('Not authenticated'); + } + + const roomMessage: RoomMessage = { + action: 'join-room', + roomId, + password + }; + + const response = await this.sendRequestWithResponse('system', roomMessage as any); + + if (response.success && response.room) { + this.currentRoom = response.room as RoomInfo; + this.eventEmitter.emit('joined-room', this.currentRoom); + return this.currentRoom; + } else { + throw new Error(response.error || 'Failed to join room'); + } + } + + /** + * 离开房间 + */ + async leaveRoom(): Promise { + if (!this.currentRoom) { + return; + } + + const roomMessage: RoomMessage = { + action: 'leave-room', + roomId: this.currentRoom.roomId + }; + + try { + await this.sendRequestWithResponse('system', roomMessage as any); + } finally { + const roomId = this.currentRoom.roomId; + this.currentRoom = null; + this.networkObjects.clear(); + this.eventEmitter.emit('left-room', roomId); + } + } + + /** + * 发送RPC调用 + */ + async sendRpc(networkId: string, methodName: string, args: NetworkValue[] = [], reliable = true): Promise { + if (!this.isInRoom()) { + throw new Error('Not in a room'); + } + + const rpcMessage: any = { + networkId, + methodName, + args, + isServer: false, + messageId: this.generateMessageId() + }; + + if (reliable) { + return this.sendRequestWithResponse('rpc', rpcMessage); + } else { + await this.transport.sendMessage({ + type: 'rpc', + data: rpcMessage as NetworkValue, + reliable: false + }); + return {}; + } + } + + /** + * 更新SyncVar + */ + async updateSyncVar(networkId: string, fieldName: string, value: NetworkValue): Promise { + if (!this.isInRoom()) { + throw new Error('Not in a room'); + } + + const syncMessage: any = { + networkId, + propertyName: fieldName, + value, + isServer: false + }; + + await this.transport.sendMessage({ + type: 'syncvar', + data: syncMessage as NetworkValue, + reliable: true + }); + } + + /** + * 设置ECS场景 + */ + setScene(scene: Scene): void { + this.scene = scene; + } + + /** + * 获取当前用户信息 + */ + getCurrentUser(): UserInfo | null { + return this.currentUser; + } + + /** + * 获取当前房间信息 + */ + getCurrentRoom(): RoomInfo | null { + return this.currentRoom; + } + + /** + * 获取连接状态 + */ + getConnectionState(): ConnectionState { + return this.transport.getState(); + } + + /** + * 是否已认证 + */ + isAuthenticated(): boolean { + return this.currentUser !== null && this.transport.isConnected(); + } + + /** + * 是否在房间中 + */ + isInRoom(): boolean { + return this.isAuthenticated() && this.currentRoom !== null; + } + + /** + * 获取网络对象 + */ + getNetworkObject(networkId: string): SharedNetworkIdentity | null { + return this.networkObjects.get(networkId) || null; + } + + /** + * 获取所有网络对象 + */ + getAllNetworkObjects(): SharedNetworkIdentity[] { + return Array.from(this.networkObjects.values()); + } + + /** + * 处理断开连接 + */ + private handleDisconnected(reason: string): void { + this.cleanup(); + this.eventEmitter.emit('disconnected', reason); + } + + /** + * 处理接收到的消息 + */ + private handleMessage(message: ClientMessage): void { + try { + switch (message.type) { + case 'system': + this.handleSystemMessage(message); + break; + case 'rpc': + this.handleRpcMessage(message); + break; + case 'syncvar': + this.handleSyncVarMessage(message); + break; + case 'custom': + this.handleCustomMessage(message); + break; + } + } catch (error) { + console.error('Error handling message:', error); + this.eventEmitter.emit('error', error as Error); + } + } + + /** + * 处理系统消息 + */ + private handleSystemMessage(message: ClientMessage): void { + const data = message.data as any; + + // 处理响应消息 + if (message.messageId && this.pendingRpcs.has(message.messageId)) { + const pending = this.pendingRpcs.get(message.messageId)!; + pending.timeout.stop(); + this.pendingRpcs.delete(message.messageId); + + if (data.success) { + pending.resolve(data); + } else { + pending.reject(new Error(data.error || 'Request failed')); + } + return; + } + + // 处理广播消息 + switch (data.action) { + case 'player-joined': + this.eventEmitter.emit('player-joined', data.userId, data.userInfo); + break; + case 'player-left': + this.eventEmitter.emit('player-left', data.userId); + break; + case 'network-object-created': + this.handleNetworkObjectCreated(data); + break; + case 'network-object-destroyed': + this.handleNetworkObjectDestroyed(data); + break; + } + } + + /** + * 处理RPC消息 + */ + private handleRpcMessage(message: ClientMessage): void { + const rpcData = message.data as any; + this.eventEmitter.emit('rpc-received', rpcData.networkId, rpcData.methodName, rpcData.args || []); + } + + /** + * 处理SyncVar消息 + */ + private handleSyncVarMessage(message: ClientMessage): void { + const syncData = message.data as any; + this.eventEmitter.emit('syncvar-updated', syncData.networkId, syncData.propertyName, syncData.value); + } + + /** + * 处理自定义消息 + */ + private handleCustomMessage(message: ClientMessage): void { + // 可扩展的自定义消息处理 + } + + /** + * 处理网络对象创建 + */ + private handleNetworkObjectCreated(data: any): void { + const networkObject = new SharedNetworkIdentity(); + this.networkObjects.set(data.networkId, networkObject); + this.eventEmitter.emit('network-object-created', data.networkId, data.data || {}); + } + + /** + * 处理网络对象销毁 + */ + private handleNetworkObjectDestroyed(data: any): void { + this.networkObjects.delete(data.networkId); + this.eventEmitter.emit('network-object-destroyed', data.networkId); + } + + /** + * 发送请求并等待响应 + */ + private sendRequestWithResponse(type: ClientMessage['type'], data: NetworkValue, timeout = 30000): Promise { + return new Promise((resolve, reject) => { + const messageId = this.generateMessageId(); + + const timeoutTimer = Core.schedule(timeout / 1000, false, this, () => { + this.pendingRpcs.delete(messageId); + reject(new Error('Request timeout')); + }); + + this.pendingRpcs.set(messageId, { + resolve, + reject, + timeout: timeoutTimer + }); + + this.transport.sendMessage({ + type, + data, + messageId, + reliable: true + }).catch(reject); + }); + } + + /** + * 生成消息ID + */ + private generateMessageId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + + /** + * 清理资源 + */ + private cleanup(): void { + this.currentUser = null; + this.currentRoom = null; + this.availableRooms.clear(); + this.networkObjects.clear(); + + // 取消所有待处理的RPC + this.pendingRpcs.forEach(pending => { + pending.timeout.stop(); + pending.reject(new Error('Connection closed')); + }); + this.pendingRpcs.clear(); + } + + /** + * 销毁客户端 + */ + destroy(): void { + this.disconnect(); + this.transport.destroy(); + // 清理事件监听器,由于Emitter没有clear方法,我们重新创建一个 + this.eventEmitter = new Emitter(); + } + + /** + * 类型安全的事件监听 + */ + on(event: K, listener: NetworkClientEvents[K]): void { + this.eventEmitter.addObserver(event, listener, this); + } + + /** + * 移除事件监听 + */ + off(event: K, listener: NetworkClientEvents[K]): void { + this.eventEmitter.removeObserver(event, listener); + } + + /** + * 类型安全的事件触发 + */ + emit(event: K, ...args: Parameters): void { + this.eventEmitter.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-client/src/core/NetworkIdentity.ts b/packages/network-client/src/core/NetworkIdentity.ts new file mode 100644 index 00000000..514d462f --- /dev/null +++ b/packages/network-client/src/core/NetworkIdentity.ts @@ -0,0 +1,378 @@ +/** + * 客户端网络标识组件 + * + * 标识网络对象并管理其状态 + */ + +import { Component, Entity } from '@esengine/ecs-framework'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { ClientNetworkBehaviour } from './ClientNetworkBehaviour'; + +/** + * 网络权威类型 + */ +export enum NetworkAuthority { + /** 服务器权威 */ + SERVER = 'server', + /** 客户端权威 */ + CLIENT = 'client', + /** 所有者权威 */ + OWNER = 'owner' +} + +/** + * SyncVar信息 + */ +export interface SyncVarInfo { + /** 字段名 */ + fieldName: string; + /** 当前值 */ + currentValue: NetworkValue; + /** 上一个值 */ + previousValue: NetworkValue; + /** 最后更新时间 */ + lastUpdateTime: number; + /** 是否已变更 */ + isDirty: boolean; +} + +/** + * 网络标识组件 + */ +export class NetworkIdentity extends Component { + /** 网络ID */ + private _networkId: string = ''; + /** 所有者用户ID */ + private _ownerId: string = ''; + /** 是否为本地玩家 */ + private _isLocalPlayer: boolean = false; + /** 权威类型 */ + private _authority: NetworkAuthority = NetworkAuthority.SERVER; + /** 是否有权威 */ + private _hasAuthority: boolean = false; + /** 网络行为组件列表 */ + private networkBehaviours: ClientNetworkBehaviour[] = []; + /** SyncVar信息映射 */ + private syncVars: Map = new Map(); + /** 预测状态 */ + private predictionEnabled: boolean = false; + /** 插值状态 */ + private interpolationEnabled: boolean = true; + + /** + * 网络ID + */ + get networkId(): string { + return this._networkId; + } + + set networkId(value: string) { + this._networkId = value; + } + + /** + * 所有者用户ID + */ + get ownerId(): string { + return this._ownerId; + } + + set ownerId(value: string) { + this._ownerId = value; + } + + /** + * 是否为本地玩家 + */ + get isLocalPlayer(): boolean { + return this._isLocalPlayer; + } + + set isLocalPlayer(value: boolean) { + if (this._isLocalPlayer !== value) { + this._isLocalPlayer = value; + this.notifyLocalPlayerChanged(); + } + } + + /** + * 权威类型 + */ + get authority(): NetworkAuthority { + return this._authority; + } + + set authority(value: NetworkAuthority) { + if (this._authority !== value) { + this._authority = value; + this.updateAuthorityStatus(); + } + } + + /** + * 是否有权威 + */ + get hasAuthority(): boolean { + return this._hasAuthority; + } + + /** + * 是否启用预测 + */ + get isPredictionEnabled(): boolean { + return this.predictionEnabled; + } + + set isPredictionEnabled(value: boolean) { + this.predictionEnabled = value; + } + + /** + * 是否启用插值 + */ + get isInterpolationEnabled(): boolean { + return this.interpolationEnabled; + } + + set isInterpolationEnabled(value: boolean) { + this.interpolationEnabled = value; + } + + /** + * 组件初始化 + */ + initialize(): void { + this.collectNetworkBehaviours(); + this.notifyNetworkStart(); + } + + /** + * 收集网络行为组件 + */ + private collectNetworkBehaviours(): void { + // 暂时留空,等待实际集成时实现 + this.networkBehaviours = []; + } + + /** + * 更新权威状态 + */ + private updateAuthorityStatus(): void { + const oldHasAuthority = this._hasAuthority; + + // 根据权威类型计算是否有权威 + switch (this._authority) { + case NetworkAuthority.SERVER: + this._hasAuthority = false; // 客户端永远没有服务器权威 + break; + case NetworkAuthority.CLIENT: + this._hasAuthority = true; // 客户端权威 + break; + case NetworkAuthority.OWNER: + this._hasAuthority = this._isLocalPlayer; // 本地玩家才有权威 + break; + } + + // 通知权威变化 + if (oldHasAuthority !== this._hasAuthority) { + this.notifyAuthorityChanged(); + } + } + + /** + * 通知权威变化 + */ + private notifyAuthorityChanged(): void { + this.networkBehaviours.forEach(behaviour => { + if (this._hasAuthority) { + behaviour.onStartAuthority(); + } else { + behaviour.onStopAuthority(); + } + }); + } + + /** + * 通知本地玩家状态变化 + */ + private notifyLocalPlayerChanged(): void { + this.updateAuthorityStatus(); // 本地玩家状态影响权威 + + this.networkBehaviours.forEach(behaviour => { + if (this._isLocalPlayer) { + behaviour.onStartLocalPlayer(); + } else { + behaviour.onStopLocalPlayer(); + } + }); + } + + /** + * 通知网络启动 + */ + private notifyNetworkStart(): void { + this.networkBehaviours.forEach(behaviour => { + behaviour.onNetworkStart(); + }); + } + + /** + * 通知网络停止 + */ + private notifyNetworkStop(): void { + this.networkBehaviours.forEach(behaviour => { + behaviour.onNetworkStop(); + }); + } + + /** + * 处理RPC调用 + */ + handleRpcCall(methodName: string, args: NetworkValue[]): void { + // 将RPC调用分发给所有网络行为组件 + this.networkBehaviours.forEach(behaviour => { + behaviour.onRpcReceived(methodName, args); + }); + } + + /** + * 注册SyncVar + */ + registerSyncVar(fieldName: string, initialValue: NetworkValue): void { + this.syncVars.set(fieldName, { + fieldName, + currentValue: initialValue, + previousValue: initialValue, + lastUpdateTime: Date.now(), + isDirty: false + }); + } + + /** + * 更新SyncVar + */ + updateSyncVar(fieldName: string, newValue: NetworkValue): void { + const syncVar = this.syncVars.get(fieldName); + if (!syncVar) { + console.warn(`SyncVar ${fieldName} not registered on ${this._networkId}`); + return; + } + + const oldValue = syncVar.currentValue; + syncVar.previousValue = oldValue; + syncVar.currentValue = newValue; + syncVar.lastUpdateTime = Date.now(); + syncVar.isDirty = true; + + // 通知所有网络行为组件 + this.networkBehaviours.forEach(behaviour => { + behaviour.onSyncVarChanged(fieldName, oldValue, newValue); + }); + } + + /** + * 获取SyncVar值 + */ + getSyncVar(fieldName: string): NetworkValue | undefined { + return this.syncVars.get(fieldName)?.currentValue; + } + + /** + * 获取所有SyncVar + */ + getAllSyncVars(): Map { + return new Map(this.syncVars); + } + + /** + * 获取脏SyncVar + */ + getDirtySyncVars(): SyncVarInfo[] { + return Array.from(this.syncVars.values()).filter(syncVar => syncVar.isDirty); + } + + /** + * 清除脏标记 + */ + clearDirtyFlags(): void { + this.syncVars.forEach(syncVar => { + syncVar.isDirty = false; + }); + } + + /** + * 序列化网络状态 + */ + serializeState(): NetworkValue { + const state: any = { + networkId: this._networkId, + ownerId: this._ownerId, + isLocalPlayer: this._isLocalPlayer, + authority: this._authority, + syncVars: {} + }; + + // 序列化SyncVar + this.syncVars.forEach((syncVar, fieldName) => { + state.syncVars[fieldName] = syncVar.currentValue; + }); + + return state; + } + + /** + * 反序列化网络状态 + */ + deserializeState(state: any): void { + if (state.networkId) this._networkId = state.networkId; + if (state.ownerId) this._ownerId = state.ownerId; + if (typeof state.isLocalPlayer === 'boolean') this.isLocalPlayer = state.isLocalPlayer; + if (state.authority) this.authority = state.authority; + + // 反序列化SyncVar + if (state.syncVars) { + Object.entries(state.syncVars).forEach(([fieldName, value]) => { + if (this.syncVars.has(fieldName)) { + this.updateSyncVar(fieldName, value as NetworkValue); + } + }); + } + } + + /** + * 设置预测状态 + */ + setPredictionState(enabled: boolean): void { + this.predictionEnabled = enabled; + } + + /** + * 设置插值状态 + */ + setInterpolationState(enabled: boolean): void { + this.interpolationEnabled = enabled; + } + + /** + * 检查是否可以发送RPC + */ + canSendRpc(): boolean { + return this._hasAuthority || this._isLocalPlayer; + } + + /** + * 检查是否可以更新SyncVar + */ + canUpdateSyncVar(): boolean { + return this._hasAuthority; + } + + /** + * 组件销毁 + */ + onDestroy(): void { + this.notifyNetworkStop(); + this.networkBehaviours = []; + this.syncVars.clear(); + } +} \ No newline at end of file diff --git a/packages/network-client/src/core/index.ts b/packages/network-client/src/core/index.ts new file mode 100644 index 00000000..b8ac8234 --- /dev/null +++ b/packages/network-client/src/core/index.ts @@ -0,0 +1,7 @@ +/** + * 核心模块导出 + */ + +export * from './NetworkClient'; +export * from './ClientNetworkBehaviour'; +export * from './NetworkIdentity'; \ No newline at end of file diff --git a/packages/network-client/src/decorators/ClientRpc.ts b/packages/network-client/src/decorators/ClientRpc.ts new file mode 100644 index 00000000..8443f0f9 --- /dev/null +++ b/packages/network-client/src/decorators/ClientRpc.ts @@ -0,0 +1,108 @@ +/** + * ClientRpc装饰器 - 客户端版本 + * + * 用于标记可以从服务器调用的客户端方法 + */ + +import 'reflect-metadata'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; + +/** + * ClientRpc配置选项 + */ +export interface ClientRpcOptions { + /** 是否可靠传输 */ + reliable?: boolean; + /** 超时时间(毫秒) */ + timeout?: number; + /** 是否仅发送给所有者 */ + ownerOnly?: boolean; + /** 是否包含发送者 */ + includeSender?: boolean; + /** 权限要求 */ + requireAuthority?: boolean; +} + +/** + * ClientRpc元数据键 + */ +export const CLIENT_RPC_METADATA_KEY = Symbol('client_rpc'); + +/** + * ClientRpc元数据 + */ +export interface ClientRpcMetadata { + /** 方法名 */ + methodName: string; + /** 配置选项 */ + options: ClientRpcOptions; + /** 原始方法 */ + originalMethod: Function; +} + +/** + * ClientRpc装饰器 + */ +export function ClientRpc(options: ClientRpcOptions = {}): MethodDecorator { + return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) { + const methodName = propertyKey as string; + const originalMethod = descriptor.value; + + // 获取已有的ClientRpc元数据 + const existingMetadata: ClientRpcMetadata[] = Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target.constructor) || []; + + // 添加新的ClientRpc元数据 + existingMetadata.push({ + methodName, + options: { + reliable: true, + timeout: 30000, + ownerOnly: false, + includeSender: false, + requireAuthority: false, + ...options + }, + originalMethod + }); + + // 设置元数据 + Reflect.defineMetadata(CLIENT_RPC_METADATA_KEY, existingMetadata, target.constructor); + + // 包装原方法(客户端接收RPC调用时执行) + descriptor.value = function (this: any, ...args: NetworkValue[]) { + try { + // 直接调用原方法,客户端接收RPC调用 + return originalMethod.apply(this, args); + } catch (error) { + console.error(`Error executing ClientRpc ${methodName}:`, error); + throw error; + } + }; + + return descriptor; + }; +} + +/** + * 获取类的所有ClientRpc元数据 + */ +export function getClientRpcMetadata(target: any): ClientRpcMetadata[] { + return Reflect.getMetadata(CLIENT_RPC_METADATA_KEY, target) || []; +} + +/** + * 检查方法是否为ClientRpc + */ +export function isClientRpc(target: any, methodName: string): boolean { + const metadata = getClientRpcMetadata(target); + return metadata.some(meta => meta.methodName === methodName); +} + +/** + * 获取特定方法的ClientRpc选项 + */ +export function getClientRpcOptions(target: any, methodName: string): ClientRpcOptions | null { + const metadata = getClientRpcMetadata(target); + const rpc = metadata.find(meta => meta.methodName === methodName); + return rpc ? rpc.options : null; +} \ No newline at end of file diff --git a/packages/network-client/src/decorators/ServerRpc.ts b/packages/network-client/src/decorators/ServerRpc.ts new file mode 100644 index 00000000..9b9f742f --- /dev/null +++ b/packages/network-client/src/decorators/ServerRpc.ts @@ -0,0 +1,138 @@ +/** + * ServerRpc装饰器 - 客户端版本 + * + * 用于标记可以向服务器发送的RPC方法 + */ + +import 'reflect-metadata'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour'; + +/** + * ServerRpc配置选项 + */ +export interface ServerRpcOptions { + /** 是否可靠传输 */ + reliable?: boolean; + /** 超时时间(毫秒) */ + timeout?: number; + /** 是否需要权威 */ + requireAuthority?: boolean; + /** 是否需要是本地玩家 */ + requireLocalPlayer?: boolean; +} + +/** + * ServerRpc元数据键 + */ +export const SERVER_RPC_METADATA_KEY = Symbol('server_rpc'); + +/** + * ServerRpc元数据 + */ +export interface ServerRpcMetadata { + /** 方法名 */ + methodName: string; + /** 配置选项 */ + options: ServerRpcOptions; + /** 原始方法 */ + originalMethod: Function; +} + +/** + * ServerRpc装饰器 + */ +export function ServerRpc(options: ServerRpcOptions = {}): MethodDecorator { + return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) { + const methodName = propertyKey as string; + const originalMethod = descriptor.value; + + // 获取已有的ServerRpc元数据 + const existingMetadata: ServerRpcMetadata[] = Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target.constructor) || []; + + // 添加新的ServerRpc元数据 + existingMetadata.push({ + methodName, + options: { + reliable: true, + timeout: 30000, + requireAuthority: false, + requireLocalPlayer: false, + ...options + }, + originalMethod + }); + + // 设置元数据 + Reflect.defineMetadata(SERVER_RPC_METADATA_KEY, existingMetadata, target.constructor); + + // 替换方法实现为发送RPC调用 + descriptor.value = async function (this: ClientNetworkBehaviour, ...args: NetworkValue[]) { + try { + // 获取NetworkIdentity + const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any); + if (!networkIdentity) { + throw new Error('NetworkIdentity component not found'); + } + + // 检查权限要求 + if (options.requireAuthority && !(networkIdentity as any).hasAuthority) { + throw new Error(`ServerRpc ${methodName} requires authority`); + } + + if (options.requireLocalPlayer && !(networkIdentity as any).isLocalPlayer) { + throw new Error(`ServerRpc ${methodName} requires local player`); + } + + // 发送RPC到服务器 + if (options.reliable) { + const result = await this.sendServerRpc(methodName, ...args); + return result; + } else { + await this.sendServerRpcUnreliable(methodName, ...args); + return null; + } + + } catch (error) { + console.error(`Error sending ServerRpc ${methodName}:`, error); + throw error; + } + }; + + // 保存原方法到特殊属性,用于本地预测或调试 + (descriptor.value as any).__originalMethod = originalMethod; + + return descriptor; + }; +} + +/** + * 获取类的所有ServerRpc元数据 + */ +export function getServerRpcMetadata(target: any): ServerRpcMetadata[] { + return Reflect.getMetadata(SERVER_RPC_METADATA_KEY, target) || []; +} + +/** + * 检查方法是否为ServerRpc + */ +export function isServerRpc(target: any, methodName: string): boolean { + const metadata = getServerRpcMetadata(target); + return metadata.some(meta => meta.methodName === methodName); +} + +/** + * 获取特定方法的ServerRpc选项 + */ +export function getServerRpcOptions(target: any, methodName: string): ServerRpcOptions | null { + const metadata = getServerRpcMetadata(target); + const rpc = metadata.find(meta => meta.methodName === methodName); + return rpc ? rpc.options : null; +} + +/** + * 获取方法的原始实现(未被装饰器修改的版本) + */ +export function getOriginalMethod(method: Function): Function | null { + return (method as any).__originalMethod || null; +} \ No newline at end of file diff --git a/packages/network-client/src/decorators/SyncVar.ts b/packages/network-client/src/decorators/SyncVar.ts new file mode 100644 index 00000000..8ebd232f --- /dev/null +++ b/packages/network-client/src/decorators/SyncVar.ts @@ -0,0 +1,146 @@ +/** + * SyncVar装饰器 - 客户端版本 + * + * 用于标记需要同步的变量 + */ + +import 'reflect-metadata'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { ClientNetworkBehaviour } from '../core/ClientNetworkBehaviour'; + +/** + * SyncVar配置选项 + */ +export interface SyncVarOptions { + /** 是否可从客户端修改 */ + clientCanModify?: boolean; + /** 同步间隔(毫秒),0表示立即同步 */ + syncInterval?: number; + /** 是否仅同步给所有者 */ + ownerOnly?: boolean; + /** 自定义序列化器 */ + serializer?: (value: any) => NetworkValue; + /** 自定义反序列化器 */ + deserializer?: (value: NetworkValue) => any; +} + +/** + * SyncVar元数据键 + */ +export const SYNCVAR_METADATA_KEY = Symbol('syncvar'); + +/** + * SyncVar元数据 + */ +export interface SyncVarMetadata { + /** 属性名 */ + propertyKey: string; + /** 配置选项 */ + options: SyncVarOptions; +} + +/** + * SyncVar装饰器 + */ +export function SyncVar(options: SyncVarOptions = {}): PropertyDecorator { + return function (target: any, propertyKey: string | symbol) { + const key = propertyKey as string; + + // 获取已有的SyncVar元数据 + const existingMetadata: SyncVarMetadata[] = Reflect.getMetadata(SYNCVAR_METADATA_KEY, target.constructor) || []; + + // 添加新的SyncVar元数据 + existingMetadata.push({ + propertyKey: key, + options: { + clientCanModify: false, + syncInterval: 0, + ownerOnly: false, + ...options + } + }); + + // 设置元数据 + Reflect.defineMetadata(SYNCVAR_METADATA_KEY, existingMetadata, target.constructor); + + // 存储原始属性名(用于内部存储) + const privateKey = `_syncvar_${key}`; + + // 创建属性访问器 + Object.defineProperty(target, key, { + get: function (this: ClientNetworkBehaviour) { + // 从NetworkIdentity获取SyncVar值 + const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any); + if (networkIdentity) { + const syncVarValue = (networkIdentity as any).getSyncVar(key); + if (syncVarValue !== undefined) { + return options.deserializer ? options.deserializer(syncVarValue) : syncVarValue; + } + } + + // 如果网络值不存在,返回本地存储的值 + return (this as any)[privateKey]; + }, + + set: function (this: ClientNetworkBehaviour, value: any) { + const oldValue = (this as any)[privateKey]; + const newValue = options.serializer ? options.serializer(value) : value; + + // 存储到本地 + (this as any)[privateKey] = value; + + // 获取NetworkIdentity + const networkIdentity = this.entity?.getComponent('NetworkIdentity' as any); + if (!networkIdentity) { + return; + } + + // 检查是否可以修改 + if (!options.clientCanModify && !(networkIdentity as any).hasAuthority) { + console.warn(`Cannot modify SyncVar ${key} without authority`); + return; + } + + // 注册SyncVar(如果尚未注册) + (networkIdentity as any).registerSyncVar(key, newValue); + + // 更新NetworkIdentity中的值 + (networkIdentity as any).updateSyncVar(key, newValue); + + // 如果有权威且值发生变化,发送到服务器 + if ((networkIdentity as any).hasAuthority && oldValue !== value) { + this.updateSyncVar(key, newValue).catch(error => { + console.error(`Failed to sync variable ${key}:`, error); + }); + } + }, + + enumerable: true, + configurable: true + }); + }; +} + +/** + * 获取类的所有SyncVar元数据 + */ +export function getSyncVarMetadata(target: any): SyncVarMetadata[] { + return Reflect.getMetadata(SYNCVAR_METADATA_KEY, target) || []; +} + +/** + * 检查属性是否为SyncVar + */ +export function isSyncVar(target: any, propertyKey: string): boolean { + const metadata = getSyncVarMetadata(target); + return metadata.some(meta => meta.propertyKey === propertyKey); +} + +/** + * 获取特定属性的SyncVar选项 + */ +export function getSyncVarOptions(target: any, propertyKey: string): SyncVarOptions | null { + const metadata = getSyncVarMetadata(target); + const syncVar = metadata.find(meta => meta.propertyKey === propertyKey); + return syncVar ? syncVar.options : null; +} \ No newline at end of file diff --git a/packages/network-client/src/decorators/index.ts b/packages/network-client/src/decorators/index.ts new file mode 100644 index 00000000..af8250fe --- /dev/null +++ b/packages/network-client/src/decorators/index.ts @@ -0,0 +1,7 @@ +/** + * 装饰器导出 + */ + +export * from './SyncVar'; +export * from './ClientRpc'; +export * from './ServerRpc'; \ No newline at end of file diff --git a/packages/network-client/src/index.ts b/packages/network-client/src/index.ts new file mode 100644 index 00000000..3ea14025 --- /dev/null +++ b/packages/network-client/src/index.ts @@ -0,0 +1,23 @@ +/** + * ECS Framework 网络库 - 客户端 + * + * 提供网络客户端功能,包括连接管理、预测、插值等 + */ + +// 核心模块 +export * from './core'; + +// 传输层 +export * from './transport'; + +// 装饰器 +export * from './decorators'; + +// 系统 +export * from './systems'; + +// 接口 +export * from './interfaces'; + +// 版本信息 +export const VERSION = '1.0.11'; \ No newline at end of file diff --git a/packages/network-client/src/interfaces/NetworkInterfaces.ts b/packages/network-client/src/interfaces/NetworkInterfaces.ts new file mode 100644 index 00000000..8e75bdc7 --- /dev/null +++ b/packages/network-client/src/interfaces/NetworkInterfaces.ts @@ -0,0 +1,34 @@ +/** + * 网络系统相关接口 + */ + +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; + +/** + * 可预测组件接口 + * + * 实现此接口的组件可以参与客户端预测系统 + */ +export interface IPredictable { + /** + * 预测更新 + * + * @param inputs 输入数据 + * @param timestamp 时间戳 + */ + predictUpdate(inputs: NetworkValue, timestamp: number): void; +} + +/** + * 可插值组件接口 + * + * 实现此接口的组件可以参与插值系统 + */ +export interface IInterpolatable { + /** + * 应用插值状态 + * + * @param state 插值后的状态数据 + */ + applyInterpolatedState(state: NetworkValue): void; +} \ No newline at end of file diff --git a/packages/network-client/src/interfaces/index.ts b/packages/network-client/src/interfaces/index.ts new file mode 100644 index 00000000..04f3289b --- /dev/null +++ b/packages/network-client/src/interfaces/index.ts @@ -0,0 +1,5 @@ +/** + * 接口导出 + */ + +export * from './NetworkInterfaces'; \ No newline at end of file diff --git a/packages/network-client/src/systems/InterpolationSystem.ts b/packages/network-client/src/systems/InterpolationSystem.ts new file mode 100644 index 00000000..cf9cccf8 --- /dev/null +++ b/packages/network-client/src/systems/InterpolationSystem.ts @@ -0,0 +1,520 @@ +/** + * 客户端插值系统 + * + * 实现网络对象的平滑插值 + */ + +import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { NetworkIdentity } from '../core/NetworkIdentity'; +import { IInterpolatable } from '../interfaces/NetworkInterfaces'; + +/** + * 插值状态快照 + */ +export interface InterpolationSnapshot { + /** 时间戳 */ + timestamp: number; + /** 网络ID */ + networkId: string; + /** 状态数据 */ + state: NetworkValue; +} + +/** + * 插值目标 + */ +export interface InterpolationTarget { + /** 网络ID */ + networkId: string; + /** 起始状态 */ + fromState: NetworkValue; + /** 目标状态 */ + toState: NetworkValue; + /** 起始时间 */ + fromTime: number; + /** 结束时间 */ + toTime: number; + /** 当前插值进度 (0-1) */ + progress: number; +} + +/** + * 插值配置 + */ +export interface InterpolationConfig { + /** 插值延迟(毫秒) */ + delay: number; + /** 最大插值时间(毫秒) */ + maxTime: number; + /** 插值缓冲区大小 */ + bufferSize: number; + /** 外推是否启用 */ + enableExtrapolation: boolean; + /** 最大外推时间(毫秒) */ + maxExtrapolationTime: number; +} + +/** + * 插值算法类型 + */ +export enum InterpolationType { + /** 线性插值 */ + LINEAR = 'linear', + /** 平滑插值 */ + SMOOTHSTEP = 'smoothstep', + /** 三次贝塞尔插值 */ + CUBIC = 'cubic' +} + +/** + * 客户端插值系统 + */ +export class InterpolationSystem extends EntitySystem { + /** 插值状态缓冲区 */ + private stateBuffer: Map = new Map(); + /** 当前插值目标 */ + private interpolationTargets: Map = new Map(); + /** 插值配置 */ + private config: InterpolationConfig; + /** 当前时间 */ + private currentTime: number = 0; + + constructor(config?: Partial) { + // 使用Matcher查询具有NetworkIdentity的实体 + super(Matcher.all(NetworkIdentity)); + + this.config = { + delay: 100, + maxTime: 500, + bufferSize: 32, + enableExtrapolation: false, + maxExtrapolationTime: 50, + ...config + }; + + this.currentTime = Date.now(); + } + + /** + * 系统初始化 + */ + override initialize(): void { + super.initialize(); + this.currentTime = Date.now(); + } + + /** + * 系统更新 + */ + override update(): void { + this.currentTime = Date.now(); + this.cleanupOldStates(); + + // 调用父类update,会自动调用process方法处理匹配的实体 + super.update(); + } + + /** + * 处理匹配的实体 + */ + protected override process(entities: Entity[]): void { + const interpolationTime = this.currentTime - this.config.delay; + + for (const entity of entities) { + const networkIdentity = entity.getComponent(NetworkIdentity); + + if (networkIdentity && networkIdentity.isInterpolationEnabled) { + const networkId = networkIdentity.networkId; + const target = this.interpolationTargets.get(networkId); + + if (target) { + // 计算插值进度 + const duration = target.toTime - target.fromTime; + if (duration > 0) { + const elapsed = interpolationTime - target.fromTime; + target.progress = Math.max(0, Math.min(1, elapsed / duration)); + + // 执行插值 + const interpolatedState = this.interpolateStates( + target.fromState, + target.toState, + target.progress, + InterpolationType.LINEAR + ); + + // 应用插值状态 + this.applyInterpolatedState(entity, interpolatedState); + + // 检查是否需要外推 + if (target.progress >= 1 && this.config.enableExtrapolation) { + this.performExtrapolation(entity, target, interpolationTime); + } + } + } + } + } + } + + /** + * 添加网络状态快照 + */ + addStateSnapshot(networkId: string, state: NetworkValue, timestamp: number): void { + // 获取或创建缓冲区 + if (!this.stateBuffer.has(networkId)) { + this.stateBuffer.set(networkId, []); + } + + const buffer = this.stateBuffer.get(networkId)!; + + const snapshot: InterpolationSnapshot = { + timestamp, + networkId, + state + }; + + // 插入到正确的位置(按时间戳排序) + const insertIndex = this.findInsertIndex(buffer, timestamp); + buffer.splice(insertIndex, 0, snapshot); + + // 保持缓冲区大小 + if (buffer.length > this.config.bufferSize) { + buffer.shift(); + } + + // 更新插值目标 + this.updateInterpolationTarget(networkId); + } + + + /** + * 更新插值目标 + */ + private updateInterpolationTarget(networkId: string): void { + const buffer = this.stateBuffer.get(networkId); + if (!buffer || buffer.length < 2) { + return; + } + + const interpolationTime = this.currentTime - this.config.delay; + + // 查找插值区间 + const { from, to } = this.findInterpolationRange(buffer, interpolationTime); + + if (!from || !to) { + return; + } + + // 更新或创建插值目标 + this.interpolationTargets.set(networkId, { + networkId, + fromState: from.state, + toState: to.state, + fromTime: from.timestamp, + toTime: to.timestamp, + progress: 0 + }); + } + + /** + * 查找插值区间 + */ + private findInterpolationRange(buffer: InterpolationSnapshot[], time: number): { + from: InterpolationSnapshot | null; + to: InterpolationSnapshot | null; + } { + let from: InterpolationSnapshot | null = null; + let to: InterpolationSnapshot | null = null; + + for (let i = 0; i < buffer.length - 1; i++) { + const current = buffer[i]; + const next = buffer[i + 1]; + + if (time >= current.timestamp && time <= next.timestamp) { + from = current; + to = next; + break; + } + } + + // 如果没有找到区间,使用最近的两个状态 + if (!from && !to && buffer.length >= 2) { + if (time < buffer[0].timestamp) { + // 时间过早,使用前两个状态 + from = buffer[0]; + to = buffer[1]; + } else if (time > buffer[buffer.length - 1].timestamp) { + // 时间过晚,使用后两个状态 + from = buffer[buffer.length - 2]; + to = buffer[buffer.length - 1]; + } + } + + return { from, to }; + } + + /** + * 状态插值 + */ + private interpolateStates( + fromState: NetworkValue, + toState: NetworkValue, + progress: number, + type: InterpolationType + ): NetworkValue { + // 调整插值进度曲线 + const adjustedProgress = this.adjustProgress(progress, type); + + try { + return this.interpolateValue(fromState, toState, adjustedProgress); + } catch (error) { + console.error('Error interpolating states:', error); + return toState; // 出错时返回目标状态 + } + } + + /** + * 递归插值值 + */ + private interpolateValue(from: NetworkValue, to: NetworkValue, progress: number): NetworkValue { + // 如果类型不同,直接返回目标值 + if (typeof from !== typeof to) { + return to; + } + + // 数字插值 + if (typeof from === 'number' && typeof to === 'number') { + return from + (to - from) * progress; + } + + // 字符串插值(直接切换) + if (typeof from === 'string' && typeof to === 'string') { + return progress < 0.5 ? from : to; + } + + // 布尔插值(直接切换) + if (typeof from === 'boolean' && typeof to === 'boolean') { + return progress < 0.5 ? from : to; + } + + // 数组插值 + if (Array.isArray(from) && Array.isArray(to)) { + const result: NetworkValue[] = []; + const maxLength = Math.max(from.length, to.length); + + for (let i = 0; i < maxLength; i++) { + const fromValue = i < from.length ? from[i] : to[i]; + const toValue = i < to.length ? to[i] : from[i]; + result[i] = this.interpolateValue(fromValue, toValue, progress); + } + + return result; + } + + // 对象插值 + if (from && to && typeof from === 'object' && typeof to === 'object') { + const result: any = {}; + const allKeys = new Set([...Object.keys(from), ...Object.keys(to)]); + + for (const key of allKeys) { + const fromValue = (from as any)[key]; + const toValue = (to as any)[key]; + + if (fromValue !== undefined && toValue !== undefined) { + result[key] = this.interpolateValue(fromValue, toValue, progress); + } else { + result[key] = toValue !== undefined ? toValue : fromValue; + } + } + + return result; + } + + // 其他类型直接返回目标值 + return to; + } + + /** + * 调整插值进度曲线 + */ + private adjustProgress(progress: number, type: InterpolationType): number { + switch (type) { + case InterpolationType.LINEAR: + return progress; + + case InterpolationType.SMOOTHSTEP: + return progress * progress * (3 - 2 * progress); + + case InterpolationType.CUBIC: + return progress < 0.5 + ? 4 * progress * progress * progress + : 1 - Math.pow(-2 * progress + 2, 3) / 2; + + default: + return progress; + } + } + + /** + * 应用插值状态到实体 + */ + private applyInterpolatedState(entity: Entity, state: NetworkValue): void { + // 获取所有可插值的组件 + const components: any[] = []; + for (const component of components) { + if (this.isInterpolatable(component)) { + try { + (component as IInterpolatable).applyInterpolatedState(state); + } catch (error) { + console.error('Error applying interpolated state:', error); + } + } + } + + // 更新NetworkIdentity中的状态 + const networkIdentity = entity.getComponent(NetworkIdentity); + if (networkIdentity && typeof networkIdentity.deserializeState === 'function') { + try { + networkIdentity.deserializeState(state); + } catch (error) { + console.error('Error deserializing interpolated state:', error); + } + } + } + + /** + * 检查组件是否实现了IInterpolatable接口 + */ + private isInterpolatable(component: any): component is IInterpolatable { + return component && typeof component.applyInterpolatedState === 'function'; + } + + /** + * 执行外推 + */ + private performExtrapolation(entity: Entity, target: InterpolationTarget, currentTime: number): void { + if (!this.config.enableExtrapolation) { + return; + } + + const extrapolationTime = currentTime - target.toTime; + if (extrapolationTime > this.config.maxExtrapolationTime) { + return; + } + + // 计算外推状态 + const extrapolationProgress = extrapolationTime / (target.toTime - target.fromTime); + const extrapolatedState = this.extrapolateState( + target.fromState, + target.toState, + 1 + extrapolationProgress + ); + + // 应用外推状态 + this.applyInterpolatedState(entity, extrapolatedState); + } + + /** + * 状态外推 + */ + private extrapolateState(fromState: NetworkValue, toState: NetworkValue, progress: number): NetworkValue { + // 简单的线性外推 + return this.interpolateValue(fromState, toState, progress); + } + + /** + * 查找插入位置 + */ + private findInsertIndex(buffer: InterpolationSnapshot[], timestamp: number): number { + let left = 0; + let right = buffer.length; + + while (left < right) { + const mid = Math.floor((left + right) / 2); + if (buffer[mid].timestamp < timestamp) { + left = mid + 1; + } else { + right = mid; + } + } + + return left; + } + + /** + * 清理过期状态 + */ + private cleanupOldStates(): void { + const cutoffTime = this.currentTime - this.config.maxTime; + + this.stateBuffer.forEach((buffer, networkId) => { + // 移除过期的状态 + const validStates = buffer.filter(snapshot => snapshot.timestamp > cutoffTime); + + if (validStates.length !== buffer.length) { + this.stateBuffer.set(networkId, validStates); + } + + // 如果缓冲区为空,移除它 + if (validStates.length === 0) { + this.stateBuffer.delete(networkId); + this.interpolationTargets.delete(networkId); + } + }); + } + + /** + * 根据网络ID查找实体 + */ + private findEntityByNetworkId(networkId: string): Entity | null { + // 使用系统的entities属性来查找 + for (const entity of this.entities) { + const networkIdentity = entity.getComponent(NetworkIdentity); + if (networkIdentity && networkIdentity.networkId === networkId) { + return entity; + } + } + + return null; + } + + /** + * 设置插值配置 + */ + setInterpolationConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + } + + /** + * 获取插值统计信息 + */ + getInterpolationStats(): { [networkId: string]: { bufferSize: number; progress: number } } { + const stats: { [networkId: string]: { bufferSize: number; progress: number } } = {}; + + this.stateBuffer.forEach((buffer, networkId) => { + const target = this.interpolationTargets.get(networkId); + stats[networkId] = { + bufferSize: buffer.length, + progress: target ? target.progress : 0 + }; + }); + + return stats; + } + + /** + * 清空所有插值数据 + */ + clearInterpolationData(): void { + this.stateBuffer.clear(); + this.interpolationTargets.clear(); + } + + /** + * 系统销毁 + */ + onDestroy(): void { + this.clearInterpolationData(); + } +} + diff --git a/packages/network-client/src/systems/PredictionSystem.ts b/packages/network-client/src/systems/PredictionSystem.ts new file mode 100644 index 00000000..c7be34be --- /dev/null +++ b/packages/network-client/src/systems/PredictionSystem.ts @@ -0,0 +1,362 @@ +/** + * 客户端预测系统 + * + * 实现客户端预测和服务器和解 + */ + +import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { NetworkIdentity } from '../core/NetworkIdentity'; +import { IPredictable } from '../interfaces/NetworkInterfaces'; + +/** + * 预测状态快照 + */ +export interface PredictionSnapshot { + /** 时间戳 */ + timestamp: number; + /** 网络ID */ + networkId: string; + /** 状态数据 */ + state: NetworkValue; + /** 输入数据 */ + inputs?: NetworkValue; +} + +/** + * 预测输入 + */ +export interface PredictionInput { + /** 时间戳 */ + timestamp: number; + /** 输入数据 */ + data: NetworkValue; +} + +/** + * 客户端预测系统 + */ +export class PredictionSystem extends EntitySystem { + /** 预测状态缓冲区 */ + private predictionBuffer: Map = new Map(); + /** 输入缓冲区 */ + private inputBuffer: PredictionInput[] = []; + /** 最大缓冲区大小 */ + private maxBufferSize: number = 64; + /** 预测时间窗口(毫秒) */ + private predictionWindow: number = 500; + /** 当前预测时间戳 */ + private currentPredictionTime: number = 0; + + constructor(maxBufferSize = 64, predictionWindow = 500) { + // 使用Matcher查询具有NetworkIdentity的实体 + super(Matcher.all(NetworkIdentity)); + + this.maxBufferSize = maxBufferSize; + this.predictionWindow = predictionWindow; + this.currentPredictionTime = Date.now(); + } + + /** + * 系统初始化 + */ + override initialize(): void { + super.initialize(); + this.currentPredictionTime = Date.now(); + } + + /** + * 系统更新 + */ + override update(): void { + this.currentPredictionTime = Date.now(); + this.cleanupOldSnapshots(); + + // 调用父类update,会自动调用process方法处理匹配的实体 + super.update(); + } + + /** + * 处理匹配的实体 + */ + protected override process(entities: Entity[]): void { + for (const entity of entities) { + const networkIdentity = entity.getComponent(NetworkIdentity); + + if (networkIdentity && + networkIdentity.isPredictionEnabled && + networkIdentity.isLocalPlayer) { + + // 保存当前状态快照 + this.saveSnapshot(entity); + + // 应用当前输入进行预测 + const currentInputs = this.getCurrentInputs(); + if (currentInputs) { + this.applyInputs(entity, currentInputs, this.currentPredictionTime); + } + } + } + } + + /** + * 添加预测输入 + */ + addInput(input: PredictionInput): void { + this.inputBuffer.push(input); + + // 保持输入缓冲区大小 + if (this.inputBuffer.length > this.maxBufferSize) { + this.inputBuffer.shift(); + } + + // 按时间戳排序 + this.inputBuffer.sort((a, b) => a.timestamp - b.timestamp); + } + + /** + * 保存预测状态快照 + */ + saveSnapshot(entity: Entity): void { + const networkIdentity = entity.getComponent(NetworkIdentity); + if (!networkIdentity || !networkIdentity.isPredictionEnabled) { + return; + } + + const networkId = networkIdentity.networkId; + const snapshot: PredictionSnapshot = { + timestamp: this.currentPredictionTime, + networkId, + state: networkIdentity.serializeState(), + inputs: this.getCurrentInputs() || undefined + }; + + // 获取或创建缓冲区 + if (!this.predictionBuffer.has(networkId)) { + this.predictionBuffer.set(networkId, []); + } + + const buffer = this.predictionBuffer.get(networkId)!; + buffer.push(snapshot); + + // 保持缓冲区大小 + if (buffer.length > this.maxBufferSize) { + buffer.shift(); + } + } + + /** + * 从服务器接收权威状态进行和解 + */ + reconcileWithServer(networkId: string, serverState: NetworkValue, serverTimestamp: number): void { + const buffer = this.predictionBuffer.get(networkId); + if (!buffer || buffer.length === 0) { + return; + } + + // 查找对应时间戳的预测状态 + const predictionSnapshot = this.findSnapshot(buffer, serverTimestamp); + if (!predictionSnapshot) { + return; + } + + // 比较预测状态和服务器状态 + if (this.statesMatch(predictionSnapshot.state, serverState)) { + // 预测正确,移除已确认的快照 + this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp); + return; + } + + // 预测错误,需要进行和解 + this.performReconciliation(networkId, serverState, serverTimestamp); + } + + /** + * 执行预测和解 + */ + private performReconciliation(networkId: string, serverState: NetworkValue, serverTimestamp: number): void { + const entity = this.findEntityByNetworkId(networkId); + if (!entity) { + return; + } + + const networkIdentity = entity.getComponent(NetworkIdentity); + if (!networkIdentity) { + return; + } + + // 回滚到服务器状态 + if (typeof networkIdentity.deserializeState === 'function') { + networkIdentity.deserializeState(serverState); + } + + // 重新应用服务器时间戳之后的输入 + const buffer = this.predictionBuffer.get(networkId)!; + const snapshotsToReplay = buffer.filter(snapshot => snapshot.timestamp > serverTimestamp); + + for (const snapshot of snapshotsToReplay) { + if (snapshot.inputs) { + this.applyInputs(entity, snapshot.inputs, snapshot.timestamp); + } + } + + // 清理已和解的快照 + this.removeSnapshotsBeforeTimestamp(buffer, serverTimestamp); + } + + + /** + * 应用输入进行预测计算 + */ + private applyInputs(entity: Entity, inputs: NetworkValue, timestamp: number): void { + const networkIdentity = entity.getComponent(NetworkIdentity); + if (!networkIdentity) return; + + // 获取实体的所有组件并检查是否实现了IPredictable接口 + const components: any[] = []; + for (const component of components) { + if (this.isPredictable(component)) { + try { + (component as IPredictable).predictUpdate(inputs, timestamp); + } catch (error) { + console.error('Error applying prediction:', error); + } + } + } + } + + /** + * 检查组件是否实现了IPredictable接口 + */ + private isPredictable(component: any): component is IPredictable { + return component && typeof component.predictUpdate === 'function'; + } + + /** + * 获取当前输入 + */ + private getCurrentInputs(): NetworkValue | null { + if (this.inputBuffer.length === 0) { + return null; + } + + // 获取最新的输入 + return this.inputBuffer[this.inputBuffer.length - 1].data; + } + + /** + * 查找指定时间戳的快照 + */ + private findSnapshot(buffer: PredictionSnapshot[], timestamp: number): PredictionSnapshot | null { + // 查找最接近的快照 + let closest: PredictionSnapshot | null = null; + let minDiff = Number.MAX_SAFE_INTEGER; + + for (const snapshot of buffer) { + const diff = Math.abs(snapshot.timestamp - timestamp); + if (diff < minDiff) { + minDiff = diff; + closest = snapshot; + } + } + + return closest; + } + + /** + * 比较两个状态是否匹配 + */ + private statesMatch(predictedState: NetworkValue, serverState: NetworkValue): boolean { + try { + // 简单的JSON比较,实际应用中可能需要更精确的比较 + return JSON.stringify(predictedState) === JSON.stringify(serverState); + } catch (error) { + return false; + } + } + + /** + * 移除指定时间戳之前的快照 + */ + private removeSnapshotsBeforeTimestamp(buffer: PredictionSnapshot[], timestamp: number): void { + for (let i = buffer.length - 1; i >= 0; i--) { + if (buffer[i].timestamp < timestamp) { + buffer.splice(0, i + 1); + break; + } + } + } + + /** + * 清理过期的快照 + */ + private cleanupOldSnapshots(): void { + const cutoffTime = this.currentPredictionTime - this.predictionWindow; + + this.predictionBuffer.forEach((buffer, networkId) => { + this.removeSnapshotsBeforeTimestamp(buffer, cutoffTime); + + // 如果缓冲区为空,移除它 + if (buffer.length === 0) { + this.predictionBuffer.delete(networkId); + } + }); + + // 清理过期的输入 + this.inputBuffer = this.inputBuffer.filter(input => + input.timestamp > cutoffTime + ); + } + + /** + * 根据网络ID查找实体 + */ + private findEntityByNetworkId(networkId: string): Entity | null { + // 使用系统的entities属性来查找 + for (const entity of this.entities) { + const networkIdentity = entity.getComponent(NetworkIdentity); + if (networkIdentity && networkIdentity.networkId === networkId) { + return entity; + } + } + + return null; + } + + /** + * 设置预测配置 + */ + setPredictionConfig(maxBufferSize: number, predictionWindow: number): void { + this.maxBufferSize = maxBufferSize; + this.predictionWindow = predictionWindow; + } + + /** + * 获取预测统计信息 + */ + getPredictionStats(): { [networkId: string]: number } { + const stats: { [networkId: string]: number } = {}; + + this.predictionBuffer.forEach((buffer, networkId) => { + stats[networkId] = buffer.length; + }); + + return stats; + } + + /** + * 清空所有预测数据 + */ + clearPredictionData(): void { + this.predictionBuffer.clear(); + this.inputBuffer = []; + } + + /** + * 系统销毁 + */ + onDestroy(): void { + this.clearPredictionData(); + } +} + diff --git a/packages/network-client/src/systems/index.ts b/packages/network-client/src/systems/index.ts new file mode 100644 index 00000000..a3b19975 --- /dev/null +++ b/packages/network-client/src/systems/index.ts @@ -0,0 +1,6 @@ +/** + * 系统导出 + */ + +export * from './PredictionSystem'; +export * from './InterpolationSystem'; \ No newline at end of file diff --git a/packages/network-client/src/transport/ClientTransport.ts b/packages/network-client/src/transport/ClientTransport.ts new file mode 100644 index 00000000..360873cc --- /dev/null +++ b/packages/network-client/src/transport/ClientTransport.ts @@ -0,0 +1,445 @@ +/** + * 客户端传输层抽象接口 + */ + +import { Emitter, ITimer, Core } from '@esengine/ecs-framework'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; + +/** + * 客户端传输配置 + */ +export interface ClientTransportConfig { + /** 服务器地址 */ + host: string; + /** 服务器端口 */ + port: number; + /** 是否使用安全连接 */ + secure?: boolean; + /** 连接超时时间(毫秒) */ + connectionTimeout?: number; + /** 重连间隔(毫秒) */ + reconnectInterval?: number; + /** 最大重连次数 */ + maxReconnectAttempts?: number; + /** 心跳间隔(毫秒) */ + heartbeatInterval?: number; + /** 消息队列最大大小 */ + maxQueueSize?: number; +} + +/** + * 连接状态 + */ +export enum ConnectionState { + /** 断开连接 */ + DISCONNECTED = 'disconnected', + /** 连接中 */ + CONNECTING = 'connecting', + /** 已连接 */ + CONNECTED = 'connected', + /** 认证中 */ + AUTHENTICATING = 'authenticating', + /** 已认证 */ + AUTHENTICATED = 'authenticated', + /** 重连中 */ + RECONNECTING = 'reconnecting', + /** 连接错误 */ + ERROR = 'error' +} + +/** + * 客户端消息 + */ +export interface ClientMessage { + /** 消息类型 */ + type: 'rpc' | 'syncvar' | 'system' | 'custom'; + /** 消息数据 */ + data: NetworkValue; + /** 消息ID(用于响应匹配) */ + messageId?: string; + /** 是否可靠传输 */ + reliable?: boolean; + /** 时间戳 */ + timestamp?: number; +} + +/** + * 连接统计信息 + */ +export interface ConnectionStats { + /** 连接时间 */ + connectedAt: Date | null; + /** 连接持续时间(毫秒) */ + connectionDuration: number; + /** 发送消息数 */ + messagesSent: number; + /** 接收消息数 */ + messagesReceived: number; + /** 发送字节数 */ + bytesSent: number; + /** 接收字节数 */ + bytesReceived: number; + /** 重连次数 */ + reconnectCount: number; + /** 丢失消息数 */ + messagesLost: number; + /** 平均延迟(毫秒) */ + averageLatency: number; +} + +/** + * 客户端传输事件 + */ +export interface ClientTransportEvents { + /** 连接建立 */ + 'connected': () => void; + /** 连接断开 */ + 'disconnected': (reason: string) => void; + /** 连接状态变化 */ + 'state-changed': (oldState: ConnectionState, newState: ConnectionState) => void; + /** 收到消息 */ + 'message': (message: ClientMessage) => void; + /** 连接错误 */ + 'error': (error: Error) => void; + /** 重连开始 */ + 'reconnecting': (attempt: number, maxAttempts: number) => void; + /** 重连成功 */ + 'reconnected': () => void; + /** 重连失败 */ + 'reconnect-failed': () => void; + /** 延迟更新 */ + 'latency-updated': (latency: number) => void; +} + +/** + * 客户端传输层抽象类 + */ +export abstract class ClientTransport { + protected config: ClientTransportConfig; + protected state: ConnectionState = ConnectionState.DISCONNECTED; + protected stats: ConnectionStats; + protected messageQueue: ClientMessage[] = []; + protected reconnectAttempts = 0; + protected reconnectTimer: ITimer | null = null; + protected heartbeatTimer: ITimer | null = null; + private latencyMeasurements: number[] = []; + private eventEmitter: Emitter; + + constructor(config: ClientTransportConfig) { + this.eventEmitter = new Emitter(); + + this.config = { + secure: false, + connectionTimeout: 10000, // 10秒 + reconnectInterval: 3000, // 3秒 + maxReconnectAttempts: 10, + heartbeatInterval: 30000, // 30秒 + maxQueueSize: 1000, + ...config + }; + + this.stats = { + connectedAt: null, + connectionDuration: 0, + messagesSent: 0, + messagesReceived: 0, + bytesSent: 0, + bytesReceived: 0, + reconnectCount: 0, + messagesLost: 0, + averageLatency: 0 + }; + } + + /** + * 连接到服务器 + */ + abstract connect(): Promise; + + /** + * 断开连接 + */ + abstract disconnect(): Promise; + + /** + * 发送消息 + */ + abstract sendMessage(message: ClientMessage): Promise; + + /** + * 获取当前连接状态 + */ + getState(): ConnectionState { + return this.state; + } + + /** + * 检查是否已连接 + */ + isConnected(): boolean { + return this.state === ConnectionState.CONNECTED || + this.state === ConnectionState.AUTHENTICATED; + } + + /** + * 获取连接统计信息 + */ + getStats(): ConnectionStats { + if (this.stats.connectedAt) { + this.stats.connectionDuration = Date.now() - this.stats.connectedAt.getTime(); + } + return { ...this.stats }; + } + + /** + * 获取配置 + */ + getConfig(): Readonly { + return this.config; + } + + /** + * 设置状态 + */ + protected setState(newState: ConnectionState): void { + if (this.state !== newState) { + const oldState = this.state; + this.state = newState; + this.eventEmitter.emit('state-changed', oldState, newState); + + // 特殊状态处理 + if (newState === ConnectionState.CONNECTED) { + this.stats.connectedAt = new Date(); + this.reconnectAttempts = 0; + this.startHeartbeat(); + this.processMessageQueue(); + this.eventEmitter.emit('connected'); + + if (oldState === ConnectionState.RECONNECTING) { + this.eventEmitter.emit('reconnected'); + } + } else if (newState === ConnectionState.DISCONNECTED) { + this.stats.connectedAt = null; + this.stopHeartbeat(); + } + } + } + + /** + * 处理接收到的消息 + */ + protected handleMessage(message: ClientMessage): void { + this.stats.messagesReceived++; + + if (message.data) { + try { + const messageSize = JSON.stringify(message.data).length; + this.stats.bytesReceived += messageSize; + } catch (error) { + // 忽略序列化错误 + } + } + + // 处理系统消息 + if (message.type === 'system') { + this.handleSystemMessage(message); + return; + } + + this.eventEmitter.emit('message', message); + } + + /** + * 处理系统消息 + */ + protected handleSystemMessage(message: ClientMessage): void { + const data = message.data as any; + + switch (data.action) { + case 'ping': + // 响应ping + this.sendMessage({ + type: 'system', + data: { action: 'pong', timestamp: data.timestamp } + }); + break; + + case 'pong': + // 计算延迟 + if (data.timestamp) { + const latency = Date.now() - data.timestamp; + this.updateLatency(latency); + } + break; + } + } + + /** + * 处理连接错误 + */ + protected handleError(error: Error): void { + console.error('Transport error:', error.message); + this.eventEmitter.emit('error', error); + + if (this.isConnected()) { + this.setState(ConnectionState.ERROR); + this.startReconnect(); + } + } + + /** + * 开始重连 + */ + protected startReconnect(): void { + if (this.reconnectAttempts >= this.config.maxReconnectAttempts!) { + this.eventEmitter.emit('reconnect-failed'); + return; + } + + this.setState(ConnectionState.RECONNECTING); + this.reconnectAttempts++; + this.stats.reconnectCount++; + + this.eventEmitter.emit('reconnecting', this.reconnectAttempts, this.config.maxReconnectAttempts!); + + this.reconnectTimer = Core.schedule(this.config.reconnectInterval! / 1000, false, this, async () => { + try { + await this.connect(); + } catch (error) { + this.startReconnect(); // 继续重连 + } + }); + } + + /** + * 停止重连 + */ + protected stopReconnect(): void { + if (this.reconnectTimer) { + this.reconnectTimer.stop(); + this.reconnectTimer = null; + } + } + + /** + * 将消息加入队列 + */ + protected queueMessage(message: ClientMessage): boolean { + if (this.messageQueue.length >= this.config.maxQueueSize!) { + this.stats.messagesLost++; + return false; + } + + this.messageQueue.push(message); + return true; + } + + /** + * 处理消息队列 + */ + protected async processMessageQueue(): Promise { + while (this.messageQueue.length > 0 && this.isConnected()) { + const message = this.messageQueue.shift()!; + await this.sendMessage(message); + } + } + + /** + * 开始心跳 + */ + protected startHeartbeat(): void { + if (this.config.heartbeatInterval && this.config.heartbeatInterval > 0) { + this.heartbeatTimer = Core.schedule(this.config.heartbeatInterval / 1000, true, this, () => { + this.sendHeartbeat(); + }); + } + } + + /** + * 停止心跳 + */ + protected stopHeartbeat(): void { + if (this.heartbeatTimer) { + this.heartbeatTimer.stop(); + this.heartbeatTimer = null; + } + } + + /** + * 发送心跳 + */ + protected sendHeartbeat(): void { + this.sendMessage({ + type: 'system', + data: { action: 'ping', timestamp: Date.now() } + }).catch(() => { + // 心跳发送失败,可能连接有问题 + }); + } + + /** + * 更新延迟统计 + */ + protected updateLatency(latency: number): void { + this.latencyMeasurements.push(latency); + + // 只保留最近的10个测量值 + if (this.latencyMeasurements.length > 10) { + this.latencyMeasurements.shift(); + } + + // 计算平均延迟 + const sum = this.latencyMeasurements.reduce((a, b) => a + b, 0); + this.stats.averageLatency = sum / this.latencyMeasurements.length; + + this.eventEmitter.emit('latency-updated', latency); + } + + /** + * 更新发送统计 + */ + protected updateSendStats(message: ClientMessage): void { + this.stats.messagesSent++; + + if (message.data) { + try { + const messageSize = JSON.stringify(message.data).length; + this.stats.bytesSent += messageSize; + } catch (error) { + // 忽略序列化错误 + } + } + } + + /** + * 销毁传输层 + */ + destroy(): void { + this.stopReconnect(); + this.stopHeartbeat(); + this.messageQueue = []; + } + + /** + * 类型安全的事件监听 + */ + on(event: K, listener: ClientTransportEvents[K]): this { + this.eventEmitter.addObserver(event, listener, this); + return this; + } + + /** + * 移除事件监听 + */ + off(event: K, listener: ClientTransportEvents[K]): this { + this.eventEmitter.removeObserver(event, listener); + return this; + } + + /** + * 类型安全的事件触发 + */ + emit(event: K, ...args: Parameters): void { + this.eventEmitter.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-client/src/transport/HttpClientTransport.ts b/packages/network-client/src/transport/HttpClientTransport.ts new file mode 100644 index 00000000..08c42247 --- /dev/null +++ b/packages/network-client/src/transport/HttpClientTransport.ts @@ -0,0 +1,427 @@ +/** + * HTTP 客户端传输实现 + * + * 支持 REST API 和长轮询 + */ + +import { Core, ITimer } from '@esengine/ecs-framework'; +import { + ClientTransport, + ClientTransportConfig, + ConnectionState, + ClientMessage +} from './ClientTransport'; + +/** + * HTTP 客户端配置 + */ +export interface HttpClientConfig extends ClientTransportConfig { + /** API 路径前缀 */ + apiPrefix?: string; + /** 请求超时时间(毫秒) */ + requestTimeout?: number; + /** 长轮询超时时间(毫秒) */ + longPollTimeout?: number; + /** 是否启用长轮询 */ + enableLongPolling?: boolean; + /** 额外的请求头 */ + headers?: Record; + /** 认证令牌 */ + authToken?: string; +} + +/** + * HTTP 响应接口 + */ +interface HttpResponse { + success: boolean; + data?: any; + error?: string; + messages?: ClientMessage[]; +} + +/** + * HTTP 客户端传输 + */ +export class HttpClientTransport extends ClientTransport { + private connectionId: string | null = null; + private longPollController: AbortController | null = null; + private longPollRunning = false; + private connectPromise: Promise | null = null; + private requestTimers: Set> = new Set(); + + protected override config: HttpClientConfig; + + constructor(config: HttpClientConfig) { + super(config); + + this.config = { + apiPrefix: '/api', + requestTimeout: 30000, // 30秒 + longPollTimeout: 25000, // 25秒 + enableLongPolling: true, + headers: { + 'Content-Type': 'application/json' + }, + ...config + }; + } + + /** + * 连接到服务器 + */ + async connect(): Promise { + if (this.state === ConnectionState.CONNECTING || + this.state === ConnectionState.CONNECTED) { + return this.connectPromise || Promise.resolve(); + } + + this.setState(ConnectionState.CONNECTING); + this.stopReconnect(); + + this.connectPromise = this.performConnect(); + return this.connectPromise; + } + + /** + * 执行连接 + */ + private async performConnect(): Promise { + try { + // 发送连接请求 + const response = await this.makeRequest('/connect', 'POST', {}); + + if (response.success && response.data.connectionId) { + this.connectionId = response.data.connectionId; + this.setState(ConnectionState.CONNECTED); + + // 启动长轮询 + if (this.config.enableLongPolling) { + this.startLongPolling(); + } + } else { + throw new Error(response.error || 'Connection failed'); + } + } catch (error) { + this.setState(ConnectionState.ERROR); + throw error; + } + } + + /** + * 断开连接 + */ + async disconnect(): Promise { + this.stopReconnect(); + this.stopLongPolling(); + + if (this.connectionId) { + try { + await this.makeRequest('/disconnect', 'POST', { + connectionId: this.connectionId + }); + } catch (error) { + // 忽略断开连接时的错误 + } + + this.connectionId = null; + } + + this.setState(ConnectionState.DISCONNECTED); + this.connectPromise = null; + } + + /** + * 发送消息 + */ + async sendMessage(message: ClientMessage): Promise { + if (!this.connectionId) { + // 如果未连接,将消息加入队列 + if (this.state === ConnectionState.CONNECTING || + this.state === ConnectionState.RECONNECTING) { + return this.queueMessage(message); + } + return false; + } + + try { + const response = await this.makeRequest('/send', 'POST', { + connectionId: this.connectionId, + message: { + ...message, + timestamp: message.timestamp || Date.now() + } + }); + + if (response.success) { + this.updateSendStats(message); + return true; + } else { + console.error('Send message failed:', response.error); + return false; + } + + } catch (error) { + this.handleError(error as Error); + return false; + } + } + + /** + * 启动长轮询 + */ + private startLongPolling(): void { + if (this.longPollRunning || !this.connectionId) { + return; + } + + this.longPollRunning = true; + this.performLongPoll(); + } + + /** + * 停止长轮询 + */ + private stopLongPolling(): void { + this.longPollRunning = false; + + if (this.longPollController) { + this.longPollController.abort(); + this.longPollController = null; + } + } + + /** + * 执行长轮询 + */ + private async performLongPoll(): Promise { + while (this.longPollRunning && this.connectionId) { + try { + this.longPollController = new AbortController(); + + const response = await this.makeRequest('/poll', 'GET', { + connectionId: this.connectionId + }, { + signal: this.longPollController.signal, + timeout: this.config.longPollTimeout + }); + + if (response.success && response.messages && response.messages.length > 0) { + // 处理接收到的消息 + for (const message of response.messages) { + this.handleMessage(message); + } + } + + // 如果服务器指示断开连接 + if (response.data && response.data.disconnected) { + this.handleServerDisconnect(); + break; + } + + } catch (error) { + if ((error as any).name === 'AbortError') { + // 被主动取消,正常情况 + break; + } + + console.warn('Long polling error:', (error as Error).message); + + // 如果是网络错误,尝试重连 + if (this.isNetworkError(error as Error)) { + this.handleError(error as Error); + break; + } + + // 短暂等待后重试 + await this.delay(1000); + } + + this.longPollController = null; + } + } + + /** + * 处理服务器主动断开连接 + */ + private handleServerDisconnect(): void { + this.connectionId = null; + this.stopLongPolling(); + this.emit('disconnected', 'Server disconnect'); + + if (this.reconnectAttempts < this.config.maxReconnectAttempts!) { + this.startReconnect(); + } else { + this.setState(ConnectionState.DISCONNECTED); + } + } + + /** + * 发送 HTTP 请求 + */ + private async makeRequest( + path: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', + data?: any, + options: { + signal?: AbortSignal; + timeout?: number; + } = {} + ): Promise { + const url = this.buildUrl(path); + const headers = this.buildHeaders(); + + const requestOptions: RequestInit = { + method, + headers, + signal: options.signal + }; + + // 添加请求体 + if (method !== 'GET' && data) { + requestOptions.body = JSON.stringify(data); + } else if (method === 'GET' && data) { + // GET 请求将数据作为查询参数 + const params = new URLSearchParams(); + Object.entries(data).forEach(([key, value]) => { + params.append(key, String(value)); + }); + const separator = url.includes('?') ? '&' : '?'; + return this.fetchWithTimeout(`${url}${separator}${params}`, requestOptions, options.timeout); + } + + return this.fetchWithTimeout(url, requestOptions, options.timeout); + } + + /** + * 带超时的 fetch 请求 + */ + private async fetchWithTimeout( + url: string, + options: RequestInit, + timeout?: number + ): Promise { + const actualTimeout = timeout || this.config.requestTimeout!; + + const controller = new AbortController(); + let timeoutTimer: ITimer | null = null; + + // 创建超时定时器 + timeoutTimer = Core.schedule(actualTimeout / 1000, false, this, () => { + controller.abort(); + if (timeoutTimer) { + this.requestTimers.delete(timeoutTimer); + } + }); + + this.requestTimers.add(timeoutTimer); + + try { + const response = await fetch(url, { + ...options, + signal: options.signal || controller.signal + }); + + // 清理定时器 + if (timeoutTimer) { + timeoutTimer.stop(); + this.requestTimers.delete(timeoutTimer); + } + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + return result as HttpResponse; + + } catch (error) { + // 清理定时器 + if (timeoutTimer) { + timeoutTimer.stop(); + this.requestTimers.delete(timeoutTimer); + } + throw error; + } + } + + /** + * 构建请求URL + */ + private buildUrl(path: string): string { + const protocol = this.config.secure ? 'https' : 'http'; + const basePath = this.config.apiPrefix || ''; + const cleanPath = path.startsWith('/') ? path : `/${path}`; + + return `${protocol}://${this.config.host}:${this.config.port}${basePath}${cleanPath}`; + } + + /** + * 构建请求头 + */ + private buildHeaders(): Record { + const headers = { ...this.config.headers }; + + if (this.config.authToken) { + headers['Authorization'] = `Bearer ${this.config.authToken}`; + } + + return headers; + } + + /** + * 检查是否为网络错误 + */ + private isNetworkError(error: Error): boolean { + return error.message.includes('fetch') || + error.message.includes('network') || + error.message.includes('timeout') || + error.name === 'TypeError'; + } + + /** + * 延迟函数 + */ + private delay(ms: number): Promise { + return new Promise(resolve => { + const timer = Core.schedule(ms / 1000, false, this, () => { + this.requestTimers.delete(timer); + resolve(); + }); + this.requestTimers.add(timer); + }); + } + + /** + * 设置认证令牌 + */ + setAuthToken(token: string): void { + this.config.authToken = token; + } + + /** + * 获取连接ID + */ + getConnectionId(): string | null { + return this.connectionId; + } + + /** + * 检查是否支持 Fetch API + */ + static isSupported(): boolean { + return typeof fetch !== 'undefined'; + } + + /** + * 销毁传输层 + */ + override destroy(): void { + // 清理所有请求定时器 + this.requestTimers.forEach(timer => timer.stop()); + this.requestTimers.clear(); + + this.disconnect(); + super.destroy(); + } +} \ No newline at end of file diff --git a/packages/network-client/src/transport/WebSocketClientTransport.ts b/packages/network-client/src/transport/WebSocketClientTransport.ts new file mode 100644 index 00000000..ef4ef114 --- /dev/null +++ b/packages/network-client/src/transport/WebSocketClientTransport.ts @@ -0,0 +1,282 @@ +/** + * WebSocket 客户端传输实现 + */ + +import { Core, ITimer } from '@esengine/ecs-framework'; +import { + ClientTransport, + ClientTransportConfig, + ConnectionState, + ClientMessage +} from './ClientTransport'; + +/** + * WebSocket 客户端配置 + */ +export interface WebSocketClientConfig extends ClientTransportConfig { + /** WebSocket 路径 */ + path?: string; + /** 协议列表 */ + protocols?: string | string[]; + /** 额外的请求头 */ + headers?: Record; + /** 是否启用二进制消息 */ + binaryType?: 'blob' | 'arraybuffer'; + /** WebSocket 扩展 */ + extensions?: any; +} + +/** + * WebSocket 客户端传输 + */ +export class WebSocketClientTransport extends ClientTransport { + private websocket: WebSocket | null = null; + private connectionPromise: Promise | null = null; + private connectionTimeoutTimer: ITimer | null = null; + + protected override config: WebSocketClientConfig; + + constructor(config: WebSocketClientConfig) { + super(config); + + this.config = { + path: '/ws', + protocols: [], + headers: {}, + binaryType: 'arraybuffer', + ...config + }; + } + + /** + * 连接到服务器 + */ + async connect(): Promise { + if (this.state === ConnectionState.CONNECTING || + this.state === ConnectionState.CONNECTED) { + return this.connectionPromise || Promise.resolve(); + } + + this.setState(ConnectionState.CONNECTING); + this.stopReconnect(); // 停止任何正在进行的重连 + + this.connectionPromise = new Promise((resolve, reject) => { + try { + // 构建WebSocket URL + const protocol = this.config.secure ? 'wss' : 'ws'; + const url = `${protocol}://${this.config.host}:${this.config.port}${this.config.path}`; + + // 创建WebSocket连接 + this.websocket = new WebSocket(url, this.config.protocols); + + if (this.config.binaryType) { + this.websocket.binaryType = this.config.binaryType; + } + + // 设置连接超时 + this.connectionTimeoutTimer = Core.schedule(this.config.connectionTimeout! / 1000, false, this, () => { + if (this.websocket && this.websocket.readyState === WebSocket.CONNECTING) { + this.websocket.close(); + reject(new Error('Connection timeout')); + } + }); + + // WebSocket 事件处理 + this.websocket.onopen = () => { + if (this.connectionTimeoutTimer) { + this.connectionTimeoutTimer.stop(); + this.connectionTimeoutTimer = null; + } + this.setState(ConnectionState.CONNECTED); + resolve(); + }; + + this.websocket.onclose = (event) => { + if (this.connectionTimeoutTimer) { + this.connectionTimeoutTimer.stop(); + this.connectionTimeoutTimer = null; + } + this.handleClose(event.code, event.reason); + + if (this.state === ConnectionState.CONNECTING) { + reject(new Error(`Connection failed: ${event.reason || 'Unknown error'}`)); + } + }; + + this.websocket.onerror = (event) => { + if (this.connectionTimeoutTimer) { + this.connectionTimeoutTimer.stop(); + this.connectionTimeoutTimer = null; + } + const error = new Error('WebSocket error'); + this.handleError(error); + + if (this.state === ConnectionState.CONNECTING) { + reject(error); + } + }; + + this.websocket.onmessage = (event) => { + this.handleWebSocketMessage(event); + }; + + } catch (error) { + this.setState(ConnectionState.ERROR); + reject(error); + } + }); + + return this.connectionPromise; + } + + /** + * 断开连接 + */ + async disconnect(): Promise { + this.stopReconnect(); + + if (this.websocket) { + // 设置状态为断开连接,避免触发重连 + this.setState(ConnectionState.DISCONNECTED); + + if (this.websocket.readyState === WebSocket.OPEN || + this.websocket.readyState === WebSocket.CONNECTING) { + this.websocket.close(1000, 'Client disconnect'); + } + + this.websocket = null; + } + + this.connectionPromise = null; + } + + /** + * 发送消息 + */ + async sendMessage(message: ClientMessage): Promise { + if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { + // 如果未连接,将消息加入队列 + if (this.state === ConnectionState.CONNECTING || + this.state === ConnectionState.RECONNECTING) { + return this.queueMessage(message); + } + return false; + } + + try { + // 序列化消息 + const serialized = JSON.stringify({ + ...message, + timestamp: message.timestamp || Date.now() + }); + + // 发送消息 + this.websocket.send(serialized); + this.updateSendStats(message); + + return true; + + } catch (error) { + this.handleError(error as Error); + return false; + } + } + + /** + * 处理 WebSocket 消息 + */ + private handleWebSocketMessage(event: MessageEvent): void { + try { + let data: string; + + if (event.data instanceof ArrayBuffer) { + // 处理二进制数据 + data = new TextDecoder().decode(event.data); + } else if (event.data instanceof Blob) { + // Blob 需要异步处理 + event.data.text().then(text => { + this.processMessage(text); + }); + return; + } else { + // 字符串数据 + data = event.data; + } + + this.processMessage(data); + + } catch (error) { + console.error('Error processing WebSocket message:', error); + } + } + + /** + * 处理消息内容 + */ + private processMessage(data: string): void { + try { + const message: ClientMessage = JSON.parse(data); + this.handleMessage(message); + } catch (error) { + console.error('Error parsing message:', error); + } + } + + /** + * 处理连接关闭 + */ + private handleClose(code: number, reason: string): void { + this.websocket = null; + this.connectionPromise = null; + + const wasConnected = this.isConnected(); + + // 根据关闭代码决定是否重连 + if (code === 1000) { + // 正常关闭,不重连 + this.setState(ConnectionState.DISCONNECTED); + this.emit('disconnected', reason || 'Normal closure'); + } else if (wasConnected && this.reconnectAttempts < this.config.maxReconnectAttempts!) { + // 异常关闭,尝试重连 + this.emit('disconnected', reason || `Abnormal closure (${code})`); + this.startReconnect(); + } else { + // 达到最大重连次数或其他情况 + this.setState(ConnectionState.DISCONNECTED); + this.emit('disconnected', reason || `Connection lost (${code})`); + } + } + + /** + * 获取 WebSocket 就绪状态 + */ + getReadyState(): number { + return this.websocket?.readyState ?? WebSocket.CLOSED; + } + + /** + * 获取 WebSocket 实例 + */ + getWebSocket(): WebSocket | null { + return this.websocket; + } + + /** + * 检查是否支持 WebSocket + */ + static isSupported(): boolean { + return typeof WebSocket !== 'undefined'; + } + + /** + * 销毁传输层 + */ + override destroy(): void { + if (this.connectionTimeoutTimer) { + this.connectionTimeoutTimer.stop(); + this.connectionTimeoutTimer = null; + } + this.disconnect(); + super.destroy(); + } +} \ No newline at end of file diff --git a/packages/network-client/src/transport/index.ts b/packages/network-client/src/transport/index.ts new file mode 100644 index 00000000..b76ce5aa --- /dev/null +++ b/packages/network-client/src/transport/index.ts @@ -0,0 +1,7 @@ +/** + * 传输层导出 + */ + +export * from './ClientTransport'; +export * from './WebSocketClientTransport'; +export * from './HttpClientTransport'; \ No newline at end of file diff --git a/packages/network-client/tests/NetworkClient.integration.test.ts b/packages/network-client/tests/NetworkClient.integration.test.ts new file mode 100644 index 00000000..9fa634c3 --- /dev/null +++ b/packages/network-client/tests/NetworkClient.integration.test.ts @@ -0,0 +1,384 @@ +/** + * NetworkClient 集成测试 + * 测试网络客户端的完整功能,包括依赖注入和错误处理 + */ + +import { NetworkClient } from '../src/core/NetworkClient'; + +// Mock 所有外部依赖 +jest.mock('@esengine/ecs-framework', () => ({ + Core: { + scene: null, + schedule: { + scheduleRepeating: jest.fn((callback: Function, interval: number) => ({ + stop: jest.fn() + })) + } + }, + Emitter: jest.fn().mockImplementation(() => ({ + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + removeAllListeners: jest.fn() + })) +})); + +jest.mock('@esengine/ecs-framework-network-shared', () => ({ + NetworkValue: {}, + generateMessageId: jest.fn(() => 'test-message-id-123'), + generateNetworkId: jest.fn(() => 12345), + NetworkUtils: { + generateMessageId: jest.fn(() => 'test-message-id-456'), + calculateDistance: jest.fn(() => 100), + isNodeEnvironment: jest.fn(() => false), + isBrowserEnvironment: jest.fn(() => true) + } +})); + +// Mock WebSocket +class MockWebSocket { + public readyState: number = WebSocket.CONNECTING; + public onopen: ((event: Event) => void) | null = null; + public onclose: ((event: CloseEvent) => void) | null = null; + public onmessage: ((event: MessageEvent) => void) | null = null; + public onerror: ((event: Event) => void) | null = null; + + constructor(public url: string, public protocols?: string | string[]) {} + + send(data: string | ArrayBuffer | Blob): void {} + close(code?: number, reason?: string): void { + this.readyState = WebSocket.CLOSED; + if (this.onclose) { + this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' })); + } + } +} + +(global as any).WebSocket = MockWebSocket; +(global as any).WebSocket.CONNECTING = 0; +(global as any).WebSocket.OPEN = 1; +(global as any).WebSocket.CLOSING = 2; +(global as any).WebSocket.CLOSED = 3; + +describe('NetworkClient 集成测试', () => { + let client: NetworkClient; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + if (client) { + client.disconnect().catch(() => {}); + client = null as any; + } + }); + + describe('依赖注入测试', () => { + it('应该正确处理所有依赖模块', () => { + expect(() => { + client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080 + } + }); + }).not.toThrow(); + + expect(client).toBeInstanceOf(NetworkClient); + }); + + it('应该正确使用network-shared中的工具函数', () => { + const { generateMessageId, NetworkUtils } = require('@esengine/ecs-framework-network-shared'); + + client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080 + } + }); + + // 验证network-shared模块被正确导入 + expect(generateMessageId).toBeDefined(); + expect(NetworkUtils).toBeDefined(); + }); + + it('应该正确使用ecs-framework中的Core模块', () => { + const { Core } = require('@esengine/ecs-framework'); + + client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080 + } + }); + + expect(Core).toBeDefined(); + expect(Core.schedule).toBeDefined(); + }); + }); + + describe('构造函数错误处理', () => { + it('应该处理network-shared模块导入失败', () => { + // 重置模块并模拟导入失败 + jest.resetModules(); + jest.doMock('@esengine/ecs-framework-network-shared', () => { + throw new Error('network-shared模块导入失败'); + }); + + expect(() => { + const { NetworkClient } = require('../src/core/NetworkClient'); + new NetworkClient({ + transportType: 'websocket', + host: 'localhost', + port: 8080 + }); + }).toThrow(); + }); + + it('应该处理ecs-framework模块导入失败', () => { + // 重置模块并模拟导入失败 + jest.resetModules(); + jest.doMock('@esengine/ecs-framework', () => { + throw new Error('ecs-framework模块导入失败'); + }); + + expect(() => { + const { NetworkClient } = require('../src/core/NetworkClient'); + new NetworkClient({ + transportType: 'websocket', + host: 'localhost', + port: 8080 + }); + }).toThrow(); + }); + + it('应该处理传输层构造失败', () => { + // Mock传输层构造函数抛出异常 + const originalWebSocket = (global as any).WebSocket; + (global as any).WebSocket = jest.fn(() => { + throw new Error('WebSocket不可用'); + }); + + client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080 + } + }); + + expect(client.connect()).rejects.toThrow(); + + // 恢复原始WebSocket + (global as any).WebSocket = originalWebSocket; + }); + }); + + describe('功能测试', () => { + beforeEach(() => { + client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080 + } + }); + }); + + it('应该能够成功连接', async () => { + const connectPromise = client.connect(); + + // 模拟连接成功 + setTimeout(() => { + const transport = (client as any).transport; + if (transport && transport.websocket && transport.websocket.onopen) { + transport.websocket.readyState = WebSocket.OPEN; + transport.websocket.onopen(new Event('open')); + } + }, 0); + + await expect(connectPromise).resolves.toBeUndefined(); + }); + + it('应该能够发送消息', async () => { + // 先连接 + const connectPromise = client.connect(); + setTimeout(() => { + const transport = (client as any).transport; + if (transport && transport.websocket && transport.websocket.onopen) { + transport.websocket.readyState = WebSocket.OPEN; + transport.websocket.onopen(new Event('open')); + } + }, 0); + await connectPromise; + + // 发送消息 + const message = { + type: 'custom' as const, + data: { test: 'message' }, + reliable: true + }; + + // NetworkClient没有直接的sendMessage方法,它通过RPC调用 + }); + + it('应该能够正确断开连接', async () => { + await expect(client.disconnect()).resolves.toBeUndefined(); + }); + + it('应该返回正确的认证状态', () => { + expect(client.isAuthenticated()).toBe(false); + }); + + it('应该能够获取网络对象列表', () => { + const networkObjects = client.getAllNetworkObjects(); + expect(Array.isArray(networkObjects)).toBe(true); + expect(networkObjects.length).toBe(0); + }); + }); + + describe('消息ID生成测试', () => { + beforeEach(() => { + client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080 + } + }); + }); + + it('应该能够生成唯一的消息ID', () => { + const messageId1 = (client as any).generateMessageId(); + const messageId2 = (client as any).generateMessageId(); + + expect(typeof messageId1).toBe('string'); + expect(typeof messageId2).toBe('string'); + expect(messageId1).not.toBe(messageId2); + }); + + it('生成的消息ID应该符合预期格式', () => { + const messageId = (client as any).generateMessageId(); + + // 检查消息ID格式(时间戳 + 随机字符串) + expect(messageId).toMatch(/^[a-z0-9]+$/); + expect(messageId.length).toBeGreaterThan(10); + }); + }); + + describe('错误恢复测试', () => { + beforeEach(() => { + client = new NetworkClient({ + transportType: 'websocket', + host: 'localhost', + port: 8080, + maxReconnectAttempts: 2, + reconnectInterval: 100 + }); + }); + + it('连接失败后应该尝试重连', async () => { + let connectAttempts = 0; + const originalWebSocket = (global as any).WebSocket; + + (global as any).WebSocket = jest.fn().mockImplementation(() => { + connectAttempts++; + const ws = new originalWebSocket('ws://localhost:8080'); + // 模拟连接失败 + setTimeout(() => { + if (ws.onerror) { + ws.onerror(new Event('error')); + } + }, 0); + return ws; + }); + + await expect(client.connect()).rejects.toThrow(); + + // 等待重连尝试 + await new Promise(resolve => setTimeout(resolve, 300)); + + expect(connectAttempts).toBeGreaterThan(1); + + // 恢复原始WebSocket + (global as any).WebSocket = originalWebSocket; + }); + + it('达到最大重连次数后应该停止重连', async () => { + const maxAttempts = 2; + client = new NetworkClient({ + transportType: 'websocket', + host: 'localhost', + port: 8080, + maxReconnectAttempts: maxAttempts, + reconnectInterval: 50 + }); + + let connectAttempts = 0; + const originalWebSocket = (global as any).WebSocket; + + (global as any).WebSocket = jest.fn().mockImplementation(() => { + connectAttempts++; + const ws = new originalWebSocket('ws://localhost:8080'); + setTimeout(() => { + if (ws.onerror) { + ws.onerror(new Event('error')); + } + }, 0); + return ws; + }); + + await expect(client.connect()).rejects.toThrow(); + + // 等待所有重连尝试完成 + await new Promise(resolve => setTimeout(resolve, 200)); + + expect(connectAttempts).toBeLessThanOrEqual(maxAttempts + 1); + + // 恢复原始WebSocket + (global as any).WebSocket = originalWebSocket; + }); + }); + + describe('内存泄漏防护测试', () => { + it('断开连接时应该清理所有资源', async () => { + client = new NetworkClient({ + transport: 'websocket', + transportConfig: { + host: 'localhost', + port: 8080 + } + }); + + const { Emitter } = require('@esengine/ecs-framework'); + const emitterInstance = Emitter.mock.results[Emitter.mock.results.length - 1].value; + + await client.disconnect(); + + expect(emitterInstance.removeAllListeners).toHaveBeenCalled(); + }); + + it('多次创建和销毁客户端不应该造成内存泄漏', () => { + const initialEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length; + + // 创建和销毁多个客户端实例 + for (let i = 0; i < 5; i++) { + const tempClient = new NetworkClient({ + transportType: 'websocket', + host: 'localhost', + port: 8080 + }); + tempClient.disconnect().catch(() => {}); + } + + const finalEmitterCallCount = require('@esengine/ecs-framework').Emitter.mock.calls.length; + + // 验证Emitter实例数量符合预期 + expect(finalEmitterCallCount - initialEmitterCallCount).toBe(5); + }); + }); +}); \ No newline at end of file diff --git a/packages/network-client/tests/setup.ts b/packages/network-client/tests/setup.ts new file mode 100644 index 00000000..efad751e --- /dev/null +++ b/packages/network-client/tests/setup.ts @@ -0,0 +1,27 @@ +import 'reflect-metadata'; + +// Mock WebSocket for testing +(global as any).WebSocket = class MockWebSocket { + onopen: ((event: Event) => void) | null = null; + onmessage: ((event: MessageEvent) => void) | null = null; + onclose: ((event: CloseEvent) => void) | null = null; + onerror: ((event: Event) => void) | null = null; + + constructor(public url: string) {} + + send(data: string | ArrayBuffer | Blob) { + // Mock implementation + } + + close() { + // Mock implementation + } +}; + +global.beforeEach(() => { + jest.clearAllMocks(); +}); + +global.afterEach(() => { + jest.restoreAllMocks(); +}); \ No newline at end of file diff --git a/packages/network-client/tests/transport/ClientTransport.test.ts b/packages/network-client/tests/transport/ClientTransport.test.ts new file mode 100644 index 00000000..55dd027a --- /dev/null +++ b/packages/network-client/tests/transport/ClientTransport.test.ts @@ -0,0 +1,374 @@ +/** + * ClientTransport 基类测试 + * 测试客户端传输层基类的构造函数和依赖问题 + */ + +import { ClientTransport, ClientTransportConfig, ConnectionState } from '../../src/transport/ClientTransport'; + +// Mock Emitter 和 Core +jest.mock('@esengine/ecs-framework', () => ({ + Emitter: jest.fn().mockImplementation(() => ({ + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + removeAllListeners: jest.fn() + })), + Core: { + schedule: { + scheduleRepeating: jest.fn((callback: Function, interval: number) => ({ + stop: jest.fn() + })) + } + } +})); + +// Mock network-shared +jest.mock('@esengine/ecs-framework-network-shared', () => ({ + NetworkValue: {} +})); + +// 创建测试用的具体实现类 +class TestClientTransport extends ClientTransport { + async connect(): Promise { + return Promise.resolve(); + } + + async disconnect(): Promise { + return Promise.resolve(); + } + + async sendMessage(message: any): Promise { + return Promise.resolve(); + } +} + +describe('ClientTransport', () => { + let transport: TestClientTransport; + const defaultConfig: ClientTransportConfig = { + host: 'localhost', + port: 8080 + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + if (transport) { + transport = null as any; + } + }); + + describe('构造函数测试', () => { + it('应该能够成功创建ClientTransport实例', () => { + expect(() => { + transport = new TestClientTransport(defaultConfig); + }).not.toThrow(); + + expect(transport).toBeInstanceOf(ClientTransport); + }); + + it('应该正确设置默认配置', () => { + transport = new TestClientTransport(defaultConfig); + + const config = (transport as any).config; + expect(config.host).toBe('localhost'); + expect(config.port).toBe(8080); + expect(config.secure).toBe(false); + expect(config.connectionTimeout).toBe(10000); + expect(config.reconnectInterval).toBe(3000); + expect(config.maxReconnectAttempts).toBe(10); + expect(config.heartbeatInterval).toBe(30000); + expect(config.maxQueueSize).toBe(1000); + }); + + it('应该允许自定义配置覆盖默认值', () => { + const customConfig: ClientTransportConfig = { + host: 'example.com', + port: 9090, + secure: true, + connectionTimeout: 15000, + reconnectInterval: 5000, + maxReconnectAttempts: 5, + heartbeatInterval: 60000, + maxQueueSize: 500 + }; + + transport = new TestClientTransport(customConfig); + + const config = (transport as any).config; + expect(config.host).toBe('example.com'); + expect(config.port).toBe(9090); + expect(config.secure).toBe(true); + expect(config.connectionTimeout).toBe(15000); + expect(config.reconnectInterval).toBe(5000); + expect(config.maxReconnectAttempts).toBe(5); + expect(config.heartbeatInterval).toBe(60000); + expect(config.maxQueueSize).toBe(500); + }); + + it('应该正确初始化内部状态', () => { + transport = new TestClientTransport(defaultConfig); + + expect((transport as any).state).toBe(ConnectionState.DISCONNECTED); + expect((transport as any).messageQueue).toEqual([]); + expect((transport as any).reconnectAttempts).toBe(0); + expect((transport as any).reconnectTimer).toBeNull(); + expect((transport as any).heartbeatTimer).toBeNull(); + expect((transport as any).latencyMeasurements).toEqual([]); + }); + + it('应该正确初始化统计信息', () => { + transport = new TestClientTransport(defaultConfig); + + const stats = transport.getStats(); + expect(stats.connectedAt).toBeNull(); + expect(stats.connectionDuration).toBe(0); + expect(stats.messagesSent).toBe(0); + expect(stats.messagesReceived).toBe(0); + expect(stats.bytesSent).toBe(0); + expect(stats.bytesReceived).toBe(0); + expect(stats.averageLatency).toBe(0); + expect(stats.averageLatency).toBe(0); + expect(stats.reconnectCount).toBe(0); + }); + }); + + describe('依赖注入测试', () => { + it('应该正确处理@esengine/ecs-framework中的Emitter', () => { + const { Emitter } = require('@esengine/ecs-framework'); + + expect(() => { + transport = new TestClientTransport(defaultConfig); + }).not.toThrow(); + + expect(Emitter).toHaveBeenCalled(); + }); + + it('构造函数中Emitter初始化失败应该抛出异常', () => { + // Mock Emitter构造函数抛出异常 + const { Emitter } = require('@esengine/ecs-framework'); + Emitter.mockImplementation(() => { + throw new Error('Emitter初始化失败'); + }); + + expect(() => { + transport = new TestClientTransport(defaultConfig); + }).toThrow('Emitter初始化失败'); + }); + + it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => { + const networkShared = require('@esengine/ecs-framework-network-shared'); + + expect(() => { + transport = new TestClientTransport(defaultConfig); + }).not.toThrow(); + + expect(networkShared).toBeDefined(); + expect(networkShared.NetworkValue).toBeDefined(); + }); + }); + + describe('事件系统测试', () => { + beforeEach(() => { + transport = new TestClientTransport(defaultConfig); + }); + + it('应该能够注册事件监听器', () => { + const mockCallback = jest.fn(); + const { Emitter } = require('@esengine/ecs-framework'); + const emitterInstance = Emitter.mock.results[0].value; + + transport.on('connected', mockCallback); + + expect(emitterInstance.on).toHaveBeenCalledWith('connected', mockCallback); + }); + + it('应该能够移除事件监听器', () => { + const mockCallback = jest.fn(); + const { Emitter } = require('@esengine/ecs-framework'); + const emitterInstance = Emitter.mock.results[0].value; + + transport.off('connected', mockCallback); + + expect(emitterInstance.off).toHaveBeenCalledWith('connected', mockCallback); + }); + + it('应该能够发出事件', () => { + const { Emitter } = require('@esengine/ecs-framework'); + const emitterInstance = Emitter.mock.results[0].value; + + (transport as any).emit('connected'); + + expect(emitterInstance.emit).toHaveBeenCalledWith('connected'); + }); + }); + + describe('消息队列测试', () => { + beforeEach(() => { + transport = new TestClientTransport(defaultConfig); + }); + + it('应该能够将消息加入队列', async () => { + const message = { + type: 'custom' as const, + data: { test: 'data' }, + reliable: true, + timestamp: Date.now() + }; + + await transport.sendMessage(message); + + const messageQueue = (transport as any).messageQueue; + expect(messageQueue).toHaveLength(1); + expect(messageQueue[0]).toEqual(message); + }); + + it('消息队列达到最大大小时应该移除旧消息', async () => { + // 设置较小的队列大小 + const smallQueueConfig = { ...defaultConfig, maxQueueSize: 2 }; + transport = new TestClientTransport(smallQueueConfig); + + const message1 = { type: 'custom' as const, data: { id: 1 }, reliable: true, timestamp: Date.now() }; + const message2 = { type: 'custom' as const, data: { id: 2 }, reliable: true, timestamp: Date.now() }; + const message3 = { type: 'custom' as const, data: { id: 3 }, reliable: true, timestamp: Date.now() }; + + await transport.sendMessage(message1); + await transport.sendMessage(message2); + await transport.sendMessage(message3); + + const messageQueue = (transport as any).messageQueue; + expect(messageQueue).toHaveLength(2); + expect(messageQueue[0]).toEqual(message2); + expect(messageQueue[1]).toEqual(message3); + }); + }); + + describe('连接状态测试', () => { + beforeEach(() => { + transport = new TestClientTransport(defaultConfig); + }); + + it('应该正确获取连接状态', () => { + expect(transport.getState()).toBe(ConnectionState.DISCONNECTED); + }); + + it('应该正确检查连接状态', () => { + expect(transport.isConnected()).toBe(false); + + (transport as any).state = ConnectionState.CONNECTED; + expect(transport.isConnected()).toBe(true); + + (transport as any).state = ConnectionState.AUTHENTICATED; + expect(transport.isConnected()).toBe(true); + }); + + it('状态变化时应该发出事件', () => { + const { Emitter } = require('@esengine/ecs-framework'); + const emitterInstance = Emitter.mock.results[0].value; + + (transport as any).setState(ConnectionState.CONNECTING); + + expect(emitterInstance.emit).toHaveBeenCalledWith( + 'state-changed', + ConnectionState.DISCONNECTED, + ConnectionState.CONNECTING + ); + }); + }); + + describe('延迟测量测试', () => { + beforeEach(() => { + transport = new TestClientTransport(defaultConfig); + }); + + it('应该能够更新延迟测量', () => { + (transport as any).updateLatency(100); + (transport as any).updateLatency(200); + (transport as any).updateLatency(150); + + const stats = transport.getStats(); + expect(stats.averageLatency).toBe(150); + }); + + it('应该限制延迟测量样本数量', () => { + // 添加超过最大样本数的测量 + for (let i = 0; i < 150; i++) { + (transport as any).updateLatency(i * 10); + } + + const latencyMeasurements = (transport as any).latencyMeasurements; + expect(latencyMeasurements.length).toBeLessThanOrEqual(100); + }); + }); + + describe('配置验证测试', () => { + it('应该拒绝无效的主机名', () => { + expect(() => { + transport = new TestClientTransport({ host: '', port: 8080 }); + }).toThrow(); + }); + + it('应该拒绝无效的端口号', () => { + expect(() => { + transport = new TestClientTransport({ host: 'localhost', port: 0 }); + }).toThrow(); + + expect(() => { + transport = new TestClientTransport({ host: 'localhost', port: 65536 }); + }).toThrow(); + }); + + it('应该拒绝负数的超时配置', () => { + expect(() => { + transport = new TestClientTransport({ + host: 'localhost', + port: 8080, + connectionTimeout: -1000 + }); + }).toThrow(); + }); + }); + + describe('资源清理测试', () => { + beforeEach(() => { + transport = new TestClientTransport(defaultConfig); + }); + + it('应该能够清理所有定时器', () => { + const { Core } = require('@esengine/ecs-framework'); + const mockTimer = { stop: jest.fn() }; + Core.schedule.scheduleRepeating.mockReturnValue(mockTimer); + + // 设置一些定时器 + (transport as any).reconnectTimer = mockTimer; + (transport as any).heartbeatTimer = mockTimer; + + // 调用清理方法 + (transport as any).cleanup(); + + expect(mockTimer.stop).toHaveBeenCalledTimes(2); + expect((transport as any).reconnectTimer).toBeNull(); + expect((transport as any).heartbeatTimer).toBeNull(); + }); + + it('应该能够清理消息队列', () => { + (transport as any).messageQueue = [ + { type: 'custom', data: {}, reliable: true, timestamp: Date.now() } + ]; + + (transport as any).cleanup(); + + expect((transport as any).messageQueue).toHaveLength(0); + }); + + it('应该能够移除所有事件监听器', () => { + const { Emitter } = require('@esengine/ecs-framework'); + const emitterInstance = Emitter.mock.results[0].value; + + (transport as any).cleanup(); + + expect(emitterInstance.removeAllListeners).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/packages/network-client/tests/transport/WebSocketClientTransport.test.ts b/packages/network-client/tests/transport/WebSocketClientTransport.test.ts new file mode 100644 index 00000000..b13caa09 --- /dev/null +++ b/packages/network-client/tests/transport/WebSocketClientTransport.test.ts @@ -0,0 +1,348 @@ +/** + * WebSocketClientTransport 测试 + * 测试WebSocket客户端传输层的构造函数和依赖问题 + */ + +import { WebSocketClientTransport, WebSocketClientConfig } from '../../src/transport/WebSocketClientTransport'; +import { ConnectionState } from '../../src/transport/ClientTransport'; + +// Mock WebSocket +class MockWebSocket { + public readyState: number = WebSocket.CONNECTING; + public onopen: ((event: Event) => void) | null = null; + public onclose: ((event: CloseEvent) => void) | null = null; + public onmessage: ((event: MessageEvent) => void) | null = null; + public onerror: ((event: Event) => void) | null = null; + + constructor(public url: string, public protocols?: string | string[]) {} + + send(data: string | ArrayBuffer | Blob): void {} + close(code?: number, reason?: string): void { + this.readyState = WebSocket.CLOSED; + if (this.onclose) { + this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' })); + } + } +} + +// Mock依赖 - 直接创建mock对象而不依赖外部模块 +const mockCore = { + schedule: { + scheduleRepeating: jest.fn((callback: Function, interval: number) => ({ + stop: jest.fn() + })) + } +}; + +const mockEmitter = { + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + removeAllListeners: jest.fn() +}; + +const mockNetworkShared = { + NetworkValue: {}, + generateMessageId: jest.fn(() => 'mock-message-id-123') +}; + +// 设置模块mock +jest.doMock('@esengine/ecs-framework', () => ({ + Core: mockCore, + Emitter: jest.fn(() => mockEmitter) +})); + +jest.doMock('@esengine/ecs-framework-network-shared', () => mockNetworkShared); + +// 设置全局WebSocket mock +(global as any).WebSocket = MockWebSocket; +(global as any).WebSocket.CONNECTING = 0; +(global as any).WebSocket.OPEN = 1; +(global as any).WebSocket.CLOSING = 2; +(global as any).WebSocket.CLOSED = 3; + +describe('WebSocketClientTransport', () => { + let transport: WebSocketClientTransport; + const defaultConfig: WebSocketClientConfig = { + host: 'localhost', + port: 8080, + secure: false, + connectionTimeout: 5000, + reconnectInterval: 1000, + maxReconnectAttempts: 3, + heartbeatInterval: 30000 + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + if (transport) { + transport.disconnect().catch(() => {}); + transport = null as any; + } + }); + + describe('构造函数测试', () => { + it('应该能够成功创建WebSocketClientTransport实例', () => { + expect(() => { + transport = new WebSocketClientTransport(defaultConfig); + }).not.toThrow(); + + expect(transport).toBeInstanceOf(WebSocketClientTransport); + }); + + it('应该正确合并默认配置', () => { + transport = new WebSocketClientTransport(defaultConfig); + + const config = (transport as any).config; + expect(config.path).toBe('/ws'); + expect(config.protocols).toEqual([]); + expect(config.headers).toEqual({}); + expect(config.binaryType).toBe('arraybuffer'); + expect(config.host).toBe('localhost'); + expect(config.port).toBe(8080); + }); + + it('应该允许自定义配置覆盖默认值', () => { + const customConfig: WebSocketClientConfig = { + ...defaultConfig, + path: '/custom-ws', + protocols: ['custom-protocol'], + headers: { 'X-Custom': 'value' }, + binaryType: 'blob' + }; + + transport = new WebSocketClientTransport(customConfig); + + const config = (transport as any).config; + expect(config.path).toBe('/custom-ws'); + expect(config.protocols).toEqual(['custom-protocol']); + expect(config.headers).toEqual({ 'X-Custom': 'value' }); + expect(config.binaryType).toBe('blob'); + }); + + it('应该正确初始化内部状态', () => { + transport = new WebSocketClientTransport(defaultConfig); + + expect((transport as any).websocket).toBeNull(); + expect((transport as any).connectionPromise).toBeNull(); + expect((transport as any).connectionTimeoutTimer).toBeNull(); + expect((transport as any).state).toBe(ConnectionState.DISCONNECTED); + }); + }); + + describe('依赖注入测试', () => { + it('应该正确处理@esengine/ecs-framework依赖', () => { + const { Core } = require('@esengine/ecs-framework'); + + expect(() => { + transport = new WebSocketClientTransport(defaultConfig); + }).not.toThrow(); + + expect(Core).toBeDefined(); + }); + + it('应该正确处理@esengine/ecs-framework-network-shared依赖', () => { + const { generateMessageId } = require('@esengine/ecs-framework-network-shared'); + + expect(() => { + transport = new WebSocketClientTransport(defaultConfig); + }).not.toThrow(); + + expect(generateMessageId).toBeDefined(); + expect(typeof generateMessageId).toBe('function'); + }); + }); + + describe('连接功能测试', () => { + beforeEach(() => { + transport = new WebSocketClientTransport(defaultConfig); + }); + + it('应该能够发起连接', async () => { + const connectPromise = transport.connect(); + + expect((transport as any).websocket).toBeInstanceOf(MockWebSocket); + expect((transport as any).state).toBe(ConnectionState.CONNECTING); + + // 模拟连接成功 + const ws = (transport as any).websocket as MockWebSocket; + ws.readyState = WebSocket.OPEN; + if (ws.onopen) { + ws.onopen(new Event('open')); + } + + await expect(connectPromise).resolves.toBeUndefined(); + }); + + it('应该构造正确的WebSocket URL', async () => { + transport.connect(); + + const ws = (transport as any).websocket as MockWebSocket; + expect(ws.url).toBe('ws://localhost:8080/ws'); + }); + + it('使用安全连接时应该构造HTTPS URL', async () => { + const secureConfig = { ...defaultConfig, secure: true }; + transport = new WebSocketClientTransport(secureConfig); + + transport.connect(); + + const ws = (transport as any).websocket as MockWebSocket; + expect(ws.url).toBe('wss://localhost:8080/ws'); + }); + + it('应该设置WebSocket事件处理器', async () => { + transport.connect(); + + const ws = (transport as any).websocket as MockWebSocket; + expect(ws.onopen).toBeDefined(); + expect(ws.onclose).toBeDefined(); + expect(ws.onmessage).toBeDefined(); + expect(ws.onerror).toBeDefined(); + }); + + it('连接超时应该被正确处理', async () => { + const shortTimeoutConfig = { ...defaultConfig, connectionTimeout: 100 }; + transport = new WebSocketClientTransport(shortTimeoutConfig); + + const connectPromise = transport.connect(); + + // 不触发onopen事件,让连接超时 + await expect(connectPromise).rejects.toThrow('连接超时'); + }); + + it('应该能够正确断开连接', async () => { + transport.connect(); + + // 模拟连接成功 + const ws = (transport as any).websocket as MockWebSocket; + ws.readyState = WebSocket.OPEN; + if (ws.onopen) { + ws.onopen(new Event('open')); + } + + await transport.disconnect(); + expect((transport as any).state).toBe(ConnectionState.DISCONNECTED); + }); + }); + + describe('消息发送测试', () => { + beforeEach(async () => { + transport = new WebSocketClientTransport(defaultConfig); + }); + + it('未连接时发送消息应该加入队列', async () => { + const message = { + type: 'custom' as const, + data: { test: 'data' }, + reliable: true, + timestamp: Date.now() + }; + + await transport.sendMessage(message); + + const messageQueue = (transport as any).messageQueue; + expect(messageQueue).toHaveLength(1); + expect(messageQueue[0]).toEqual(message); + }); + + it('连接后应该发送队列中的消息', async () => { + const message = { + type: 'custom' as const, + data: { test: 'data' }, + reliable: true, + timestamp: Date.now() + }; + + // 先发送消息到队列 + await transport.sendMessage(message); + + // 然后连接 + transport.connect(); + const ws = (transport as any).websocket as MockWebSocket; + const sendSpy = jest.spyOn(ws, 'send'); + + // 模拟连接成功 + ws.readyState = WebSocket.OPEN; + if (ws.onopen) { + ws.onopen(new Event('open')); + } + + expect(sendSpy).toHaveBeenCalled(); + expect((transport as any).messageQueue).toHaveLength(0); + }); + }); + + describe('错误处理测试', () => { + it('应该处理WebSocket构造函数异常', () => { + // Mock WebSocket构造函数抛出异常 + const originalWebSocket = (global as any).WebSocket; + (global as any).WebSocket = jest.fn(() => { + throw new Error('WebSocket构造失败'); + }); + + transport = new WebSocketClientTransport(defaultConfig); + + expect(transport.connect()).rejects.toThrow('WebSocket构造失败'); + + // 恢复原始WebSocket + (global as any).WebSocket = originalWebSocket; + }); + + it('应该处理网络连接错误', async () => { + transport = new WebSocketClientTransport(defaultConfig); + + const connectPromise = transport.connect(); + + // 模拟连接错误 + const ws = (transport as any).websocket as MockWebSocket; + if (ws.onerror) { + ws.onerror(new Event('error')); + } + + await expect(connectPromise).rejects.toThrow(); + }); + + it('应该处理意外的连接关闭', () => { + transport = new WebSocketClientTransport(defaultConfig); + transport.connect(); + + const ws = (transport as any).websocket as MockWebSocket; + + // 模拟连接意外关闭 + if (ws.onclose) { + ws.onclose(new CloseEvent('close', { code: 1006, reason: '意外关闭' })); + } + + expect((transport as any).state).toBe(ConnectionState.DISCONNECTED); + }); + }); + + describe('统计信息测试', () => { + it('应该正确计算连接统计信息', async () => { + transport = new WebSocketClientTransport(defaultConfig); + + const initialStats = transport.getStats(); + expect(initialStats.connectedAt).toBeNull(); + expect(initialStats.messagesSent).toBe(0); + expect(initialStats.messagesReceived).toBe(0); + }); + + it('连接后应该更新统计信息', async () => { + transport = new WebSocketClientTransport(defaultConfig); + + transport.connect(); + const ws = (transport as any).websocket as MockWebSocket; + ws.readyState = WebSocket.OPEN; + if (ws.onopen) { + ws.onopen(new Event('open')); + } + + const stats = transport.getStats(); + expect(stats.connectedAt).toBeInstanceOf(Date); + }); + }); +}); \ No newline at end of file diff --git a/packages/network/tsconfig.json b/packages/network-client/tsconfig.json similarity index 94% rename from packages/network/tsconfig.json rename to packages/network-client/tsconfig.json index 0f836e08..b6817988 100644 --- a/packages/network/tsconfig.json +++ b/packages/network-client/tsconfig.json @@ -3,8 +3,10 @@ "target": "ES2020", "module": "ES2020", "moduleResolution": "node", + "allowImportingTsExtensions": false, "lib": ["ES2020", "DOM"], "outDir": "./bin", + "rootDir": "./src", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, @@ -37,8 +39,6 @@ "exclude": [ "node_modules", "bin", - "dist", - "tests", "**/*.test.ts", "**/*.spec.ts" ] diff --git a/packages/network-server/README.md b/packages/network-server/README.md new file mode 100644 index 00000000..84839447 --- /dev/null +++ b/packages/network-server/README.md @@ -0,0 +1,132 @@ +# @esengine/ecs-framework-network-server + +ECS Framework 网络库 - 服务端实现 + +## 概述 + +这是 ECS Framework 网络库的服务端包,提供了: + +- 权威服务端实现 +- 客户端会话管理 +- 房间和匹配系统 +- 反作弊验证 +- 网络同步权威控制 + +## 特性 + +- **权威服务端**: 所有网络状态由服务端权威控制 +- **客户端验证**: 验证客户端输入和操作的合法性 +- **房间系统**: 支持多房间和实例管理 +- **反作弊**: 内置反作弊验证机制 +- **高性能**: 针对大量客户端连接进行优化 + +## 安装 + +```bash +npm install @esengine/ecs-framework-network-server +``` + +## 基本用法 + +```typescript +import { NetworkServerManager } from '@esengine/ecs-framework-network-server'; +import { NetworkComponent, SyncVar, ServerRpc } from '@esengine/ecs-framework-network-shared'; + +// 启动服务端 +const server = new NetworkServerManager(); +await server.startServer({ + port: 7777, + maxConnections: 100 +}); + +// 创建权威网络组件 +@NetworkComponent() +class ServerPlayerController extends NetworkBehaviour { + @SyncVar() + public position: Vector3 = { x: 0, y: 0, z: 0 }; + + @SyncVar() + public health: number = 100; + + @ServerRpc({ requiresOwnership: true, rateLimit: 10 }) + public movePlayer(direction: Vector3): void { + // 服务端权威的移动处理 + if (this.validateMovement(direction)) { + this.position.add(direction); + } + } + + @ServerRpc({ requiresAuth: true }) + public takeDamage(damage: number, attackerId: number): void { + // 服务端权威的伤害处理 + if (this.validateDamage(damage, attackerId)) { + this.health -= damage; + + if (this.health <= 0) { + this.handlePlayerDeath(); + } + } + } +} +``` + +## 房间系统 + +```typescript +import { RoomManager, Room } from '@esengine/ecs-framework-network-server'; + +// 创建房间管理器 +const roomManager = new RoomManager(); + +// 创建房间 +const gameRoom = roomManager.createRoom({ + name: 'Game Room 1', + maxPlayers: 4, + isPrivate: false +}); + +// 玩家加入房间 +gameRoom.addPlayer(clientId, playerData); + +// 房间事件处理 +gameRoom.onPlayerJoined((player) => { + console.log(`Player ${player.name} joined room ${gameRoom.name}`); +}); + +gameRoom.onPlayerLeft((player) => { + console.log(`Player ${player.name} left room ${gameRoom.name}`); +}); +``` + +## 权限验证 + +```typescript +import { AuthSystem } from '@esengine/ecs-framework-network-server'; + +// 配置认证系统 +const authSystem = new AuthSystem({ + tokenSecret: 'your-secret-key', + sessionTimeout: 30 * 60 * 1000, // 30分钟 + maxLoginAttempts: 5 +}); + +// 客户端认证 +authSystem.onClientAuth(async (clientId, credentials) => { + const user = await validateCredentials(credentials); + if (user) { + return { userId: user.id, permissions: user.permissions }; + } + return null; +}); + +// RPC 权限检查 +@ServerRpc({ requiresAuth: true, requiresOwnership: true }) +public adminCommand(command: string): void { + // 只有已认证且拥有权限的客户端可以调用 + this.executeAdminCommand(command); +} +``` + +## License + +MIT \ No newline at end of file diff --git a/packages/network-server/build-rollup.cjs b/packages/network-server/build-rollup.cjs new file mode 100644 index 00000000..d50cd196 --- /dev/null +++ b/packages/network-server/build-rollup.cjs @@ -0,0 +1,115 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +console.log('🚀 使用 Rollup 构建 network-server 包...'); + +async function main() { + try { + if (fs.existsSync('./dist')) { + console.log('🧹 清理旧的构建文件...'); + execSync('rimraf ./dist', { stdio: 'inherit' }); + } + + console.log('📦 执行 Rollup 构建...'); + execSync('rollup -c rollup.config.cjs', { stdio: 'inherit' }); + + console.log('📋 生成 package.json...'); + generatePackageJson(); + + console.log('📁 复制必要文件...'); + copyFiles(); + + showBuildResults(); + + console.log('✅ network-server 构建完成!'); + console.log('\n🚀 发布命令:'); + console.log('cd dist && npm publish'); + + } catch (error) { + console.error('❌ 构建失败:', error.message); + process.exit(1); + } +} + +function generatePackageJson() { + const sourcePackage = JSON.parse(fs.readFileSync('./package.json', 'utf8')); + + const distPackage = { + name: sourcePackage.name, + version: sourcePackage.version, + description: sourcePackage.description, + main: 'index.cjs', + module: 'index.mjs', + types: 'index.d.ts', + exports: { + '.': { + import: './index.mjs', + require: './index.cjs', + types: './index.d.ts' + } + }, + files: [ + 'index.mjs', + 'index.mjs.map', + 'index.cjs', + 'index.cjs.map', + 'index.d.ts', + 'README.md', + 'LICENSE' + ], + keywords: [ + 'ecs', + 'networking', + 'server', + 'authority', + 'validation', + 'rooms', + 'game-server', + 'typescript' + ], + author: sourcePackage.author, + license: sourcePackage.license, + repository: sourcePackage.repository, + dependencies: sourcePackage.dependencies, + peerDependencies: sourcePackage.peerDependencies, + engines: { + node: '>=16.0.0' + }, + sideEffects: false + }; + + fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2)); +} + +function copyFiles() { + const filesToCopy = [ + { src: './README.md', dest: './dist/README.md' }, + { src: '../../LICENSE', dest: './dist/LICENSE' } + ]; + + filesToCopy.forEach(({ src, dest }) => { + if (fs.existsSync(src)) { + fs.copyFileSync(src, dest); + console.log(` ✓ 复制: ${path.basename(dest)}`); + } else { + console.log(` ⚠️ 文件不存在: ${src}`); + } + }); +} + +function showBuildResults() { + const distDir = './dist'; + const files = ['index.mjs', 'index.cjs', 'index.d.ts']; + + console.log('\n📊 构建结果:'); + files.forEach(file => { + const filePath = path.join(distDir, file); + if (fs.existsSync(filePath)) { + const size = fs.statSync(filePath).size; + console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`); + } + }); +} + +main().catch(console.error); \ No newline at end of file diff --git a/packages/network-server/jest.config.cjs b/packages/network-server/jest.config.cjs new file mode 100644 index 00000000..99f23f58 --- /dev/null +++ b/packages/network-server/jest.config.cjs @@ -0,0 +1,53 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', // 服务端库使用 node 环境 + roots: ['/tests'], + testMatch: ['**/*.test.ts', '**/*.spec.ts'], + testPathIgnorePatterns: ['/node_modules/'], + collectCoverage: false, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/index.ts', + '!src/**/index.ts', + '!**/*.d.ts', + '!src/**/*.test.ts', + '!src/**/*.spec.ts' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + coverageThreshold: { + global: { + branches: 60, + functions: 70, + lines: 70, + statements: 70 + }, + './src/core/': { + branches: 70, + functions: 80, + lines: 80, + statements: 80 + } + }, + verbose: true, + transform: { + '^.+\\.tsx?$': ['ts-jest', { + tsconfig: 'tsconfig.json', + useESM: false, + }], + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + setupFilesAfterEnv: ['/tests/setup.ts'], + testTimeout: 10000, + clearMocks: true, + restoreMocks: true, + modulePathIgnorePatterns: [ + '/bin/', + '/dist/', + '/node_modules/' + ] +}; \ No newline at end of file diff --git a/packages/network-server/package.json b/packages/network-server/package.json new file mode 100644 index 00000000..813dfafc --- /dev/null +++ b/packages/network-server/package.json @@ -0,0 +1,89 @@ +{ + "name": "@esengine/ecs-framework-network-server", + "version": "1.0.5", + "description": "ECS Framework 网络库 - 服务端实现", + "type": "module", + "main": "bin/index.js", + "types": "bin/index.d.ts", + "exports": { + ".": { + "types": "./bin/index.d.ts", + "import": "./bin/index.js", + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + } + }, + "files": [ + "bin/**/*", + "README.md", + "LICENSE" + ], + "keywords": [ + "ecs", + "networking", + "server", + "authority", + "validation", + "rooms", + "game-server", + "typescript" + ], + "scripts": { + "clean": "rimraf bin dist", + "build:ts": "tsc", + "prebuild": "npm run clean", + "build": "npm run build:ts", + "build:watch": "tsc --watch", + "rebuild": "npm run clean && npm run build", + "build:npm": "npm run build && node build-rollup.cjs", + "publish:npm": "npm run build:npm && cd dist && npm publish", + "publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish", + "publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish", + "publish:major": "npm version major && npm run build:npm && cd dist && npm publish", + "preversion": "npm run rebuild", + "test": "jest --config jest.config.cjs", + "test:watch": "jest --watch --config jest.config.cjs", + "test:coverage": "jest --coverage --config jest.config.cjs", + "test:ci": "jest --ci --coverage --config jest.config.cjs", + "test:clear": "jest --clearCache" + }, + "author": "yhh", + "license": "MIT", + "dependencies": { + "ws": "^8.18.0", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@esengine/ecs-framework": ">=2.1.29", + "@esengine/ecs-framework-network-shared": ">=1.0.0" + }, + "devDependencies": { + "@esengine/ecs-framework": "*", + "@esengine/ecs-framework-network-shared": "*", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.0", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.13", + "jest": "^29.7.0", + "jest-environment-node": "^29.7.0", + "rimraf": "^5.0.0", + "rollup": "^4.42.0", + "rollup-plugin-dts": "^6.2.1", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/network-server" + } +} diff --git a/packages/network-server/rollup.config.cjs b/packages/network-server/rollup.config.cjs new file mode 100644 index 00000000..25a4a33c --- /dev/null +++ b/packages/network-server/rollup.config.cjs @@ -0,0 +1,102 @@ +const resolve = require('@rollup/plugin-node-resolve'); +const commonjs = require('@rollup/plugin-commonjs'); +const terser = require('@rollup/plugin-terser'); +const dts = require('rollup-plugin-dts').default; +const { readFileSync } = require('fs'); + +const pkg = JSON.parse(readFileSync('./package.json', 'utf8')); + +const banner = `/** + * @esengine/ecs-framework-network-server v${pkg.version} + * ECS Framework 网络库 - 服务端实现 + * + * @author ${pkg.author} + * @license ${pkg.license} + */`; + +const external = [ + 'ws', + 'uuid', + '@esengine/ecs-framework', + '@esengine/ecs-framework-network-shared' +]; + +const commonPlugins = [ + resolve({ + preferBuiltins: true + }), + commonjs({ + include: /node_modules/ + }) +]; + +module.exports = [ + // ES模块构建 + { + input: 'bin/index.js', + output: { + file: 'dist/index.mjs', + format: 'es', + banner, + sourcemap: true, + exports: 'named' + }, + plugins: [ + ...commonPlugins, + terser({ + format: { + comments: /^!/ + } + }) + ], + external, + treeshake: { + moduleSideEffects: false, + propertyReadSideEffects: false, + unknownGlobalSideEffects: false + } + }, + + // CommonJS构建 + { + input: 'bin/index.js', + output: { + file: 'dist/index.cjs', + format: 'cjs', + banner, + sourcemap: true, + exports: 'named' + }, + plugins: [ + ...commonPlugins, + terser({ + format: { + comments: /^!/ + } + }) + ], + external, + treeshake: { + moduleSideEffects: false + } + }, + + // 类型定义构建 + { + input: 'bin/index.d.ts', + output: { + file: 'dist/index.d.ts', + format: 'es', + banner: `/** + * @esengine/ecs-framework-network-server v${pkg.version} + * TypeScript definitions + */` + }, + plugins: [ + dts({ + respectExternal: true + }) + ], + external + } +]; \ No newline at end of file diff --git a/packages/network-server/src/auth/AuthenticationManager.ts b/packages/network-server/src/auth/AuthenticationManager.ts new file mode 100644 index 00000000..1ccd7ac4 --- /dev/null +++ b/packages/network-server/src/auth/AuthenticationManager.ts @@ -0,0 +1,622 @@ +/** + * 身份验证管理器 + * + * 处理客户端身份验证、令牌验证等功能 + */ + +import { EventEmitter } from 'events'; +import { createHash, randomBytes } from 'crypto'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { ClientConnection } from '../core/ClientConnection'; + +/** + * 认证配置 + */ +export interface AuthConfig { + /** 令牌过期时间(毫秒) */ + tokenExpirationTime?: number; + /** 最大登录尝试次数 */ + maxLoginAttempts?: number; + /** 登录尝试重置时间(毫秒) */ + loginAttemptResetTime?: number; + /** 是否启用令牌刷新 */ + enableTokenRefresh?: boolean; + /** 令牌刷新阈值(毫秒) */ + tokenRefreshThreshold?: number; + /** 是否启用IP限制 */ + enableIpRestriction?: boolean; + /** 密码哈希算法 */ + passwordHashAlgorithm?: 'sha256' | 'sha512'; +} + +/** + * 用户信息 + */ +export interface UserInfo { + /** 用户ID */ + id: string; + /** 用户名 */ + username: string; + /** 密码哈希 */ + passwordHash: string; + /** 用户角色 */ + roles: string[]; + /** 用户元数据 */ + metadata: Record; + /** 创建时间 */ + createdAt: Date; + /** 最后登录时间 */ + lastLoginAt?: Date; + /** 是否激活 */ + isActive: boolean; + /** 允许的IP地址列表 */ + allowedIps?: string[]; +} + +/** + * 认证令牌 + */ +export interface AuthToken { + /** 令牌ID */ + id: string; + /** 用户ID */ + userId: string; + /** 令牌值 */ + token: string; + /** 创建时间 */ + createdAt: Date; + /** 过期时间 */ + expiresAt: Date; + /** 是否已撤销 */ + isRevoked: boolean; + /** 令牌元数据 */ + metadata: Record; +} + +/** + * 登录尝试记录 + */ +interface LoginAttempt { + /** IP地址 */ + ip: string; + /** 用户名 */ + username: string; + /** 尝试次数 */ + attempts: number; + /** 最后尝试时间 */ + lastAttempt: Date; +} + +/** + * 认证结果 + */ +export interface AuthResult { + /** 是否成功 */ + success: boolean; + /** 用户信息 */ + user?: UserInfo; + /** 认证令牌 */ + token?: AuthToken; + /** 错误信息 */ + error?: string; + /** 错误代码 */ + errorCode?: string; +} + +/** + * 认证管理器事件 + */ +export interface AuthManagerEvents { + /** 用户登录成功 */ + 'login-success': (user: UserInfo, token: AuthToken, clientId: string) => void; + /** 用户登录失败 */ + 'login-failed': (username: string, reason: string, clientId: string) => void; + /** 用户注销 */ + 'logout': (userId: string, clientId: string) => void; + /** 令牌过期 */ + 'token-expired': (userId: string, tokenId: string) => void; + /** 令牌刷新 */ + 'token-refreshed': (userId: string, oldTokenId: string, newTokenId: string) => void; + /** 认证错误 */ + 'auth-error': (error: Error, clientId?: string) => void; +} + +/** + * 身份验证管理器 + */ +export class AuthenticationManager extends EventEmitter { + private config: AuthConfig; + private users = new Map(); + private tokens = new Map(); + private loginAttempts = new Map(); + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor(config: AuthConfig = {}) { + super(); + + this.config = { + tokenExpirationTime: 24 * 60 * 60 * 1000, // 24小时 + maxLoginAttempts: 5, + loginAttemptResetTime: 15 * 60 * 1000, // 15分钟 + enableTokenRefresh: true, + tokenRefreshThreshold: 60 * 60 * 1000, // 1小时 + enableIpRestriction: false, + passwordHashAlgorithm: 'sha256', + ...config + }; + + this.initialize(); + } + + /** + * 注册用户 + */ + async registerUser(userData: { + username: string; + password: string; + roles?: string[]; + metadata?: Record; + allowedIps?: string[]; + }): Promise { + const { username, password, roles = ['user'], metadata = {}, allowedIps } = userData; + + // 检查用户名是否已存在 + if (this.findUserByUsername(username)) { + throw new Error('Username already exists'); + } + + const userId = this.generateId(); + const passwordHash = this.hashPassword(password); + + const user: UserInfo = { + id: userId, + username, + passwordHash, + roles, + metadata, + createdAt: new Date(), + isActive: true, + allowedIps + }; + + this.users.set(userId, user); + + console.log(`User registered: ${username} (${userId})`); + return user; + } + + /** + * 用户登录 + */ + async login( + username: string, + password: string, + client: ClientConnection + ): Promise { + try { + const clientIp = client.remoteAddress; + const attemptKey = `${clientIp}-${username}`; + + // 检查登录尝试次数 + if (this.isLoginBlocked(attemptKey)) { + const result: AuthResult = { + success: false, + error: 'Too many login attempts. Please try again later.', + errorCode: 'LOGIN_BLOCKED' + }; + this.emit('login-failed', username, result.error!, client.id); + return result; + } + + // 查找用户 + const user = this.findUserByUsername(username); + if (!user || !user.isActive) { + this.recordLoginAttempt(attemptKey); + const result: AuthResult = { + success: false, + error: 'Invalid username or password', + errorCode: 'INVALID_CREDENTIALS' + }; + this.emit('login-failed', username, result.error!, client.id); + return result; + } + + // 验证密码 + const passwordHash = this.hashPassword(password); + if (user.passwordHash !== passwordHash) { + this.recordLoginAttempt(attemptKey); + const result: AuthResult = { + success: false, + error: 'Invalid username or password', + errorCode: 'INVALID_CREDENTIALS' + }; + this.emit('login-failed', username, result.error!, client.id); + return result; + } + + // IP限制检查 + if (this.config.enableIpRestriction && user.allowedIps && user.allowedIps.length > 0) { + if (!user.allowedIps.includes(clientIp)) { + const result: AuthResult = { + success: false, + error: 'Access denied from this IP address', + errorCode: 'IP_RESTRICTED' + }; + this.emit('login-failed', username, result.error!, client.id); + return result; + } + } + + // 创建认证令牌 + const token = this.createToken(user.id); + + // 更新用户最后登录时间 + user.lastLoginAt = new Date(); + + // 清除登录尝试记录 + this.loginAttempts.delete(attemptKey); + + const result: AuthResult = { + success: true, + user, + token + }; + + console.log(`User logged in: ${username} (${user.id}) from ${clientIp}`); + this.emit('login-success', user, token, client.id); + + return result; + + } catch (error) { + const result: AuthResult = { + success: false, + error: (error as Error).message, + errorCode: 'INTERNAL_ERROR' + }; + this.emit('auth-error', error as Error, client.id); + return result; + } + } + + /** + * 用户注销 + */ + async logout(tokenValue: string, client: ClientConnection): Promise { + try { + const token = this.findTokenByValue(tokenValue); + if (!token) { + return false; + } + + // 撤销令牌 + token.isRevoked = true; + + console.log(`User logged out: ${token.userId} from ${client.remoteAddress}`); + this.emit('logout', token.userId, client.id); + + return true; + + } catch (error) { + this.emit('auth-error', error as Error, client.id); + return false; + } + } + + /** + * 验证令牌 + */ + async validateToken(tokenValue: string): Promise { + try { + const token = this.findTokenByValue(tokenValue); + + if (!token || token.isRevoked) { + return { + success: false, + error: 'Invalid token', + errorCode: 'INVALID_TOKEN' + }; + } + + if (token.expiresAt < new Date()) { + token.isRevoked = true; + this.emit('token-expired', token.userId, token.id); + return { + success: false, + error: 'Token expired', + errorCode: 'TOKEN_EXPIRED' + }; + } + + const user = this.users.get(token.userId); + if (!user || !user.isActive) { + return { + success: false, + error: 'User not found or inactive', + errorCode: 'USER_NOT_FOUND' + }; + } + + return { + success: true, + user, + token + }; + + } catch (error) { + this.emit('auth-error', error as Error); + return { + success: false, + error: (error as Error).message, + errorCode: 'INTERNAL_ERROR' + }; + } + } + + /** + * 刷新令牌 + */ + async refreshToken(tokenValue: string): Promise { + try { + const validationResult = await this.validateToken(tokenValue); + if (!validationResult.success || !validationResult.user || !validationResult.token) { + return validationResult; + } + + const token = validationResult.token; + const timeUntilExpiration = token.expiresAt.getTime() - Date.now(); + + // 检查是否需要刷新 + if (timeUntilExpiration > this.config.tokenRefreshThreshold!) { + return validationResult; // 不需要刷新 + } + + // 创建新令牌 + const newToken = this.createToken(token.userId, token.metadata); + + // 撤销旧令牌 + token.isRevoked = true; + + console.log(`Token refreshed for user: ${token.userId}`); + this.emit('token-refreshed', token.userId, token.id, newToken.id); + + return { + success: true, + user: validationResult.user, + token: newToken + }; + + } catch (error) { + this.emit('auth-error', error as Error); + return { + success: false, + error: (error as Error).message, + errorCode: 'INTERNAL_ERROR' + }; + } + } + + /** + * 获取用户信息 + */ + getUserById(userId: string): UserInfo | undefined { + return this.users.get(userId); + } + + /** + * 获取用户信息(通过用户名) + */ + getUserByUsername(username: string): UserInfo | undefined { + return this.findUserByUsername(username); + } + + /** + * 更新用户信息 + */ + async updateUser(userId: string, updates: Partial): Promise { + const user = this.users.get(userId); + if (!user) { + return false; + } + + // 不允许更新某些字段 + const { id, createdAt, ...allowedUpdates } = updates as any; + Object.assign(user, allowedUpdates); + + return true; + } + + /** + * 撤销所有用户令牌 + */ + async revokeAllUserTokens(userId: string): Promise { + let revokedCount = 0; + + for (const token of this.tokens.values()) { + if (token.userId === userId && !token.isRevoked) { + token.isRevoked = true; + revokedCount++; + } + } + + return revokedCount; + } + + /** + * 获取活跃令牌数量 + */ + getActiveTokenCount(): number { + return Array.from(this.tokens.values()) + .filter(token => !token.isRevoked && token.expiresAt > new Date()).length; + } + + /** + * 清理过期令牌和登录尝试记录 + */ + cleanup(): void { + const now = new Date(); + let cleanedTokens = 0; + let cleanedAttempts = 0; + + // 清理过期令牌 + for (const [tokenId, token] of this.tokens.entries()) { + if (token.expiresAt < now || token.isRevoked) { + this.tokens.delete(tokenId); + cleanedTokens++; + } + } + + // 清理过期的登录尝试记录 + const resetTime = this.config.loginAttemptResetTime!; + for (const [attemptKey, attempt] of this.loginAttempts.entries()) { + if (now.getTime() - attempt.lastAttempt.getTime() > resetTime) { + this.loginAttempts.delete(attemptKey); + cleanedAttempts++; + } + } + + if (cleanedTokens > 0 || cleanedAttempts > 0) { + console.log(`Auth cleanup: ${cleanedTokens} tokens, ${cleanedAttempts} login attempts`); + } + } + + /** + * 销毁认证管理器 + */ + destroy(): void { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + + this.users.clear(); + this.tokens.clear(); + this.loginAttempts.clear(); + this.removeAllListeners(); + } + + /** + * 初始化 + */ + private initialize(): void { + // 启动清理定时器(每小时清理一次) + this.cleanupTimer = setInterval(() => { + this.cleanup(); + }, 60 * 60 * 1000); + } + + /** + * 查找用户(通过用户名) + */ + private findUserByUsername(username: string): UserInfo | undefined { + return Array.from(this.users.values()) + .find(user => user.username === username); + } + + /** + * 查找令牌(通过令牌值) + */ + private findTokenByValue(tokenValue: string): AuthToken | undefined { + return Array.from(this.tokens.values()) + .find(token => token.token === tokenValue); + } + + /** + * 生成ID + */ + private generateId(): string { + return randomBytes(16).toString('hex'); + } + + /** + * 哈希密码 + */ + private hashPassword(password: string): string { + return createHash(this.config.passwordHashAlgorithm!) + .update(password) + .digest('hex'); + } + + /** + * 创建认证令牌 + */ + private createToken(userId: string, metadata: Record = {}): AuthToken { + const tokenId = this.generateId(); + const tokenValue = randomBytes(32).toString('hex'); + const now = new Date(); + const expiresAt = new Date(now.getTime() + this.config.tokenExpirationTime!); + + const token: AuthToken = { + id: tokenId, + userId, + token: tokenValue, + createdAt: now, + expiresAt, + isRevoked: false, + metadata + }; + + this.tokens.set(tokenId, token); + return token; + } + + /** + * 检查登录是否被阻止 + */ + private isLoginBlocked(attemptKey: string): boolean { + const attempt = this.loginAttempts.get(attemptKey); + if (!attempt) { + return false; + } + + const now = new Date(); + const resetTime = this.config.loginAttemptResetTime!; + + // 检查重置时间 + if (now.getTime() - attempt.lastAttempt.getTime() > resetTime) { + this.loginAttempts.delete(attemptKey); + return false; + } + + return attempt.attempts >= this.config.maxLoginAttempts!; + } + + /** + * 记录登录尝试 + */ + private recordLoginAttempt(attemptKey: string): void { + const now = new Date(); + const [ip, username] = attemptKey.split('-', 2); + + const existingAttempt = this.loginAttempts.get(attemptKey); + if (existingAttempt) { + // 检查是否需要重置 + if (now.getTime() - existingAttempt.lastAttempt.getTime() > this.config.loginAttemptResetTime!) { + existingAttempt.attempts = 1; + } else { + existingAttempt.attempts++; + } + existingAttempt.lastAttempt = now; + } else { + this.loginAttempts.set(attemptKey, { + ip, + username, + attempts: 1, + lastAttempt: now + }); + } + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: AuthManagerEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-server/src/auth/AuthorizationManager.ts b/packages/network-server/src/auth/AuthorizationManager.ts new file mode 100644 index 00000000..44878f12 --- /dev/null +++ b/packages/network-server/src/auth/AuthorizationManager.ts @@ -0,0 +1,684 @@ +/** + * 权限管理器 + * + * 处理用户权限、角色管理、访问控制等功能 + */ + +import { EventEmitter } from 'events'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { UserInfo } from './AuthenticationManager'; + +/** + * 权限类型 + */ +export type Permission = string; + +/** + * 角色定义 + */ +export interface Role { + /** 角色ID */ + id: string; + /** 角色名称 */ + name: string; + /** 角色描述 */ + description?: string; + /** 权限列表 */ + permissions: Permission[]; + /** 父角色ID */ + parentRoleId?: string; + /** 是否系统角色 */ + isSystemRole: boolean; + /** 角色元数据 */ + metadata: Record; + /** 创建时间 */ + createdAt: Date; +} + +/** + * 权限检查上下文 + */ +export interface PermissionContext { + /** 用户ID */ + userId: string; + /** 用户角色 */ + userRoles: string[]; + /** 请求的权限 */ + permission: Permission; + /** 资源ID(可选) */ + resourceId?: string; + /** 附加上下文数据 */ + context?: Record; +} + +/** + * 权限检查结果 + */ +export interface PermissionResult { + /** 是否允许 */ + granted: boolean; + /** 原因 */ + reason?: string; + /** 匹配的角色 */ + matchingRole?: string; + /** 使用的权限 */ + usedPermission?: Permission; +} + +/** + * 权限管理器配置 + */ +export interface AuthorizationConfig { + /** 是否启用权限继承 */ + enableInheritance?: boolean; + /** 是否启用权限缓存 */ + enableCache?: boolean; + /** 缓存过期时间(毫秒) */ + cacheExpirationTime?: number; + /** 默认权限策略 */ + defaultPolicy?: 'deny' | 'allow'; +} + +/** + * 权限管理器事件 + */ +export interface AuthorizationEvents { + /** 权限被授予 */ + 'permission-granted': (context: PermissionContext, result: PermissionResult) => void; + /** 权限被拒绝 */ + 'permission-denied': (context: PermissionContext, result: PermissionResult) => void; + /** 角色创建 */ + 'role-created': (role: Role) => void; + /** 角色更新 */ + 'role-updated': (roleId: string, updates: Partial) => void; + /** 角色删除 */ + 'role-deleted': (roleId: string) => void; + /** 权限错误 */ + 'authorization-error': (error: Error, context?: PermissionContext) => void; +} + +/** + * 权限缓存项 + */ +interface CacheItem { + result: PermissionResult; + expiresAt: Date; +} + +/** + * 预定义权限 + */ +export const Permissions = { + // 系统权限 + SYSTEM_ADMIN: 'system:admin', + SYSTEM_CONFIG: 'system:config', + + // 用户管理权限 + USER_CREATE: 'user:create', + USER_READ: 'user:read', + USER_UPDATE: 'user:update', + USER_DELETE: 'user:delete', + USER_MANAGE_ROLES: 'user:manage-roles', + + // 房间权限 + ROOM_CREATE: 'room:create', + ROOM_JOIN: 'room:join', + ROOM_LEAVE: 'room:leave', + ROOM_MANAGE: 'room:manage', + ROOM_KICK_PLAYERS: 'room:kick-players', + + // 网络权限 + NETWORK_SEND_RPC: 'network:send-rpc', + NETWORK_SYNC_VARS: 'network:sync-vars', + NETWORK_BROADCAST: 'network:broadcast', + + // 聊天权限 + CHAT_SEND: 'chat:send', + CHAT_MODERATE: 'chat:moderate', + CHAT_PRIVATE: 'chat:private', + + // 文件权限 + FILE_UPLOAD: 'file:upload', + FILE_DOWNLOAD: 'file:download', + FILE_DELETE: 'file:delete' +} as const; + +/** + * 预定义角色 + */ +export const SystemRoles = { + ADMIN: 'admin', + MODERATOR: 'moderator', + USER: 'user', + GUEST: 'guest' +} as const; + +/** + * 权限管理器 + */ +export class AuthorizationManager extends EventEmitter { + private config: AuthorizationConfig; + private roles = new Map(); + private permissionCache = new Map(); + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor(config: AuthorizationConfig = {}) { + super(); + + this.config = { + enableInheritance: true, + enableCache: true, + cacheExpirationTime: 5 * 60 * 1000, // 5分钟 + defaultPolicy: 'deny', + ...config + }; + + this.initialize(); + } + + /** + * 创建角色 + */ + async createRole(roleData: { + id: string; + name: string; + description?: string; + permissions: Permission[]; + parentRoleId?: string; + metadata?: Record; + }): Promise { + const { id, name, description, permissions, parentRoleId, metadata = {} } = roleData; + + if (this.roles.has(id)) { + throw new Error(`Role with id "${id}" already exists`); + } + + // 验证父角色是否存在 + if (parentRoleId && !this.roles.has(parentRoleId)) { + throw new Error(`Parent role "${parentRoleId}" not found`); + } + + const role: Role = { + id, + name, + description, + permissions: [...permissions], + parentRoleId, + isSystemRole: false, + metadata, + createdAt: new Date() + }; + + this.roles.set(id, role); + this.clearPermissionCache(); // 清除缓存 + + console.log(`Role created: ${name} (${id})`); + this.emit('role-created', role); + + return role; + } + + /** + * 获取角色 + */ + getRole(roleId: string): Role | undefined { + return this.roles.get(roleId); + } + + /** + * 获取所有角色 + */ + getAllRoles(): Role[] { + return Array.from(this.roles.values()); + } + + /** + * 更新角色 + */ + async updateRole(roleId: string, updates: Partial): Promise { + const role = this.roles.get(roleId); + if (!role) { + return false; + } + + // 系统角色不允许修改某些字段 + if (role.isSystemRole) { + const { permissions, parentRoleId, ...allowedUpdates } = updates; + Object.assign(role, allowedUpdates); + } else { + // 不允许更新某些字段 + const { id, createdAt, isSystemRole, ...allowedUpdates } = updates as any; + Object.assign(role, allowedUpdates); + } + + this.clearPermissionCache(); // 清除缓存 + + console.log(`Role updated: ${role.name} (${roleId})`); + this.emit('role-updated', roleId, updates); + + return true; + } + + /** + * 删除角色 + */ + async deleteRole(roleId: string): Promise { + const role = this.roles.get(roleId); + if (!role) { + return false; + } + + if (role.isSystemRole) { + throw new Error('Cannot delete system role'); + } + + // 检查是否有子角色依赖此角色 + const childRoles = Array.from(this.roles.values()) + .filter(r => r.parentRoleId === roleId); + + if (childRoles.length > 0) { + throw new Error(`Cannot delete role "${roleId}": ${childRoles.length} child roles depend on it`); + } + + this.roles.delete(roleId); + this.clearPermissionCache(); // 清除缓存 + + console.log(`Role deleted: ${role.name} (${roleId})`); + this.emit('role-deleted', roleId); + + return true; + } + + /** + * 检查权限 + */ + async checkPermission(context: PermissionContext): Promise { + try { + // 检查缓存 + const cacheKey = this.getCacheKey(context); + if (this.config.enableCache) { + const cached = this.permissionCache.get(cacheKey); + if (cached && cached.expiresAt > new Date()) { + return cached.result; + } + } + + const result = await this.performPermissionCheck(context); + + // 缓存结果 + if (this.config.enableCache) { + const expiresAt = new Date(Date.now() + this.config.cacheExpirationTime!); + this.permissionCache.set(cacheKey, { result, expiresAt }); + } + + // 触发事件 + if (result.granted) { + this.emit('permission-granted', context, result); + } else { + this.emit('permission-denied', context, result); + } + + return result; + + } catch (error) { + this.emit('authorization-error', error as Error, context); + + return { + granted: this.config.defaultPolicy === 'allow', + reason: `Authorization error: ${(error as Error).message}` + }; + } + } + + /** + * 检查用户是否有权限 + */ + async hasPermission(user: UserInfo, permission: Permission, resourceId?: string): Promise { + const context: PermissionContext = { + userId: user.id, + userRoles: user.roles, + permission, + resourceId + }; + + const result = await this.checkPermission(context); + return result.granted; + } + + /** + * 获取用户的所有权限 + */ + async getUserPermissions(user: UserInfo): Promise { + const permissions = new Set(); + + for (const roleId of user.roles) { + const rolePermissions = await this.getRolePermissions(roleId); + rolePermissions.forEach(p => permissions.add(p)); + } + + return Array.from(permissions); + } + + /** + * 获取角色的所有权限(包括继承的权限) + */ + async getRolePermissions(roleId: string): Promise { + const permissions = new Set(); + const visited = new Set(); + + const collectPermissions = (currentRoleId: string) => { + if (visited.has(currentRoleId)) { + return; // 防止循环引用 + } + visited.add(currentRoleId); + + const role = this.roles.get(currentRoleId); + if (!role) { + return; + } + + // 添加当前角色的权限 + role.permissions.forEach(p => permissions.add(p)); + + // 递归添加父角色的权限 + if (this.config.enableInheritance && role.parentRoleId) { + collectPermissions(role.parentRoleId); + } + }; + + collectPermissions(roleId); + return Array.from(permissions); + } + + /** + * 为角色添加权限 + */ + async addPermissionToRole(roleId: string, permission: Permission): Promise { + const role = this.roles.get(roleId); + if (!role) { + return false; + } + + if (!role.permissions.includes(permission)) { + role.permissions.push(permission); + this.clearPermissionCache(); + console.log(`Permission "${permission}" added to role "${roleId}"`); + } + + return true; + } + + /** + * 从角色移除权限 + */ + async removePermissionFromRole(roleId: string, permission: Permission): Promise { + const role = this.roles.get(roleId); + if (!role) { + return false; + } + + const index = role.permissions.indexOf(permission); + if (index !== -1) { + role.permissions.splice(index, 1); + this.clearPermissionCache(); + console.log(`Permission "${permission}" removed from role "${roleId}"`); + } + + return true; + } + + /** + * 检查用户是否有指定角色 + */ + hasRole(user: UserInfo, roleId: string): boolean { + return user.roles.includes(roleId); + } + + /** + * 为用户添加角色 + */ + async addRoleToUser(user: UserInfo, roleId: string): Promise { + if (!this.roles.has(roleId)) { + return false; + } + + if (!user.roles.includes(roleId)) { + user.roles.push(roleId); + this.clearUserPermissionCache(user.id); + console.log(`Role "${roleId}" added to user "${user.id}"`); + } + + return true; + } + + /** + * 从用户移除角色 + */ + async removeRoleFromUser(user: UserInfo, roleId: string): Promise { + const index = user.roles.indexOf(roleId); + if (index !== -1) { + user.roles.splice(index, 1); + this.clearUserPermissionCache(user.id); + console.log(`Role "${roleId}" removed from user "${user.id}"`); + return true; + } + + return false; + } + + /** + * 清除权限缓存 + */ + clearPermissionCache(): void { + this.permissionCache.clear(); + } + + /** + * 清除指定用户的权限缓存 + */ + clearUserPermissionCache(userId: string): void { + const keysToDelete: string[] = []; + + for (const [key] of this.permissionCache) { + if (key.startsWith(`${userId}:`)) { + keysToDelete.push(key); + } + } + + keysToDelete.forEach(key => this.permissionCache.delete(key)); + } + + /** + * 销毁权限管理器 + */ + destroy(): void { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + + this.roles.clear(); + this.permissionCache.clear(); + this.removeAllListeners(); + } + + /** + * 初始化 + */ + private initialize(): void { + // 创建系统角色 + this.createSystemRoles(); + + // 启动缓存清理定时器(每30分钟清理一次) + if (this.config.enableCache) { + this.cleanupTimer = setInterval(() => { + this.cleanupCache(); + }, 30 * 60 * 1000); + } + } + + /** + * 创建系统角色 + */ + private createSystemRoles(): void { + // 管理员角色 + const adminRole: Role = { + id: SystemRoles.ADMIN, + name: 'Administrator', + description: 'Full system access', + permissions: Object.values(Permissions), + isSystemRole: true, + metadata: {}, + createdAt: new Date() + }; + + // 版主角色 + const moderatorRole: Role = { + id: SystemRoles.MODERATOR, + name: 'Moderator', + description: 'Room and user management', + permissions: [ + Permissions.USER_READ, + Permissions.ROOM_CREATE, + Permissions.ROOM_JOIN, + Permissions.ROOM_MANAGE, + Permissions.ROOM_KICK_PLAYERS, + Permissions.NETWORK_SEND_RPC, + Permissions.NETWORK_SYNC_VARS, + Permissions.CHAT_SEND, + Permissions.CHAT_MODERATE, + Permissions.CHAT_PRIVATE + ], + parentRoleId: SystemRoles.USER, + isSystemRole: true, + metadata: {}, + createdAt: new Date() + }; + + // 普通用户角色 + const userRole: Role = { + id: SystemRoles.USER, + name: 'User', + description: 'Basic user permissions', + permissions: [ + Permissions.ROOM_JOIN, + Permissions.ROOM_LEAVE, + Permissions.NETWORK_SEND_RPC, + Permissions.NETWORK_SYNC_VARS, + Permissions.CHAT_SEND, + Permissions.FILE_DOWNLOAD + ], + parentRoleId: SystemRoles.GUEST, + isSystemRole: true, + metadata: {}, + createdAt: new Date() + }; + + // 访客角色 + const guestRole: Role = { + id: SystemRoles.GUEST, + name: 'Guest', + description: 'Limited access for guests', + permissions: [ + Permissions.ROOM_JOIN + ], + isSystemRole: true, + metadata: {}, + createdAt: new Date() + }; + + this.roles.set(adminRole.id, adminRole); + this.roles.set(moderatorRole.id, moderatorRole); + this.roles.set(userRole.id, userRole); + this.roles.set(guestRole.id, guestRole); + + console.log('System roles created'); + } + + /** + * 执行权限检查 + */ + private async performPermissionCheck(context: PermissionContext): Promise { + // 获取用户的所有角色权限 + const userPermissions = new Set(); + + for (const roleId of context.userRoles) { + const rolePermissions = await this.getRolePermissions(roleId); + rolePermissions.forEach(p => userPermissions.add(p)); + } + + // 直接权限匹配 + if (userPermissions.has(context.permission)) { + return { + granted: true, + reason: 'Direct permission match', + usedPermission: context.permission + }; + } + + // 通配符权限匹配 + const wildcardPermissions = Array.from(userPermissions) + .filter(p => p.endsWith('*')); + + for (const wildcardPerm of wildcardPermissions) { + const prefix = wildcardPerm.slice(0, -1); + if (context.permission.startsWith(prefix)) { + return { + granted: true, + reason: 'Wildcard permission match', + usedPermission: wildcardPerm + }; + } + } + + // 如果没有匹配的权限 + return { + granted: this.config.defaultPolicy === 'allow', + reason: this.config.defaultPolicy === 'allow' + ? 'Default allow policy' + : 'No matching permissions found' + }; + } + + /** + * 获取缓存键 + */ + private getCacheKey(context: PermissionContext): string { + const roleString = context.userRoles.sort().join(','); + const resourcePart = context.resourceId ? `:${context.resourceId}` : ''; + return `${context.userId}:${roleString}:${context.permission}${resourcePart}`; + } + + /** + * 清理过期缓存 + */ + private cleanupCache(): void { + const now = new Date(); + let cleanedCount = 0; + + for (const [key, item] of this.permissionCache.entries()) { + if (item.expiresAt < now) { + this.permissionCache.delete(key); + cleanedCount++; + } + } + + if (cleanedCount > 0) { + console.log(`Permission cache cleanup: ${cleanedCount} entries removed`); + } + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: AuthorizationEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-server/src/auth/index.ts b/packages/network-server/src/auth/index.ts new file mode 100644 index 00000000..cb42810a --- /dev/null +++ b/packages/network-server/src/auth/index.ts @@ -0,0 +1,6 @@ +/** + * 认证系统导出 + */ + +export * from './AuthenticationManager'; +export * from './AuthorizationManager'; \ No newline at end of file diff --git a/packages/network-server/src/core/ClientConnection.ts b/packages/network-server/src/core/ClientConnection.ts new file mode 100644 index 00000000..f069765a --- /dev/null +++ b/packages/network-server/src/core/ClientConnection.ts @@ -0,0 +1,478 @@ +/** + * 客户端连接管理 + */ + +import { EventEmitter } from 'events'; +import { NetworkValue, NetworkMessage } from '@esengine/ecs-framework-network-shared'; +import { TransportMessage } from './Transport'; + +/** + * 客户端连接状态 + */ +export enum ClientConnectionState { + /** 连接中 */ + CONNECTING = 'connecting', + /** 已连接 */ + CONNECTED = 'connected', + /** 认证中 */ + AUTHENTICATING = 'authenticating', + /** 已认证 */ + AUTHENTICATED = 'authenticated', + /** 断开连接中 */ + DISCONNECTING = 'disconnecting', + /** 已断开 */ + DISCONNECTED = 'disconnected', + /** 错误状态 */ + ERROR = 'error' +} + +/** + * 客户端权限 + */ +export interface ClientPermissions { + /** 是否可以加入房间 */ + canJoinRooms?: boolean; + /** 是否可以创建房间 */ + canCreateRooms?: boolean; + /** 是否可以发送RPC */ + canSendRpc?: boolean; + /** 是否可以同步变量 */ + canSyncVars?: boolean; + /** 自定义权限 */ + customPermissions?: Record; +} + +/** + * 客户端连接事件 + */ +export interface ClientConnectionEvents { + /** 状态变化 */ + 'state-changed': (oldState: ClientConnectionState, newState: ClientConnectionState) => void; + /** 收到消息 */ + 'message': (message: TransportMessage) => void; + /** 连接错误 */ + 'error': (error: Error) => void; + /** 连接超时 */ + 'timeout': () => void; + /** 身份验证成功 */ + 'authenticated': (userData: Record) => void; + /** 身份验证失败 */ + 'authentication-failed': (reason: string) => void; +} + +/** + * 客户端统计信息 + */ +export interface ClientStats { + /** 消息发送数 */ + messagesSent: number; + /** 消息接收数 */ + messagesReceived: number; + /** 字节发送数 */ + bytesSent: number; + /** 字节接收数 */ + bytesReceived: number; + /** 最后活跃时间 */ + lastActivity: Date; + /** 连接时长(毫秒) */ + connectionDuration: number; +} + +/** + * 客户端连接管理类 + */ +export class ClientConnection extends EventEmitter { + /** 连接ID */ + public readonly id: string; + + /** 客户端IP地址 */ + public readonly remoteAddress: string; + + /** 连接创建时间 */ + public readonly connectedAt: Date; + + /** 当前状态 */ + private _state: ClientConnectionState = ClientConnectionState.CONNECTING; + + /** 用户数据 */ + private _userData: Record = {}; + + /** 权限信息 */ + private _permissions: ClientPermissions = {}; + + /** 所在房间ID */ + private _currentRoomId: string | null = null; + + /** 统计信息 */ + private _stats: ClientStats; + + /** 最后活跃时间 */ + private _lastActivity: Date; + + /** 超时定时器 */ + private _timeoutTimer: NodeJS.Timeout | null = null; + + /** 连接超时时间(毫秒) */ + private _connectionTimeout: number; + + /** 发送消息回调 */ + private _sendMessageCallback: (message: TransportMessage) => Promise; + + constructor( + id: string, + remoteAddress: string, + sendMessageCallback: (message: TransportMessage) => Promise, + options: { + connectionTimeout?: number; + userData?: Record; + permissions?: ClientPermissions; + } = {} + ) { + super(); + + this.id = id; + this.remoteAddress = remoteAddress; + this.connectedAt = new Date(); + this._lastActivity = new Date(); + this._connectionTimeout = options.connectionTimeout || 60000; // 1分钟 + this._sendMessageCallback = sendMessageCallback; + + if (options.userData) { + this._userData = { ...options.userData }; + } + + if (options.permissions) { + this._permissions = { ...options.permissions }; + } + + this._stats = { + messagesSent: 0, + messagesReceived: 0, + bytesSent: 0, + bytesReceived: 0, + lastActivity: this._lastActivity, + connectionDuration: 0 + }; + + this.setState(ClientConnectionState.CONNECTED); + this.startTimeout(); + } + + /** + * 获取当前状态 + */ + get state(): ClientConnectionState { + return this._state; + } + + /** + * 获取用户数据 + */ + get userData(): Readonly> { + return this._userData; + } + + /** + * 获取权限信息 + */ + get permissions(): Readonly { + return this._permissions; + } + + /** + * 获取当前房间ID + */ + get currentRoomId(): string | null { + return this._currentRoomId; + } + + /** + * 获取统计信息 + */ + get stats(): Readonly { + this._stats.connectionDuration = Date.now() - this.connectedAt.getTime(); + this._stats.lastActivity = this._lastActivity; + return this._stats; + } + + /** + * 获取最后活跃时间 + */ + get lastActivity(): Date { + return this._lastActivity; + } + + /** + * 是否已连接 + */ + get isConnected(): boolean { + return this._state === ClientConnectionState.CONNECTED || + this._state === ClientConnectionState.AUTHENTICATED; + } + + /** + * 是否已认证 + */ + get isAuthenticated(): boolean { + return this._state === ClientConnectionState.AUTHENTICATED; + } + + /** + * 发送消息 + */ + async sendMessage(message: TransportMessage): Promise { + if (!this.isConnected) { + return false; + } + + try { + const success = await this._sendMessageCallback(message); + if (success) { + this._stats.messagesSent++; + const messageSize = JSON.stringify(message).length; + this._stats.bytesSent += messageSize; + this.updateActivity(); + } + return success; + } catch (error) { + this.handleError(error as Error); + return false; + } + } + + /** + * 处理接收到的消息 + */ + handleMessage(message: TransportMessage): void { + if (!this.isConnected) { + return; + } + + this._stats.messagesReceived++; + const messageSize = JSON.stringify(message).length; + this._stats.bytesReceived += messageSize; + this.updateActivity(); + + this.emit('message', message); + } + + /** + * 设置用户数据 + */ + setUserData(key: string, value: NetworkValue): void { + this._userData[key] = value; + } + + /** + * 获取用户数据 + */ + getUserData(key: string): T | undefined { + return this._userData[key] as T; + } + + /** + * 批量设置用户数据 + */ + setUserDataBatch(data: Record): void { + Object.assign(this._userData, data); + } + + /** + * 设置权限 + */ + setPermission(permission: keyof ClientPermissions, value: boolean): void { + (this._permissions as any)[permission] = value; + } + + /** + * 检查权限 + */ + hasPermission(permission: keyof ClientPermissions): boolean { + return (this._permissions as any)[permission] || false; + } + + /** + * 设置自定义权限 + */ + setCustomPermission(permission: string, value: boolean): void { + if (!this._permissions.customPermissions) { + this._permissions.customPermissions = {}; + } + this._permissions.customPermissions[permission] = value; + } + + /** + * 检查自定义权限 + */ + hasCustomPermission(permission: string): boolean { + return this._permissions.customPermissions?.[permission] || false; + } + + /** + * 进行身份认证 + */ + async authenticate(credentials: Record): Promise { + if (this._state !== ClientConnectionState.CONNECTED) { + return false; + } + + this.setState(ClientConnectionState.AUTHENTICATING); + + try { + // 这里可以添加实际的认证逻辑 + // 目前简单地认为所有认证都成功 + + this.setUserDataBatch(credentials); + this.setState(ClientConnectionState.AUTHENTICATED); + this.emit('authenticated', credentials); + + return true; + } catch (error) { + this.setState(ClientConnectionState.CONNECTED); + this.emit('authentication-failed', (error as Error).message); + return false; + } + } + + /** + * 加入房间 + */ + joinRoom(roomId: string): void { + this._currentRoomId = roomId; + } + + /** + * 离开房间 + */ + leaveRoom(): void { + this._currentRoomId = null; + } + + /** + * 断开连接 + */ + disconnect(reason?: string): void { + if (this._state === ClientConnectionState.DISCONNECTED) { + return; + } + + this.setState(ClientConnectionState.DISCONNECTING); + this.stopTimeout(); + + // 发送断开连接消息 + this.sendMessage({ + type: 'system', + data: { + action: 'disconnect', + reason: reason || 'server-disconnect' + } + }).finally(() => { + this.setState(ClientConnectionState.DISCONNECTED); + }); + } + + /** + * 更新活跃时间 + */ + updateActivity(): void { + this._lastActivity = new Date(); + this.resetTimeout(); + } + + /** + * 设置连接状态 + */ + private setState(newState: ClientConnectionState): void { + const oldState = this._state; + if (oldState !== newState) { + this._state = newState; + this.emit('state-changed', oldState, newState); + } + } + + /** + * 处理错误 + */ + private handleError(error: Error): void { + this.setState(ClientConnectionState.ERROR); + this.emit('error', error); + } + + /** + * 启动超时检测 + */ + private startTimeout(): void { + this.resetTimeout(); + } + + /** + * 重置超时定时器 + */ + private resetTimeout(): void { + this.stopTimeout(); + + if (this._connectionTimeout > 0) { + this._timeoutTimer = setTimeout(() => { + this.handleTimeout(); + }, this._connectionTimeout); + } + } + + /** + * 停止超时检测 + */ + private stopTimeout(): void { + if (this._timeoutTimer) { + clearTimeout(this._timeoutTimer); + this._timeoutTimer = null; + } + } + + /** + * 处理超时 + */ + private handleTimeout(): void { + this.emit('timeout'); + this.disconnect('timeout'); + } + + /** + * 销毁连接 + */ + destroy(): void { + this.stopTimeout(); + this.removeAllListeners(); + this.setState(ClientConnectionState.DISCONNECTED); + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: ClientConnectionEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } + + /** + * 序列化连接信息 + */ + toJSON(): object { + return { + id: this.id, + remoteAddress: this.remoteAddress, + state: this._state, + connectedAt: this.connectedAt.toISOString(), + lastActivity: this._lastActivity.toISOString(), + currentRoomId: this._currentRoomId, + userData: this._userData, + permissions: this._permissions, + stats: this.stats + }; + } +} \ No newline at end of file diff --git a/packages/network-server/src/core/HttpTransport.ts b/packages/network-server/src/core/HttpTransport.ts new file mode 100644 index 00000000..cefe2ada --- /dev/null +++ b/packages/network-server/src/core/HttpTransport.ts @@ -0,0 +1,602 @@ +/** + * HTTP 传输层实现 + * + * 用于处理 REST API 请求和长轮询连接 + */ + +import { createServer, IncomingMessage, ServerResponse, Server as HttpServer } from 'http'; +import { parse as parseUrl } from 'url'; +import { v4 as uuidv4 } from 'uuid'; +import { Transport, TransportConfig, ClientConnectionInfo, TransportMessage } from './Transport'; + +/** + * HTTP 传输配置 + */ +export interface HttpTransportConfig extends TransportConfig { + /** API 路径前缀 */ + apiPrefix?: string; + /** 最大请求大小(字节) */ + maxRequestSize?: number; + /** 长轮询超时(毫秒) */ + longPollTimeout?: number; + /** 是否启用 CORS */ + enableCors?: boolean; + /** 允许的域名 */ + corsOrigins?: string[]; +} + +/** + * HTTP 请求上下文 + */ +interface HttpRequestContext { + /** 请求ID */ + id: string; + /** HTTP 请求 */ + request: IncomingMessage; + /** HTTP 响应 */ + response: ServerResponse; + /** 解析后的URL */ + parsedUrl: any; + /** 请求体数据 */ + body?: string; + /** 查询参数 */ + query: Record; +} + +/** + * HTTP 客户端连接信息(用于长轮询) + */ +interface HttpConnectionInfo extends ClientConnectionInfo { + /** 长轮询响应对象 */ + longPollResponse?: ServerResponse; + /** 消息队列 */ + messageQueue: TransportMessage[]; + /** 长轮询超时定时器 */ + longPollTimer?: NodeJS.Timeout; +} + +/** + * HTTP 传输层实现 + */ +export class HttpTransport extends Transport { + private httpServer: HttpServer | null = null; + private httpConnections = new Map(); + + protected override config: HttpTransportConfig; + + constructor(config: HttpTransportConfig) { + super(config); + this.config = { + apiPrefix: '/api', + maxRequestSize: 1024 * 1024, // 1MB + longPollTimeout: 30000, // 30秒 + enableCors: true, + corsOrigins: ['*'], + heartbeatInterval: 60000, + connectionTimeout: 120000, + maxConnections: 1000, + ...config + }; + } + + /** + * 启动 HTTP 服务器 + */ + async start(): Promise { + if (this.isRunning) { + throw new Error('HTTP transport is already running'); + } + + try { + this.httpServer = createServer((req, res) => { + this.handleHttpRequest(req, res); + }); + + this.httpServer.on('error', (error: Error) => { + this.handleError(error); + }); + + await new Promise((resolve, reject) => { + this.httpServer!.listen(this.config.port, this.config.host, (error?: Error) => { + if (error) { + reject(error); + } else { + this.isRunning = true; + resolve(); + } + }); + }); + + this.emit('server-started', this.config); + } catch (error) { + await this.cleanup(); + throw error; + } + } + + /** + * 停止 HTTP 服务器 + */ + async stop(): Promise { + if (!this.isRunning) { + return; + } + + this.isRunning = false; + + // 断开所有长轮询连接 + for (const [connectionId] of this.httpConnections) { + this.disconnectClient(connectionId, 'server-shutdown'); + } + + await this.cleanup(); + this.emit('server-stopped'); + } + + /** + * 发送消息给指定客户端 + */ + async sendToClient(connectionId: string, message: TransportMessage): Promise { + const connection = this.httpConnections.get(connectionId); + if (!connection) { + return false; + } + + // 如果有长轮询连接,直接发送 + if (connection.longPollResponse && !connection.longPollResponse.headersSent) { + this.sendLongPollResponse(connection, [message]); + return true; + } + + // 否则加入消息队列 + connection.messageQueue.push(message); + return true; + } + + /** + * 广播消息给所有客户端 + */ + async broadcast(message: TransportMessage, excludeId?: string): Promise { + let sentCount = 0; + + for (const [connectionId, connection] of this.httpConnections) { + if (excludeId && connectionId === excludeId) { + continue; + } + + if (await this.sendToClient(connectionId, message)) { + sentCount++; + } + } + + return sentCount; + } + + /** + * 发送消息给指定客户端列表 + */ + async sendToClients(connectionIds: string[], message: TransportMessage): Promise { + let sentCount = 0; + + for (const connectionId of connectionIds) { + if (await this.sendToClient(connectionId, message)) { + sentCount++; + } + } + + return sentCount; + } + + /** + * 断开指定客户端连接 + */ + async disconnectClient(connectionId: string, reason?: string): Promise { + const connection = this.httpConnections.get(connectionId); + if (connection) { + this.cleanupConnection(connectionId); + this.removeConnection(connectionId, reason); + } + } + + /** + * 处理 HTTP 请求 + */ + private async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise { + try { + // 设置 CORS 头 + if (this.config.enableCors) { + this.setCorsHeaders(res); + } + + // 处理 OPTIONS 请求 + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + const parsedUrl = parseUrl(req.url || '', true); + const pathname = parsedUrl.pathname || ''; + + // 检查是否为 API 请求 + if (!pathname.startsWith(this.config.apiPrefix!)) { + this.sendErrorResponse(res, 404, 'Not Found'); + return; + } + + const context: HttpRequestContext = { + id: uuidv4(), + request: req, + response: res, + parsedUrl, + query: parsedUrl.query as Record, + }; + + // 读取请求体 + if (req.method === 'POST' || req.method === 'PUT') { + context.body = await this.readRequestBody(req); + } + + // 路由处理 + const apiPath = pathname.substring(this.config.apiPrefix!.length); + await this.routeApiRequest(context, apiPath); + + } catch (error) { + this.handleError(error as Error); + this.sendErrorResponse(res, 500, 'Internal Server Error'); + } + } + + /** + * API 路由处理 + */ + private async routeApiRequest(context: HttpRequestContext, apiPath: string): Promise { + const { request, response } = context; + + switch (apiPath) { + case '/connect': + if (request.method === 'POST') { + await this.handleConnect(context); + } else { + this.sendErrorResponse(response, 405, 'Method Not Allowed'); + } + break; + + case '/disconnect': + if (request.method === 'POST') { + await this.handleDisconnect(context); + } else { + this.sendErrorResponse(response, 405, 'Method Not Allowed'); + } + break; + + case '/poll': + if (request.method === 'GET') { + await this.handleLongPoll(context); + } else { + this.sendErrorResponse(response, 405, 'Method Not Allowed'); + } + break; + + case '/send': + if (request.method === 'POST') { + await this.handleSendMessage(context); + } else { + this.sendErrorResponse(response, 405, 'Method Not Allowed'); + } + break; + + case '/status': + if (request.method === 'GET') { + await this.handleStatus(context); + } else { + this.sendErrorResponse(response, 405, 'Method Not Allowed'); + } + break; + + default: + this.sendErrorResponse(response, 404, 'API endpoint not found'); + break; + } + } + + /** + * 处理连接请求 + */ + private async handleConnect(context: HttpRequestContext): Promise { + const { request, response } = context; + + try { + // 检查连接数限制 + if (this.config.maxConnections && this.httpConnections.size >= this.config.maxConnections) { + this.sendErrorResponse(response, 429, 'Too many connections'); + return; + } + + const connectionId = uuidv4(); + const remoteAddress = request.socket.remoteAddress || request.headers['x-forwarded-for'] || 'unknown'; + + const connectionInfo: HttpConnectionInfo = { + id: connectionId, + remoteAddress: Array.isArray(remoteAddress) ? remoteAddress[0] : remoteAddress, + connectedAt: new Date(), + lastActivity: new Date(), + userData: {}, + messageQueue: [] + }; + + this.httpConnections.set(connectionId, connectionInfo); + this.addConnection(connectionInfo); + + this.sendJsonResponse(response, 200, { + success: true, + connectionId, + serverTime: Date.now() + }); + + } catch (error) { + this.handleError(error as Error); + this.sendErrorResponse(response, 500, 'Failed to create connection'); + } + } + + /** + * 处理断开连接请求 + */ + private async handleDisconnect(context: HttpRequestContext): Promise { + const { response, query } = context; + + const connectionId = query.connectionId; + if (!connectionId) { + this.sendErrorResponse(response, 400, 'Missing connectionId'); + return; + } + + await this.disconnectClient(connectionId, 'client-disconnect'); + + this.sendJsonResponse(response, 200, { + success: true, + message: 'Disconnected successfully' + }); + } + + /** + * 处理长轮询请求 + */ + private async handleLongPoll(context: HttpRequestContext): Promise { + const { response, query } = context; + + const connectionId = query.connectionId; + if (!connectionId) { + this.sendErrorResponse(response, 400, 'Missing connectionId'); + return; + } + + const connection = this.httpConnections.get(connectionId); + if (!connection) { + this.sendErrorResponse(response, 404, 'Connection not found'); + return; + } + + this.updateClientActivity(connectionId); + + // 如果有排队的消息,立即返回 + if (connection.messageQueue.length > 0) { + const messages = connection.messageQueue.splice(0); + this.sendLongPollResponse(connection, messages); + return; + } + + // 设置长轮询 + connection.longPollResponse = response; + + // 设置超时 + connection.longPollTimer = setTimeout(() => { + this.sendLongPollResponse(connection, []); + }, this.config.longPollTimeout); + } + + /** + * 处理发送消息请求 + */ + private async handleSendMessage(context: HttpRequestContext): Promise { + const { response, query, body } = context; + + const connectionId = query.connectionId; + if (!connectionId) { + this.sendErrorResponse(response, 400, 'Missing connectionId'); + return; + } + + const connection = this.httpConnections.get(connectionId); + if (!connection) { + this.sendErrorResponse(response, 404, 'Connection not found'); + return; + } + + if (!body) { + this.sendErrorResponse(response, 400, 'Missing message body'); + return; + } + + try { + const message = JSON.parse(body) as TransportMessage; + message.senderId = connectionId; + + this.handleMessage(connectionId, message); + + this.sendJsonResponse(response, 200, { + success: true, + message: 'Message sent successfully' + }); + + } catch (error) { + this.sendErrorResponse(response, 400, 'Invalid message format'); + } + } + + /** + * 处理状态请求 + */ + private async handleStatus(context: HttpRequestContext): Promise { + const { response } = context; + + this.sendJsonResponse(response, 200, { + success: true, + status: 'running', + connections: this.httpConnections.size, + uptime: process.uptime(), + serverTime: Date.now() + }); + } + + /** + * 读取请求体 + */ + private readRequestBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = ''; + let totalSize = 0; + + req.on('data', (chunk: Buffer) => { + totalSize += chunk.length; + if (totalSize > this.config.maxRequestSize!) { + reject(new Error('Request body too large')); + return; + } + body += chunk.toString(); + }); + + req.on('end', () => { + resolve(body); + }); + + req.on('error', (error) => { + reject(error); + }); + }); + } + + /** + * 发送长轮询响应 + */ + private sendLongPollResponse(connection: HttpConnectionInfo, messages: TransportMessage[]): void { + if (!connection.longPollResponse || connection.longPollResponse.headersSent) { + return; + } + + // 清理定时器 + if (connection.longPollTimer) { + clearTimeout(connection.longPollTimer); + connection.longPollTimer = undefined; + } + + this.sendJsonResponse(connection.longPollResponse, 200, { + success: true, + messages + }); + + connection.longPollResponse = undefined; + } + + /** + * 设置 CORS 头 + */ + private setCorsHeaders(res: ServerResponse): void { + const origins = this.config.corsOrigins!; + const origin = origins.includes('*') ? '*' : origins[0]; + + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + res.setHeader('Access-Control-Max-Age', '86400'); + } + + /** + * 发送 JSON 响应 + */ + private sendJsonResponse(res: ServerResponse, statusCode: number, data: any): void { + if (res.headersSent) return; + + res.setHeader('Content-Type', 'application/json'); + res.writeHead(statusCode); + res.end(JSON.stringify(data)); + } + + /** + * 发送错误响应 + */ + private sendErrorResponse(res: ServerResponse, statusCode: number, message: string): void { + if (res.headersSent) return; + + this.sendJsonResponse(res, statusCode, { + success: false, + error: message, + code: statusCode + }); + } + + /** + * 清理连接资源 + */ + private cleanupConnection(connectionId: string): void { + const connection = this.httpConnections.get(connectionId); + if (connection) { + if (connection.longPollTimer) { + clearTimeout(connection.longPollTimer); + } + if (connection.longPollResponse && !connection.longPollResponse.headersSent) { + this.sendJsonResponse(connection.longPollResponse, 200, { + success: true, + messages: [], + disconnected: true + }); + } + this.httpConnections.delete(connectionId); + } + } + + /** + * 清理所有资源 + */ + private async cleanup(): Promise { + // 清理所有连接 + for (const connectionId of this.httpConnections.keys()) { + this.cleanupConnection(connectionId); + } + this.clearConnections(); + + // 关闭 HTTP 服务器 + if (this.httpServer) { + await new Promise((resolve) => { + this.httpServer!.close(() => resolve()); + }); + this.httpServer = null; + } + } + + /** + * 获取 HTTP 连接统计信息 + */ + getHttpStats(): { + totalConnections: number; + activeLongPolls: number; + queuedMessages: number; + } { + let activeLongPolls = 0; + let queuedMessages = 0; + + for (const connection of this.httpConnections.values()) { + if (connection.longPollResponse && !connection.longPollResponse.headersSent) { + activeLongPolls++; + } + queuedMessages += connection.messageQueue.length; + } + + return { + totalConnections: this.httpConnections.size, + activeLongPolls, + queuedMessages + }; + } +} \ No newline at end of file diff --git a/packages/network-server/src/core/NetworkServer.ts b/packages/network-server/src/core/NetworkServer.ts new file mode 100644 index 00000000..a180a6d1 --- /dev/null +++ b/packages/network-server/src/core/NetworkServer.ts @@ -0,0 +1,452 @@ +/** + * 网络服务器主类 + * + * 整合 WebSocket 和 HTTP 传输,提供统一的网络服务接口 + */ + +import { EventEmitter } from 'events'; +import { Transport, TransportConfig, TransportMessage } from './Transport'; +import { WebSocketTransport, WebSocketTransportConfig } from './WebSocketTransport'; +import { HttpTransport, HttpTransportConfig } from './HttpTransport'; +import { ClientConnection, ClientConnectionState, ClientPermissions } from './ClientConnection'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; + +/** + * 网络服务器配置 + */ +export interface NetworkServerConfig { + /** 服务器名称 */ + name?: string; + /** WebSocket 配置 */ + websocket?: WebSocketTransportConfig; + /** HTTP 配置 */ + http?: HttpTransportConfig; + /** 默认客户端权限 */ + defaultPermissions?: ClientPermissions; + /** 最大客户端连接数 */ + maxConnections?: number; + /** 客户端认证超时(毫秒) */ + authenticationTimeout?: number; + /** 是否启用统计 */ + enableStats?: boolean; +} + +/** + * 服务器统计信息 + */ +export interface ServerStats { + /** 总连接数 */ + totalConnections: number; + /** 当前活跃连接数 */ + activeConnections: number; + /** 已认证连接数 */ + authenticatedConnections: number; + /** 消息总数 */ + totalMessages: number; + /** 错误总数 */ + totalErrors: number; + /** 服务器启动时间 */ + startTime: Date; + /** 服务器运行时间(毫秒) */ + uptime: number; +} + +/** + * 网络服务器事件 + */ +export interface NetworkServerEvents { + /** 服务器启动 */ + 'server-started': () => void; + /** 服务器停止 */ + 'server-stopped': () => void; + /** 客户端连接 */ + 'client-connected': (client: ClientConnection) => void; + /** 客户端断开连接 */ + 'client-disconnected': (clientId: string, reason?: string) => void; + /** 客户端认证成功 */ + 'client-authenticated': (client: ClientConnection) => void; + /** 收到消息 */ + 'message': (client: ClientConnection, message: TransportMessage) => void; + /** 服务器错误 */ + 'error': (error: Error, clientId?: string) => void; +} + +/** + * 网络服务器主类 + */ +export class NetworkServer extends EventEmitter { + private config: NetworkServerConfig; + private wsTransport: WebSocketTransport | null = null; + private httpTransport: HttpTransport | null = null; + private clients = new Map(); + private isRunning = false; + private stats: ServerStats; + + constructor(config: NetworkServerConfig) { + super(); + + this.config = { + name: 'NetworkServer', + maxConnections: 1000, + authenticationTimeout: 30000, // 30秒 + enableStats: true, + defaultPermissions: { + canJoinRooms: true, + canCreateRooms: false, + canSendRpc: true, + canSyncVars: true + }, + ...config + }; + + this.stats = { + totalConnections: 0, + activeConnections: 0, + authenticatedConnections: 0, + totalMessages: 0, + totalErrors: 0, + startTime: new Date(), + uptime: 0 + }; + + this.initialize(); + } + + /** + * 启动服务器 + */ + async start(): Promise { + if (this.isRunning) { + throw new Error('Server is already running'); + } + + try { + const promises: Promise[] = []; + + // 启动 WebSocket 传输 + if (this.config.websocket && this.wsTransport) { + promises.push(this.wsTransport.start()); + } + + // 启动 HTTP 传输 + if (this.config.http && this.httpTransport) { + promises.push(this.httpTransport.start()); + } + + if (promises.length === 0) { + throw new Error('No transport configured. Please configure at least one transport (WebSocket or HTTP)'); + } + + await Promise.all(promises); + + this.isRunning = true; + this.stats.startTime = new Date(); + + console.log(`Network Server "${this.config.name}" started successfully`); + if (this.config.websocket) { + console.log(`- WebSocket: ws://${this.config.websocket.host || 'localhost'}:${this.config.websocket.port}${this.config.websocket.path || '/ws'}`); + } + if (this.config.http) { + console.log(`- HTTP: http://${this.config.http.host || 'localhost'}:${this.config.http.port}${this.config.http.apiPrefix || '/api'}`); + } + + this.emit('server-started'); + + } catch (error) { + await this.stop(); + throw error; + } + } + + /** + * 停止服务器 + */ + async stop(): Promise { + if (!this.isRunning) { + return; + } + + this.isRunning = false; + + // 断开所有客户端 + const clients = Array.from(this.clients.values()); + for (const client of clients) { + client.disconnect('server-shutdown'); + } + + // 停止传输层 + const promises: Promise[] = []; + + if (this.wsTransport) { + promises.push(this.wsTransport.stop()); + } + + if (this.httpTransport) { + promises.push(this.httpTransport.stop()); + } + + await Promise.all(promises); + + console.log(`Network Server "${this.config.name}" stopped`); + this.emit('server-stopped'); + } + + /** + * 获取服务器配置 + */ + getConfig(): Readonly { + return this.config; + } + + /** + * 获取服务器统计信息 + */ + getStats(): ServerStats { + this.stats.uptime = Date.now() - this.stats.startTime.getTime(); + this.stats.activeConnections = this.clients.size; + this.stats.authenticatedConnections = Array.from(this.clients.values()) + .filter(client => client.isAuthenticated).length; + + return { ...this.stats }; + } + + /** + * 获取所有客户端连接 + */ + getClients(): ClientConnection[] { + return Array.from(this.clients.values()); + } + + /** + * 获取指定客户端连接 + */ + getClient(clientId: string): ClientConnection | undefined { + return this.clients.get(clientId); + } + + /** + * 检查客户端是否存在 + */ + hasClient(clientId: string): boolean { + return this.clients.has(clientId); + } + + /** + * 获取客户端数量 + */ + getClientCount(): number { + return this.clients.size; + } + + /** + * 发送消息给指定客户端 + */ + async sendToClient(clientId: string, message: TransportMessage): Promise { + const client = this.clients.get(clientId); + if (!client) { + return false; + } + + return await client.sendMessage(message); + } + + /** + * 广播消息给所有客户端 + */ + async broadcast(message: TransportMessage, excludeId?: string): Promise { + const promises = Array.from(this.clients.entries()) + .filter(([clientId]) => clientId !== excludeId) + .map(([, client]) => client.sendMessage(message)); + + const results = await Promise.allSettled(promises); + return results.filter(result => result.status === 'fulfilled' && result.value).length; + } + + /** + * 发送消息给指定房间的所有客户端 + */ + async broadcastToRoom(roomId: string, message: TransportMessage, excludeId?: string): Promise { + const roomClients = Array.from(this.clients.values()) + .filter(client => client.currentRoomId === roomId && client.id !== excludeId); + + const promises = roomClients.map(client => client.sendMessage(message)); + const results = await Promise.allSettled(promises); + + return results.filter(result => result.status === 'fulfilled' && result.value).length; + } + + /** + * 断开指定客户端连接 + */ + async disconnectClient(clientId: string, reason?: string): Promise { + const client = this.clients.get(clientId); + if (client) { + client.disconnect(reason); + } + } + + /** + * 获取在指定房间的客户端列表 + */ + getClientsInRoom(roomId: string): ClientConnection[] { + return Array.from(this.clients.values()) + .filter(client => client.currentRoomId === roomId); + } + + /** + * 检查服务器是否正在运行 + */ + isServerRunning(): boolean { + return this.isRunning; + } + + /** + * 初始化服务器 + */ + private initialize(): void { + // 初始化 WebSocket 传输 + if (this.config.websocket) { + this.wsTransport = new WebSocketTransport(this.config.websocket); + this.setupTransportEvents(this.wsTransport); + } + + // 初始化 HTTP 传输 + if (this.config.http) { + this.httpTransport = new HttpTransport(this.config.http); + this.setupTransportEvents(this.httpTransport); + } + } + + /** + * 设置传输层事件监听 + */ + private setupTransportEvents(transport: Transport): void { + transport.on('client-connected', (connectionInfo) => { + this.handleClientConnected(connectionInfo.id, connectionInfo.remoteAddress || 'unknown', transport); + }); + + transport.on('client-disconnected', (connectionId, reason) => { + this.handleClientDisconnected(connectionId, reason); + }); + + transport.on('message', (connectionId, message) => { + this.handleTransportMessage(connectionId, message); + }); + + transport.on('error', (error, connectionId) => { + this.handleTransportError(error, connectionId); + }); + } + + /** + * 处理客户端连接 + */ + private handleClientConnected(connectionId: string, remoteAddress: string, transport: Transport): void { + // 检查连接数限制 + if (this.config.maxConnections && this.clients.size >= this.config.maxConnections) { + transport.disconnectClient(connectionId, 'Max connections reached'); + return; + } + + const client = new ClientConnection( + connectionId, + remoteAddress, + (message) => transport.sendToClient(connectionId, message), + { + connectionTimeout: this.config.authenticationTimeout, + permissions: this.config.defaultPermissions + } + ); + + // 设置客户端事件监听 + this.setupClientEvents(client); + + this.clients.set(connectionId, client); + this.stats.totalConnections++; + + console.log(`Client connected: ${connectionId} from ${remoteAddress}`); + this.emit('client-connected', client); + } + + /** + * 处理客户端断开连接 + */ + private handleClientDisconnected(connectionId: string, reason?: string): void { + const client = this.clients.get(connectionId); + if (client) { + client.destroy(); + this.clients.delete(connectionId); + + console.log(`Client disconnected: ${connectionId}, reason: ${reason || 'unknown'}`); + this.emit('client-disconnected', connectionId, reason); + } + } + + /** + * 处理传输层消息 + */ + private handleTransportMessage(connectionId: string, message: TransportMessage): void { + const client = this.clients.get(connectionId); + if (!client) { + return; + } + + client.handleMessage(message); + this.stats.totalMessages++; + + this.emit('message', client, message); + } + + /** + * 处理传输层错误 + */ + private handleTransportError(error: Error, connectionId?: string): void { + this.stats.totalErrors++; + + console.error(`Transport error${connectionId ? ` (client: ${connectionId})` : ''}:`, error.message); + this.emit('error', error, connectionId); + + // 如果是特定客户端的错误,断开该客户端 + if (connectionId) { + this.disconnectClient(connectionId, 'transport-error'); + } + } + + /** + * 设置客户端事件监听 + */ + private setupClientEvents(client: ClientConnection): void { + client.on('authenticated', (userData) => { + console.log(`Client authenticated: ${client.id}`, userData); + this.emit('client-authenticated', client); + }); + + client.on('error', (error) => { + console.error(`Client error (${client.id}):`, error.message); + this.emit('error', error, client.id); + }); + + client.on('timeout', () => { + console.log(`Client timeout: ${client.id}`); + this.disconnectClient(client.id, 'timeout'); + }); + + client.on('state-changed', (oldState, newState) => { + console.log(`Client ${client.id} state changed: ${oldState} -> ${newState}`); + }); + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: NetworkServerEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-server/src/core/Transport.ts b/packages/network-server/src/core/Transport.ts new file mode 100644 index 00000000..8a8f54a5 --- /dev/null +++ b/packages/network-server/src/core/Transport.ts @@ -0,0 +1,224 @@ +/** + * 网络传输层抽象接口 + */ + +import { EventEmitter } from 'events'; +import { NetworkMessage, NetworkValue } from '@esengine/ecs-framework-network-shared'; + +/** + * 传输层配置 + */ +export interface TransportConfig { + /** 服务器端口 */ + port: number; + /** 主机地址 */ + host?: string; + /** 最大连接数 */ + maxConnections?: number; + /** 心跳间隔(毫秒) */ + heartbeatInterval?: number; + /** 连接超时(毫秒) */ + connectionTimeout?: number; +} + +/** + * 客户端连接信息 + */ +export interface ClientConnectionInfo { + /** 连接ID */ + id: string; + /** 客户端IP */ + remoteAddress?: string; + /** 连接时间 */ + connectedAt: Date; + /** 最后活跃时间 */ + lastActivity: Date; + /** 用户数据 */ + userData?: Record; +} + +/** + * 网络消息包装 + */ +export interface TransportMessage { + /** 消息类型 */ + type: 'rpc' | 'syncvar' | 'system' | 'custom'; + /** 消息数据 */ + data: NetworkValue; + /** 发送者ID */ + senderId?: string; + /** 目标客户端ID(可选,用于单播) */ + targetId?: string; + /** 是否可靠传输 */ + reliable?: boolean; +} + +/** + * 网络传输层事件 + */ +export interface TransportEvents { + /** 客户端连接 */ + 'client-connected': (connectionInfo: ClientConnectionInfo) => void; + /** 客户端断开连接 */ + 'client-disconnected': (connectionId: string, reason?: string) => void; + /** 收到消息 */ + 'message': (connectionId: string, message: TransportMessage) => void; + /** 传输错误 */ + 'error': (error: Error, connectionId?: string) => void; + /** 服务器启动 */ + 'server-started': (config: TransportConfig) => void; + /** 服务器关闭 */ + 'server-stopped': () => void; +} + +/** + * 网络传输层抽象类 + */ +export abstract class Transport extends EventEmitter { + protected config: TransportConfig; + protected isRunning = false; + protected connections = new Map(); + + constructor(config: TransportConfig) { + super(); + this.config = config; + } + + /** + * 启动传输层服务 + */ + abstract start(): Promise; + + /** + * 停止传输层服务 + */ + abstract stop(): Promise; + + /** + * 发送消息给指定客户端 + */ + abstract sendToClient(connectionId: string, message: TransportMessage): Promise; + + /** + * 广播消息给所有客户端 + */ + abstract broadcast(message: TransportMessage, excludeId?: string): Promise; + + /** + * 广播消息给指定客户端列表 + */ + abstract sendToClients(connectionIds: string[], message: TransportMessage): Promise; + + /** + * 断开指定客户端连接 + */ + abstract disconnectClient(connectionId: string, reason?: string): Promise; + + /** + * 获取在线客户端数量 + */ + getConnectionCount(): number { + return this.connections.size; + } + + /** + * 获取所有连接信息 + */ + getConnections(): ClientConnectionInfo[] { + return Array.from(this.connections.values()); + } + + /** + * 获取指定连接信息 + */ + getConnection(connectionId: string): ClientConnectionInfo | undefined { + return this.connections.get(connectionId); + } + + /** + * 检查连接是否存在 + */ + hasConnection(connectionId: string): boolean { + return this.connections.has(connectionId); + } + + /** + * 服务器是否正在运行 + */ + isServerRunning(): boolean { + return this.isRunning; + } + + /** + * 获取传输层配置 + */ + getConfig(): TransportConfig { + return { ...this.config }; + } + + /** + * 更新客户端最后活跃时间 + */ + protected updateClientActivity(connectionId: string): void { + const connection = this.connections.get(connectionId); + if (connection) { + connection.lastActivity = new Date(); + } + } + + /** + * 添加客户端连接 + */ + protected addConnection(connectionInfo: ClientConnectionInfo): void { + this.connections.set(connectionInfo.id, connectionInfo); + this.emit('client-connected', connectionInfo); + } + + /** + * 移除客户端连接 + */ + protected removeConnection(connectionId: string, reason?: string): void { + if (this.connections.delete(connectionId)) { + this.emit('client-disconnected', connectionId, reason); + } + } + + /** + * 处理接收到的消息 + */ + protected handleMessage(connectionId: string, message: TransportMessage): void { + this.updateClientActivity(connectionId); + this.emit('message', connectionId, message); + } + + /** + * 处理传输错误 + */ + protected handleError(error: Error, connectionId?: string): void { + this.emit('error', error, connectionId); + } + + /** + * 清理所有连接 + */ + protected clearConnections(): void { + const connectionIds = Array.from(this.connections.keys()); + for (const id of connectionIds) { + this.removeConnection(id, 'server-shutdown'); + } + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: TransportEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-server/src/core/WebSocketTransport.ts b/packages/network-server/src/core/WebSocketTransport.ts new file mode 100644 index 00000000..150bb281 --- /dev/null +++ b/packages/network-server/src/core/WebSocketTransport.ts @@ -0,0 +1,406 @@ +/** + * WebSocket 传输层实现 + */ + +import { WebSocketServer, WebSocket } from 'ws'; +import { createServer, Server as HttpServer } from 'http'; +import { v4 as uuidv4 } from 'uuid'; +import { Transport, TransportConfig, ClientConnectionInfo, TransportMessage } from './Transport'; + +/** + * WebSocket 传输配置 + */ +export interface WebSocketTransportConfig extends TransportConfig { + /** WebSocket 路径 */ + path?: string; + /** 是否启用压缩 */ + compression?: boolean; + /** 最大消息大小(字节) */ + maxMessageSize?: number; + /** ping 间隔(毫秒) */ + pingInterval?: number; + /** pong 超时(毫秒) */ + pongTimeout?: number; +} + +/** + * WebSocket 客户端连接扩展信息 + */ +interface WebSocketConnectionInfo extends ClientConnectionInfo { + /** WebSocket 实例 */ + socket: WebSocket; + /** ping 定时器 */ + pingTimer?: NodeJS.Timeout; + /** pong 超时定时器 */ + pongTimer?: NodeJS.Timeout; +} + +/** + * WebSocket 传输层实现 + */ +export class WebSocketTransport extends Transport { + private httpServer: HttpServer | null = null; + private wsServer: WebSocketServer | null = null; + private wsConnections = new Map(); + + protected override config: WebSocketTransportConfig; + + constructor(config: WebSocketTransportConfig) { + super(config); + this.config = { + path: '/ws', + compression: true, + maxMessageSize: 1024 * 1024, // 1MB + pingInterval: 30000, // 30秒 + pongTimeout: 5000, // 5秒 + heartbeatInterval: 30000, + connectionTimeout: 60000, + maxConnections: 1000, + ...config + }; + } + + /** + * 启动 WebSocket 服务器 + */ + async start(): Promise { + if (this.isRunning) { + throw new Error('WebSocket transport is already running'); + } + + try { + // 创建 HTTP 服务器 + this.httpServer = createServer(); + + // 创建 WebSocket 服务器 + this.wsServer = new WebSocketServer({ + server: this.httpServer, + path: this.config.path, + maxPayload: this.config.maxMessageSize, + perMessageDeflate: this.config.compression + }); + + // 设置事件监听 + this.setupEventListeners(); + + // 启动服务器 + await new Promise((resolve, reject) => { + this.httpServer!.listen(this.config.port, this.config.host, (error?: Error) => { + if (error) { + reject(error); + } else { + this.isRunning = true; + resolve(); + } + }); + }); + + this.emit('server-started', this.config); + } catch (error) { + await this.cleanup(); + throw error; + } + } + + /** + * 停止 WebSocket 服务器 + */ + async stop(): Promise { + if (!this.isRunning) { + return; + } + + this.isRunning = false; + + // 断开所有客户端连接 + for (const [connectionId, connection] of this.wsConnections) { + this.disconnectClient(connectionId, 'server-shutdown'); + } + + await this.cleanup(); + this.emit('server-stopped'); + } + + /** + * 发送消息给指定客户端 + */ + async sendToClient(connectionId: string, message: TransportMessage): Promise { + const connection = this.wsConnections.get(connectionId); + if (!connection || connection.socket.readyState !== WebSocket.OPEN) { + return false; + } + + try { + const data = JSON.stringify(message); + connection.socket.send(data); + this.updateClientActivity(connectionId); + return true; + } catch (error) { + this.handleError(error as Error, connectionId); + return false; + } + } + + /** + * 广播消息给所有客户端 + */ + async broadcast(message: TransportMessage, excludeId?: string): Promise { + const data = JSON.stringify(message); + let sentCount = 0; + + for (const [connectionId, connection] of this.wsConnections) { + if (excludeId && connectionId === excludeId) { + continue; + } + + if (connection.socket.readyState === WebSocket.OPEN) { + try { + connection.socket.send(data); + sentCount++; + } catch (error) { + this.handleError(error as Error, connectionId); + } + } + } + + return sentCount; + } + + /** + * 发送消息给指定客户端列表 + */ + async sendToClients(connectionIds: string[], message: TransportMessage): Promise { + const data = JSON.stringify(message); + let sentCount = 0; + + for (const connectionId of connectionIds) { + const connection = this.wsConnections.get(connectionId); + if (connection && connection.socket.readyState === WebSocket.OPEN) { + try { + connection.socket.send(data); + sentCount++; + } catch (error) { + this.handleError(error as Error, connectionId); + } + } + } + + return sentCount; + } + + /** + * 断开指定客户端连接 + */ + async disconnectClient(connectionId: string, reason?: string): Promise { + const connection = this.wsConnections.get(connectionId); + if (connection) { + this.cleanupConnection(connectionId); + connection.socket.close(1000, reason); + } + } + + /** + * 设置事件监听器 + */ + private setupEventListeners(): void { + if (!this.wsServer) return; + + this.wsServer.on('connection', (socket: WebSocket, request) => { + this.handleNewConnection(socket, request); + }); + + this.wsServer.on('error', (error: Error) => { + this.handleError(error); + }); + + if (this.httpServer) { + this.httpServer.on('error', (error: Error) => { + this.handleError(error); + }); + } + } + + /** + * 处理新连接 + */ + private handleNewConnection(socket: WebSocket, request: any): void { + // 检查连接数限制 + if (this.config.maxConnections && this.wsConnections.size >= this.config.maxConnections) { + socket.close(1013, 'Too many connections'); + return; + } + + const connectionId = uuidv4(); + const remoteAddress = request.socket.remoteAddress || request.headers['x-forwarded-for'] || 'unknown'; + + const connectionInfo: WebSocketConnectionInfo = { + id: connectionId, + socket, + remoteAddress: Array.isArray(remoteAddress) ? remoteAddress[0] : remoteAddress, + connectedAt: new Date(), + lastActivity: new Date(), + userData: {} + }; + + this.wsConnections.set(connectionId, connectionInfo); + this.addConnection(connectionInfo); + + // 设置 socket 事件监听 + socket.on('message', (data: Buffer) => { + this.handleClientMessage(connectionId, data); + }); + + socket.on('close', (code: number, reason: Buffer) => { + this.handleClientDisconnect(connectionId, code, reason.toString()); + }); + + socket.on('error', (error: Error) => { + this.handleError(error, connectionId); + this.handleClientDisconnect(connectionId, 1006, 'Socket error'); + }); + + socket.on('pong', () => { + this.handlePong(connectionId); + }); + + // 启动心跳检测 + this.startHeartbeat(connectionId); + } + + /** + * 处理客户端消息 + */ + private handleClientMessage(connectionId: string, data: Buffer): void { + try { + const message = JSON.parse(data.toString()) as TransportMessage; + message.senderId = connectionId; + this.handleMessage(connectionId, message); + } catch (error) { + this.handleError(new Error(`Invalid message format from client ${connectionId}`), connectionId); + } + } + + /** + * 处理客户端断开连接 + */ + private handleClientDisconnect(connectionId: string, code: number, reason: string): void { + this.cleanupConnection(connectionId); + this.removeConnection(connectionId, `${code}: ${reason}`); + } + + /** + * 启动心跳检测 + */ + private startHeartbeat(connectionId: string): void { + const connection = this.wsConnections.get(connectionId); + if (!connection) return; + + if (this.config.pingInterval && this.config.pingInterval > 0) { + connection.pingTimer = setInterval(() => { + this.sendPing(connectionId); + }, this.config.pingInterval); + } + } + + /** + * 发送 ping + */ + private sendPing(connectionId: string): void { + const connection = this.wsConnections.get(connectionId); + if (!connection || connection.socket.readyState !== WebSocket.OPEN) { + return; + } + + connection.socket.ping(); + + // 设置 pong 超时 + if (this.config.pongTimeout && this.config.pongTimeout > 0) { + if (connection.pongTimer) { + clearTimeout(connection.pongTimer); + } + + connection.pongTimer = setTimeout(() => { + this.disconnectClient(connectionId, 'Pong timeout'); + }, this.config.pongTimeout); + } + } + + /** + * 处理 pong 响应 + */ + private handlePong(connectionId: string): void { + const connection = this.wsConnections.get(connectionId); + if (connection && connection.pongTimer) { + clearTimeout(connection.pongTimer); + connection.pongTimer = undefined; + } + this.updateClientActivity(connectionId); + } + + /** + * 清理连接资源 + */ + private cleanupConnection(connectionId: string): void { + const connection = this.wsConnections.get(connectionId); + if (connection) { + if (connection.pingTimer) { + clearInterval(connection.pingTimer); + } + if (connection.pongTimer) { + clearTimeout(connection.pongTimer); + } + this.wsConnections.delete(connectionId); + } + } + + /** + * 清理所有资源 + */ + private async cleanup(): Promise { + // 清理所有连接 + for (const connectionId of this.wsConnections.keys()) { + this.cleanupConnection(connectionId); + } + this.clearConnections(); + + // 关闭 WebSocket 服务器 + if (this.wsServer) { + this.wsServer.close(); + this.wsServer = null; + } + + // 关闭 HTTP 服务器 + if (this.httpServer) { + await new Promise((resolve) => { + this.httpServer!.close(() => resolve()); + }); + this.httpServer = null; + } + } + + /** + * 获取 WebSocket 连接统计信息 + */ + getWebSocketStats(): { + totalConnections: number; + activeConnections: number; + inactiveConnections: number; + } { + let activeConnections = 0; + let inactiveConnections = 0; + + for (const connection of this.wsConnections.values()) { + if (connection.socket.readyState === WebSocket.OPEN) { + activeConnections++; + } else { + inactiveConnections++; + } + } + + return { + totalConnections: this.wsConnections.size, + activeConnections, + inactiveConnections + }; + } +} \ No newline at end of file diff --git a/packages/network-server/src/core/index.ts b/packages/network-server/src/core/index.ts new file mode 100644 index 00000000..32e8f6e3 --- /dev/null +++ b/packages/network-server/src/core/index.ts @@ -0,0 +1,9 @@ +/** + * 核心模块导出 + */ + +export * from './Transport'; +export * from './WebSocketTransport'; +export * from './HttpTransport'; +export * from './ClientConnection'; +export * from './NetworkServer'; \ No newline at end of file diff --git a/packages/network-server/src/index.ts b/packages/network-server/src/index.ts new file mode 100644 index 00000000..aec17655 --- /dev/null +++ b/packages/network-server/src/index.ts @@ -0,0 +1,79 @@ +/** + * ECS Framework Network Server + * + * 提供完整的网络服务端功能,包括: + * - WebSocket 和 HTTP 传输层 + * - 客户端连接管理 + * - 房间系统 + * - 身份验证和权限管理 + * - SyncVar 和 RPC 系统 + * - 消息验证 + */ + +// 核心模块 +export * from './core'; + +// 房间系统 +export * from './rooms'; + +// 认证系统 +export * from './auth'; + +// 网络系统 +export * from './systems'; + +// 验证系统 +export * from './validation'; + +// 版本信息 +export const VERSION = '1.0.0'; + +// 导出常用组合配置 +export interface ServerConfigPreset { + /** 服务器名称 */ + name: string; + /** WebSocket 端口 */ + wsPort: number; + /** HTTP 端口(可选) */ + httpPort?: number; + /** 最大连接数 */ + maxConnections: number; + /** 是否启用认证 */ + enableAuth: boolean; + /** 是否启用房间系统 */ + enableRooms: boolean; +} + +/** + * 预定义服务器配置 + */ +export const ServerPresets = { + /** 开发环境配置 */ + Development: { + name: 'Development Server', + wsPort: 8080, + httpPort: 3000, + maxConnections: 100, + enableAuth: false, + enableRooms: true + } as ServerConfigPreset, + + /** 生产环境配置 */ + Production: { + name: 'Production Server', + wsPort: 443, + httpPort: 80, + maxConnections: 10000, + enableAuth: true, + enableRooms: true + } as ServerConfigPreset, + + /** 测试环境配置 */ + Testing: { + name: 'Test Server', + wsPort: 9090, + maxConnections: 10, + enableAuth: false, + enableRooms: false + } as ServerConfigPreset +}; \ No newline at end of file diff --git a/packages/network-server/src/rooms/Room.ts b/packages/network-server/src/rooms/Room.ts new file mode 100644 index 00000000..56bd62e9 --- /dev/null +++ b/packages/network-server/src/rooms/Room.ts @@ -0,0 +1,637 @@ +/** + * 房间管理 + * + * 类似于 Unity Mirror 的 Scene 概念,管理一组客户端和网络对象 + */ + +import { EventEmitter } from 'events'; +import { Entity, Scene } from '@esengine/ecs-framework'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { ClientConnection } from '../core/ClientConnection'; +import { TransportMessage } from '../core/Transport'; + +/** + * 房间状态 + */ +export enum RoomState { + /** 创建中 */ + CREATING = 'creating', + /** 活跃状态 */ + ACTIVE = 'active', + /** 暂停状态 */ + PAUSED = 'paused', + /** 关闭中 */ + CLOSING = 'closing', + /** 已关闭 */ + CLOSED = 'closed' +} + +/** + * 房间配置 + */ +export interface RoomConfig { + /** 房间ID */ + id: string; + /** 房间名称 */ + name: string; + /** 房间描述 */ + description?: string; + /** 最大玩家数 */ + maxPlayers: number; + /** 是否私有房间 */ + isPrivate?: boolean; + /** 房间密码 */ + password?: string; + /** 房间元数据 */ + metadata?: Record; + /** 是否持久化 */ + persistent?: boolean; + /** 房间过期时间(毫秒) */ + expirationTime?: number; +} + +/** + * 玩家数据 + */ +export interface PlayerData { + /** 客户端连接 */ + client: ClientConnection; + /** 加入时间 */ + joinedAt: Date; + /** 是否为房主 */ + isOwner: boolean; + /** 玩家自定义数据 */ + customData: Record; +} + +/** + * 房间统计信息 + */ +export interface RoomStats { + /** 当前玩家数 */ + currentPlayers: number; + /** 最大玩家数 */ + maxPlayers: number; + /** 总加入过的玩家数 */ + totalPlayersJoined: number; + /** 消息总数 */ + totalMessages: number; + /** 创建时间 */ + createdAt: Date; + /** 房间存活时间(毫秒) */ + lifetime: number; +} + +/** + * 房间事件 + */ +export interface RoomEvents { + /** 玩家加入 */ + 'player-joined': (player: PlayerData) => void; + /** 玩家离开 */ + 'player-left': (clientId: string, reason?: string) => void; + /** 房主变更 */ + 'owner-changed': (newOwnerId: string, oldOwnerId?: string) => void; + /** 房间状态变化 */ + 'state-changed': (oldState: RoomState, newState: RoomState) => void; + /** 收到消息 */ + 'message': (clientId: string, message: TransportMessage) => void; + /** 房间更新 */ + 'room-updated': (updatedFields: Partial) => void; + /** 房间错误 */ + 'error': (error: Error, clientId?: string) => void; + /** 房间即将关闭 */ + 'closing': (reason: string) => void; + /** 房间已关闭 */ + 'closed': (reason: string) => void; +} + +/** + * 房间类 + */ +export class Room extends EventEmitter { + private config: RoomConfig; + private state: RoomState = RoomState.CREATING; + private players = new Map(); + private ownerId: string | null = null; + private ecsScene: Scene | null = null; + private stats: RoomStats; + private expirationTimer: NodeJS.Timeout | null = null; + + constructor(config: RoomConfig) { + super(); + + this.config = { ...config }; + this.stats = { + currentPlayers: 0, + maxPlayers: config.maxPlayers, + totalPlayersJoined: 0, + totalMessages: 0, + createdAt: new Date(), + lifetime: 0 + }; + + this.initialize(); + } + + /** + * 获取房间ID + */ + get id(): string { + return this.config.id; + } + + /** + * 获取房间名称 + */ + get name(): string { + return this.config.name; + } + + /** + * 获取房间状态 + */ + get currentState(): RoomState { + return this.state; + } + + /** + * 获取房间配置 + */ + getConfig(): Readonly { + return this.config; + } + + /** + * 获取房间统计信息 + */ + getStats(): RoomStats { + this.stats.lifetime = Date.now() - this.stats.createdAt.getTime(); + this.stats.currentPlayers = this.players.size; + return { ...this.stats }; + } + + /** + * 获取所有玩家 + */ + getPlayers(): PlayerData[] { + return Array.from(this.players.values()); + } + + /** + * 获取指定玩家 + */ + getPlayer(clientId: string): PlayerData | undefined { + return this.players.get(clientId); + } + + /** + * 检查玩家是否在房间中 + */ + hasPlayer(clientId: string): boolean { + return this.players.has(clientId); + } + + /** + * 获取当前玩家数量 + */ + getPlayerCount(): number { + return this.players.size; + } + + /** + * 检查房间是否已满 + */ + isFull(): boolean { + return this.players.size >= this.config.maxPlayers; + } + + /** + * 检查房间是否为空 + */ + isEmpty(): boolean { + return this.players.size === 0; + } + + /** + * 获取房主 + */ + getOwner(): PlayerData | undefined { + return this.ownerId ? this.players.get(this.ownerId) : undefined; + } + + /** + * 获取 ECS 场景 + */ + getEcsScene(): Scene | null { + return this.ecsScene; + } + + /** + * 玩家加入房间 + */ + async addPlayer(client: ClientConnection, customData: Record = {}): Promise { + if (this.state !== RoomState.ACTIVE) { + throw new Error(`Cannot join room in state: ${this.state}`); + } + + if (this.hasPlayer(client.id)) { + throw new Error(`Player ${client.id} is already in the room`); + } + + if (this.isFull()) { + throw new Error('Room is full'); + } + + // 检查房间密码 + if (this.config.isPrivate && this.config.password) { + const providedPassword = customData.password as string; + if (providedPassword !== this.config.password) { + throw new Error('Invalid room password'); + } + } + + const isFirstPlayer = this.isEmpty(); + const playerData: PlayerData = { + client, + joinedAt: new Date(), + isOwner: isFirstPlayer, + customData: { ...customData } + }; + + this.players.set(client.id, playerData); + client.joinRoom(this.id); + + // 设置房主 + if (isFirstPlayer) { + this.ownerId = client.id; + } + + this.stats.totalPlayersJoined++; + + // 通知其他玩家 + await this.broadcast({ + type: 'system', + data: { + action: 'player-joined', + playerId: client.id, + playerData: { + id: client.id, + joinedAt: playerData.joinedAt.toISOString(), + isOwner: playerData.isOwner, + customData: playerData.customData + } + } + }, client.id); + + console.log(`Player ${client.id} joined room ${this.id}`); + this.emit('player-joined', playerData); + + return true; + } + + /** + * 玩家离开房间 + */ + async removePlayer(clientId: string, reason?: string): Promise { + const player = this.players.get(clientId); + if (!player) { + return false; + } + + this.players.delete(clientId); + player.client.leaveRoom(); + + // 如果离开的是房主,转移房主权限 + if (this.ownerId === clientId) { + await this.transferOwnership(); + } + + // 通知其他玩家 + await this.broadcast({ + type: 'system', + data: { + action: 'player-left', + playerId: clientId, + reason: reason || 'unknown' + } + }); + + console.log(`Player ${clientId} left room ${this.id}, reason: ${reason || 'unknown'}`); + this.emit('player-left', clientId, reason); + + // 如果房间为空,考虑关闭 + if (this.isEmpty() && !this.config.persistent) { + await this.close('empty-room'); + } + + return true; + } + + /** + * 转移房主权限 + */ + async transferOwnership(newOwnerId?: string): Promise { + const oldOwnerId = this.ownerId; + + if (newOwnerId) { + const newOwner = this.players.get(newOwnerId); + if (!newOwner) { + return false; + } + this.ownerId = newOwnerId; + newOwner.isOwner = true; + } else { + // 自动选择下一个玩家作为房主 + const players = Array.from(this.players.values()); + if (players.length > 0) { + const newOwner = players[0]; + this.ownerId = newOwner.client.id; + newOwner.isOwner = true; + } else { + this.ownerId = null; + } + } + + // 更新旧房主状态 + if (oldOwnerId) { + const oldOwner = this.players.get(oldOwnerId); + if (oldOwner) { + oldOwner.isOwner = false; + } + } + + // 通知所有玩家房主变更 + if (this.ownerId) { + await this.broadcast({ + type: 'system', + data: { + action: 'owner-changed', + newOwnerId: this.ownerId, + oldOwnerId: oldOwnerId || '' + } + }); + + console.log(`Room ${this.id} ownership transferred from ${oldOwnerId || 'none'} to ${this.ownerId}`); + this.emit('owner-changed', this.ownerId, oldOwnerId || undefined); + } + + return true; + } + + /** + * 广播消息给房间内所有玩家 + */ + async broadcast(message: TransportMessage, excludeClientId?: string): Promise { + const players = Array.from(this.players.values()) + .filter(player => player.client.id !== excludeClientId); + + const promises = players.map(player => player.client.sendMessage(message)); + const results = await Promise.allSettled(promises); + + return results.filter(result => result.status === 'fulfilled' && result.value).length; + } + + /** + * 发送消息给指定玩家 + */ + async sendToPlayer(clientId: string, message: TransportMessage): Promise { + const player = this.players.get(clientId); + if (!player) { + return false; + } + + return await player.client.sendMessage(message); + } + + /** + * 处理玩家消息 + */ + async handleMessage(clientId: string, message: TransportMessage): Promise { + if (!this.hasPlayer(clientId)) { + return; + } + + this.stats.totalMessages++; + this.emit('message', clientId, message); + + // 根据消息类型进行处理 + switch (message.type) { + case 'rpc': + await this.handleRpcMessage(clientId, message); + break; + case 'syncvar': + await this.handleSyncVarMessage(clientId, message); + break; + case 'system': + await this.handleSystemMessage(clientId, message); + break; + default: + // 转发自定义消息 + await this.broadcast(message, clientId); + break; + } + } + + /** + * 更新房间配置 + */ + async updateConfig(updates: Partial): Promise { + // 验证更新 + if (updates.maxPlayers !== undefined && updates.maxPlayers < this.players.size) { + throw new Error('Cannot reduce maxPlayers below current player count'); + } + + const oldConfig = { ...this.config }; + Object.assign(this.config, updates); + + // 通知所有玩家房间更新 + await this.broadcast({ + type: 'system', + data: { + action: 'room-updated', + updates + } + }); + + this.emit('room-updated', updates); + } + + /** + * 暂停房间 + */ + async pause(): Promise { + if (this.state === RoomState.ACTIVE) { + this.setState(RoomState.PAUSED); + + await this.broadcast({ + type: 'system', + data: { + action: 'room-paused' + } + }); + } + } + + /** + * 恢复房间 + */ + async resume(): Promise { + if (this.state === RoomState.PAUSED) { + this.setState(RoomState.ACTIVE); + + await this.broadcast({ + type: 'system', + data: { + action: 'room-resumed' + } + }); + } + } + + /** + * 关闭房间 + */ + async close(reason: string = 'server-shutdown'): Promise { + if (this.state === RoomState.CLOSED || this.state === RoomState.CLOSING) { + return; + } + + this.setState(RoomState.CLOSING); + this.emit('closing', reason); + + // 通知所有玩家房间即将关闭 + await this.broadcast({ + type: 'system', + data: { + action: 'room-closing', + reason + } + }); + + // 移除所有玩家 + const playerIds = Array.from(this.players.keys()); + for (const clientId of playerIds) { + await this.removePlayer(clientId, 'room-closed'); + } + + this.cleanup(); + this.setState(RoomState.CLOSED); + + console.log(`Room ${this.id} closed, reason: ${reason}`); + this.emit('closed', reason); + } + + /** + * 初始化房间 + */ + private initialize(): void { + // 创建 ECS 场景 + this.ecsScene = new Scene(); + + // 设置过期定时器 + if (this.config.expirationTime && this.config.expirationTime > 0) { + this.expirationTimer = setTimeout(() => { + this.close('expired'); + }, this.config.expirationTime); + } + + this.setState(RoomState.ACTIVE); + } + + /** + * 处理 RPC 消息 + */ + private async handleRpcMessage(clientId: string, message: TransportMessage): Promise { + // RPC 消息处理逻辑 + // 这里可以添加权限检查、速率限制等 + await this.broadcast(message, clientId); + } + + /** + * 处理 SyncVar 消息 + */ + private async handleSyncVarMessage(clientId: string, message: TransportMessage): Promise { + // SyncVar 消息处理逻辑 + // 这里可以添加权限检查、数据验证等 + await this.broadcast(message, clientId); + } + + /** + * 处理系统消息 + */ + private async handleSystemMessage(clientId: string, message: TransportMessage): Promise { + const data = message.data as any; + + switch (data.action) { + case 'request-ownership': + // 处理房主权限转移请求 + if (this.ownerId === clientId) { + await this.transferOwnership(data.newOwnerId); + } + break; + // 其他系统消息处理... + } + } + + /** + * 设置房间状态 + */ + private setState(newState: RoomState): void { + const oldState = this.state; + if (oldState !== newState) { + this.state = newState; + this.emit('state-changed', oldState, newState); + } + } + + /** + * 清理资源 + */ + private cleanup(): void { + if (this.expirationTimer) { + clearTimeout(this.expirationTimer); + this.expirationTimer = null; + } + + this.removeAllListeners(); + + if (this.ecsScene) { + this.ecsScene = null; + } + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: RoomEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } + + /** + * 序列化房间信息 + */ + toJSON(): object { + return { + id: this.id, + name: this.name, + state: this.state, + config: this.config, + stats: this.getStats(), + players: this.getPlayers().map(player => ({ + id: player.client.id, + joinedAt: player.joinedAt.toISOString(), + isOwner: player.isOwner, + customData: player.customData + })), + ownerId: this.ownerId + }; + } +} \ No newline at end of file diff --git a/packages/network-server/src/rooms/RoomManager.ts b/packages/network-server/src/rooms/RoomManager.ts new file mode 100644 index 00000000..63d1bc1c --- /dev/null +++ b/packages/network-server/src/rooms/RoomManager.ts @@ -0,0 +1,499 @@ +/** + * 房间管理器 + * + * 管理所有房间的创建、销毁、查找等操作 + */ + +import { EventEmitter } from 'events'; +import { Room, RoomConfig, RoomState, PlayerData } from './Room'; +import { ClientConnection } from '../core/ClientConnection'; +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; + +/** + * 房间管理器配置 + */ +export interface RoomManagerConfig { + /** 最大房间数量 */ + maxRooms?: number; + /** 默认房间过期时间(毫秒) */ + defaultExpirationTime?: number; + /** 是否启用房间统计 */ + enableStats?: boolean; + /** 房间清理间隔(毫秒) */ + cleanupInterval?: number; +} + +/** + * 房间查询选项 + */ +export interface RoomQueryOptions { + /** 房间名称模糊搜索 */ + namePattern?: string; + /** 房间状态过滤 */ + state?: RoomState; + /** 是否私有房间 */ + isPrivate?: boolean; + /** 最小空位数 */ + minAvailableSlots?: number; + /** 最大空位数 */ + maxAvailableSlots?: number; + /** 元数据过滤 */ + metadata?: Record; + /** 限制结果数量 */ + limit?: number; + /** 跳过条数 */ + offset?: number; +} + +/** + * 房间管理器统计信息 + */ +export interface RoomManagerStats { + /** 总房间数 */ + totalRooms: number; + /** 活跃房间数 */ + activeRooms: number; + /** 总玩家数 */ + totalPlayers: number; + /** 私有房间数 */ + privateRooms: number; + /** 持久化房间数 */ + persistentRooms: number; + /** 创建的房间总数 */ + roomsCreated: number; + /** 关闭的房间总数 */ + roomsClosed: number; +} + +/** + * 房间管理器事件 + */ +export interface RoomManagerEvents { + /** 房间创建 */ + 'room-created': (room: Room) => void; + /** 房间关闭 */ + 'room-closed': (roomId: string, reason: string) => void; + /** 玩家加入房间 */ + 'player-joined-room': (roomId: string, player: PlayerData) => void; + /** 玩家离开房间 */ + 'player-left-room': (roomId: string, clientId: string, reason?: string) => void; + /** 房间管理器错误 */ + 'error': (error: Error, roomId?: string) => void; +} + +/** + * 房间管理器 + */ +export class RoomManager extends EventEmitter { + private config: RoomManagerConfig; + private rooms = new Map(); + private stats: RoomManagerStats; + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor(config: RoomManagerConfig = {}) { + super(); + + this.config = { + maxRooms: 1000, + defaultExpirationTime: 0, // 0 = 不过期 + enableStats: true, + cleanupInterval: 60000, // 1分钟 + ...config + }; + + this.stats = { + totalRooms: 0, + activeRooms: 0, + totalPlayers: 0, + privateRooms: 0, + persistentRooms: 0, + roomsCreated: 0, + roomsClosed: 0 + }; + + this.initialize(); + } + + /** + * 获取房间管理器配置 + */ + getConfig(): Readonly { + return this.config; + } + + /** + * 获取房间管理器统计信息 + */ + getStats(): RoomManagerStats { + this.updateStats(); + return { ...this.stats }; + } + + /** + * 创建房间 + */ + async createRoom(config: RoomConfig, creatorClient?: ClientConnection): Promise { + // 检查房间数量限制 + if (this.config.maxRooms && this.rooms.size >= this.config.maxRooms) { + throw new Error('Maximum number of rooms reached'); + } + + // 检查房间ID是否已存在 + if (this.rooms.has(config.id)) { + throw new Error(`Room with id "${config.id}" already exists`); + } + + // 应用默认过期时间 + const roomConfig: RoomConfig = { + expirationTime: this.config.defaultExpirationTime, + ...config + }; + + const room = new Room(roomConfig); + + // 设置房间事件监听 + this.setupRoomEvents(room); + + this.rooms.set(room.id, room); + this.stats.roomsCreated++; + + console.log(`Room created: ${room.id} by ${creatorClient?.id || 'system'}`); + this.emit('room-created', room); + + // 如果有创建者,自动加入房间 + if (creatorClient) { + try { + await room.addPlayer(creatorClient); + } catch (error) { + console.error(`Failed to add creator to room ${room.id}:`, error); + } + } + + return room; + } + + /** + * 获取房间 + */ + getRoom(roomId: string): Room | undefined { + return this.rooms.get(roomId); + } + + /** + * 检查房间是否存在 + */ + hasRoom(roomId: string): boolean { + return this.rooms.has(roomId); + } + + /** + * 获取所有房间 + */ + getAllRooms(): Room[] { + return Array.from(this.rooms.values()); + } + + /** + * 查询房间 + */ + findRooms(options: RoomQueryOptions = {}): Room[] { + let rooms = Array.from(this.rooms.values()); + + // 状态过滤 + if (options.state !== undefined) { + rooms = rooms.filter(room => room.currentState === options.state); + } + + // 私有房间过滤 + if (options.isPrivate !== undefined) { + rooms = rooms.filter(room => room.getConfig().isPrivate === options.isPrivate); + } + + // 名称模糊搜索 + if (options.namePattern) { + const pattern = options.namePattern.toLowerCase(); + rooms = rooms.filter(room => + room.getConfig().name.toLowerCase().includes(pattern) + ); + } + + // 空位数过滤 + if (options.minAvailableSlots !== undefined) { + rooms = rooms.filter(room => { + const available = room.getConfig().maxPlayers - room.getPlayerCount(); + return available >= options.minAvailableSlots!; + }); + } + + if (options.maxAvailableSlots !== undefined) { + rooms = rooms.filter(room => { + const available = room.getConfig().maxPlayers - room.getPlayerCount(); + return available <= options.maxAvailableSlots!; + }); + } + + // 元数据过滤 + if (options.metadata) { + rooms = rooms.filter(room => { + const roomMetadata = room.getConfig().metadata || {}; + return Object.entries(options.metadata!).every(([key, value]) => + roomMetadata[key] === value + ); + }); + } + + // 排序(按创建时间,最新的在前) + rooms.sort((a, b) => + b.getStats().createdAt.getTime() - a.getStats().createdAt.getTime() + ); + + // 分页 + const offset = options.offset || 0; + const limit = options.limit || rooms.length; + return rooms.slice(offset, offset + limit); + } + + /** + * 关闭房间 + */ + async closeRoom(roomId: string, reason: string = 'manual'): Promise { + const room = this.rooms.get(roomId); + if (!room) { + return false; + } + + try { + await room.close(reason); + return true; + } catch (error) { + this.emit('error', error as Error, roomId); + return false; + } + } + + /** + * 玩家加入房间 + */ + async joinRoom( + roomId: string, + client: ClientConnection, + customData: Record = {} + ): Promise { + const room = this.rooms.get(roomId); + if (!room) { + throw new Error(`Room "${roomId}" not found`); + } + + try { + return await room.addPlayer(client, customData); + } catch (error) { + this.emit('error', error as Error, roomId); + throw error; + } + } + + /** + * 玩家离开房间 + */ + async leaveRoom(roomId: string, clientId: string, reason?: string): Promise { + const room = this.rooms.get(roomId); + if (!room) { + return false; + } + + try { + return await room.removePlayer(clientId, reason); + } catch (error) { + this.emit('error', error as Error, roomId); + return false; + } + } + + /** + * 玩家离开所有房间 + */ + async leaveAllRooms(clientId: string, reason?: string): Promise { + let leftCount = 0; + + for (const room of this.rooms.values()) { + if (room.hasPlayer(clientId)) { + try { + await room.removePlayer(clientId, reason); + leftCount++; + } catch (error) { + console.error(`Error removing player ${clientId} from room ${room.id}:`, error); + } + } + } + + return leftCount; + } + + /** + * 获取玩家所在的房间 + */ + getPlayerRooms(clientId: string): Room[] { + return Array.from(this.rooms.values()) + .filter(room => room.hasPlayer(clientId)); + } + + /** + * 获取房间数量 + */ + getRoomCount(): number { + return this.rooms.size; + } + + /** + * 获取总玩家数量 + */ + getTotalPlayerCount(): number { + return Array.from(this.rooms.values()) + .reduce((total, room) => total + room.getPlayerCount(), 0); + } + + /** + * 清理空闲房间 + */ + async cleanupRooms(): Promise { + let cleanedCount = 0; + const now = Date.now(); + + for (const room of this.rooms.values()) { + const config = room.getConfig(); + const stats = room.getStats(); + + // 清理条件: + // 1. 非持久化的空房间 + // 2. 已过期的房间 + // 3. 已关闭的房间 + let shouldClean = false; + let reason = ''; + + if (room.currentState === RoomState.CLOSED) { + shouldClean = true; + reason = 'room-closed'; + } else if (!config.persistent && room.isEmpty()) { + shouldClean = true; + reason = 'empty-room'; + } else if (config.expirationTime && config.expirationTime > 0) { + const expireTime = stats.createdAt.getTime() + config.expirationTime; + if (now >= expireTime) { + shouldClean = true; + reason = 'expired'; + } + } + + if (shouldClean) { + try { + if (room.currentState !== RoomState.CLOSED) { + await room.close(reason); + } + this.rooms.delete(room.id); + cleanedCount++; + console.log(`Cleaned up room: ${room.id}, reason: ${reason}`); + } catch (error) { + console.error(`Error cleaning up room ${room.id}:`, error); + } + } + } + + return cleanedCount; + } + + /** + * 关闭所有房间 + */ + async closeAllRooms(reason: string = 'shutdown'): Promise { + const rooms = Array.from(this.rooms.values()); + const promises = rooms.map(room => room.close(reason)); + + await Promise.allSettled(promises); + this.rooms.clear(); + + console.log(`Closed ${rooms.length} rooms, reason: ${reason}`); + } + + /** + * 销毁房间管理器 + */ + async destroy(): Promise { + // 停止清理定时器 + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + + // 关闭所有房间 + await this.closeAllRooms('manager-destroyed'); + + // 移除所有事件监听器 + this.removeAllListeners(); + } + + /** + * 初始化房间管理器 + */ + private initialize(): void { + // 启动清理定时器 + if (this.config.cleanupInterval && this.config.cleanupInterval > 0) { + this.cleanupTimer = setInterval(() => { + this.cleanupRooms().catch(error => { + console.error('Error during room cleanup:', error); + }); + }, this.config.cleanupInterval); + } + } + + /** + * 设置房间事件监听 + */ + private setupRoomEvents(room: Room): void { + room.on('player-joined', (player) => { + this.emit('player-joined-room', room.id, player); + }); + + room.on('player-left', (clientId, reason) => { + this.emit('player-left-room', room.id, clientId, reason); + }); + + room.on('closed', (reason) => { + this.rooms.delete(room.id); + this.stats.roomsClosed++; + console.log(`Room ${room.id} removed from manager, reason: ${reason}`); + this.emit('room-closed', room.id, reason); + }); + + room.on('error', (error) => { + this.emit('error', error, room.id); + }); + } + + /** + * 更新统计信息 + */ + private updateStats(): void { + this.stats.totalRooms = this.rooms.size; + this.stats.activeRooms = Array.from(this.rooms.values()) + .filter(room => room.currentState === RoomState.ACTIVE).length; + this.stats.totalPlayers = this.getTotalPlayerCount(); + this.stats.privateRooms = Array.from(this.rooms.values()) + .filter(room => room.getConfig().isPrivate).length; + this.stats.persistentRooms = Array.from(this.rooms.values()) + .filter(room => room.getConfig().persistent).length; + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: RoomManagerEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-server/src/rooms/index.ts b/packages/network-server/src/rooms/index.ts new file mode 100644 index 00000000..a42b05f9 --- /dev/null +++ b/packages/network-server/src/rooms/index.ts @@ -0,0 +1,6 @@ +/** + * 房间系统导出 + */ + +export * from './Room'; +export * from './RoomManager'; \ No newline at end of file diff --git a/packages/network-server/src/systems/RpcSystem.ts b/packages/network-server/src/systems/RpcSystem.ts new file mode 100644 index 00000000..a6c6f520 --- /dev/null +++ b/packages/network-server/src/systems/RpcSystem.ts @@ -0,0 +1,762 @@ +/** + * RPC 系统 + * + * 处理服务端的 RPC 调用、权限验证、参数验证等 + */ + +import { EventEmitter } from 'events'; +import { v4 as uuidv4 } from 'uuid'; +import { + NetworkValue, + RpcMetadata +} from '@esengine/ecs-framework-network-shared'; +import { ClientConnection } from '../core/ClientConnection'; +import { Room } from '../rooms/Room'; +import { TransportMessage } from '../core/Transport'; + +/** + * RPC 调用记录 + */ +export interface RpcCall { + /** 调用ID */ + id: string; + /** 网络对象ID */ + networkId: number; + /** 组件类型 */ + componentType: string; + /** 方法名 */ + methodName: string; + /** 参数 */ + parameters: NetworkValue[]; + /** 元数据 */ + metadata: RpcMetadata; + /** 发送者客户端ID */ + senderId: string; + /** 目标客户端IDs(用于 ClientRpc) */ + targetClientIds?: string[]; + /** 是否需要响应 */ + requiresResponse: boolean; + /** 时间戳 */ + timestamp: Date; + /** 过期时间 */ + expiresAt?: Date; +} + +/** + * RPC 响应 + */ +export interface RpcResponse { + /** 调用ID */ + callId: string; + /** 是否成功 */ + success: boolean; + /** 返回值 */ + result?: NetworkValue; + /** 错误信息 */ + error?: string; + /** 错误代码 */ + errorCode?: string; + /** 时间戳 */ + timestamp: Date; +} + +/** + * RPC 系统配置 + */ +export interface RpcSystemConfig { + /** RPC 调用超时时间(毫秒) */ + callTimeout?: number; + /** 最大并发 RPC 调用数 */ + maxConcurrentCalls?: number; + /** 是否启用权限检查 */ + enablePermissionCheck?: boolean; + /** 是否启用参数验证 */ + enableParameterValidation?: boolean; + /** 是否启用频率限制 */ + enableRateLimit?: boolean; + /** 最大 RPC 频率(调用/秒) */ + maxRpcRate?: number; + /** 单个参数最大大小(字节) */ + maxParameterSize?: number; +} + +/** + * RPC 系统事件 + */ +export interface RpcSystemEvents { + /** ClientRpc 调用 */ + 'client-rpc-called': (call: RpcCall) => void; + /** ServerRpc 调用 */ + 'server-rpc-called': (call: RpcCall) => void; + /** RPC 调用完成 */ + 'rpc-completed': (call: RpcCall, response?: RpcResponse) => void; + /** RPC 调用超时 */ + 'rpc-timeout': (callId: string) => void; + /** 权限验证失败 */ + 'permission-denied': (clientId: string, call: RpcCall) => void; + /** 参数验证失败 */ + 'parameter-validation-failed': (clientId: string, call: RpcCall, reason: string) => void; + /** 频率限制触发 */ + 'rate-limit-exceeded': (clientId: string) => void; + /** RPC 错误 */ + 'rpc-error': (error: Error, callId?: string, clientId?: string) => void; +} + +/** + * 客户端 RPC 状态 + */ +interface ClientRpcState { + /** 客户端ID */ + clientId: string; + /** 活跃的调用 */ + activeCalls: Map; + /** RPC 调用计数 */ + rpcCount: number; + /** 频率重置时间 */ + rateResetTime: Date; +} + +/** + * 待处理的 RPC 响应 + */ +interface PendingRpcResponse { + /** 调用信息 */ + call: RpcCall; + /** 超时定时器 */ + timeoutTimer: NodeJS.Timeout; + /** 响应回调 */ + responseCallback: (response: RpcResponse) => void; +} + +/** + * RPC 系统 + */ +export class RpcSystem extends EventEmitter { + private config: RpcSystemConfig; + private clientStates = new Map(); + private pendingCalls = new Map(); + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor(config: RpcSystemConfig = {}) { + super(); + + this.config = { + callTimeout: 30000, // 30秒 + maxConcurrentCalls: 10, + enablePermissionCheck: true, + enableParameterValidation: true, + enableRateLimit: true, + maxRpcRate: 30, // 30次/秒 + maxParameterSize: 65536, // 64KB + ...config + }; + + this.initialize(); + } + + /** + * 处理 ClientRpc 调用 + */ + async handleClientRpcCall( + client: ClientConnection, + message: TransportMessage, + room: Room + ): Promise { + try { + const data = message.data as any; + const { + networkId, + componentType, + methodName, + parameters = [], + metadata, + targetFilter = 'all' + } = data; + + // 创建 RPC 调用记录 + const rpcCall: RpcCall = { + id: uuidv4(), + networkId, + componentType, + methodName, + parameters, + metadata, + senderId: client.id, + requiresResponse: metadata?.requiresResponse || false, + timestamp: new Date() + }; + + // 权限检查 + if (this.config.enablePermissionCheck) { + if (!this.checkRpcPermission(client, rpcCall, 'client-rpc')) { + this.emit('permission-denied', client.id, rpcCall); + return; + } + } + + // 频率限制检查 + if (this.config.enableRateLimit && !this.checkRpcRate(client.id)) { + this.emit('rate-limit-exceeded', client.id); + return; + } + + // 参数验证 + if (this.config.enableParameterValidation) { + const validationResult = this.validateRpcParameters(rpcCall); + if (!validationResult.valid) { + this.emit('parameter-validation-failed', client.id, rpcCall, validationResult.reason!); + return; + } + } + + // 确定目标客户端 + const targetClientIds = this.getClientRpcTargets(room, client.id, targetFilter); + rpcCall.targetClientIds = targetClientIds; + + // 记录活跃调用 + this.recordActiveCall(client.id, rpcCall); + + // 触发事件 + this.emit('client-rpc-called', rpcCall); + + // 发送到目标客户端 + await this.sendClientRpc(room, rpcCall, targetClientIds); + + // 如果不需要响应,立即标记完成 + if (!rpcCall.requiresResponse) { + this.completeRpcCall(rpcCall); + } + + } catch (error) { + this.emit('rpc-error', error as Error, undefined, client.id); + } + } + + /** + * 处理 ServerRpc 调用 + */ + async handleServerRpcCall( + client: ClientConnection, + message: TransportMessage, + room: Room + ): Promise { + try { + const data = message.data as any; + const { + networkId, + componentType, + methodName, + parameters = [], + metadata + } = data; + + // 创建 RPC 调用记录 + const rpcCall: RpcCall = { + id: uuidv4(), + networkId, + componentType, + methodName, + parameters, + metadata, + senderId: client.id, + requiresResponse: metadata?.requiresResponse || false, + timestamp: new Date() + }; + + // 权限检查 + if (this.config.enablePermissionCheck) { + if (!this.checkRpcPermission(client, rpcCall, 'server-rpc')) { + this.emit('permission-denied', client.id, rpcCall); + return; + } + } + + // 频率限制检查 + if (this.config.enableRateLimit && !this.checkRpcRate(client.id)) { + this.emit('rate-limit-exceeded', client.id); + return; + } + + // 参数验证 + if (this.config.enableParameterValidation) { + const validationResult = this.validateRpcParameters(rpcCall); + if (!validationResult.valid) { + this.emit('parameter-validation-failed', client.id, rpcCall, validationResult.reason!); + return; + } + } + + // 记录活跃调用 + this.recordActiveCall(client.id, rpcCall); + + // 触发事件 + this.emit('server-rpc-called', rpcCall); + + // ServerRpc 在服务端执行,这里需要实际的执行逻辑 + // 在实际使用中,应该通过事件或回调来执行具体的方法 + const response = await this.executeServerRpc(rpcCall); + + // 发送响应(如果需要) + if (rpcCall.requiresResponse && response) { + await this.sendRpcResponse(client, response); + } + + this.completeRpcCall(rpcCall, response || undefined); + + } catch (error) { + this.emit('rpc-error', error as Error, undefined, client.id); + + // 发送错误响应 + if (message.data && (message.data as any).requiresResponse) { + const errorResponse: RpcResponse = { + callId: (message.data as any).callId || uuidv4(), + success: false, + error: (error as Error).message, + errorCode: 'EXECUTION_ERROR', + timestamp: new Date() + }; + await this.sendRpcResponse(client, errorResponse); + } + } + } + + /** + * 处理 RPC 响应 + */ + async handleRpcResponse( + client: ClientConnection, + message: TransportMessage + ): Promise { + try { + const response = message.data as any as RpcResponse; + const pendingCall = this.pendingCalls.get(response.callId); + + if (pendingCall) { + // 清除超时定时器 + clearTimeout(pendingCall.timeoutTimer); + this.pendingCalls.delete(response.callId); + + // 调用响应回调 + pendingCall.responseCallback(response); + + // 完成调用 + this.completeRpcCall(pendingCall.call, response); + } + + } catch (error) { + this.emit('rpc-error', error as Error, undefined, client.id); + } + } + + /** + * 调用 ClientRpc(从服务端向客户端发送) + */ + async callClientRpc( + room: Room, + networkId: number, + componentType: string, + methodName: string, + parameters: NetworkValue[] = [], + options: { + targetFilter?: 'all' | 'others' | 'owner' | string[]; + requiresResponse?: boolean; + timeout?: number; + } = {} + ): Promise { + const rpcCall: RpcCall = { + id: uuidv4(), + networkId, + componentType, + methodName, + parameters, + metadata: { + methodName, + rpcType: 'client-rpc', + requiresAuth: false, + reliable: true, + requiresResponse: options.requiresResponse || false + }, + senderId: 'server', + requiresResponse: options.requiresResponse || false, + timestamp: new Date() + }; + + // 确定目标客户端 + const targetClientIds = typeof options.targetFilter === 'string' + ? this.getClientRpcTargets(room, 'server', options.targetFilter) + : options.targetFilter || []; + + rpcCall.targetClientIds = targetClientIds; + + // 发送到目标客户端 + await this.sendClientRpc(room, rpcCall, targetClientIds); + + // 如果需要响应,等待响应 + if (options.requiresResponse) { + return await this.waitForRpcResponses(rpcCall, targetClientIds, options.timeout); + } + + this.completeRpcCall(rpcCall); + return []; + } + + /** + * 获取客户端统计信息 + */ + getClientRpcStats(clientId: string): { + activeCalls: number; + totalCalls: number; + } { + const state = this.clientStates.get(clientId); + return { + activeCalls: state?.activeCalls.size || 0, + totalCalls: state?.rpcCount || 0 + }; + } + + /** + * 取消所有客户端的 RPC 调用 + */ + cancelClientRpcs(clientId: string): number { + const state = this.clientStates.get(clientId); + if (!state) { + return 0; + } + + const cancelledCount = state.activeCalls.size; + + // 取消所有活跃调用 + for (const call of state.activeCalls.values()) { + this.completeRpcCall(call); + } + + state.activeCalls.clear(); + return cancelledCount; + } + + /** + * 销毁 RPC 系统 + */ + destroy(): void { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + + // 清除所有待处理的调用 + for (const pending of this.pendingCalls.values()) { + clearTimeout(pending.timeoutTimer); + } + + this.clientStates.clear(); + this.pendingCalls.clear(); + this.removeAllListeners(); + } + + /** + * 初始化系统 + */ + private initialize(): void { + // 启动清理定时器(每分钟清理一次) + this.cleanupTimer = setInterval(() => { + this.cleanup(); + }, 60000); + } + + /** + * 检查 RPC 权限 + */ + private checkRpcPermission( + client: ClientConnection, + call: RpcCall, + rpcType: 'client-rpc' | 'server-rpc' + ): boolean { + // 基本权限检查 + if (!client.hasPermission('canSendRpc')) { + return false; + } + + // ServerRpc 额外权限检查 + if (rpcType === 'server-rpc' && call.metadata.requiresAuth) { + if (!client.isAuthenticated) { + return false; + } + } + + // 可以添加更多特定的权限检查逻辑 + return true; + } + + /** + * 检查 RPC 频率 + */ + private checkRpcRate(clientId: string): boolean { + if (!this.config.maxRpcRate || this.config.maxRpcRate <= 0) { + return true; + } + + const now = new Date(); + let state = this.clientStates.get(clientId); + + if (!state) { + state = { + clientId, + activeCalls: new Map(), + rpcCount: 1, + rateResetTime: new Date(now.getTime() + 1000) + }; + this.clientStates.set(clientId, state); + return true; + } + + // 检查是否需要重置计数 + if (now >= state.rateResetTime) { + state.rpcCount = 1; + state.rateResetTime = new Date(now.getTime() + 1000); + return true; + } + + // 检查频率限制 + if (state.rpcCount >= this.config.maxRpcRate) { + return false; + } + + state.rpcCount++; + return true; + } + + /** + * 验证 RPC 参数 + */ + private validateRpcParameters(call: RpcCall): { valid: boolean; reason?: string } { + // 检查参数数量 + if (call.parameters.length > 10) { + return { valid: false, reason: 'Too many parameters' }; + } + + // 检查每个参数的大小 + for (let i = 0; i < call.parameters.length; i++) { + const param = call.parameters[i]; + try { + const serialized = JSON.stringify(param); + if (serialized.length > this.config.maxParameterSize!) { + return { valid: false, reason: `Parameter ${i} is too large` }; + } + } catch (error) { + return { valid: false, reason: `Parameter ${i} is not serializable` }; + } + } + + return { valid: true }; + } + + /** + * 获取 ClientRpc 目标客户端 + */ + private getClientRpcTargets( + room: Room, + senderId: string, + targetFilter: string + ): string[] { + const players = room.getPlayers(); + + switch (targetFilter) { + case 'all': + return players.map(p => p.client.id); + + case 'others': + return players + .filter(p => p.client.id !== senderId) + .map(p => p.client.id); + + case 'owner': + const owner = room.getOwner(); + return owner ? [owner.client.id] : []; + + default: + return []; + } + } + + /** + * 发送 ClientRpc + */ + private async sendClientRpc( + room: Room, + call: RpcCall, + targetClientIds: string[] + ): Promise { + const message: TransportMessage = { + type: 'rpc', + data: { + action: 'client-rpc', + callId: call.id, + networkId: call.networkId, + componentType: call.componentType, + methodName: call.methodName, + parameters: call.parameters, + metadata: call.metadata as any, + requiresResponse: call.requiresResponse, + timestamp: call.timestamp.getTime() + } as any + }; + + // 发送给目标客户端 + const promises = targetClientIds.map(clientId => + room.sendToPlayer(clientId, message) + ); + + await Promise.allSettled(promises); + } + + /** + * 执行 ServerRpc + */ + private async executeServerRpc(call: RpcCall): Promise { + // 这里应该是实际的服务端方法执行逻辑 + // 在实际实现中,可能需要通过事件或回调来执行具体的方法 + + // 示例响应 + const response: RpcResponse = { + callId: call.id, + success: true, + result: undefined, // 实际执行结果 + timestamp: new Date() + }; + + return response; + } + + /** + * 发送 RPC 响应 + */ + private async sendRpcResponse( + client: ClientConnection, + response: RpcResponse + ): Promise { + const message: TransportMessage = { + type: 'rpc', + data: { + action: 'rpc-response', + ...response + } as any + }; + + await client.sendMessage(message); + } + + /** + * 等待 RPC 响应 + */ + private async waitForRpcResponses( + call: RpcCall, + targetClientIds: string[], + timeout?: number + ): Promise { + return new Promise((resolve) => { + const responses: RpcResponse[] = []; + const responseTimeout = timeout || this.config.callTimeout!; + let responseCount = 0; + + const responseCallback = (response: RpcResponse) => { + responses.push(response); + responseCount++; + + // 如果收到所有响应,立即resolve + if (responseCount >= targetClientIds.length) { + resolve(responses); + } + }; + + // 设置超时 + const timeoutTimer = setTimeout(() => { + resolve(responses); // 返回已收到的响应 + this.emit('rpc-timeout', call.id); + }, responseTimeout); + + // 注册待处理的响应 + this.pendingCalls.set(call.id, { + call, + timeoutTimer, + responseCallback + }); + }); + } + + /** + * 记录活跃调用 + */ + private recordActiveCall(clientId: string, call: RpcCall): void { + let state = this.clientStates.get(clientId); + if (!state) { + state = { + clientId, + activeCalls: new Map(), + rpcCount: 0, + rateResetTime: new Date() + }; + this.clientStates.set(clientId, state); + } + + state.activeCalls.set(call.id, call); + } + + /** + * 完成 RPC 调用 + */ + private completeRpcCall(call: RpcCall, response?: RpcResponse): void { + // 从活跃调用中移除 + const state = this.clientStates.get(call.senderId); + if (state) { + state.activeCalls.delete(call.id); + } + + // 触发完成事件 + this.emit('rpc-completed', call, response); + } + + /** + * 清理过期的调用和状态 + */ + private cleanup(): void { + const now = new Date(); + let cleanedCalls = 0; + let cleanedStates = 0; + + // 清理过期的待处理调用 + for (const [callId, pending] of this.pendingCalls.entries()) { + if (pending.call.expiresAt && pending.call.expiresAt < now) { + clearTimeout(pending.timeoutTimer); + this.pendingCalls.delete(callId); + cleanedCalls++; + } + } + + // 清理空的客户端状态 + for (const [clientId, state] of this.clientStates.entries()) { + if (state.activeCalls.size === 0 && + now.getTime() - state.rateResetTime.getTime() > 60000) { + this.clientStates.delete(clientId); + cleanedStates++; + } + } + + if (cleanedCalls > 0 || cleanedStates > 0) { + console.log(`RPC cleanup: ${cleanedCalls} calls, ${cleanedStates} states`); + } + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: RpcSystemEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-server/src/systems/SyncVarSystem.ts b/packages/network-server/src/systems/SyncVarSystem.ts new file mode 100644 index 00000000..84bc2b42 --- /dev/null +++ b/packages/network-server/src/systems/SyncVarSystem.ts @@ -0,0 +1,587 @@ +/** + * SyncVar 同步系统 + * + * 处理服务端的 SyncVar 同步逻辑、权限验证、数据传播等 + */ + +import { EventEmitter } from 'events'; +import { + NetworkValue, + SyncVarMetadata, + NetworkSerializer +} from '@esengine/ecs-framework-network-shared'; +import { ClientConnection } from '../core/ClientConnection'; +import { Room } from '../rooms/Room'; +import { TransportMessage } from '../core/Transport'; + +/** + * SyncVar 更改记录 + */ +export interface SyncVarChange { + /** 网络对象ID */ + networkId: number; + /** 组件类型 */ + componentType: string; + /** 属性名 */ + propertyName: string; + /** 旧值 */ + oldValue: NetworkValue; + /** 新值 */ + newValue: NetworkValue; + /** 元数据 */ + metadata: SyncVarMetadata; + /** 发送者客户端ID */ + senderId: string; + /** 时间戳 */ + timestamp: Date; +} + +/** + * SyncVar 同步配置 + */ +export interface SyncVarSystemConfig { + /** 批量同步间隔(毫秒) */ + batchInterval?: number; + /** 单次批量最大数量 */ + maxBatchSize?: number; + /** 是否启用增量同步 */ + enableDeltaSync?: boolean; + /** 是否启用权限检查 */ + enablePermissionCheck?: boolean; + /** 是否启用数据验证 */ + enableDataValidation?: boolean; + /** 最大同步频率(次/秒) */ + maxSyncRate?: number; +} + +/** + * 网络对象状态 + */ +export interface NetworkObjectState { + /** 网络对象ID */ + networkId: number; + /** 拥有者客户端ID */ + ownerId: string; + /** 组件状态 */ + components: Map>; + /** 最后更新时间 */ + lastUpdateTime: Date; + /** 权威状态 */ + hasAuthority: boolean; +} + +/** + * SyncVar 系统事件 + */ +export interface SyncVarSystemEvents { + /** SyncVar 值变化 */ + 'syncvar-changed': (change: SyncVarChange) => void; + /** 同步批次完成 */ + 'batch-synced': (changes: SyncVarChange[], targetClients: string[]) => void; + /** 权限验证失败 */ + 'permission-denied': (clientId: string, change: SyncVarChange) => void; + /** 数据验证失败 */ + 'validation-failed': (clientId: string, change: SyncVarChange, reason: string) => void; + /** 同步错误 */ + 'sync-error': (error: Error, clientId?: string) => void; +} + +/** + * 客户端同步状态 + */ +interface ClientSyncState { + /** 客户端ID */ + clientId: string; + /** 待同步的变化列表 */ + pendingChanges: SyncVarChange[]; + /** 最后同步时间 */ + lastSyncTime: Date; + /** 同步频率限制 */ + syncCount: number; + /** 频率重置时间 */ + rateResetTime: Date; +} + +/** + * SyncVar 同步系统 + */ +export class SyncVarSystem extends EventEmitter { + private config: SyncVarSystemConfig; + private networkObjects = new Map(); + private clientSyncStates = new Map(); + private serializer: NetworkSerializer; + private batchTimer: NodeJS.Timeout | null = null; + + constructor(config: SyncVarSystemConfig = {}) { + super(); + + this.config = { + batchInterval: 50, // 50ms批量间隔 + maxBatchSize: 100, + enableDeltaSync: true, + enablePermissionCheck: true, + enableDataValidation: true, + maxSyncRate: 60, // 60次/秒 + ...config + }; + + this.serializer = new NetworkSerializer(); + this.initialize(); + } + + /** + * 注册网络对象 + */ + registerNetworkObject( + networkId: number, + ownerId: string, + hasAuthority: boolean = true + ): void { + if (this.networkObjects.has(networkId)) { + console.warn(`Network object ${networkId} is already registered`); + return; + } + + const networkObject: NetworkObjectState = { + networkId, + ownerId, + components: new Map(), + lastUpdateTime: new Date(), + hasAuthority + }; + + this.networkObjects.set(networkId, networkObject); + console.log(`Network object registered: ${networkId} owned by ${ownerId}`); + } + + /** + * 注销网络对象 + */ + unregisterNetworkObject(networkId: number): boolean { + const removed = this.networkObjects.delete(networkId); + if (removed) { + console.log(`Network object unregistered: ${networkId}`); + } + return removed; + } + + /** + * 获取网络对象 + */ + getNetworkObject(networkId: number): NetworkObjectState | undefined { + return this.networkObjects.get(networkId); + } + + /** + * 处理 SyncVar 变化消息 + */ + async handleSyncVarChange( + client: ClientConnection, + message: TransportMessage, + room?: Room + ): Promise { + try { + const data = message.data as any; + const { + networkId, + componentType, + propertyName, + oldValue, + newValue, + metadata + } = data; + + // 创建变化记录 + const change: SyncVarChange = { + networkId, + componentType, + propertyName, + oldValue, + newValue, + metadata, + senderId: client.id, + timestamp: new Date() + }; + + // 权限检查 + if (this.config.enablePermissionCheck) { + if (!this.checkSyncVarPermission(client, change)) { + this.emit('permission-denied', client.id, change); + return; + } + } + + // 频率限制检查 + if (!this.checkSyncRate(client.id)) { + console.warn(`SyncVar rate limit exceeded for client ${client.id}`); + return; + } + + // 数据验证 + if (this.config.enableDataValidation) { + const validationResult = this.validateSyncVarData(change); + if (!validationResult.valid) { + this.emit('validation-failed', client.id, change, validationResult.reason!); + return; + } + } + + // 更新网络对象状态 + this.updateNetworkObjectState(change); + + // 触发变化事件 + this.emit('syncvar-changed', change); + + // 添加到待同步列表 + if (room) { + this.addToBatchSync(change, room); + } + + } catch (error) { + this.emit('sync-error', error as Error, client.id); + } + } + + /** + * 获取网络对象的完整状态 + */ + getNetworkObjectSnapshot(networkId: number): Record | null { + const networkObject = this.networkObjects.get(networkId); + if (!networkObject) { + return null; + } + + const snapshot: Record = {}; + + for (const [componentType, componentData] of networkObject.components) { + snapshot[componentType] = {}; + for (const [propertyName, value] of componentData) { + snapshot[componentType][propertyName] = value; + } + } + + return snapshot; + } + + /** + * 向客户端发送网络对象快照 + */ + async sendNetworkObjectSnapshot( + client: ClientConnection, + networkId: number + ): Promise { + const snapshot = this.getNetworkObjectSnapshot(networkId); + if (!snapshot) { + return false; + } + + const message: TransportMessage = { + type: 'syncvar', + data: { + action: 'snapshot', + networkId, + snapshot + } + }; + + return await client.sendMessage(message); + } + + /** + * 同步所有网络对象给新客户端 + */ + async syncAllNetworkObjects(client: ClientConnection, room: Room): Promise { + let syncedCount = 0; + + for (const networkObject of this.networkObjects.values()) { + // 检查客户端是否有权限看到这个网络对象 + if (this.canClientSeeNetworkObject(client.id, networkObject)) { + const success = await this.sendNetworkObjectSnapshot(client, networkObject.networkId); + if (success) { + syncedCount++; + } + } + } + + console.log(`Synced ${syncedCount} network objects to client ${client.id}`); + return syncedCount; + } + + /** + * 设置网络对象拥有者 + */ + setNetworkObjectOwner(networkId: number, newOwnerId: string): boolean { + const networkObject = this.networkObjects.get(networkId); + if (!networkObject) { + return false; + } + + const oldOwnerId = networkObject.ownerId; + networkObject.ownerId = newOwnerId; + networkObject.lastUpdateTime = new Date(); + + console.log(`Network object ${networkId} ownership changed: ${oldOwnerId} -> ${newOwnerId}`); + return true; + } + + /** + * 获取网络对象拥有者 + */ + getNetworkObjectOwner(networkId: number): string | undefined { + const networkObject = this.networkObjects.get(networkId); + return networkObject?.ownerId; + } + + /** + * 销毁 SyncVar 系统 + */ + destroy(): void { + if (this.batchTimer) { + clearInterval(this.batchTimer); + this.batchTimer = null; + } + + this.networkObjects.clear(); + this.clientSyncStates.clear(); + this.removeAllListeners(); + } + + /** + * 初始化系统 + */ + private initialize(): void { + // 启动批量同步定时器 + if (this.config.batchInterval && this.config.batchInterval > 0) { + this.batchTimer = setInterval(() => { + this.processBatchSync(); + }, this.config.batchInterval); + } + } + + /** + * 检查 SyncVar 权限 + */ + private checkSyncVarPermission(client: ClientConnection, change: SyncVarChange): boolean { + // 检查客户端是否有网络同步权限 + if (!client.hasPermission('canSyncVars')) { + return false; + } + + // 获取网络对象 + const networkObject = this.networkObjects.get(change.networkId); + if (!networkObject) { + return false; + } + + // 检查权威权限 + if (change.metadata.authorityOnly) { + // 只有网络对象拥有者或有权威权限的客户端可以修改 + return networkObject.ownerId === client.id || networkObject.hasAuthority; + } + + return true; + } + + /** + * 检查同步频率 + */ + private checkSyncRate(clientId: string): boolean { + if (!this.config.maxSyncRate || this.config.maxSyncRate <= 0) { + return true; + } + + const now = new Date(); + let syncState = this.clientSyncStates.get(clientId); + + if (!syncState) { + syncState = { + clientId, + pendingChanges: [], + lastSyncTime: now, + syncCount: 1, + rateResetTime: new Date(now.getTime() + 1000) // 1秒后重置 + }; + this.clientSyncStates.set(clientId, syncState); + return true; + } + + // 检查是否需要重置计数 + if (now >= syncState.rateResetTime) { + syncState.syncCount = 1; + syncState.rateResetTime = new Date(now.getTime() + 1000); + return true; + } + + // 检查频率限制 + if (syncState.syncCount >= this.config.maxSyncRate) { + return false; + } + + syncState.syncCount++; + return true; + } + + /** + * 验证 SyncVar 数据 + */ + private validateSyncVarData(change: SyncVarChange): { valid: boolean; reason?: string } { + // 基本类型检查 + if (change.newValue === null || change.newValue === undefined) { + return { valid: false, reason: 'Value cannot be null or undefined' }; + } + + // 检查数据大小(防止过大的数据) + try { + const serialized = JSON.stringify(change.newValue); + if (serialized.length > 65536) { // 64KB限制 + return { valid: false, reason: 'Data too large' }; + } + } catch (error) { + return { valid: false, reason: 'Data is not serializable' }; + } + + // 可以添加更多特定的验证逻辑 + return { valid: true }; + } + + /** + * 更新网络对象状态 + */ + private updateNetworkObjectState(change: SyncVarChange): void { + let networkObject = this.networkObjects.get(change.networkId); + + if (!networkObject) { + // 如果网络对象不存在,创建一个新的(可能是客户端创建的) + networkObject = { + networkId: change.networkId, + ownerId: change.senderId, + components: new Map(), + lastUpdateTime: new Date(), + hasAuthority: true + }; + this.networkObjects.set(change.networkId, networkObject); + } + + // 获取或创建组件数据 + let componentData = networkObject.components.get(change.componentType); + if (!componentData) { + componentData = new Map(); + networkObject.components.set(change.componentType, componentData); + } + + // 更新属性值 + componentData.set(change.propertyName, change.newValue); + networkObject.lastUpdateTime = change.timestamp; + } + + /** + * 添加到批量同步 + */ + private addToBatchSync(change: SyncVarChange, room: Room): void { + // 获取房间内需要同步的客户端 + const roomPlayers = room.getPlayers(); + const targetClientIds = roomPlayers + .filter(player => player.client.id !== change.senderId) // 不发送给发送者 + .map(player => player.client.id); + + // 为每个目标客户端添加变化记录 + for (const clientId of targetClientIds) { + let syncState = this.clientSyncStates.get(clientId); + if (!syncState) { + syncState = { + clientId, + pendingChanges: [], + lastSyncTime: new Date(), + syncCount: 0, + rateResetTime: new Date() + }; + this.clientSyncStates.set(clientId, syncState); + } + + syncState.pendingChanges.push(change); + } + } + + /** + * 处理批量同步 + */ + private async processBatchSync(): Promise { + const syncPromises: Promise[] = []; + + for (const [clientId, syncState] of this.clientSyncStates.entries()) { + if (syncState.pendingChanges.length === 0) { + continue; + } + + // 获取要同步的变化(限制批量大小) + const changesToSync = syncState.pendingChanges.splice( + 0, + this.config.maxBatchSize + ); + + if (changesToSync.length > 0) { + syncPromises.push(this.sendBatchChanges(clientId, changesToSync)); + } + } + + if (syncPromises.length > 0) { + await Promise.allSettled(syncPromises); + } + } + + /** + * 发送批量变化 + */ + private async sendBatchChanges(clientId: string, changes: SyncVarChange[]): Promise { + try { + // 这里需要获取客户端连接,实际实现中可能需要从外部传入 + // 为了简化,这里假设有一个方法可以获取客户端连接 + // 实际使用时,可能需要通过回调或事件来发送消息 + + const message: TransportMessage = { + type: 'syncvar', + data: { + action: 'batch-update', + changes: changes.map(change => ({ + networkId: change.networkId, + componentType: change.componentType, + propertyName: change.propertyName, + newValue: change.newValue, + metadata: change.metadata as any, + timestamp: change.timestamp.getTime() + })) + } as any + }; + + // 这里需要实际的发送逻辑 + // 在实际使用中,应该通过事件或回调来发送消息 + this.emit('batch-synced', changes, [clientId]); + + } catch (error) { + this.emit('sync-error', error as Error, clientId); + } + } + + /** + * 检查客户端是否可以看到网络对象 + */ + private canClientSeeNetworkObject(clientId: string, networkObject: NetworkObjectState): boolean { + // 基本实现:客户端可以看到自己拥有的对象和公共对象 + // 实际实现中可能需要更复杂的可见性逻辑 + return true; + } + + /** + * 类型安全的事件监听 + */ + override on(event: K, listener: SyncVarSystemEvents[K]): this { + return super.on(event, listener); + } + + /** + * 类型安全的事件触发 + */ + override emit(event: K, ...args: Parameters): boolean { + return super.emit(event, ...args); + } +} \ No newline at end of file diff --git a/packages/network-server/src/systems/index.ts b/packages/network-server/src/systems/index.ts new file mode 100644 index 00000000..6e614e7f --- /dev/null +++ b/packages/network-server/src/systems/index.ts @@ -0,0 +1,6 @@ +/** + * 系统模块导出 + */ + +export * from './SyncVarSystem'; +export * from './RpcSystem'; \ No newline at end of file diff --git a/packages/network-server/src/validation/MessageValidator.ts b/packages/network-server/src/validation/MessageValidator.ts new file mode 100644 index 00000000..01698dac --- /dev/null +++ b/packages/network-server/src/validation/MessageValidator.ts @@ -0,0 +1,572 @@ +/** + * 消息验证器 + * + * 验证网络消息的格式、大小、内容等 + */ + +import { NetworkValue } from '@esengine/ecs-framework-network-shared'; +import { TransportMessage } from '../core/Transport'; + +/** + * 验证结果 + */ +export interface ValidationResult { + /** 是否有效 */ + valid: boolean; + /** 错误信息 */ + error?: string; + /** 错误代码 */ + errorCode?: string; + /** 详细信息 */ + details?: Record; +} + +/** + * 验证配置 + */ +export interface ValidationConfig { + /** 最大消息大小(字节) */ + maxMessageSize?: number; + /** 最大数组长度 */ + maxArrayLength?: number; + /** 最大对象深度 */ + maxObjectDepth?: number; + /** 最大字符串长度 */ + maxStringLength?: number; + /** 允许的消息类型 */ + allowedMessageTypes?: string[]; + /** 是否允许null值 */ + allowNullValues?: boolean; + /** 是否允许undefined值 */ + allowUndefinedValues?: boolean; +} + +/** + * 验证规则 + */ +export interface ValidationRule { + /** 规则名称 */ + name: string; + /** 验证函数 */ + validate: (value: any, context: ValidationContext) => ValidationResult; + /** 是否必需 */ + required?: boolean; +} + +/** + * 验证上下文 + */ +export interface ValidationContext { + /** 当前路径 */ + path: string[]; + /** 当前深度 */ + depth: number; + /** 配置 */ + config: ValidationConfig; + /** 消息类型 */ + messageType?: string; +} + +/** + * 消息验证器 + */ +export class MessageValidator { + private config: ValidationConfig; + private customRules = new Map(); + + constructor(config: ValidationConfig = {}) { + this.config = { + maxMessageSize: 1024 * 1024, // 1MB + maxArrayLength: 1000, + maxObjectDepth: 10, + maxStringLength: 10000, + allowedMessageTypes: ['rpc', 'syncvar', 'system', 'custom'], + allowNullValues: true, + allowUndefinedValues: false, + ...config + }; + } + + /** + * 验证传输消息 + */ + validateMessage(message: TransportMessage): ValidationResult { + try { + // 基本结构验证 + const structureResult = this.validateMessageStructure(message); + if (!structureResult.valid) { + return structureResult; + } + + // 消息大小验证 + const sizeResult = this.validateMessageSize(message); + if (!sizeResult.valid) { + return sizeResult; + } + + // 消息类型验证 + const typeResult = this.validateMessageType(message); + if (!typeResult.valid) { + return typeResult; + } + + // 数据内容验证 + const dataResult = this.validateMessageData(message); + if (!dataResult.valid) { + return dataResult; + } + + // 自定义规则验证 + const customResult = this.validateCustomRules(message); + if (!customResult.valid) { + return customResult; + } + + return { valid: true }; + + } catch (error) { + return { + valid: false, + error: (error as Error).message, + errorCode: 'VALIDATION_ERROR' + }; + } + } + + /** + * 验证网络值 + */ + validateNetworkValue(value: NetworkValue, context?: Partial): ValidationResult { + const fullContext: ValidationContext = { + path: [], + depth: 0, + config: this.config, + ...context + }; + + return this.validateValue(value, fullContext); + } + + /** + * 添加自定义验证规则 + */ + addValidationRule(rule: ValidationRule): void { + this.customRules.set(rule.name, rule); + } + + /** + * 移除自定义验证规则 + */ + removeValidationRule(ruleName: string): boolean { + return this.customRules.delete(ruleName); + } + + /** + * 获取所有自定义规则 + */ + getCustomRules(): ValidationRule[] { + return Array.from(this.customRules.values()); + } + + /** + * 验证消息结构 + */ + private validateMessageStructure(message: TransportMessage): ValidationResult { + // 检查必需字段 + if (!message.type) { + return { + valid: false, + error: 'Message type is required', + errorCode: 'MISSING_TYPE' + }; + } + + if (message.data === undefined) { + return { + valid: false, + error: 'Message data is required', + errorCode: 'MISSING_DATA' + }; + } + + // 检查字段类型 + if (typeof message.type !== 'string') { + return { + valid: false, + error: 'Message type must be a string', + errorCode: 'INVALID_TYPE_FORMAT' + }; + } + + // 检查可选字段 + if (message.senderId && typeof message.senderId !== 'string') { + return { + valid: false, + error: 'Sender ID must be a string', + errorCode: 'INVALID_SENDER_ID' + }; + } + + if (message.targetId && typeof message.targetId !== 'string') { + return { + valid: false, + error: 'Target ID must be a string', + errorCode: 'INVALID_TARGET_ID' + }; + } + + if (message.reliable !== undefined && typeof message.reliable !== 'boolean') { + return { + valid: false, + error: 'Reliable flag must be a boolean', + errorCode: 'INVALID_RELIABLE_FLAG' + }; + } + + return { valid: true }; + } + + /** + * 验证消息大小 + */ + private validateMessageSize(message: TransportMessage): ValidationResult { + try { + const serialized = JSON.stringify(message); + const size = new TextEncoder().encode(serialized).length; + + if (size > this.config.maxMessageSize!) { + return { + valid: false, + error: `Message size (${size} bytes) exceeds maximum (${this.config.maxMessageSize} bytes)`, + errorCode: 'MESSAGE_TOO_LARGE', + details: { actualSize: size, maxSize: this.config.maxMessageSize } + }; + } + + return { valid: true }; + + } catch (error) { + return { + valid: false, + error: 'Failed to serialize message for size validation', + errorCode: 'SERIALIZATION_ERROR' + }; + } + } + + /** + * 验证消息类型 + */ + private validateMessageType(message: TransportMessage): ValidationResult { + if (!this.config.allowedMessageTypes!.includes(message.type)) { + return { + valid: false, + error: `Message type '${message.type}' is not allowed`, + errorCode: 'INVALID_MESSAGE_TYPE', + details: { + messageType: message.type, + allowedTypes: this.config.allowedMessageTypes + } + }; + } + + return { valid: true }; + } + + /** + * 验证消息数据 + */ + private validateMessageData(message: TransportMessage): ValidationResult { + const context: ValidationContext = { + path: ['data'], + depth: 0, + config: this.config, + messageType: message.type + }; + + return this.validateValue(message.data, context); + } + + /** + * 验证值 + */ + private validateValue(value: any, context: ValidationContext): ValidationResult { + // 深度检查 + if (context.depth > this.config.maxObjectDepth!) { + return { + valid: false, + error: `Object depth (${context.depth}) exceeds maximum (${this.config.maxObjectDepth})`, + errorCode: 'OBJECT_TOO_DEEP', + details: { path: context.path.join('.'), depth: context.depth } + }; + } + + // null/undefined 检查 + if (value === null) { + if (!this.config.allowNullValues) { + return { + valid: false, + error: 'Null values are not allowed', + errorCode: 'NULL_NOT_ALLOWED', + details: { path: context.path.join('.') } + }; + } + return { valid: true }; + } + + if (value === undefined) { + if (!this.config.allowUndefinedValues) { + return { + valid: false, + error: 'Undefined values are not allowed', + errorCode: 'UNDEFINED_NOT_ALLOWED', + details: { path: context.path.join('.') } + }; + } + return { valid: true }; + } + + // 根据类型验证 + switch (typeof value) { + case 'string': + return this.validateString(value, context); + + case 'number': + return this.validateNumber(value, context); + + case 'boolean': + return { valid: true }; + + case 'object': + if (Array.isArray(value)) { + return this.validateArray(value, context); + } else { + return this.validateObject(value, context); + } + + default: + return { + valid: false, + error: `Unsupported value type: ${typeof value}`, + errorCode: 'UNSUPPORTED_TYPE', + details: { path: context.path.join('.'), type: typeof value } + }; + } + } + + /** + * 验证字符串 + */ + private validateString(value: string, context: ValidationContext): ValidationResult { + if (value.length > this.config.maxStringLength!) { + return { + valid: false, + error: `String length (${value.length}) exceeds maximum (${this.config.maxStringLength})`, + errorCode: 'STRING_TOO_LONG', + details: { + path: context.path.join('.'), + actualLength: value.length, + maxLength: this.config.maxStringLength + } + }; + } + + return { valid: true }; + } + + /** + * 验证数字 + */ + private validateNumber(value: number, context: ValidationContext): ValidationResult { + // 检查是否为有效数字 + if (!Number.isFinite(value)) { + return { + valid: false, + error: 'Number must be finite', + errorCode: 'INVALID_NUMBER', + details: { path: context.path.join('.'), value } + }; + } + + return { valid: true }; + } + + /** + * 验证数组 + */ + private validateArray(value: any[], context: ValidationContext): ValidationResult { + // 长度检查 + if (value.length > this.config.maxArrayLength!) { + return { + valid: false, + error: `Array length (${value.length}) exceeds maximum (${this.config.maxArrayLength})`, + errorCode: 'ARRAY_TOO_LONG', + details: { + path: context.path.join('.'), + actualLength: value.length, + maxLength: this.config.maxArrayLength + } + }; + } + + // 验证每个元素 + for (let i = 0; i < value.length; i++) { + const elementContext: ValidationContext = { + ...context, + path: [...context.path, `[${i}]`], + depth: context.depth + 1 + }; + + const result = this.validateValue(value[i], elementContext); + if (!result.valid) { + return result; + } + } + + return { valid: true }; + } + + /** + * 验证对象 + */ + private validateObject(value: Record, context: ValidationContext): ValidationResult { + // 验证每个属性 + for (const [key, propertyValue] of Object.entries(value)) { + const propertyContext: ValidationContext = { + ...context, + path: [...context.path, key], + depth: context.depth + 1 + }; + + const result = this.validateValue(propertyValue, propertyContext); + if (!result.valid) { + return result; + } + } + + return { valid: true }; + } + + /** + * 验证自定义规则 + */ + private validateCustomRules(message: TransportMessage): ValidationResult { + for (const rule of this.customRules.values()) { + const context: ValidationContext = { + path: [], + depth: 0, + config: this.config, + messageType: message.type + }; + + const result = rule.validate(message, context); + if (!result.valid) { + return { + ...result, + details: { + ...result.details, + rule: rule.name + } + }; + } + } + + return { valid: true }; + } +} + +/** + * 预定义验证规则 + */ +export const DefaultValidationRules = { + /** + * RPC 消息验证规则 + */ + RpcMessage: { + name: 'RpcMessage', + validate: (message: TransportMessage, context: ValidationContext): ValidationResult => { + if (message.type !== 'rpc') { + return { valid: true }; // 不是 RPC 消息,跳过验证 + } + + const data = message.data as any; + + // 检查必需字段 + if (!data.networkId || typeof data.networkId !== 'number') { + return { + valid: false, + error: 'RPC message must have a valid networkId', + errorCode: 'RPC_INVALID_NETWORK_ID' + }; + } + + if (!data.componentType || typeof data.componentType !== 'string') { + return { + valid: false, + error: 'RPC message must have a valid componentType', + errorCode: 'RPC_INVALID_COMPONENT_TYPE' + }; + } + + if (!data.methodName || typeof data.methodName !== 'string') { + return { + valid: false, + error: 'RPC message must have a valid methodName', + errorCode: 'RPC_INVALID_METHOD_NAME' + }; + } + + // 检查参数数组 + if (data.parameters && !Array.isArray(data.parameters)) { + return { + valid: false, + error: 'RPC parameters must be an array', + errorCode: 'RPC_INVALID_PARAMETERS' + }; + } + + return { valid: true }; + } + } as ValidationRule, + + /** + * SyncVar 消息验证规则 + */ + SyncVarMessage: { + name: 'SyncVarMessage', + validate: (message: TransportMessage, context: ValidationContext): ValidationResult => { + if (message.type !== 'syncvar') { + return { valid: true }; // 不是 SyncVar 消息,跳过验证 + } + + const data = message.data as any; + + // 检查必需字段 + if (!data.networkId || typeof data.networkId !== 'number') { + return { + valid: false, + error: 'SyncVar message must have a valid networkId', + errorCode: 'SYNCVAR_INVALID_NETWORK_ID' + }; + } + + if (!data.componentType || typeof data.componentType !== 'string') { + return { + valid: false, + error: 'SyncVar message must have a valid componentType', + errorCode: 'SYNCVAR_INVALID_COMPONENT_TYPE' + }; + } + + if (!data.propertyName || typeof data.propertyName !== 'string') { + return { + valid: false, + error: 'SyncVar message must have a valid propertyName', + errorCode: 'SYNCVAR_INVALID_PROPERTY_NAME' + }; + } + + return { valid: true }; + } + } as ValidationRule +}; \ No newline at end of file diff --git a/packages/network-server/src/validation/RpcValidator.ts b/packages/network-server/src/validation/RpcValidator.ts new file mode 100644 index 00000000..163abd8d --- /dev/null +++ b/packages/network-server/src/validation/RpcValidator.ts @@ -0,0 +1,776 @@ +/** + * RPC 验证器 + * + * 专门用于验证 RPC 调用的参数、权限、频率等 + */ + +import { NetworkValue, RpcMetadata } from '@esengine/ecs-framework-network-shared'; +import { ClientConnection } from '../core/ClientConnection'; +import { ValidationResult } from './MessageValidator'; + +/** + * RPC 验证配置 + */ +export interface RpcValidationConfig { + /** 最大参数数量 */ + maxParameterCount?: number; + /** 单个参数最大大小(字节) */ + maxParameterSize?: number; + /** 允许的参数类型 */ + allowedParameterTypes?: string[]; + /** 方法名黑名单 */ + blacklistedMethods?: string[]; + /** 方法名白名单 */ + whitelistedMethods?: string[]; + /** 是否启用参数类型检查 */ + enableTypeCheck?: boolean; + /** 是否启用参数内容过滤 */ + enableContentFilter?: boolean; +} + +/** + * RPC 调用上下文 + */ +export interface RpcCallContext { + /** 客户端连接 */ + client: ClientConnection; + /** 网络对象ID */ + networkId: number; + /** 组件类型 */ + componentType: string; + /** 方法名 */ + methodName: string; + /** 参数列表 */ + parameters: NetworkValue[]; + /** RPC 元数据 */ + metadata: RpcMetadata; + /** RPC 类型 */ + rpcType: 'client-rpc' | 'server-rpc'; +} + +/** + * 参数类型定义 + */ +export interface ParameterTypeDefinition { + /** 参数名 */ + name: string; + /** 参数类型 */ + type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any'; + /** 是否必需 */ + required?: boolean; + /** 最小值/长度 */ + min?: number; + /** 最大值/长度 */ + max?: number; + /** 允许的值列表 */ + allowedValues?: NetworkValue[]; + /** 正则表达式(仅用于字符串) */ + pattern?: RegExp; + /** 自定义验证函数 */ + customValidator?: (value: NetworkValue) => ValidationResult; +} + +/** + * 方法签名定义 + */ +export interface MethodSignature { + /** 方法名 */ + methodName: string; + /** 组件类型 */ + componentType: string; + /** 参数定义 */ + parameters: ParameterTypeDefinition[]; + /** 返回值类型 */ + returnType?: string; + /** 是否需要权限验证 */ + requiresAuth?: boolean; + /** 所需权限 */ + requiredPermissions?: string[]; + /** 频率限制(调用/分钟) */ + rateLimit?: number; + /** 自定义验证函数 */ + customValidator?: (context: RpcCallContext) => ValidationResult; +} + +/** + * RPC 频率跟踪 + */ +interface RpcRateTracker { + /** 客户端ID */ + clientId: string; + /** 方法调用计数 */ + methodCalls: Map; + /** 最后更新时间 */ + lastUpdate: Date; +} + +/** + * RPC 验证器 + */ +export class RpcValidator { + private config: RpcValidationConfig; + private methodSignatures = new Map(); + private rateTrackers = new Map(); + private cleanupTimer: NodeJS.Timeout | null = null; + + constructor(config: RpcValidationConfig = {}) { + this.config = { + maxParameterCount: 10, + maxParameterSize: 65536, // 64KB + allowedParameterTypes: ['string', 'number', 'boolean', 'object', 'array'], + blacklistedMethods: [], + whitelistedMethods: [], + enableTypeCheck: true, + enableContentFilter: true, + ...config + }; + + this.initialize(); + } + + /** + * 验证 RPC 调用 + */ + validateRpcCall(context: RpcCallContext): ValidationResult { + try { + // 基本验证 + const basicResult = this.validateBasicRpcCall(context); + if (!basicResult.valid) { + return basicResult; + } + + // 方法名验证 + const methodResult = this.validateMethodName(context); + if (!methodResult.valid) { + return methodResult; + } + + // 权限验证 + const permissionResult = this.validateRpcPermissions(context); + if (!permissionResult.valid) { + return permissionResult; + } + + // 参数验证 + const parameterResult = this.validateParameters(context); + if (!parameterResult.valid) { + return parameterResult; + } + + // 频率限制验证 + const rateResult = this.validateRateLimit(context); + if (!rateResult.valid) { + return rateResult; + } + + // 签名验证(如果有定义) + const signatureResult = this.validateMethodSignature(context); + if (!signatureResult.valid) { + return signatureResult; + } + + return { valid: true }; + + } catch (error) { + return { + valid: false, + error: (error as Error).message, + errorCode: 'RPC_VALIDATION_ERROR' + }; + } + } + + /** + * 注册方法签名 + */ + registerMethodSignature(signature: MethodSignature): void { + const key = `${signature.componentType}.${signature.methodName}`; + this.methodSignatures.set(key, signature); + } + + /** + * 移除方法签名 + */ + removeMethodSignature(componentType: string, methodName: string): boolean { + const key = `${componentType}.${methodName}`; + return this.methodSignatures.delete(key); + } + + /** + * 获取方法签名 + */ + getMethodSignature(componentType: string, methodName: string): MethodSignature | undefined { + const key = `${componentType}.${methodName}`; + return this.methodSignatures.get(key); + } + + /** + * 添加方法到黑名单 + */ + addToBlacklist(methodName: string): void { + if (!this.config.blacklistedMethods!.includes(methodName)) { + this.config.blacklistedMethods!.push(methodName); + } + } + + /** + * 从黑名单移除方法 + */ + removeFromBlacklist(methodName: string): boolean { + const index = this.config.blacklistedMethods!.indexOf(methodName); + if (index !== -1) { + this.config.blacklistedMethods!.splice(index, 1); + return true; + } + return false; + } + + /** + * 添加方法到白名单 + */ + addToWhitelist(methodName: string): void { + if (!this.config.whitelistedMethods!.includes(methodName)) { + this.config.whitelistedMethods!.push(methodName); + } + } + + /** + * 获取客户端的 RPC 统计 + */ + getClientRpcStats(clientId: string): { + totalCalls: number; + methodStats: Record; + } { + const tracker = this.rateTrackers.get(clientId); + if (!tracker) { + return { totalCalls: 0, methodStats: {} }; + } + + let totalCalls = 0; + const methodStats: Record = {}; + + for (const [method, data] of tracker.methodCalls) { + totalCalls += data.count; + methodStats[method] = data.count; + } + + return { totalCalls, methodStats }; + } + + /** + * 重置客户端的频率限制 + */ + resetClientRateLimit(clientId: string): boolean { + const tracker = this.rateTrackers.get(clientId); + if (!tracker) { + return false; + } + + tracker.methodCalls.clear(); + tracker.lastUpdate = new Date(); + return true; + } + + /** + * 销毁验证器 + */ + destroy(): void { + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + + this.methodSignatures.clear(); + this.rateTrackers.clear(); + } + + /** + * 初始化验证器 + */ + private initialize(): void { + // 启动清理定时器(每5分钟清理一次) + this.cleanupTimer = setInterval(() => { + this.cleanupRateTrackers(); + }, 5 * 60 * 1000); + } + + /** + * 基本 RPC 调用验证 + */ + private validateBasicRpcCall(context: RpcCallContext): ValidationResult { + // 网络对象ID验证 + if (!Number.isInteger(context.networkId) || context.networkId <= 0) { + return { + valid: false, + error: 'Invalid network object ID', + errorCode: 'INVALID_NETWORK_ID' + }; + } + + // 组件类型验证 + if (!context.componentType || typeof context.componentType !== 'string') { + return { + valid: false, + error: 'Invalid component type', + errorCode: 'INVALID_COMPONENT_TYPE' + }; + } + + // 方法名验证 + if (!context.methodName || typeof context.methodName !== 'string') { + return { + valid: false, + error: 'Invalid method name', + errorCode: 'INVALID_METHOD_NAME' + }; + } + + // 参数数组验证 + if (!Array.isArray(context.parameters)) { + return { + valid: false, + error: 'Parameters must be an array', + errorCode: 'INVALID_PARAMETERS_FORMAT' + }; + } + + // 参数数量检查 + if (context.parameters.length > this.config.maxParameterCount!) { + return { + valid: false, + error: `Too many parameters: ${context.parameters.length} (max: ${this.config.maxParameterCount})`, + errorCode: 'TOO_MANY_PARAMETERS' + }; + } + + return { valid: true }; + } + + /** + * 方法名验证 + */ + private validateMethodName(context: RpcCallContext): ValidationResult { + const methodName = context.methodName; + + // 黑名单检查 + if (this.config.blacklistedMethods!.length > 0) { + if (this.config.blacklistedMethods!.includes(methodName)) { + return { + valid: false, + error: `Method '${methodName}' is blacklisted`, + errorCode: 'METHOD_BLACKLISTED' + }; + } + } + + // 白名单检查 + if (this.config.whitelistedMethods!.length > 0) { + if (!this.config.whitelistedMethods!.includes(methodName)) { + return { + valid: false, + error: `Method '${methodName}' is not whitelisted`, + errorCode: 'METHOD_NOT_WHITELISTED' + }; + } + } + + // 危险方法名检查 + const dangerousPatterns = [ + /^__/, // 私有方法 + /constructor/i, + /prototype/i, + /eval/i, + /function/i + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(methodName)) { + return { + valid: false, + error: `Potentially dangerous method name: '${methodName}'`, + errorCode: 'DANGEROUS_METHOD_NAME' + }; + } + } + + return { valid: true }; + } + + /** + * RPC 权限验证 + */ + private validateRpcPermissions(context: RpcCallContext): ValidationResult { + // 基本 RPC 权限检查 + if (!context.client.hasPermission('canSendRpc')) { + return { + valid: false, + error: 'Client does not have RPC permission', + errorCode: 'RPC_PERMISSION_DENIED' + }; + } + + // ServerRpc 特殊权限检查 + if (context.rpcType === 'server-rpc') { + if (context.metadata.requiresAuth && !context.client.isAuthenticated) { + return { + valid: false, + error: 'Authentication required for this RPC', + errorCode: 'AUTHENTICATION_REQUIRED' + }; + } + } + + // 检查方法签名中的权限要求 + const signature = this.getMethodSignature(context.componentType, context.methodName); + if (signature && signature.requiredPermissions) { + for (const permission of signature.requiredPermissions) { + if (!context.client.hasCustomPermission(permission)) { + return { + valid: false, + error: `Required permission '${permission}' not found`, + errorCode: 'INSUFFICIENT_PERMISSIONS' + }; + } + } + } + + return { valid: true }; + } + + /** + * 参数验证 + */ + private validateParameters(context: RpcCallContext): ValidationResult { + // 参数大小检查 + for (let i = 0; i < context.parameters.length; i++) { + const param = context.parameters[i]; + + try { + const serialized = JSON.stringify(param); + const size = new TextEncoder().encode(serialized).length; + + if (size > this.config.maxParameterSize!) { + return { + valid: false, + error: `Parameter ${i} is too large: ${size} bytes (max: ${this.config.maxParameterSize})`, + errorCode: 'PARAMETER_TOO_LARGE' + }; + } + } catch (error) { + return { + valid: false, + error: `Parameter ${i} is not serializable`, + errorCode: 'PARAMETER_NOT_SERIALIZABLE' + }; + } + } + + // 参数类型检查 + if (this.config.enableTypeCheck) { + const typeResult = this.validateParameterTypes(context); + if (!typeResult.valid) { + return typeResult; + } + } + + // 参数内容过滤 + if (this.config.enableContentFilter) { + const contentResult = this.validateParameterContent(context); + if (!contentResult.valid) { + return contentResult; + } + } + + return { valid: true }; + } + + /** + * 参数类型验证 + */ + private validateParameterTypes(context: RpcCallContext): ValidationResult { + for (let i = 0; i < context.parameters.length; i++) { + const param = context.parameters[i]; + const paramType = this.getParameterType(param); + + if (!this.config.allowedParameterTypes!.includes(paramType)) { + return { + valid: false, + error: `Parameter ${i} type '${paramType}' is not allowed`, + errorCode: 'INVALID_PARAMETER_TYPE' + }; + } + } + + return { valid: true }; + } + + /** + * 参数内容验证 + */ + private validateParameterContent(context: RpcCallContext): ValidationResult { + for (let i = 0; i < context.parameters.length; i++) { + const param = context.parameters[i]; + + // 检查危险内容 + if (typeof param === 'string') { + const dangerousPatterns = [ + /