Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统 * feat(engine): 完善2D渲染管线和编辑器视口功能 * feat(editor): 实现Viewport变换工具系统 * feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示 * feat(editor): 实现Run on Device移动预览功能 * feat(editor): 添加组件属性控制和依赖关系系统 * feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器 * feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(ci): 迁移项目到pnpm并修复CI构建问题 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 移除 network 相关包 * chore: 移除 network 相关包
This commit is contained in:
@@ -1,119 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建 @esengine/network-server 包...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('npx rollup -c rollup.config.cjs', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ @esengine/network-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',
|
||||
'network',
|
||||
'server',
|
||||
'multiplayer',
|
||||
'game',
|
||||
'nodejs',
|
||||
'typescript'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
dependencies: sourcePackage.dependencies,
|
||||
publishConfig: sourcePackage.publishConfig,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
sideEffects: false
|
||||
};
|
||||
|
||||
fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2));
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
if (fs.existsSync(src)) {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` ✓ 复制: ${path.basename(dest)}`);
|
||||
} else {
|
||||
console.log(` ⚠️ 文件不存在: ${src}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showBuildResults() {
|
||||
const distDir = './dist';
|
||||
const files = ['index.mjs', 'index.cjs', 'index.d.ts'];
|
||||
|
||||
console.log('\n📊 构建结果:');
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(distDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const size = fs.statSync(filePath).size;
|
||||
console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/network-server",
|
||||
"version": "1.0.1",
|
||||
"description": "ECS Framework网络层 - 服务端实现",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./bin/index.d.ts",
|
||||
"import": "./bin/index.js",
|
||||
"development": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"bin/**/*",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"network",
|
||||
"server",
|
||||
"multiplayer",
|
||||
"game",
|
||||
"nodejs",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
"build:watch": "tsc --watch",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build:npm": "npm run build && node build-rollup.cjs",
|
||||
"publish:npm": "npm run build:npm && cd dist && npm publish",
|
||||
"publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish",
|
||||
"publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish",
|
||||
"publish:major": "npm version major && npm run build:npm && cd dist && npm publish",
|
||||
"preversion": "npm run rebuild",
|
||||
"dev": "ts-node src/dev-server.ts",
|
||||
"start": "node bin/index.js",
|
||||
"test": "echo \"No tests configured for network-server\""
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../core",
|
||||
"@esengine/network-shared": "file:../network-shared",
|
||||
"ws": "^8.18.2",
|
||||
"uuid": "^10.0.0",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-node": "^10.9.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"
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
const resolve = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const terser = require('@rollup/plugin-terser');
|
||||
const dts = require('rollup-plugin-dts').default;
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/network-server v${pkg.version}
|
||||
* ECS网络层服务端实现
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
// 外部依赖,不打包进bundle (Node.js环境,保持依赖外部化)
|
||||
const external = [
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/network-shared',
|
||||
'ws',
|
||||
'reflect-metadata',
|
||||
'http',
|
||||
'https',
|
||||
'crypto',
|
||||
'events',
|
||||
'stream',
|
||||
'util',
|
||||
'fs',
|
||||
'path'
|
||||
];
|
||||
|
||||
const commonPlugins = [
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: false
|
||||
}),
|
||||
commonjs({
|
||||
include: /node_modules/
|
||||
})
|
||||
];
|
||||
|
||||
module.exports = [
|
||||
// ES模块构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.mjs',
|
||||
format: 'es',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
unknownGlobalSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// CommonJS构建 (Node.js主要格式)
|
||||
{
|
||||
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/network-server v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
respectExternal: true
|
||||
})
|
||||
],
|
||||
external
|
||||
}
|
||||
];
|
||||
@@ -1,410 +0,0 @@
|
||||
/**
|
||||
* 服务端连接管理器
|
||||
* 负责管理所有客户端连接的生命周期
|
||||
*/
|
||||
import { createLogger, ITimer, Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ITransportClientInfo,
|
||||
ConnectionState,
|
||||
IConnectionStats,
|
||||
EventEmitter
|
||||
} from '@esengine/network-shared';
|
||||
|
||||
/**
|
||||
* 客户端会话信息
|
||||
*/
|
||||
export interface ClientSession {
|
||||
id: string;
|
||||
info: ITransportClientInfo;
|
||||
state: ConnectionState;
|
||||
lastHeartbeat: number;
|
||||
stats: IConnectionStats;
|
||||
authenticated: boolean;
|
||||
roomId?: string;
|
||||
userData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接管理器配置
|
||||
*/
|
||||
export interface ConnectionManagerConfig {
|
||||
heartbeatInterval: number;
|
||||
heartbeatTimeout: number;
|
||||
maxIdleTime: number;
|
||||
cleanupInterval: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接管理器
|
||||
*/
|
||||
export class ConnectionManager extends EventEmitter {
|
||||
private logger = createLogger('ConnectionManager');
|
||||
private sessions: Map<string, ClientSession> = new Map();
|
||||
private config: ConnectionManagerConfig;
|
||||
private heartbeatTimer?: ITimer;
|
||||
private cleanupTimer?: ITimer;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<ConnectionManagerConfig> = {}) {
|
||||
super();
|
||||
this.config = {
|
||||
heartbeatInterval: 30000, // 30秒心跳间隔
|
||||
heartbeatTimeout: 60000, // 60秒心跳超时
|
||||
maxIdleTime: 300000, // 5分钟最大空闲时间
|
||||
cleanupInterval: 60000, // 1分钟清理间隔
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动连接管理器
|
||||
*/
|
||||
start(): void {
|
||||
this.logger.info('连接管理器启动');
|
||||
this.startHeartbeatTimer();
|
||||
this.startCleanupTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止连接管理器
|
||||
*/
|
||||
stop(): void {
|
||||
this.logger.info('连接管理器停止');
|
||||
this.stopHeartbeatTimer();
|
||||
this.stopCleanupTimer();
|
||||
this.sessions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加客户端会话
|
||||
*/
|
||||
addSession(clientInfo: ITransportClientInfo): ClientSession {
|
||||
const session: ClientSession = {
|
||||
id: clientInfo.id,
|
||||
info: clientInfo,
|
||||
state: ConnectionState.Connected,
|
||||
lastHeartbeat: Date.now(),
|
||||
authenticated: false,
|
||||
stats: {
|
||||
state: ConnectionState.Connected,
|
||||
connectTime: clientInfo.connectTime,
|
||||
reconnectCount: 0,
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
messagesSent: 0,
|
||||
messagesReceived: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.sessions.set(clientInfo.id, session);
|
||||
this.logger.info(`添加客户端会话: ${clientInfo.id}`);
|
||||
|
||||
this.emit('sessionAdded', session);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除客户端会话
|
||||
*/
|
||||
removeSession(clientId: string, reason?: string): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
session.state = ConnectionState.Disconnected;
|
||||
session.stats.disconnectTime = Date.now();
|
||||
|
||||
this.sessions.delete(clientId);
|
||||
this.logger.info(`移除客户端会话: ${clientId}, 原因: ${reason || '未知'}`);
|
||||
|
||||
this.emit('sessionRemoved', session, reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端会话
|
||||
*/
|
||||
getSession(clientId: string): ClientSession | undefined {
|
||||
return this.sessions.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端会话
|
||||
*/
|
||||
getAllSessions(): ClientSession[] {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已认证的会话
|
||||
*/
|
||||
getAuthenticatedSessions(): ClientSession[] {
|
||||
return Array.from(this.sessions.values()).filter((session) => session.authenticated);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定房间的会话
|
||||
*/
|
||||
getSessionsByRoom(roomId: string): ClientSession[] {
|
||||
return Array.from(this.sessions.values()).filter((session) => session.roomId === roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话心跳时间
|
||||
*/
|
||||
updateHeartbeat(clientId: string): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
session.lastHeartbeat = Date.now();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话认证状态
|
||||
*/
|
||||
setSessionAuthenticated(clientId: string, authenticated: boolean): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const wasAuthenticated = session.authenticated;
|
||||
session.authenticated = authenticated;
|
||||
|
||||
if (wasAuthenticated !== authenticated) {
|
||||
this.emit('sessionAuthChanged', session, authenticated);
|
||||
this.logger.info(`客户端 ${clientId} 认证状态变更: ${authenticated}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话所在房间
|
||||
*/
|
||||
setSessionRoom(clientId: string, roomId?: string): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldRoomId = session.roomId;
|
||||
session.roomId = roomId;
|
||||
|
||||
if (oldRoomId !== roomId) {
|
||||
this.emit('sessionRoomChanged', session, oldRoomId, roomId);
|
||||
this.logger.info(`客户端 ${clientId} 房间变更: ${oldRoomId} -> ${roomId}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话数据统计
|
||||
*/
|
||||
updateSessionStats(clientId: string, stats: Partial<IConnectionStats>): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(session.stats, stats);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话用户数据
|
||||
*/
|
||||
setSessionUserData(clientId: string, userData: Record<string, any>): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
session.userData = { ...session.userData, ...userData };
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查会话是否存活
|
||||
*/
|
||||
isSessionAlive(clientId: string): boolean {
|
||||
const session = this.sessions.get(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
return (now - session.lastHeartbeat) <= this.config.heartbeatTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取超时的会话
|
||||
*/
|
||||
getTimeoutSessions(): ClientSession[] {
|
||||
const now = Date.now();
|
||||
return Array.from(this.sessions.values()).filter((session) => {
|
||||
return (now - session.lastHeartbeat) > this.config.heartbeatTimeout;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取空闲的会话
|
||||
*/
|
||||
getIdleSessions(): ClientSession[] {
|
||||
const now = Date.now();
|
||||
return Array.from(this.sessions.values()).filter((session) => {
|
||||
return (now - session.lastHeartbeat) > this.config.maxIdleTime;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接统计信息
|
||||
*/
|
||||
getConnectionStats() {
|
||||
const allSessions = this.getAllSessions();
|
||||
const authenticatedSessions = this.getAuthenticatedSessions();
|
||||
const timeoutSessions = this.getTimeoutSessions();
|
||||
|
||||
return {
|
||||
totalConnections: allSessions.length,
|
||||
authenticatedConnections: authenticatedSessions.length,
|
||||
timeoutConnections: timeoutSessions.length,
|
||||
averageLatency: this.calculateAverageLatency(allSessions),
|
||||
connectionsByRoom: this.getConnectionsByRoom(),
|
||||
totalBytesSent: allSessions.reduce((sum, s) => sum + s.stats.bytesSent, 0),
|
||||
totalBytesReceived: allSessions.reduce((sum, s) => sum + s.stats.bytesReceived, 0),
|
||||
totalMessagesSent: allSessions.reduce((sum, s) => sum + s.stats.messagesSent, 0),
|
||||
totalMessagesReceived: allSessions.reduce((sum, s) => sum + s.stats.messagesReceived, 0)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算平均延迟
|
||||
*/
|
||||
private calculateAverageLatency(sessions: ClientSession[]): number {
|
||||
const validLatencies = sessions
|
||||
.map((s) => s.stats.latency)
|
||||
.filter((latency) => latency !== undefined) as number[];
|
||||
|
||||
if (validLatencies.length === 0) return 0;
|
||||
|
||||
return validLatencies.reduce((sum, latency) => sum + latency, 0) / validLatencies.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按房间统计连接数
|
||||
*/
|
||||
private getConnectionsByRoom(): Record<string, number> {
|
||||
const roomCounts: Record<string, number> = {};
|
||||
|
||||
for (const session of this.sessions.values()) {
|
||||
const roomId = session.roomId || 'lobby';
|
||||
roomCounts[roomId] = (roomCounts[roomId] || 0) + 1;
|
||||
}
|
||||
|
||||
return roomCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳定时器
|
||||
*/
|
||||
private startHeartbeatTimer(): void {
|
||||
this.heartbeatTimer = Core.schedule(this.config.heartbeatInterval / 1000, true, this, () => {
|
||||
this.checkHeartbeats();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳定时器
|
||||
*/
|
||||
private stopHeartbeatTimer(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
this.heartbeatTimer.stop();
|
||||
this.heartbeatTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动清理定时器
|
||||
*/
|
||||
private startCleanupTimer(): void {
|
||||
this.cleanupTimer = Core.schedule(this.config.cleanupInterval / 1000, true, this, () => {
|
||||
this.performCleanup();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止清理定时器
|
||||
*/
|
||||
private stopCleanupTimer(): void {
|
||||
if (this.cleanupTimer) {
|
||||
this.cleanupTimer.stop();
|
||||
this.cleanupTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查心跳超时
|
||||
*/
|
||||
private checkHeartbeats(): void {
|
||||
const timeoutSessions = this.getTimeoutSessions();
|
||||
|
||||
for (const session of timeoutSessions) {
|
||||
this.logger.warn(`客户端心跳超时: ${session.id}`);
|
||||
this.emit('heartbeatTimeout', session);
|
||||
|
||||
// 可以选择断开超时的连接
|
||||
// this.removeSession(session.id, '心跳超时');
|
||||
}
|
||||
|
||||
if (timeoutSessions.length > 0) {
|
||||
this.logger.warn(`发现 ${timeoutSessions.length} 个心跳超时的连接`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行清理操作
|
||||
*/
|
||||
private performCleanup(): void {
|
||||
const idleSessions = this.getIdleSessions();
|
||||
|
||||
for (const session of idleSessions) {
|
||||
this.logger.info(`清理空闲连接: ${session.id}`);
|
||||
this.removeSession(session.id, '空闲超时');
|
||||
}
|
||||
|
||||
if (idleSessions.length > 0) {
|
||||
this.logger.info(`清理了 ${idleSessions.length} 个空闲连接`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作:踢出指定房间的所有客户端
|
||||
*/
|
||||
kickRoomClients(roomId: string, reason?: string): number {
|
||||
const roomSessions = this.getSessionsByRoom(roomId);
|
||||
|
||||
for (const session of roomSessions) {
|
||||
this.removeSession(session.id, reason || '房间解散');
|
||||
}
|
||||
|
||||
this.logger.info(`踢出房间 ${roomId} 的 ${roomSessions.length} 个客户端`);
|
||||
return roomSessions.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作:向指定房间广播消息(这里只返回会话列表)
|
||||
*/
|
||||
getRoomSessionsForBroadcast(roomId: string, excludeClientId?: string): ClientSession[] {
|
||||
return this.getSessionsByRoom(roomId).filter((session) =>
|
||||
session.id !== excludeClientId && session.authenticated
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,700 +0,0 @@
|
||||
/**
|
||||
* 网络服务器核心类
|
||||
* 负责服务器的启动/停止、传输层管理和客户端会话管理
|
||||
*/
|
||||
import { createLogger, Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ITransportConfig,
|
||||
MessageType,
|
||||
INetworkMessage,
|
||||
IConnectMessage,
|
||||
IConnectResponseMessage,
|
||||
IHeartbeatMessage,
|
||||
EventEmitter
|
||||
} from '@esengine/network-shared';
|
||||
import { WebSocketTransport } from '../transport/WebSocketTransport';
|
||||
import { ConnectionManager, ClientSession } from './ConnectionManager';
|
||||
import { JSONSerializer } from '@esengine/network-shared';
|
||||
import { MessageManager } from '@esengine/network-shared';
|
||||
import { ErrorHandler } from '@esengine/network-shared';
|
||||
|
||||
/**
|
||||
* 网络服务器配置
|
||||
*/
|
||||
export interface NetworkServerConfig {
|
||||
transport: ITransportConfig;
|
||||
authentication: {
|
||||
required: boolean;
|
||||
timeout: number;
|
||||
maxAttempts: number;
|
||||
};
|
||||
rateLimit: {
|
||||
enabled: boolean;
|
||||
maxRequestsPerMinute: number;
|
||||
banDuration: number;
|
||||
};
|
||||
features: {
|
||||
enableCompression: boolean;
|
||||
enableHeartbeat: boolean;
|
||||
enableRooms: boolean;
|
||||
enableMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器状态
|
||||
*/
|
||||
export enum ServerState {
|
||||
Stopped = 'stopped',
|
||||
Starting = 'starting',
|
||||
Running = 'running',
|
||||
Stopping = 'stopping',
|
||||
Error = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器统计信息
|
||||
*/
|
||||
export interface ServerStats {
|
||||
state: ServerState;
|
||||
uptime: number;
|
||||
startTime?: number;
|
||||
connections: {
|
||||
total: number;
|
||||
authenticated: number;
|
||||
peak: number;
|
||||
};
|
||||
messages: {
|
||||
sent: number;
|
||||
received: number;
|
||||
errors: number;
|
||||
};
|
||||
bandwidth: {
|
||||
inbound: number;
|
||||
outbound: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络服务器事件接口
|
||||
*/
|
||||
export interface NetworkServerEvents {
|
||||
serverStarted: (port: number) => void;
|
||||
serverStopped: () => void;
|
||||
serverError: (error: Error) => void;
|
||||
clientConnected: (session: ClientSession) => void;
|
||||
clientDisconnected: (session: ClientSession, reason?: string) => void;
|
||||
clientAuthenticated: (session: ClientSession) => void;
|
||||
messageReceived: (session: ClientSession, message: INetworkMessage) => void;
|
||||
messageSent: (session: ClientSession, message: INetworkMessage) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络服务器核心实现
|
||||
*/
|
||||
export class NetworkServer extends EventEmitter {
|
||||
private logger = createLogger('NetworkServer');
|
||||
private config: NetworkServerConfig;
|
||||
private state: ServerState = ServerState.Stopped;
|
||||
private stats: ServerStats;
|
||||
|
||||
// 核心组件
|
||||
private transport?: WebSocketTransport;
|
||||
private connectionManager: ConnectionManager;
|
||||
private serializer: JSONSerializer;
|
||||
private messageManager: MessageManager;
|
||||
private errorHandler: ErrorHandler;
|
||||
|
||||
// 事件处理器
|
||||
private eventHandlers: Partial<NetworkServerEvents> = {};
|
||||
|
||||
// 速率限制
|
||||
private rateLimitMap: Map<string, { count: number; resetTime: number; banned: boolean }> = new Map();
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<NetworkServerConfig> = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
transport: {
|
||||
port: 8080,
|
||||
host: '0.0.0.0',
|
||||
maxConnections: 1000,
|
||||
heartbeatInterval: 30000,
|
||||
connectionTimeout: 60000,
|
||||
maxMessageSize: 1024 * 1024,
|
||||
compression: true,
|
||||
...config.transport
|
||||
},
|
||||
authentication: {
|
||||
required: false,
|
||||
timeout: 30000,
|
||||
maxAttempts: 3,
|
||||
...config.authentication
|
||||
},
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
maxRequestsPerMinute: 100,
|
||||
banDuration: 300000, // 5分钟
|
||||
...config.rateLimit
|
||||
},
|
||||
features: {
|
||||
enableCompression: true,
|
||||
enableHeartbeat: true,
|
||||
enableRooms: true,
|
||||
enableMetrics: true,
|
||||
...config.features
|
||||
}
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
state: ServerState.Stopped,
|
||||
uptime: 0,
|
||||
connections: {
|
||||
total: 0,
|
||||
authenticated: 0,
|
||||
peak: 0
|
||||
},
|
||||
messages: {
|
||||
sent: 0,
|
||||
received: 0,
|
||||
errors: 0
|
||||
},
|
||||
bandwidth: {
|
||||
inbound: 0,
|
||||
outbound: 0
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化核心组件
|
||||
this.connectionManager = new ConnectionManager({
|
||||
heartbeatInterval: this.config.transport.heartbeatInterval,
|
||||
heartbeatTimeout: this.config.transport.connectionTimeout
|
||||
});
|
||||
|
||||
this.serializer = new JSONSerializer({
|
||||
enableTypeChecking: true,
|
||||
enableCompression: this.config.features.enableCompression,
|
||||
maxMessageSize: this.config.transport.maxMessageSize
|
||||
});
|
||||
|
||||
this.messageManager = new MessageManager({
|
||||
enableTimestampValidation: true,
|
||||
enableMessageDeduplication: true
|
||||
});
|
||||
|
||||
this.errorHandler = new ErrorHandler({
|
||||
maxRetryAttempts: 3,
|
||||
enableAutoRecovery: true
|
||||
});
|
||||
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.state !== ServerState.Stopped) {
|
||||
throw new Error(`服务器状态错误: ${this.state}`);
|
||||
}
|
||||
|
||||
this.setState(ServerState.Starting);
|
||||
this.logger.info('正在启动网络服务器...');
|
||||
|
||||
try {
|
||||
// 创建传输层
|
||||
this.transport = new WebSocketTransport(this.config.transport);
|
||||
this.setupTransportEvents();
|
||||
|
||||
// 启动传输层
|
||||
await this.transport.start(
|
||||
this.config.transport.port,
|
||||
this.config.transport.host
|
||||
);
|
||||
|
||||
// 启动连接管理器
|
||||
this.connectionManager.start();
|
||||
|
||||
// 记录启动时间
|
||||
this.stats.startTime = Date.now();
|
||||
this.setState(ServerState.Running);
|
||||
|
||||
this.logger.info(`网络服务器已启动: ${this.config.transport.host}:${this.config.transport.port}`);
|
||||
this.eventHandlers.serverStarted?.(this.config.transport.port);
|
||||
|
||||
} catch (error) {
|
||||
this.setState(ServerState.Error);
|
||||
this.logger.error('启动网络服务器失败:', error);
|
||||
this.eventHandlers.serverError?.(error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务器
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (this.state === ServerState.Stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(ServerState.Stopping);
|
||||
this.logger.info('正在停止网络服务器...');
|
||||
|
||||
try {
|
||||
// 停止连接管理器
|
||||
this.connectionManager.stop();
|
||||
|
||||
// 停止传输层
|
||||
if (this.transport) {
|
||||
await this.transport.stop();
|
||||
this.transport = undefined;
|
||||
}
|
||||
|
||||
// 清理速率限制数据
|
||||
this.rateLimitMap.clear();
|
||||
|
||||
this.setState(ServerState.Stopped);
|
||||
this.logger.info('网络服务器已停止');
|
||||
this.eventHandlers.serverStopped?.();
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('停止网络服务器失败:', error);
|
||||
this.eventHandlers.serverError?.(error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到指定客户端
|
||||
*/
|
||||
sendToClient<T extends INetworkMessage>(clientId: string, message: T): boolean {
|
||||
if (!this.transport || this.state !== ServerState.Running) {
|
||||
this.logger.warn('服务器未运行,无法发送消息');
|
||||
return false;
|
||||
}
|
||||
|
||||
const session = this.connectionManager.getSession(clientId);
|
||||
if (!session) {
|
||||
this.logger.warn(`客户端会话不存在: ${clientId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const serializedMessage = this.serializer.serialize(message);
|
||||
this.transport.send(clientId, serializedMessage.data);
|
||||
|
||||
// 更新统计
|
||||
this.stats.messages.sent++;
|
||||
this.stats.bandwidth.outbound += serializedMessage.size;
|
||||
|
||||
this.eventHandlers.messageSent?.(session, message);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`发送消息到客户端 ${clientId} 失败:`, error);
|
||||
this.stats.messages.errors++;
|
||||
this.errorHandler.handleError(error as Error, `sendToClient:${clientId}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息到所有客户端
|
||||
*/
|
||||
broadcast<T extends INetworkMessage>(message: T, exclude?: string[]): number {
|
||||
if (!this.transport || this.state !== ServerState.Running) {
|
||||
this.logger.warn('服务器未运行,无法广播消息');
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const serializedMessage = this.serializer.serialize(message);
|
||||
this.transport.broadcast(serializedMessage.data, exclude);
|
||||
|
||||
const clientCount = this.connectionManager.getAllSessions().length - (exclude?.length || 0);
|
||||
|
||||
// 更新统计
|
||||
this.stats.messages.sent += clientCount;
|
||||
this.stats.bandwidth.outbound += serializedMessage.size * clientCount;
|
||||
|
||||
return clientCount;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('广播消息失败:', error);
|
||||
this.stats.messages.errors++;
|
||||
this.errorHandler.handleError(error as Error, 'broadcast');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢出客户端
|
||||
*/
|
||||
kickClient(clientId: string, reason?: string): boolean {
|
||||
const session = this.connectionManager.getSession(clientId);
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.transport) {
|
||||
this.transport.disconnectClient(clientId, reason);
|
||||
}
|
||||
|
||||
return this.connectionManager.removeSession(clientId, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器状态
|
||||
*/
|
||||
getState(): ServerState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务器是否正在运行
|
||||
*/
|
||||
isRunning(): boolean {
|
||||
return this.state === ServerState.Running;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器统计信息
|
||||
*/
|
||||
getStats(): ServerStats {
|
||||
const currentStats = { ...this.stats };
|
||||
|
||||
if (this.stats.startTime) {
|
||||
currentStats.uptime = Date.now() - this.stats.startTime;
|
||||
}
|
||||
|
||||
const connectionStats = this.connectionManager.getConnectionStats();
|
||||
currentStats.connections.total = connectionStats.totalConnections;
|
||||
currentStats.connections.authenticated = connectionStats.authenticatedConnections;
|
||||
|
||||
return currentStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端会话
|
||||
*/
|
||||
getAllSessions(): ClientSession[] {
|
||||
return this.connectionManager.getAllSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定客户端会话
|
||||
*/
|
||||
getSession(clientId: string): ClientSession | undefined {
|
||||
return this.connectionManager.getSession(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件处理器
|
||||
*/
|
||||
override on<K extends keyof NetworkServerEvents>(event: K, handler: NetworkServerEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof NetworkServerEvents>(event: K): this {
|
||||
delete this.eventHandlers[event];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<NetworkServerConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
this.logger.info('服务器配置已更新:', newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器状态
|
||||
*/
|
||||
private setState(newState: ServerState): void {
|
||||
if (this.state === newState) return;
|
||||
|
||||
const oldState = this.state;
|
||||
this.state = newState;
|
||||
this.stats.state = newState;
|
||||
|
||||
this.logger.info(`服务器状态变化: ${oldState} -> ${newState}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件处理器
|
||||
*/
|
||||
private setupEventHandlers(): void {
|
||||
// 连接管理器事件
|
||||
this.connectionManager.on('sessionAdded', (session: ClientSession) => {
|
||||
this.eventHandlers.clientConnected?.(session);
|
||||
this.updateConnectionPeak();
|
||||
});
|
||||
|
||||
this.connectionManager.on('sessionRemoved', (session: ClientSession, reason?: string) => {
|
||||
this.eventHandlers.clientDisconnected?.(session, reason);
|
||||
});
|
||||
|
||||
this.connectionManager.on('sessionAuthChanged', (session: ClientSession, authenticated: boolean) => {
|
||||
if (authenticated) {
|
||||
this.eventHandlers.clientAuthenticated?.(session);
|
||||
}
|
||||
});
|
||||
|
||||
// 错误处理器事件
|
||||
this.errorHandler.on('criticalError', (error: any) => {
|
||||
this.logger.error('严重错误:', error);
|
||||
this.eventHandlers.serverError?.(new Error(error.message));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置传输层事件
|
||||
*/
|
||||
private setupTransportEvents(): void {
|
||||
if (!this.transport) return;
|
||||
|
||||
this.transport.onConnect((clientInfo) => {
|
||||
this.handleClientConnect(clientInfo);
|
||||
});
|
||||
|
||||
this.transport.onDisconnect((clientId, reason) => {
|
||||
this.handleClientDisconnect(clientId, reason);
|
||||
});
|
||||
|
||||
this.transport.onMessage((clientId, data) => {
|
||||
this.handleClientMessage(clientId, data);
|
||||
});
|
||||
|
||||
this.transport.onError((error) => {
|
||||
this.handleTransportError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端连接
|
||||
*/
|
||||
private handleClientConnect(clientInfo: any): void {
|
||||
try {
|
||||
// 检查速率限制
|
||||
if (this.isRateLimited(clientInfo.remoteAddress)) {
|
||||
this.transport?.disconnectClient(clientInfo.id, '速率限制');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建客户端会话
|
||||
const session = this.connectionManager.addSession(clientInfo);
|
||||
|
||||
this.logger.info(`客户端已连接: ${clientInfo.id} from ${clientInfo.remoteAddress}`);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('处理客户端连接失败:', error);
|
||||
this.transport?.disconnectClient(clientInfo.id, '服务器错误');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接
|
||||
*/
|
||||
private handleClientDisconnect(clientId: string, reason?: string): void {
|
||||
this.connectionManager.removeSession(clientId, reason);
|
||||
this.logger.info(`客户端已断开连接: ${clientId}, 原因: ${reason || '未知'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端消息
|
||||
*/
|
||||
private handleClientMessage(clientId: string, data: ArrayBuffer | string): void {
|
||||
try {
|
||||
// 获取客户端会话
|
||||
const session = this.connectionManager.getSession(clientId);
|
||||
if (!session) {
|
||||
this.logger.warn(`收到未知客户端消息: ${clientId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查速率限制
|
||||
if (this.isRateLimited(session.info.remoteAddress)) {
|
||||
this.kickClient(clientId, '速率限制');
|
||||
return;
|
||||
}
|
||||
|
||||
// 反序列化消息
|
||||
const deserializationResult = this.serializer.deserialize<INetworkMessage>(data);
|
||||
if (!deserializationResult.isValid) {
|
||||
this.logger.debug(`消息反序列化失败 (${clientId}): ${deserializationResult.errors?.join(', ')}`);
|
||||
this.stats.messages.errors++;
|
||||
return;
|
||||
}
|
||||
|
||||
const message = deserializationResult.data;
|
||||
|
||||
// 验证消息
|
||||
const validationResult = this.messageManager.validateMessage(message, clientId);
|
||||
if (!validationResult.isValid) {
|
||||
this.logger.warn(`消息验证失败: ${validationResult.errors.join(', ')}`);
|
||||
this.stats.messages.errors++;
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新心跳
|
||||
this.connectionManager.updateHeartbeat(clientId);
|
||||
|
||||
// 更新统计
|
||||
this.stats.messages.received++;
|
||||
this.stats.bandwidth.inbound += (typeof data === 'string' ? data.length : data.byteLength);
|
||||
|
||||
// 处理不同类型的消息
|
||||
this.processMessage(session, message);
|
||||
|
||||
this.eventHandlers.messageReceived?.(session, message);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`处理客户端 ${clientId} 消息失败:`, error);
|
||||
this.stats.messages.errors++;
|
||||
this.errorHandler.handleError(error as Error, `handleClientMessage:${clientId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传输层错误
|
||||
*/
|
||||
private handleTransportError(error: Error): void {
|
||||
this.logger.error('传输层错误:', error);
|
||||
this.errorHandler.handleError(error, 'transport');
|
||||
this.eventHandlers.serverError?.(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理具体消息类型
|
||||
*/
|
||||
private processMessage(session: ClientSession, message: INetworkMessage): void {
|
||||
switch (message.type) {
|
||||
case MessageType.CONNECT:
|
||||
this.handleConnectMessage(session, message as IConnectMessage);
|
||||
break;
|
||||
|
||||
case MessageType.HEARTBEAT:
|
||||
this.handleHeartbeatMessage(session, message as IHeartbeatMessage);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 其他消息类型由外部处理器处理
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接消息
|
||||
*/
|
||||
private handleConnectMessage(session: ClientSession, message: IConnectMessage): void {
|
||||
const response: IConnectResponseMessage = this.messageManager.createMessage(
|
||||
MessageType.CONNECT,
|
||||
{
|
||||
success: true,
|
||||
clientId: session.id,
|
||||
serverInfo: {
|
||||
name: 'ECS Network Server',
|
||||
version: '1.0.0',
|
||||
maxPlayers: this.config.transport.maxConnections || 1000,
|
||||
currentPlayers: this.connectionManager.getAllSessions().length
|
||||
}
|
||||
},
|
||||
'server'
|
||||
);
|
||||
|
||||
this.sendToClient(session.id, response);
|
||||
|
||||
if (this.config.authentication.required) {
|
||||
// 设置认证超时
|
||||
Core.schedule(this.config.authentication.timeout / 1000, false, this, () => {
|
||||
if (!session.authenticated) {
|
||||
this.kickClient(session.id, '认证超时');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 自动设置为已认证
|
||||
this.connectionManager.setSessionAuthenticated(session.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳消息
|
||||
*/
|
||||
private handleHeartbeatMessage(session: ClientSession, message: IHeartbeatMessage): void {
|
||||
const response: IHeartbeatMessage = this.messageManager.createMessage(
|
||||
MessageType.HEARTBEAT,
|
||||
{
|
||||
clientTime: message.data.clientTime,
|
||||
serverTime: Date.now()
|
||||
},
|
||||
'server'
|
||||
);
|
||||
|
||||
this.sendToClient(session.id, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查速率限制
|
||||
*/
|
||||
private isRateLimited(address: string): boolean {
|
||||
if (!this.config.rateLimit.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const limit = this.rateLimitMap.get(address);
|
||||
|
||||
if (!limit) {
|
||||
this.rateLimitMap.set(address, {
|
||||
count: 1,
|
||||
resetTime: now + 60000, // 1分钟重置
|
||||
banned: false
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否被封禁
|
||||
if (limit.banned && now < limit.resetTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 重置计数
|
||||
if (now > limit.resetTime) {
|
||||
limit.count = 1;
|
||||
limit.resetTime = now + 60000;
|
||||
limit.banned = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
limit.count++;
|
||||
|
||||
// 检查是否超过限制
|
||||
if (limit.count > this.config.rateLimit.maxRequestsPerMinute) {
|
||||
limit.banned = true;
|
||||
limit.resetTime = now + this.config.rateLimit.banDuration;
|
||||
this.logger.warn(`客户端 ${address} 被封禁,原因: 速率限制`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新连接峰值
|
||||
*/
|
||||
private updateConnectionPeak(): void {
|
||||
const current = this.connectionManager.getAllSessions().length;
|
||||
if (current > this.stats.connections.peak) {
|
||||
this.stats.connections.peak = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* @esengine/network-server
|
||||
* ECS Framework网络层 - 服务端实现
|
||||
*/
|
||||
|
||||
// 核心服务器
|
||||
export * from './core/NetworkServer';
|
||||
export * from './core/ConnectionManager';
|
||||
|
||||
// 传输层
|
||||
export * from './transport/WebSocketTransport';
|
||||
|
||||
// 房间管理
|
||||
export * from './rooms/Room';
|
||||
export * from './rooms/RoomManager';
|
||||
|
||||
// 系统
|
||||
export * from './systems';
|
||||
|
||||
// 同步模块
|
||||
export * from './sync';
|
||||
|
||||
// 重新导出shared包的类型
|
||||
export * from '@esengine/network-shared';
|
||||
@@ -1,507 +0,0 @@
|
||||
/**
|
||||
* 房间基础实现
|
||||
* 提供房间的基本功能,包括玩家管理和房间状态管理
|
||||
*/
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { RoomState, IRoomInfo, INetworkMessage, EventEmitter } from '@esengine/network-shared';
|
||||
import { ClientSession } from '../core/ConnectionManager';
|
||||
|
||||
/**
|
||||
* 房间配置
|
||||
*/
|
||||
export interface RoomConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
maxPlayers: number;
|
||||
isPublic: boolean;
|
||||
password?: string;
|
||||
metadata?: Record<string, any>;
|
||||
autoDestroy: boolean; // 是否在空房间时自动销毁
|
||||
customData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家信息
|
||||
*/
|
||||
export interface PlayerInfo {
|
||||
sessionId: string;
|
||||
name: string;
|
||||
isHost: boolean;
|
||||
joinTime: number;
|
||||
customData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间事件接口
|
||||
*/
|
||||
export interface RoomEvents {
|
||||
playerJoined: (player: PlayerInfo) => void;
|
||||
playerLeft: (player: PlayerInfo, reason?: string) => void;
|
||||
hostChanged: (oldHost: PlayerInfo, newHost: PlayerInfo) => void;
|
||||
stateChanged: (oldState: RoomState, newState: RoomState) => void;
|
||||
messageReceived: (message: INetworkMessage, fromPlayer: PlayerInfo) => void;
|
||||
roomDestroyed: (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间统计信息
|
||||
*/
|
||||
export interface RoomStats {
|
||||
id: string;
|
||||
playerCount: number;
|
||||
maxPlayers: number;
|
||||
createTime: number;
|
||||
totalPlayersJoined: number;
|
||||
messagesSent: number;
|
||||
messagesReceived: number;
|
||||
state: RoomState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间类
|
||||
*/
|
||||
export class Room extends EventEmitter {
|
||||
private logger = createLogger('Room');
|
||||
private config: RoomConfig;
|
||||
private state: RoomState = RoomState.Waiting;
|
||||
private players: Map<string, PlayerInfo> = new Map();
|
||||
private hostId?: string;
|
||||
private createTime: number = Date.now();
|
||||
private stats: RoomStats;
|
||||
|
||||
// 事件处理器
|
||||
private eventHandlers: Partial<RoomEvents> = {};
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: RoomConfig) {
|
||||
super();
|
||||
this.config = { ...config };
|
||||
|
||||
this.stats = {
|
||||
id: config.id,
|
||||
playerCount: 0,
|
||||
maxPlayers: config.maxPlayers,
|
||||
createTime: this.createTime,
|
||||
totalPlayersJoined: 0,
|
||||
messagesSent: 0,
|
||||
messagesReceived: 0,
|
||||
state: this.state
|
||||
};
|
||||
|
||||
this.logger.info(`房间已创建: ${config.id} (${config.name})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入房间
|
||||
*/
|
||||
addPlayer(session: ClientSession, playerName?: string, password?: string): boolean {
|
||||
// 检查房间是否已满
|
||||
if (this.players.size >= this.config.maxPlayers) {
|
||||
this.logger.warn(`房间已满,拒绝玩家加入: ${session.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查玩家是否已在房间中
|
||||
if (this.players.has(session.id)) {
|
||||
this.logger.warn(`玩家已在房间中: ${session.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查房间密码
|
||||
if (this.config.password && password !== this.config.password) {
|
||||
this.logger.warn(`密码错误,拒绝玩家加入: ${session.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查房间状态
|
||||
if (this.state === RoomState.Finished) {
|
||||
this.logger.warn(`房间已结束,拒绝玩家加入: ${session.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建玩家信息
|
||||
const player: PlayerInfo = {
|
||||
sessionId: session.id,
|
||||
name: playerName || `Player_${session.id.substr(-6)}`,
|
||||
isHost: this.players.size === 0, // 第一个加入的玩家成为房主
|
||||
joinTime: Date.now(),
|
||||
customData: {}
|
||||
};
|
||||
|
||||
// 添加玩家到房间
|
||||
this.players.set(session.id, player);
|
||||
this.stats.playerCount = this.players.size;
|
||||
this.stats.totalPlayersJoined++;
|
||||
|
||||
// 设置房主
|
||||
if (player.isHost) {
|
||||
this.hostId = session.id;
|
||||
}
|
||||
|
||||
this.logger.info(`玩家加入房间: ${player.name} (${session.id}) -> 房间 ${this.config.id}`);
|
||||
|
||||
// 触发事件
|
||||
this.eventHandlers.playerJoined?.(player);
|
||||
this.emit('playerJoined', player);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开房间
|
||||
*/
|
||||
removePlayer(sessionId: string, reason?: string): boolean {
|
||||
const player = this.players.get(sessionId);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从房间移除玩家
|
||||
this.players.delete(sessionId);
|
||||
this.stats.playerCount = this.players.size;
|
||||
|
||||
this.logger.info(`玩家离开房间: ${player.name} (${sessionId}) <- 房间 ${this.config.id}, 原因: ${reason || '未知'}`);
|
||||
|
||||
// 如果离开的是房主,需要转移房主权限
|
||||
if (player.isHost && this.players.size > 0) {
|
||||
this.transferHost();
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
this.eventHandlers.playerLeft?.(player, reason);
|
||||
this.emit('playerLeft', player, reason);
|
||||
|
||||
// 检查是否需要自动销毁房间
|
||||
if (this.config.autoDestroy && this.players.size === 0) {
|
||||
this.destroy('房间为空');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家信息
|
||||
*/
|
||||
getPlayer(sessionId: string): PlayerInfo | undefined {
|
||||
return this.players.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家
|
||||
*/
|
||||
getAllPlayers(): PlayerInfo[] {
|
||||
return Array.from(this.players.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房主
|
||||
*/
|
||||
getHost(): PlayerInfo | undefined {
|
||||
return this.hostId ? this.players.get(this.hostId) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转移房主权限
|
||||
*/
|
||||
transferHost(newHostId?: string): boolean {
|
||||
if (this.players.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldHost = this.getHost();
|
||||
let newHost: PlayerInfo | undefined;
|
||||
|
||||
if (newHostId) {
|
||||
newHost = this.players.get(newHostId);
|
||||
if (!newHost) {
|
||||
this.logger.warn(`指定的新房主不存在: ${newHostId}`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 自动选择第一个玩家作为新房主
|
||||
newHost = Array.from(this.players.values())[0];
|
||||
}
|
||||
|
||||
// 更新房主信息
|
||||
if (oldHost) {
|
||||
oldHost.isHost = false;
|
||||
}
|
||||
newHost.isHost = true;
|
||||
this.hostId = newHost.sessionId;
|
||||
|
||||
this.logger.info(`房主权限转移: ${oldHost?.name || 'unknown'} -> ${newHost.name}`);
|
||||
|
||||
// 触发事件
|
||||
if (oldHost) {
|
||||
this.eventHandlers.hostChanged?.(oldHost, newHost);
|
||||
this.emit('hostChanged', oldHost, newHost);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间状态
|
||||
*/
|
||||
setState(newState: RoomState): void {
|
||||
if (this.state === newState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = this.state;
|
||||
this.state = newState;
|
||||
this.stats.state = newState;
|
||||
|
||||
this.logger.info(`房间状态变化: ${oldState} -> ${newState}`);
|
||||
|
||||
// 触发事件
|
||||
this.eventHandlers.stateChanged?.(oldState, newState);
|
||||
this.emit('stateChanged', oldState, newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理房间消息
|
||||
*/
|
||||
handleMessage(message: INetworkMessage, fromSessionId: string): void {
|
||||
const player = this.players.get(fromSessionId);
|
||||
if (!player) {
|
||||
this.logger.warn(`收到非房间成员的消息: ${fromSessionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stats.messagesReceived++;
|
||||
|
||||
// 触发事件
|
||||
this.eventHandlers.messageReceived?.(message, player);
|
||||
this.emit('messageReceived', message, player);
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息到房间内所有玩家
|
||||
*/
|
||||
broadcast(message: INetworkMessage, exclude?: string[], onSend?: (sessionId: string) => void): void {
|
||||
const excludeSet = new Set(exclude || []);
|
||||
let sentCount = 0;
|
||||
|
||||
for (const player of this.players.values()) {
|
||||
if (!excludeSet.has(player.sessionId)) {
|
||||
if (onSend) {
|
||||
onSend(player.sessionId);
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.stats.messagesSent += sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否在房间中
|
||||
*/
|
||||
hasPlayer(sessionId: string): boolean {
|
||||
return this.players.has(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否已满
|
||||
*/
|
||||
isFull(): boolean {
|
||||
return this.players.size >= this.config.maxPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否为空
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this.players.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间信息
|
||||
*/
|
||||
getRoomInfo(): IRoomInfo {
|
||||
return {
|
||||
id: this.config.id,
|
||||
name: this.config.name,
|
||||
playerCount: this.players.size,
|
||||
maxPlayers: this.config.maxPlayers,
|
||||
state: this.state,
|
||||
metadata: this.config.metadata
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间配置
|
||||
*/
|
||||
getConfig(): RoomConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间统计信息
|
||||
*/
|
||||
getStats(): RoomStats {
|
||||
return {
|
||||
...this.stats,
|
||||
playerCount: this.players.size
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新房间配置
|
||||
*/
|
||||
updateConfig(updates: Partial<RoomConfig>): void {
|
||||
Object.assign(this.config, updates);
|
||||
this.logger.info(`房间配置已更新: ${this.config.id}`, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置玩家自定义数据
|
||||
*/
|
||||
setPlayerData(sessionId: string, data: Record<string, any>): boolean {
|
||||
const player = this.players.get(sessionId);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
player.customData = { ...player.customData, ...data };
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间运行时间
|
||||
*/
|
||||
getUptime(): number {
|
||||
return Date.now() - this.createTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
validatePassword(password?: string): boolean {
|
||||
if (!this.config.password) {
|
||||
return true; // 无密码房间
|
||||
}
|
||||
return password === this.config.password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件处理器
|
||||
*/
|
||||
override on<K extends keyof RoomEvents>(event: K, handler: RoomEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return super.on(event, handler as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof RoomEvents>(event: K): this {
|
||||
delete this.eventHandlers[event];
|
||||
return super.off(event, this.eventHandlers[event] as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁房间
|
||||
*/
|
||||
destroy(reason: string = '房间关闭'): void {
|
||||
this.logger.info(`房间销毁: ${this.config.id}, 原因: ${reason}`);
|
||||
|
||||
// 清理所有玩家
|
||||
const playersToRemove = Array.from(this.players.keys());
|
||||
for (const sessionId of playersToRemove) {
|
||||
this.removePlayer(sessionId, reason);
|
||||
}
|
||||
|
||||
// 触发销毁事件
|
||||
this.eventHandlers.roomDestroyed?.(reason);
|
||||
this.emit('roomDestroyed', reason);
|
||||
|
||||
// 清理资源
|
||||
this.players.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间详细状态
|
||||
*/
|
||||
getDetailedStatus() {
|
||||
return {
|
||||
config: this.getConfig(),
|
||||
info: this.getRoomInfo(),
|
||||
stats: this.getStats(),
|
||||
players: this.getAllPlayers(),
|
||||
host: this.getHost(),
|
||||
uptime: this.getUptime(),
|
||||
isEmpty: this.isEmpty(),
|
||||
isFull: this.isFull()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢出玩家
|
||||
*/
|
||||
kickPlayer(sessionId: string, reason: string = '被踢出房间'): boolean {
|
||||
if (!this.hasPlayer(sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.removePlayer(sessionId, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停房间
|
||||
*/
|
||||
pause(): void {
|
||||
if (this.state === RoomState.Playing) {
|
||||
this.setState(RoomState.Paused);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复房间
|
||||
*/
|
||||
resume(): void {
|
||||
if (this.state === RoomState.Paused) {
|
||||
this.setState(RoomState.Playing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始游戏
|
||||
*/
|
||||
startGame(): boolean {
|
||||
if (this.state !== RoomState.Waiting) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.players.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState(RoomState.Playing);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束游戏
|
||||
*/
|
||||
endGame(): boolean {
|
||||
if (this.state !== RoomState.Playing && this.state !== RoomState.Paused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState(RoomState.Finished);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置房间到等待状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.setState(RoomState.Waiting);
|
||||
// 可以根据需要重置其他状态
|
||||
}
|
||||
}
|
||||
@@ -1,621 +0,0 @@
|
||||
/**
|
||||
* 房间管理器
|
||||
* 负责房间的创建、销毁和管理
|
||||
*/
|
||||
import { createLogger, ITimer, Core } from '@esengine/ecs-framework';
|
||||
import { Room, RoomConfig, PlayerInfo, RoomEvents } from './Room';
|
||||
import { ClientSession } from '../core/ConnectionManager';
|
||||
import { RoomState, IRoomInfo, EventEmitter } from '@esengine/network-shared';
|
||||
|
||||
/**
|
||||
* 房间管理器配置
|
||||
*/
|
||||
export interface RoomManagerConfig {
|
||||
maxRooms: number;
|
||||
defaultMaxPlayers: number;
|
||||
autoCleanupInterval: number; // 自动清理间隔(毫秒)
|
||||
roomIdLength: number;
|
||||
allowDuplicateNames: boolean;
|
||||
defaultAutoDestroy: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间查询选项
|
||||
*/
|
||||
export interface RoomQueryOptions {
|
||||
state?: RoomState;
|
||||
hasPassword?: boolean;
|
||||
minPlayers?: number;
|
||||
maxPlayers?: number;
|
||||
notFull?: boolean;
|
||||
publicOnly?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间创建选项
|
||||
*/
|
||||
export interface CreateRoomOptions {
|
||||
id?: string;
|
||||
name: string;
|
||||
maxPlayers?: number;
|
||||
isPublic?: boolean;
|
||||
password?: string;
|
||||
metadata?: Record<string, any>;
|
||||
autoDestroy?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器事件接口
|
||||
*/
|
||||
export interface RoomManagerEvents {
|
||||
roomCreated: (room: Room) => void;
|
||||
roomDestroyed: (room: Room, reason: string) => void;
|
||||
playerJoinedRoom: (room: Room, player: PlayerInfo) => void;
|
||||
playerLeftRoom: (room: Room, player: PlayerInfo, reason?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器统计
|
||||
*/
|
||||
export interface RoomManagerStats {
|
||||
totalRooms: number;
|
||||
activeRooms: number;
|
||||
totalPlayers: number;
|
||||
roomsByState: Record<RoomState, number>;
|
||||
roomsCreated: number;
|
||||
roomsDestroyed: number;
|
||||
playersJoined: number;
|
||||
playersLeft: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器
|
||||
*/
|
||||
export class RoomManager extends EventEmitter {
|
||||
private logger = createLogger('RoomManager');
|
||||
private config: RoomManagerConfig;
|
||||
private rooms: Map<string, Room> = new Map();
|
||||
private playerRoomMap: Map<string, string> = new Map(); // sessionId -> roomId
|
||||
private stats: RoomManagerStats;
|
||||
private cleanupTimer?: ITimer;
|
||||
|
||||
// 事件处理器
|
||||
private eventHandlers: Partial<RoomManagerEvents> = {};
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: Partial<RoomManagerConfig> = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
maxRooms: 1000,
|
||||
defaultMaxPlayers: 8,
|
||||
autoCleanupInterval: 300000, // 5分钟
|
||||
roomIdLength: 8,
|
||||
allowDuplicateNames: true,
|
||||
defaultAutoDestroy: true,
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
totalRooms: 0,
|
||||
activeRooms: 0,
|
||||
totalPlayers: 0,
|
||||
roomsByState: {
|
||||
[RoomState.Waiting]: 0,
|
||||
[RoomState.Playing]: 0,
|
||||
[RoomState.Paused]: 0,
|
||||
[RoomState.Finished]: 0
|
||||
},
|
||||
roomsCreated: 0,
|
||||
roomsDestroyed: 0,
|
||||
playersJoined: 0,
|
||||
playersLeft: 0
|
||||
};
|
||||
|
||||
this.startAutoCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*/
|
||||
createRoom(creatorSession: ClientSession, options: CreateRoomOptions): Room | null {
|
||||
// 检查房间数量限制
|
||||
if (this.rooms.size >= this.config.maxRooms) {
|
||||
this.logger.warn(`房间数量已达上限: ${this.config.maxRooms}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查玩家是否已在其他房间
|
||||
if (this.playerRoomMap.has(creatorSession.id)) {
|
||||
this.logger.warn(`玩家已在其他房间中: ${creatorSession.id}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查房间名称重复
|
||||
if (!this.config.allowDuplicateNames && this.isNameExists(options.name)) {
|
||||
this.logger.warn(`房间名称已存在: ${options.name}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成房间ID
|
||||
const roomId = options.id || this.generateRoomId();
|
||||
if (this.rooms.has(roomId)) {
|
||||
this.logger.warn(`房间ID已存在: ${roomId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建房间配置
|
||||
const roomConfig: RoomConfig = {
|
||||
id: roomId,
|
||||
name: options.name,
|
||||
maxPlayers: options.maxPlayers || this.config.defaultMaxPlayers,
|
||||
isPublic: options.isPublic !== false, // 默认为公开
|
||||
password: options.password,
|
||||
metadata: options.metadata || {},
|
||||
autoDestroy: options.autoDestroy ?? this.config.defaultAutoDestroy
|
||||
};
|
||||
|
||||
try {
|
||||
// 创建房间实例
|
||||
const room = new Room(roomConfig);
|
||||
this.setupRoomEvents(room);
|
||||
|
||||
// 添加到房间列表
|
||||
this.rooms.set(roomId, room);
|
||||
|
||||
// 创建者自动加入房间
|
||||
const success = room.addPlayer(creatorSession, `Creator_${creatorSession.id.substr(-6)}`);
|
||||
if (!success) {
|
||||
// 加入失败,销毁房间
|
||||
this.destroyRoom(roomId, '创建者加入失败');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新玩家房间映射
|
||||
this.playerRoomMap.set(creatorSession.id, roomId);
|
||||
|
||||
// 更新统计
|
||||
this.stats.roomsCreated++;
|
||||
this.updateStats();
|
||||
|
||||
this.logger.info(`房间创建成功: ${roomId} by ${creatorSession.id}`);
|
||||
|
||||
// 触发事件
|
||||
this.eventHandlers.roomCreated?.(room);
|
||||
this.emit('roomCreated', room);
|
||||
|
||||
return room;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`创建房间失败: ${roomId}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁房间
|
||||
*/
|
||||
destroyRoom(roomId: string, reason: string = '房间关闭'): boolean {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 移除所有玩家的房间映射
|
||||
for (const player of room.getAllPlayers()) {
|
||||
this.playerRoomMap.delete(player.sessionId);
|
||||
}
|
||||
|
||||
// 销毁房间
|
||||
room.destroy(reason);
|
||||
|
||||
// 从房间列表移除
|
||||
this.rooms.delete(roomId);
|
||||
|
||||
// 更新统计
|
||||
this.stats.roomsDestroyed++;
|
||||
this.updateStats();
|
||||
|
||||
this.logger.info(`房间已销毁: ${roomId}, 原因: ${reason}`);
|
||||
|
||||
// 触发事件
|
||||
this.eventHandlers.roomDestroyed?.(room, reason);
|
||||
this.emit('roomDestroyed', room, reason);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入房间
|
||||
*/
|
||||
joinRoom(session: ClientSession, roomId: string, password?: string, playerName?: string): boolean {
|
||||
// 检查玩家是否已在其他房间
|
||||
if (this.playerRoomMap.has(session.id)) {
|
||||
this.logger.warn(`玩家已在其他房间中: ${session.id}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取房间
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
this.logger.warn(`房间不存在: ${roomId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试加入房间
|
||||
const success = room.addPlayer(session, playerName, password);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新玩家房间映射
|
||||
this.playerRoomMap.set(session.id, roomId);
|
||||
|
||||
// 更新统计
|
||||
this.stats.playersJoined++;
|
||||
this.updateStats();
|
||||
|
||||
const player = room.getPlayer(session.id)!;
|
||||
this.eventHandlers.playerJoinedRoom?.(room, player);
|
||||
this.emit('playerJoinedRoom', room, player);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开房间
|
||||
*/
|
||||
leaveRoom(sessionId: string, reason?: string): boolean {
|
||||
const roomId = this.playerRoomMap.get(sessionId);
|
||||
if (!roomId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
this.playerRoomMap.delete(sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
const player = room.getPlayer(sessionId);
|
||||
if (!player) {
|
||||
this.playerRoomMap.delete(sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从房间移除玩家
|
||||
const success = room.removePlayer(sessionId, reason);
|
||||
if (success) {
|
||||
// 更新玩家房间映射
|
||||
this.playerRoomMap.delete(sessionId);
|
||||
|
||||
// 更新统计
|
||||
this.stats.playersLeft++;
|
||||
this.updateStats();
|
||||
|
||||
this.eventHandlers.playerLeftRoom?.(room, player, reason);
|
||||
this.emit('playerLeftRoom', room, player, reason);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间
|
||||
*/
|
||||
getRoom(roomId: string): Room | undefined {
|
||||
return this.rooms.get(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家所在房间
|
||||
*/
|
||||
getPlayerRoom(sessionId: string): Room | undefined {
|
||||
const roomId = this.playerRoomMap.get(sessionId);
|
||||
return roomId ? this.rooms.get(roomId) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询房间列表
|
||||
*/
|
||||
queryRooms(options: RoomQueryOptions = {}): Room[] {
|
||||
let rooms = Array.from(this.rooms.values());
|
||||
|
||||
// 应用过滤条件
|
||||
if (options.state !== undefined) {
|
||||
rooms = rooms.filter((room) => room.getRoomInfo().state === options.state);
|
||||
}
|
||||
|
||||
if (options.hasPassword !== undefined) {
|
||||
rooms = rooms.filter((room) => {
|
||||
const config = room.getConfig();
|
||||
return options.hasPassword ? !!config.password : !config.password;
|
||||
});
|
||||
}
|
||||
|
||||
if (options.minPlayers !== undefined) {
|
||||
rooms = rooms.filter((room) => room.getAllPlayers().length >= options.minPlayers!);
|
||||
}
|
||||
|
||||
if (options.maxPlayers !== undefined) {
|
||||
rooms = rooms.filter((room) => room.getAllPlayers().length <= options.maxPlayers!);
|
||||
}
|
||||
|
||||
if (options.notFull) {
|
||||
rooms = rooms.filter((room) => !room.isFull());
|
||||
}
|
||||
|
||||
if (options.publicOnly) {
|
||||
rooms = rooms.filter((room) => room.getConfig().isPublic);
|
||||
}
|
||||
|
||||
// 分页
|
||||
if (options.offset) {
|
||||
rooms = rooms.slice(options.offset);
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
rooms = rooms.slice(0, options.limit);
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间信息列表
|
||||
*/
|
||||
getRoomInfoList(options: RoomQueryOptions = {}): IRoomInfo[] {
|
||||
return this.queryRooms(options).map((room) => room.getRoomInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStats(): RoomManagerStats {
|
||||
this.updateStats();
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计信息
|
||||
*/
|
||||
resetStats(): void {
|
||||
this.stats = {
|
||||
totalRooms: this.rooms.size,
|
||||
activeRooms: this.rooms.size,
|
||||
totalPlayers: this.playerRoomMap.size,
|
||||
roomsByState: {
|
||||
[RoomState.Waiting]: 0,
|
||||
[RoomState.Playing]: 0,
|
||||
[RoomState.Paused]: 0,
|
||||
[RoomState.Finished]: 0
|
||||
},
|
||||
roomsCreated: 0,
|
||||
roomsDestroyed: 0,
|
||||
playersJoined: 0,
|
||||
playersLeft: 0
|
||||
};
|
||||
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig: Partial<RoomManagerConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
this.logger.info('房间管理器配置已更新:', newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件处理器
|
||||
*/
|
||||
override on<K extends keyof RoomManagerEvents>(event: K, handler: RoomManagerEvents[K]): this {
|
||||
this.eventHandlers[event] = handler;
|
||||
return super.on(event, handler as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件处理器
|
||||
*/
|
||||
override off<K extends keyof RoomManagerEvents>(event: K): this {
|
||||
delete this.eventHandlers[event];
|
||||
return super.off(event, this.eventHandlers[event] as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁管理器
|
||||
*/
|
||||
destroy(): void {
|
||||
// 停止自动清理
|
||||
if (this.cleanupTimer) {
|
||||
this.cleanupTimer.stop();
|
||||
this.cleanupTimer = undefined;
|
||||
}
|
||||
|
||||
// 销毁所有房间
|
||||
const roomIds = Array.from(this.rooms.keys());
|
||||
for (const roomId of roomIds) {
|
||||
this.destroyRoom(roomId, '管理器销毁');
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
this.rooms.clear();
|
||||
this.playerRoomMap.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成房间ID
|
||||
*/
|
||||
private generateRoomId(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < this.config.roomIdLength; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
// 确保ID唯一
|
||||
if (this.rooms.has(result)) {
|
||||
return this.generateRoomId();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间名称是否存在
|
||||
*/
|
||||
private isNameExists(name: string): boolean {
|
||||
for (const room of this.rooms.values()) {
|
||||
if (room.getConfig().name === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间事件监听
|
||||
*/
|
||||
private setupRoomEvents(room: Room): void {
|
||||
room.on('roomDestroyed', (reason) => {
|
||||
// 自动清理已销毁的房间
|
||||
this.rooms.delete(room.getConfig().id);
|
||||
this.updateStats();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
private updateStats(): void {
|
||||
this.stats.totalRooms = this.rooms.size;
|
||||
this.stats.activeRooms = this.rooms.size;
|
||||
this.stats.totalPlayers = this.playerRoomMap.size;
|
||||
|
||||
// 重置状态统计
|
||||
this.stats.roomsByState = {
|
||||
[RoomState.Waiting]: 0,
|
||||
[RoomState.Playing]: 0,
|
||||
[RoomState.Paused]: 0,
|
||||
[RoomState.Finished]: 0
|
||||
};
|
||||
|
||||
// 统计各状态房间数量
|
||||
for (const room of this.rooms.values()) {
|
||||
const state = room.getRoomInfo().state;
|
||||
this.stats.roomsByState[state]++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自动清理
|
||||
*/
|
||||
private startAutoCleanup(): void {
|
||||
this.cleanupTimer = Core.schedule(this.config.autoCleanupInterval / 1000, true, this, () => {
|
||||
this.performAutoCleanup();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自动清理
|
||||
*/
|
||||
private performAutoCleanup(): void {
|
||||
const now = Date.now();
|
||||
const roomsToDestroy: string[] = [];
|
||||
|
||||
for (const [roomId, room] of this.rooms) {
|
||||
const config = room.getConfig();
|
||||
const stats = room.getStats();
|
||||
|
||||
// 清理空房间(如果启用了自动销毁)
|
||||
if (config.autoDestroy && room.isEmpty()) {
|
||||
roomsToDestroy.push(roomId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 清理长时间无活动的已结束房间
|
||||
if (stats.state === RoomState.Finished &&
|
||||
now - stats.createTime > 3600000) { // 1小时
|
||||
roomsToDestroy.push(roomId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行清理
|
||||
for (const roomId of roomsToDestroy) {
|
||||
this.destroyRoom(roomId, '自动清理');
|
||||
}
|
||||
|
||||
if (roomsToDestroy.length > 0) {
|
||||
this.logger.info(`自动清理了 ${roomsToDestroy.length} 个房间`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理器状态摘要
|
||||
*/
|
||||
getStatusSummary() {
|
||||
const stats = this.getStats();
|
||||
const rooms = Array.from(this.rooms.values());
|
||||
|
||||
return {
|
||||
stats,
|
||||
roomCount: rooms.length,
|
||||
playerCount: this.playerRoomMap.size,
|
||||
publicRooms: rooms.filter((r) => r.getConfig().isPublic).length,
|
||||
privateRooms: rooms.filter((r) => !r.getConfig().isPublic).length,
|
||||
fullRooms: rooms.filter((r) => r.isFull()).length,
|
||||
emptyRooms: rooms.filter((r) => r.isEmpty()).length,
|
||||
averagePlayersPerRoom: rooms.length > 0 ?
|
||||
rooms.reduce((sum, r) => sum + r.getAllPlayers().length, 0) / rooms.length : 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢出玩家(从其所在房间)
|
||||
*/
|
||||
kickPlayer(sessionId: string, reason: string = '被管理员踢出'): boolean {
|
||||
const room = this.getPlayerRoom(sessionId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return room.kickPlayer(sessionId, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量销毁房间
|
||||
*/
|
||||
destroyRoomsBatch(roomIds: string[], reason: string = '批量清理'): number {
|
||||
let destroyedCount = 0;
|
||||
|
||||
for (const roomId of roomIds) {
|
||||
if (this.destroyRoom(roomId, reason)) {
|
||||
destroyedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return destroyedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否在房间中
|
||||
*/
|
||||
isPlayerInRoom(sessionId: string): boolean {
|
||||
return this.playerRoomMap.has(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家所在房间ID
|
||||
*/
|
||||
getPlayerRoomId(sessionId: string): string | undefined {
|
||||
return this.playerRoomMap.get(sessionId);
|
||||
}
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { NetworkScope, SyncBatch } from '@esengine/network-shared';
|
||||
import { EventEmitter } from '@esengine/network-shared';
|
||||
|
||||
/**
|
||||
* 客户端位置信息
|
||||
*/
|
||||
export interface ClientPosition {
|
||||
clientId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 作用域配置
|
||||
*/
|
||||
export interface ScopeConfig {
|
||||
/** 默认可视范围 */
|
||||
defaultRange: number;
|
||||
/** 最大可视范围 */
|
||||
maxRange: number;
|
||||
/** 位置更新间隔(毫秒) */
|
||||
positionUpdateInterval: number;
|
||||
/** 是否启用LOD(细节层次) */
|
||||
enableLOD: boolean;
|
||||
/** LOD距离阈值 */
|
||||
lodDistances: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 作用域查询结果
|
||||
*/
|
||||
export interface ScopeQueryResult {
|
||||
/** 在范围内的客户端ID列表 */
|
||||
clientsInRange: string[];
|
||||
/** 距离映射 */
|
||||
distances: Map<string, number>;
|
||||
/** LOD级别映射 */
|
||||
lodLevels: Map<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义作用域规则
|
||||
*/
|
||||
export interface CustomScopeRule {
|
||||
name: string;
|
||||
condition: (batch: SyncBatch, clientId: string, clientPosition?: ClientPosition) => boolean;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络作用域管理器
|
||||
* 负责管理客户端的可见范围和网络作用域优化
|
||||
*/
|
||||
export class NetworkScopeManager extends EventEmitter {
|
||||
private logger = createLogger('NetworkScopeManager');
|
||||
private config: ScopeConfig;
|
||||
|
||||
/** 客户端位置信息 */
|
||||
private clientPositions = new Map<string, ClientPosition>();
|
||||
|
||||
/** 客户端可视范围 */
|
||||
private clientRanges = new Map<string, number>();
|
||||
|
||||
/** 房间映射 */
|
||||
private clientRooms = new Map<string, string>();
|
||||
private roomClients = new Map<string, Set<string>>();
|
||||
|
||||
/** 自定义作用域规则 */
|
||||
private customRules: CustomScopeRule[] = [];
|
||||
|
||||
/** 作用域缓存 */
|
||||
private scopeCache = new Map<string, { result: ScopeQueryResult; timestamp: number }>();
|
||||
private cacheTimeout = 100; // 100ms缓存
|
||||
|
||||
constructor(config: Partial<ScopeConfig> = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
defaultRange: 100,
|
||||
maxRange: 500,
|
||||
positionUpdateInterval: 100,
|
||||
enableLOD: true,
|
||||
lodDistances: [50, 150, 300],
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加客户端
|
||||
*/
|
||||
public addClient(clientId: string, position?: { x: number; y: number; z: number }): void {
|
||||
this.clientRanges.set(clientId, this.config.defaultRange);
|
||||
|
||||
if (position) {
|
||||
this.updateClientPosition(clientId, position.x, position.y, position.z);
|
||||
}
|
||||
|
||||
this.logger.debug(`客户端 ${clientId} 已添加到作用域管理器`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除客户端
|
||||
*/
|
||||
public removeClient(clientId: string): void {
|
||||
this.clientPositions.delete(clientId);
|
||||
this.clientRanges.delete(clientId);
|
||||
|
||||
// 从房间中移除
|
||||
const roomId = this.clientRooms.get(clientId);
|
||||
if (roomId) {
|
||||
this.leaveRoom(clientId, roomId);
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
this.clearClientCache(clientId);
|
||||
|
||||
this.logger.debug(`客户端 ${clientId} 已从作用域管理器移除`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户端位置
|
||||
*/
|
||||
public updateClientPosition(clientId: string, x: number, y: number, z: number): void {
|
||||
const clientPosition: ClientPosition = {
|
||||
clientId,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
lastUpdate: Date.now()
|
||||
};
|
||||
|
||||
this.clientPositions.set(clientId, clientPosition);
|
||||
|
||||
// 清理相关缓存
|
||||
this.clearClientCache(clientId);
|
||||
|
||||
this.emit('positionUpdated', clientId, clientPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端可视范围
|
||||
*/
|
||||
public setClientRange(clientId: string, range: number): void {
|
||||
const clampedRange = Math.min(range, this.config.maxRange);
|
||||
this.clientRanges.set(clientId, clampedRange);
|
||||
|
||||
// 清理相关缓存
|
||||
this.clearClientCache(clientId);
|
||||
|
||||
this.logger.debug(`客户端 ${clientId} 可视范围设置为: ${clampedRange}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间
|
||||
*/
|
||||
public joinRoom(clientId: string, roomId: string): void {
|
||||
// 从旧房间离开
|
||||
const oldRoom = this.clientRooms.get(clientId);
|
||||
if (oldRoom) {
|
||||
this.leaveRoom(clientId, oldRoom);
|
||||
}
|
||||
|
||||
// 加入新房间
|
||||
this.clientRooms.set(clientId, roomId);
|
||||
|
||||
if (!this.roomClients.has(roomId)) {
|
||||
this.roomClients.set(roomId, new Set());
|
||||
}
|
||||
this.roomClients.get(roomId)!.add(clientId);
|
||||
|
||||
this.logger.debug(`客户端 ${clientId} 已加入房间 ${roomId}`);
|
||||
this.emit('clientJoinedRoom', clientId, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间
|
||||
*/
|
||||
public leaveRoom(clientId: string, roomId: string): void {
|
||||
this.clientRooms.delete(clientId);
|
||||
|
||||
const roomClientSet = this.roomClients.get(roomId);
|
||||
if (roomClientSet) {
|
||||
roomClientSet.delete(clientId);
|
||||
|
||||
// 如果房间为空,删除房间
|
||||
if (roomClientSet.size === 0) {
|
||||
this.roomClients.delete(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`客户端 ${clientId} 已离开房间 ${roomId}`);
|
||||
this.emit('clientLeftRoom', clientId, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义作用域规则
|
||||
*/
|
||||
public addCustomRule(rule: CustomScopeRule): void {
|
||||
this.customRules.push(rule);
|
||||
this.customRules.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
this.logger.debug(`已添加自定义作用域规则: ${rule.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除自定义作用域规则
|
||||
*/
|
||||
public removeCustomRule(ruleName: string): boolean {
|
||||
const index = this.customRules.findIndex((rule) => rule.name === ruleName);
|
||||
if (index >= 0) {
|
||||
this.customRules.splice(index, 1);
|
||||
this.logger.debug(`已移除自定义作用域规则: ${ruleName}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查批次是否应该发送给特定客户端
|
||||
*/
|
||||
public shouldSendToClient(batch: SyncBatch, clientId: string): boolean {
|
||||
// 检查自定义规则
|
||||
for (const rule of this.customRules) {
|
||||
const clientPosition = this.clientPositions.get(clientId);
|
||||
if (!rule.condition(batch, clientId, clientPosition)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查网络作用域
|
||||
for (const [prop, scope] of Object.entries(batch.scopes)) {
|
||||
if (!this.checkPropertyScope(scope, batch, clientId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询在范围内的客户端
|
||||
*/
|
||||
public queryClientsInRange(
|
||||
position: { x: number; y: number; z: number },
|
||||
range: number,
|
||||
excludeClientId?: string
|
||||
): ScopeQueryResult {
|
||||
const cacheKey = `${position.x},${position.y},${position.z},${range},${excludeClientId || ''}`;
|
||||
const cached = this.scopeCache.get(cacheKey);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
||||
return cached.result;
|
||||
}
|
||||
|
||||
const clientsInRange: string[] = [];
|
||||
const distances = new Map<string, number>();
|
||||
const lodLevels = new Map<string, number>();
|
||||
|
||||
for (const [clientId, clientPosition] of this.clientPositions) {
|
||||
if (excludeClientId && clientId === excludeClientId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const distance = this.calculateDistance(position, clientPosition);
|
||||
|
||||
if (distance <= range) {
|
||||
clientsInRange.push(clientId);
|
||||
distances.set(clientId, distance);
|
||||
|
||||
// 计算LOD级别
|
||||
if (this.config.enableLOD) {
|
||||
const lodLevel = this.calculateLODLevel(distance);
|
||||
lodLevels.set(clientId, lodLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: ScopeQueryResult = {
|
||||
clientsInRange,
|
||||
distances,
|
||||
lodLevels
|
||||
};
|
||||
|
||||
// 缓存结果
|
||||
this.scopeCache.set(cacheKey, {
|
||||
result,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间内的所有客户端
|
||||
*/
|
||||
public getRoomClients(roomId: string): string[] {
|
||||
const roomClientSet = this.roomClients.get(roomId);
|
||||
return roomClientSet ? Array.from(roomClientSet) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端所在房间
|
||||
*/
|
||||
public getClientRoom(clientId: string): string | undefined {
|
||||
return this.clientRooms.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端位置
|
||||
*/
|
||||
public getClientPosition(clientId: string): ClientPosition | undefined {
|
||||
return this.clientPositions.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端位置
|
||||
*/
|
||||
public getAllClientPositions(): Map<string, ClientPosition> {
|
||||
return new Map(this.clientPositions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
public cleanupCache(): void {
|
||||
const now = Date.now();
|
||||
for (const [key, cache] of this.scopeCache) {
|
||||
if (now - cache.timestamp > this.cacheTimeout * 2) {
|
||||
this.scopeCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
public updateConfig(newConfig: Partial<ScopeConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
public getStats(): {
|
||||
clientCount: number;
|
||||
roomCount: number;
|
||||
cacheSize: number;
|
||||
customRuleCount: number;
|
||||
} {
|
||||
return {
|
||||
clientCount: this.clientPositions.size,
|
||||
roomCount: this.roomClients.size,
|
||||
cacheSize: this.scopeCache.size,
|
||||
customRuleCount: this.customRules.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁管理器
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.clientPositions.clear();
|
||||
this.clientRanges.clear();
|
||||
this.clientRooms.clear();
|
||||
this.roomClients.clear();
|
||||
this.customRules.length = 0;
|
||||
this.scopeCache.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性作用域
|
||||
*/
|
||||
private checkPropertyScope(scope: NetworkScope, batch: SyncBatch, clientId: string): boolean {
|
||||
switch (scope) {
|
||||
case NetworkScope.Global:
|
||||
return true;
|
||||
|
||||
case NetworkScope.Room:
|
||||
const clientRoom = this.clientRooms.get(clientId);
|
||||
// 这里需要知道batch来源的实体所在房间,简化实现
|
||||
return true;
|
||||
|
||||
case NetworkScope.Owner:
|
||||
return batch.instanceId === clientId;
|
||||
|
||||
case NetworkScope.Nearby:
|
||||
return this.isClientNearby(batch, clientId);
|
||||
|
||||
case NetworkScope.Custom:
|
||||
// 由自定义规则处理
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否在附近
|
||||
*/
|
||||
private isClientNearby(batch: SyncBatch, clientId: string): boolean {
|
||||
const clientPosition = this.clientPositions.get(clientId);
|
||||
if (!clientPosition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const clientRange = this.clientRanges.get(clientId) || this.config.defaultRange;
|
||||
|
||||
// 这里需要知道batch来源实体的位置,简化实现
|
||||
// 实际项目中应该从实体的Transform组件获取位置
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的距离
|
||||
*/
|
||||
private calculateDistance(
|
||||
pos1: { x: number; y: number; z: number },
|
||||
pos2: { x: number; y: number; z: number }
|
||||
): number {
|
||||
const dx = pos1.x - pos2.x;
|
||||
const dy = pos1.y - pos2.y;
|
||||
const dz = pos1.z - pos2.z;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算LOD级别
|
||||
*/
|
||||
private calculateLODLevel(distance: number): number {
|
||||
for (let i = 0; i < this.config.lodDistances.length; i++) {
|
||||
if (distance <= this.config.lodDistances[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return this.config.lodDistances.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理客户端相关缓存
|
||||
*/
|
||||
private clearClientCache(clientId: string): void {
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
for (const key of this.scopeCache.keys()) {
|
||||
if (key.includes(clientId)) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
this.scopeCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import { SyncBatch } from '@esengine/network-shared';
|
||||
import { EventEmitter } from '@esengine/network-shared';
|
||||
|
||||
/**
|
||||
* 调度配置
|
||||
*/
|
||||
export interface SyncSchedulerConfig {
|
||||
/** 目标帧率 */
|
||||
targetFPS: number;
|
||||
/** 最大延迟(毫秒) */
|
||||
maxLatency: number;
|
||||
/** 带宽限制(字节/秒) */
|
||||
bandwidthLimit: number;
|
||||
/** 优先级权重 */
|
||||
priorityWeights: {
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
};
|
||||
/** 自适应调整间隔(毫秒) */
|
||||
adaptiveInterval: number;
|
||||
/** 是否启用自适应调整 */
|
||||
enableAdaptive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度任务
|
||||
*/
|
||||
interface ScheduledTask {
|
||||
id: string;
|
||||
batch: SyncBatch;
|
||||
clientId: string;
|
||||
priority: number;
|
||||
deadline: number;
|
||||
size: number;
|
||||
retryCount: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端调度状态
|
||||
*/
|
||||
interface ClientScheduleState {
|
||||
clientId: string;
|
||||
bandwidth: {
|
||||
used: number;
|
||||
limit: number;
|
||||
resetTime: number;
|
||||
};
|
||||
latency: number;
|
||||
queueSize: number;
|
||||
lastSendTime: number;
|
||||
adaptiveRate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度统计
|
||||
*/
|
||||
interface SchedulerStats {
|
||||
totalTasks: number;
|
||||
completedTasks: number;
|
||||
droppedTasks: number;
|
||||
averageLatency: number;
|
||||
bandwidthUtilization: number;
|
||||
queueSizes: { [clientId: string]: number };
|
||||
adaptiveRates: { [clientId: string]: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步调度器
|
||||
* 负责优化同步数据的发送时机和优先级
|
||||
*/
|
||||
export class SyncScheduler extends EventEmitter {
|
||||
private logger = createLogger('SyncScheduler');
|
||||
private config: SyncSchedulerConfig;
|
||||
|
||||
/** 任务队列 */
|
||||
private taskQueue: ScheduledTask[] = [];
|
||||
private taskIdCounter = 0;
|
||||
|
||||
/** 客户端状态 */
|
||||
private clientStates = new Map<string, ClientScheduleState>();
|
||||
|
||||
/** 调度定时器 */
|
||||
private scheduleTimer: any = null;
|
||||
private adaptiveTimer: any = null;
|
||||
|
||||
/** 统计信息 */
|
||||
private stats: SchedulerStats = {
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
droppedTasks: 0,
|
||||
averageLatency: 0,
|
||||
bandwidthUtilization: 0,
|
||||
queueSizes: {},
|
||||
adaptiveRates: {}
|
||||
};
|
||||
|
||||
/** 自适应调整历史 */
|
||||
private latencyHistory = new Map<string, number[]>();
|
||||
private bandwidthHistory = new Map<string, number[]>();
|
||||
|
||||
constructor(config: Partial<SyncSchedulerConfig> = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
targetFPS: 60,
|
||||
maxLatency: 100,
|
||||
bandwidthLimit: 1024 * 1024, // 1MB/s
|
||||
priorityWeights: {
|
||||
high: 3,
|
||||
medium: 2,
|
||||
low: 1
|
||||
},
|
||||
adaptiveInterval: 1000,
|
||||
enableAdaptive: true,
|
||||
...config
|
||||
};
|
||||
|
||||
this.startScheduler();
|
||||
|
||||
if (this.config.enableAdaptive) {
|
||||
this.startAdaptiveAdjustment();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加客户端
|
||||
*/
|
||||
public addClient(clientId: string, bandwidth: number = this.config.bandwidthLimit): void {
|
||||
const clientState: ClientScheduleState = {
|
||||
clientId,
|
||||
bandwidth: {
|
||||
used: 0,
|
||||
limit: bandwidth,
|
||||
resetTime: Date.now() + 1000
|
||||
},
|
||||
latency: 0,
|
||||
queueSize: 0,
|
||||
lastSendTime: 0,
|
||||
adaptiveRate: 1000 / this.config.targetFPS // 初始发送间隔
|
||||
};
|
||||
|
||||
this.clientStates.set(clientId, clientState);
|
||||
this.latencyHistory.set(clientId, []);
|
||||
this.bandwidthHistory.set(clientId, []);
|
||||
|
||||
this.logger.debug(`客户端 ${clientId} 已添加到调度器`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除客户端
|
||||
*/
|
||||
public removeClient(clientId: string): void {
|
||||
this.clientStates.delete(clientId);
|
||||
this.latencyHistory.delete(clientId);
|
||||
this.bandwidthHistory.delete(clientId);
|
||||
|
||||
// 移除该客户端的所有任务
|
||||
this.taskQueue = this.taskQueue.filter((task) => task.clientId !== clientId);
|
||||
|
||||
this.logger.debug(`客户端 ${clientId} 已从调度器移除`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度同步任务
|
||||
*/
|
||||
public schedule(batch: SyncBatch, clientId: string, priority: number = 5): string {
|
||||
const clientState = this.clientStates.get(clientId);
|
||||
if (!clientState) {
|
||||
this.logger.warn(`客户端 ${clientId} 不存在,无法调度任务`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const taskId = `task_${++this.taskIdCounter}`;
|
||||
const now = Date.now();
|
||||
|
||||
const task: ScheduledTask = {
|
||||
id: taskId,
|
||||
batch,
|
||||
clientId,
|
||||
priority,
|
||||
deadline: now + this.config.maxLatency,
|
||||
size: this.estimateBatchSize(batch),
|
||||
retryCount: 0,
|
||||
createdAt: now
|
||||
};
|
||||
|
||||
this.taskQueue.push(task);
|
||||
this.stats.totalTasks++;
|
||||
|
||||
// 按优先级和截止时间排序
|
||||
this.sortTaskQueue();
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户端延迟
|
||||
*/
|
||||
public updateClientLatency(clientId: string, latency: number): void {
|
||||
const clientState = this.clientStates.get(clientId);
|
||||
if (clientState) {
|
||||
clientState.latency = latency;
|
||||
|
||||
// 记录延迟历史
|
||||
const history = this.latencyHistory.get(clientId) || [];
|
||||
history.push(latency);
|
||||
|
||||
// 保持最近50个记录
|
||||
if (history.length > 50) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
this.latencyHistory.set(clientId, history);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端带宽限制
|
||||
*/
|
||||
public setClientBandwidth(clientId: string, bandwidth: number): void {
|
||||
const clientState = this.clientStates.get(clientId);
|
||||
if (clientState) {
|
||||
clientState.bandwidth.limit = bandwidth;
|
||||
this.logger.debug(`客户端 ${clientId} 带宽限制设置为: ${bandwidth} 字节/秒`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
public getStats(): SchedulerStats {
|
||||
// 更新队列大小统计
|
||||
for (const [clientId, clientState] of this.clientStates) {
|
||||
const clientTasks = this.taskQueue.filter((task) => task.clientId === clientId);
|
||||
this.stats.queueSizes[clientId] = clientTasks.length;
|
||||
this.stats.adaptiveRates[clientId] = clientState.adaptiveRate;
|
||||
}
|
||||
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空任务队列
|
||||
*/
|
||||
public clearQueue(): number {
|
||||
const count = this.taskQueue.length;
|
||||
this.taskQueue.length = 0;
|
||||
this.stats.droppedTasks += count;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
public updateConfig(newConfig: Partial<SyncSchedulerConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
|
||||
if (newConfig.enableAdaptive !== undefined) {
|
||||
if (newConfig.enableAdaptive) {
|
||||
this.startAdaptiveAdjustment();
|
||||
} else {
|
||||
this.stopAdaptiveAdjustment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁调度器
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.stopScheduler();
|
||||
this.stopAdaptiveAdjustment();
|
||||
this.taskQueue.length = 0;
|
||||
this.clientStates.clear();
|
||||
this.latencyHistory.clear();
|
||||
this.bandwidthHistory.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动调度器
|
||||
*/
|
||||
private startScheduler(): void {
|
||||
if (this.scheduleTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = 1000 / this.config.targetFPS;
|
||||
this.scheduleTimer = setInterval(() => {
|
||||
this.processTasks();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止调度器
|
||||
*/
|
||||
private stopScheduler(): void {
|
||||
if (this.scheduleTimer) {
|
||||
clearInterval(this.scheduleTimer);
|
||||
this.scheduleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自适应调整
|
||||
*/
|
||||
private startAdaptiveAdjustment(): void {
|
||||
if (this.adaptiveTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.adaptiveTimer = setInterval(() => {
|
||||
this.performAdaptiveAdjustment();
|
||||
}, this.config.adaptiveInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止自适应调整
|
||||
*/
|
||||
private stopAdaptiveAdjustment(): void {
|
||||
if (this.adaptiveTimer) {
|
||||
clearInterval(this.adaptiveTimer);
|
||||
this.adaptiveTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理任务队列
|
||||
*/
|
||||
private processTasks(): void {
|
||||
const now = Date.now();
|
||||
const processedTasks: string[] = [];
|
||||
|
||||
for (const task of this.taskQueue) {
|
||||
const clientState = this.clientStates.get(task.clientId);
|
||||
if (!clientState) {
|
||||
processedTasks.push(task.id);
|
||||
this.stats.droppedTasks++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查截止时间
|
||||
if (now > task.deadline) {
|
||||
processedTasks.push(task.id);
|
||||
this.stats.droppedTasks++;
|
||||
this.logger.warn(`任务 ${task.id} 超时被丢弃`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查发送间隔
|
||||
if (now - clientState.lastSendTime < clientState.adaptiveRate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查带宽限制
|
||||
if (!this.checkBandwidthLimit(clientState, task.size)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 发送任务
|
||||
this.sendTask(task, clientState);
|
||||
processedTasks.push(task.id);
|
||||
this.stats.completedTasks++;
|
||||
}
|
||||
|
||||
// 移除已处理的任务
|
||||
this.taskQueue = this.taskQueue.filter((task) => !processedTasks.includes(task.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送任务
|
||||
*/
|
||||
private sendTask(task: ScheduledTask, clientState: ClientScheduleState): void {
|
||||
const now = Date.now();
|
||||
|
||||
// 更新客户端状态
|
||||
clientState.lastSendTime = now;
|
||||
clientState.bandwidth.used += task.size;
|
||||
|
||||
// 记录带宽历史
|
||||
const bandwidthHistory = this.bandwidthHistory.get(task.clientId) || [];
|
||||
bandwidthHistory.push(task.size);
|
||||
|
||||
if (bandwidthHistory.length > 50) {
|
||||
bandwidthHistory.shift();
|
||||
}
|
||||
|
||||
this.bandwidthHistory.set(task.clientId, bandwidthHistory);
|
||||
|
||||
// 发出事件
|
||||
this.emit('taskReady', task.batch, task.clientId);
|
||||
|
||||
this.logger.debug(`任务 ${task.id} 已发送给客户端 ${task.clientId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查带宽限制
|
||||
*/
|
||||
private checkBandwidthLimit(clientState: ClientScheduleState, taskSize: number): boolean {
|
||||
const now = Date.now();
|
||||
|
||||
// 重置带宽计数器
|
||||
if (now >= clientState.bandwidth.resetTime) {
|
||||
clientState.bandwidth.used = 0;
|
||||
clientState.bandwidth.resetTime = now + 1000;
|
||||
}
|
||||
|
||||
return clientState.bandwidth.used + taskSize <= clientState.bandwidth.limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算批次大小
|
||||
*/
|
||||
private estimateBatchSize(batch: SyncBatch): number {
|
||||
const propertyCount = Object.keys(batch.changes).length;
|
||||
return propertyCount * 50 + 200; // 基础开销 + 每个属性的估算大小
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序任务队列
|
||||
*/
|
||||
private sortTaskQueue(): void {
|
||||
this.taskQueue.sort((a, b) => {
|
||||
// 首先按优先级排序
|
||||
const priorityDiff = b.priority - a.priority;
|
||||
if (priorityDiff !== 0) {
|
||||
return priorityDiff;
|
||||
}
|
||||
|
||||
// 然后按截止时间排序
|
||||
return a.deadline - b.deadline;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自适应调整
|
||||
*/
|
||||
private performAdaptiveAdjustment(): void {
|
||||
for (const [clientId, clientState] of this.clientStates) {
|
||||
this.adjustClientRate(clientId, clientState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整客户端发送频率
|
||||
*/
|
||||
private adjustClientRate(clientId: string, clientState: ClientScheduleState): void {
|
||||
const latencyHistory = this.latencyHistory.get(clientId) || [];
|
||||
const bandwidthHistory = this.bandwidthHistory.get(clientId) || [];
|
||||
|
||||
if (latencyHistory.length < 5) {
|
||||
return; // 数据不足,不进行调整
|
||||
}
|
||||
|
||||
// 计算平均延迟
|
||||
const avgLatency = latencyHistory.reduce((sum, lat) => sum + lat, 0) / latencyHistory.length;
|
||||
|
||||
// 计算带宽利用率
|
||||
const totalBandwidth = bandwidthHistory.reduce((sum, size) => sum + size, 0);
|
||||
const bandwidthUtilization = totalBandwidth / clientState.bandwidth.limit;
|
||||
|
||||
// 根据延迟和带宽利用率调整发送频率
|
||||
let adjustment = 1;
|
||||
|
||||
if (avgLatency > this.config.maxLatency) {
|
||||
// 延迟过高,降低发送频率
|
||||
adjustment = 1.2;
|
||||
} else if (avgLatency < this.config.maxLatency * 0.5) {
|
||||
// 延迟较低,可以提高发送频率
|
||||
adjustment = 0.9;
|
||||
}
|
||||
|
||||
if (bandwidthUtilization > 0.9) {
|
||||
// 带宽利用率过高,降低发送频率
|
||||
adjustment *= 1.1;
|
||||
} else if (bandwidthUtilization < 0.5) {
|
||||
// 带宽利用率较低,可以提高发送频率
|
||||
adjustment *= 0.95;
|
||||
}
|
||||
|
||||
// 应用调整
|
||||
clientState.adaptiveRate = Math.max(
|
||||
clientState.adaptiveRate * adjustment,
|
||||
1000 / (this.config.targetFPS * 2) // 最小间隔
|
||||
);
|
||||
|
||||
clientState.adaptiveRate = Math.min(
|
||||
clientState.adaptiveRate,
|
||||
1000 // 最大间隔1秒
|
||||
);
|
||||
|
||||
this.logger.debug(
|
||||
`客户端 ${clientId} 自适应调整: 延迟=${avgLatency.toFixed(1)}ms, ` +
|
||||
`带宽利用率=${(bandwidthUtilization * 100).toFixed(1)}%, ` +
|
||||
`新间隔=${clientState.adaptiveRate.toFixed(1)}ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 服务端同步模块导出
|
||||
*/
|
||||
|
||||
export * from './NetworkScopeManager';
|
||||
export * from './SyncScheduler';
|
||||
@@ -1,543 +0,0 @@
|
||||
import { EntitySystem, createLogger } from '@esengine/ecs-framework';
|
||||
import {
|
||||
SyncVarManager,
|
||||
SyncBatch,
|
||||
SyncVarSerializer,
|
||||
NetworkScope,
|
||||
SyncMode,
|
||||
AuthorityType
|
||||
} from '@esengine/network-shared';
|
||||
import { NetworkServer } from '../core/NetworkServer';
|
||||
import { ConnectionManager } from '../core/ConnectionManager';
|
||||
|
||||
/**
|
||||
* 服务端SyncVar系统配置
|
||||
*/
|
||||
export interface SyncVarSystemConfig {
|
||||
/** 同步频率(毫秒) */
|
||||
syncRate: number;
|
||||
/** 最大同步批次大小 */
|
||||
maxBatchSize: number;
|
||||
/** 是否启用网络作用域优化 */
|
||||
enableScopeOptimization: boolean;
|
||||
/** 是否启用带宽限制 */
|
||||
enableBandwidthLimit: boolean;
|
||||
/** 每客户端最大带宽(字节/秒) */
|
||||
maxBandwidthPerClient: number;
|
||||
/** 是否启用优先级调度 */
|
||||
enablePriorityScheduling: boolean;
|
||||
/** 是否启用批量优化 */
|
||||
enableBatching: boolean;
|
||||
/** 批量超时时间(毫秒) */
|
||||
batchTimeout: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端同步状态
|
||||
*/
|
||||
interface ClientSyncState {
|
||||
clientId: string;
|
||||
lastSyncTime: number;
|
||||
pendingBatches: SyncBatch[];
|
||||
bandwidth: {
|
||||
used: number;
|
||||
limit: number;
|
||||
resetTime: number;
|
||||
};
|
||||
scope: {
|
||||
position?: { x: number; y: number; z: number };
|
||||
range: number;
|
||||
customFilter?: (batch: SyncBatch) => boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步统计信息
|
||||
*/
|
||||
interface SyncSystemStats {
|
||||
totalSyncs: number;
|
||||
totalBytes: number;
|
||||
clientCount: number;
|
||||
averageLatency: number;
|
||||
syncsPerSecond: number;
|
||||
droppedSyncs: number;
|
||||
scopeFiltered: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端SyncVar系统
|
||||
* 负责收集所有SyncVar变化并向客户端同步
|
||||
*/
|
||||
export class SyncVarSystem extends EntitySystem {
|
||||
private config: SyncVarSystemConfig;
|
||||
private syncVarManager: SyncVarManager;
|
||||
private serializer: SyncVarSerializer;
|
||||
private networkServer?: NetworkServer;
|
||||
private connectionManager?: ConnectionManager;
|
||||
|
||||
/** 客户端同步状态 */
|
||||
private clientStates = new Map<string, ClientSyncState>();
|
||||
|
||||
/** 待发送的批次队列 */
|
||||
private pendingBatches: SyncBatch[] = [];
|
||||
|
||||
/** 同步统计 */
|
||||
private stats: SyncSystemStats = {
|
||||
totalSyncs: 0,
|
||||
totalBytes: 0,
|
||||
clientCount: 0,
|
||||
averageLatency: 0,
|
||||
syncsPerSecond: 0,
|
||||
droppedSyncs: 0,
|
||||
scopeFiltered: 0
|
||||
};
|
||||
|
||||
/** 最后统计重置时间 */
|
||||
private lastStatsReset = Date.now();
|
||||
|
||||
/** 同步定时器 */
|
||||
private syncTimer: any = null;
|
||||
|
||||
constructor(config: Partial<SyncVarSystemConfig> = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
syncRate: 60, // 60ms = ~16fps
|
||||
maxBatchSize: 50,
|
||||
enableScopeOptimization: true,
|
||||
enableBandwidthLimit: true,
|
||||
maxBandwidthPerClient: 10240, // 10KB/s
|
||||
enablePriorityScheduling: true,
|
||||
enableBatching: true,
|
||||
batchTimeout: 16,
|
||||
...config
|
||||
};
|
||||
|
||||
this.syncVarManager = SyncVarManager.getInstance();
|
||||
this.serializer = new SyncVarSerializer({
|
||||
enableCompression: true,
|
||||
enableDeltaSync: true,
|
||||
enableBatching: this.config.enableBatching,
|
||||
batchTimeout: this.config.batchTimeout
|
||||
});
|
||||
|
||||
this.setupSyncVarManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统
|
||||
*/
|
||||
public override initialize(): void {
|
||||
super.initialize();
|
||||
|
||||
this.logger.info('SyncVar系统初始化');
|
||||
this.startSyncTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统更新
|
||||
*/
|
||||
protected override process(): void {
|
||||
this.updateClientStates();
|
||||
this.processScheduledSyncs();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网络服务器
|
||||
*/
|
||||
public setNetworkServer(server: NetworkServer): void {
|
||||
this.networkServer = server;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接管理器
|
||||
*/
|
||||
public setConnectionManager(manager: ConnectionManager): void {
|
||||
this.connectionManager = manager;
|
||||
|
||||
// 监听客户端连接事件
|
||||
manager.on('clientConnected', (clientId: string) => {
|
||||
this.addClient(clientId);
|
||||
});
|
||||
|
||||
manager.on('clientDisconnected', (clientId: string) => {
|
||||
this.removeClient(clientId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册网络实体
|
||||
*/
|
||||
public registerNetworkEntity(entity: any): void {
|
||||
this.syncVarManager.registerInstance(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销网络实体
|
||||
*/
|
||||
public unregisterNetworkEntity(entity: any): void {
|
||||
this.syncVarManager.unregisterInstance(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端作用域
|
||||
*/
|
||||
public setClientScope(clientId: string, position?: { x: number; y: number; z: number }, range: number = 100): void {
|
||||
const clientState = this.clientStates.get(clientId);
|
||||
if (clientState) {
|
||||
clientState.scope.position = position;
|
||||
clientState.scope.range = range;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端自定义过滤器
|
||||
*/
|
||||
public setClientFilter(clientId: string, filter: (batch: SyncBatch) => boolean): void {
|
||||
const clientState = this.clientStates.get(clientId);
|
||||
if (clientState) {
|
||||
clientState.scope.customFilter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
public getStats(): SyncSystemStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计信息
|
||||
*/
|
||||
public resetStats(): void {
|
||||
this.stats = {
|
||||
totalSyncs: 0,
|
||||
totalBytes: 0,
|
||||
clientCount: this.clientStates.size,
|
||||
averageLatency: 0,
|
||||
syncsPerSecond: 0,
|
||||
droppedSyncs: 0,
|
||||
scopeFiltered: 0
|
||||
};
|
||||
this.lastStatsReset = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
public updateConfig(newConfig: Partial<SyncVarSystemConfig>): void {
|
||||
Object.assign(this.config, newConfig);
|
||||
|
||||
if (newConfig.syncRate !== undefined) {
|
||||
this.restartSyncTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁系统
|
||||
*/
|
||||
public override destroy(): void {
|
||||
this.stopSyncTimer();
|
||||
this.clientStates.clear();
|
||||
this.pendingBatches.length = 0;
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
protected override getLoggerName(): string {
|
||||
return 'SyncVarSystem';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置SyncVar管理器事件
|
||||
*/
|
||||
private setupSyncVarManager(): void {
|
||||
this.syncVarManager.on('syncBatchReady', (batch: SyncBatch) => {
|
||||
this.enqueueBatch(batch);
|
||||
});
|
||||
|
||||
this.syncVarManager.on('syncError', (error: Error) => {
|
||||
this.logger.error('SyncVar同步错误:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加客户端
|
||||
*/
|
||||
private addClient(clientId: string): void {
|
||||
const clientState: ClientSyncState = {
|
||||
clientId,
|
||||
lastSyncTime: 0,
|
||||
pendingBatches: [],
|
||||
bandwidth: {
|
||||
used: 0,
|
||||
limit: this.config.maxBandwidthPerClient,
|
||||
resetTime: Date.now() + 1000
|
||||
},
|
||||
scope: {
|
||||
range: 100
|
||||
}
|
||||
};
|
||||
|
||||
this.clientStates.set(clientId, clientState);
|
||||
this.stats.clientCount = this.clientStates.size;
|
||||
|
||||
this.logger.info(`客户端 ${clientId} 已添加到同步系统`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除客户端
|
||||
*/
|
||||
private removeClient(clientId: string): void {
|
||||
this.clientStates.delete(clientId);
|
||||
this.stats.clientCount = this.clientStates.size;
|
||||
|
||||
this.logger.info(`客户端 ${clientId} 已从同步系统移除`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将批次加入队列
|
||||
*/
|
||||
private enqueueBatch(batch: SyncBatch): void {
|
||||
this.pendingBatches.push(batch);
|
||||
|
||||
// 如果队列过长,移除最旧的批次
|
||||
if (this.pendingBatches.length > this.config.maxBatchSize * 2) {
|
||||
this.pendingBatches.shift();
|
||||
this.stats.droppedSyncs++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理计划的同步
|
||||
*/
|
||||
private processScheduledSyncs(): void {
|
||||
if (this.pendingBatches.length === 0 || this.clientStates.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const batchesToProcess = this.pendingBatches.splice(0, this.config.maxBatchSize);
|
||||
|
||||
for (const batch of batchesToProcess) {
|
||||
this.distributeBatchToClients(batch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将批次分发给客户端
|
||||
*/
|
||||
private distributeBatchToClients(batch: SyncBatch): void {
|
||||
for (const [clientId, clientState] of this.clientStates) {
|
||||
// 检查网络作用域
|
||||
if (this.config.enableScopeOptimization && !this.isInClientScope(batch, clientState)) {
|
||||
this.stats.scopeFiltered++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查带宽限制
|
||||
if (this.config.enableBandwidthLimit && !this.checkBandwidthLimit(clientId, batch)) {
|
||||
// 将批次添加到待发送队列
|
||||
clientState.pendingBatches.push(batch);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.sendBatchToClient(clientId, batch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查批次是否在客户端作用域内
|
||||
*/
|
||||
private isInClientScope(batch: SyncBatch, clientState: ClientSyncState): boolean {
|
||||
// 检查自定义过滤器
|
||||
if (clientState.scope.customFilter) {
|
||||
return clientState.scope.customFilter(batch);
|
||||
}
|
||||
|
||||
// 检查权限和作用域
|
||||
for (const [prop, scope] of Object.entries(batch.scopes)) {
|
||||
const authority = batch.authorities[prop];
|
||||
const syncMode = batch.syncModes[prop];
|
||||
|
||||
// 检查权限
|
||||
if (authority === AuthorityType.Client) {
|
||||
// 只有拥有权限的客户端才能看到
|
||||
if (batch.instanceId !== clientState.clientId) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查同步模式
|
||||
switch (syncMode) {
|
||||
case SyncMode.Owner:
|
||||
if (batch.instanceId !== clientState.clientId) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case SyncMode.Others:
|
||||
if (batch.instanceId === clientState.clientId) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case SyncMode.Nearby:
|
||||
if (!this.isNearby(batch, clientState)) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查网络作用域
|
||||
switch (scope) {
|
||||
case NetworkScope.Owner:
|
||||
if (batch.instanceId !== clientState.clientId) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkScope.Nearby:
|
||||
if (!this.isNearby(batch, clientState)) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在附近范围内
|
||||
*/
|
||||
private isNearby(batch: SyncBatch, clientState: ClientSyncState): boolean {
|
||||
// 简化实现,实际项目中需要根据具体的位置信息判断
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查带宽限制
|
||||
*/
|
||||
private checkBandwidthLimit(clientId: string, batch: SyncBatch): boolean {
|
||||
if (!this.config.enableBandwidthLimit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clientState = this.clientStates.get(clientId);
|
||||
if (!clientState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// 重置带宽计数器
|
||||
if (now >= clientState.bandwidth.resetTime) {
|
||||
clientState.bandwidth.used = 0;
|
||||
clientState.bandwidth.resetTime = now + 1000;
|
||||
}
|
||||
|
||||
// 估算批次大小
|
||||
const estimatedSize = this.estimateBatchSize(batch);
|
||||
|
||||
return clientState.bandwidth.used + estimatedSize <= clientState.bandwidth.limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算批次大小
|
||||
*/
|
||||
private estimateBatchSize(batch: SyncBatch): number {
|
||||
// 简化实现,根据变化属性数量估算
|
||||
const propertyCount = Object.keys(batch.changes).length;
|
||||
return propertyCount * 50; // 假设每个属性平均50字节
|
||||
}
|
||||
|
||||
/**
|
||||
* 向客户端发送批次
|
||||
*/
|
||||
private sendBatchToClient(clientId: string, batch: SyncBatch): void {
|
||||
if (!this.networkServer || !this.connectionManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = this.serializer.createSyncMessage(batch, 'server');
|
||||
this.networkServer.sendToClient(clientId, message);
|
||||
|
||||
// 更新统计
|
||||
const clientState = this.clientStates.get(clientId);
|
||||
if (clientState) {
|
||||
clientState.lastSyncTime = Date.now();
|
||||
const estimatedSize = this.estimateBatchSize(batch);
|
||||
clientState.bandwidth.used += estimatedSize;
|
||||
this.stats.totalBytes += estimatedSize;
|
||||
this.stats.totalSyncs++;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`向客户端 ${clientId} 发送同步数据失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户端状态
|
||||
*/
|
||||
private updateClientStates(): void {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [clientId, clientState] of this.clientStates) {
|
||||
// 处理待发送的批次
|
||||
if (clientState.pendingBatches.length > 0 &&
|
||||
this.checkBandwidthLimit(clientId, clientState.pendingBatches[0])) {
|
||||
|
||||
const batch = clientState.pendingBatches.shift()!;
|
||||
this.sendBatchToClient(clientId, batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
private updateStats(): void {
|
||||
const now = Date.now();
|
||||
const deltaTime = now - this.lastStatsReset;
|
||||
|
||||
if (deltaTime >= 1000) { // 每秒更新一次
|
||||
this.stats.syncsPerSecond = this.stats.totalSyncs / (deltaTime / 1000);
|
||||
this.lastStatsReset = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动同步定时器
|
||||
*/
|
||||
private startSyncTimer(): void {
|
||||
if (this.syncTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncTimer = setInterval(() => {
|
||||
this.processScheduledSyncs();
|
||||
}, this.config.syncRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止同步定时器
|
||||
*/
|
||||
private stopSyncTimer(): void {
|
||||
if (this.syncTimer) {
|
||||
clearInterval(this.syncTimer);
|
||||
this.syncTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启同步定时器
|
||||
*/
|
||||
private restartSyncTimer(): void {
|
||||
this.stopSyncTimer();
|
||||
this.startSyncTimer();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* 服务端系统导出
|
||||
*/
|
||||
|
||||
export * from './SyncVarSystem';
|
||||
@@ -1,442 +0,0 @@
|
||||
/**
|
||||
* WebSocket传输层服务端实现
|
||||
*/
|
||||
import WebSocket, { WebSocketServer } from 'ws';
|
||||
import { createLogger, Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ITransport,
|
||||
ITransportClientInfo,
|
||||
ITransportConfig,
|
||||
ConnectionState,
|
||||
EventEmitter
|
||||
} from '@esengine/network-shared';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* WebSocket传输层实现
|
||||
*/
|
||||
export class WebSocketTransport extends EventEmitter implements ITransport {
|
||||
private logger = createLogger('WebSocketTransport');
|
||||
private server?: WebSocketServer;
|
||||
private clients: Map<string, WebSocket> = new Map();
|
||||
private clientInfo: Map<string, ITransportClientInfo> = new Map();
|
||||
private config: ITransportConfig;
|
||||
private isRunning = false;
|
||||
|
||||
/**
|
||||
* 连接事件处理器
|
||||
*/
|
||||
private connectHandlers: ((clientInfo: ITransportClientInfo) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 断开连接事件处理器
|
||||
*/
|
||||
private disconnectHandlers: ((clientId: string, reason?: string) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 消息接收事件处理器
|
||||
*/
|
||||
private messageHandlers: ((clientId: string, data: ArrayBuffer | string) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 错误事件处理器
|
||||
*/
|
||||
private errorHandlers: ((error: Error) => void)[] = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
constructor(config: ITransportConfig) {
|
||||
super();
|
||||
this.config = {
|
||||
maxConnections: 1000,
|
||||
heartbeatInterval: 30000,
|
||||
connectionTimeout: 60000,
|
||||
maxMessageSize: 1024 * 1024, // 1MB
|
||||
compression: true,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动传输层
|
||||
*/
|
||||
async start(port: number, host?: string): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
this.logger.warn('WebSocket传输层已在运行');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.server = new WebSocketServer({
|
||||
port,
|
||||
host: host || '0.0.0.0',
|
||||
maxPayload: this.config.maxMessageSize,
|
||||
perMessageDeflate: this.config.compression ? {
|
||||
threshold: 1024,
|
||||
concurrencyLimit: 10,
|
||||
clientNoContextTakeover: false,
|
||||
serverNoContextTakeover: false
|
||||
} : false
|
||||
});
|
||||
|
||||
this.setupServerEvents();
|
||||
this.isRunning = true;
|
||||
|
||||
this.logger.info(`WebSocket服务器已启动: ${host || '0.0.0.0'}:${port}`);
|
||||
this.logger.info(`最大连接数: ${this.config.maxConnections}`);
|
||||
this.logger.info(`压缩: ${this.config.compression ? '启用' : '禁用'}`);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('启动WebSocket服务器失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止传输层
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning || !this.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 断开所有客户端连接
|
||||
for (const [clientId, ws] of this.clients) {
|
||||
ws.close(1001, '服务器关闭');
|
||||
this.handleClientDisconnect(clientId, '服务器关闭');
|
||||
}
|
||||
|
||||
// 关闭服务器
|
||||
this.server!.close(() => {
|
||||
this.isRunning = false;
|
||||
this.server = undefined;
|
||||
this.logger.info('WebSocket服务器已停止');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据到指定客户端
|
||||
*/
|
||||
send(clientId: string, data: ArrayBuffer | string): void {
|
||||
const ws = this.clients.get(clientId);
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
this.logger.warn(`尝试向未连接的客户端发送消息: ${clientId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (error) {
|
||||
this.logger.error(`发送消息到客户端 ${clientId} 失败:`, error);
|
||||
this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播数据到所有客户端
|
||||
*/
|
||||
broadcast(data: ArrayBuffer | string, exclude?: string[]): void {
|
||||
const excludeSet = new Set(exclude || []);
|
||||
|
||||
for (const [clientId, ws] of this.clients) {
|
||||
if (excludeSet.has(clientId) || ws.readyState !== WebSocket.OPEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (error) {
|
||||
this.logger.error(`广播消息到客户端 ${clientId} 失败:`, error);
|
||||
this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端连接事件
|
||||
*/
|
||||
onConnect(handler: (clientInfo: ITransportClientInfo) => void): void {
|
||||
this.connectHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端断开事件
|
||||
*/
|
||||
onDisconnect(handler: (clientId: string, reason?: string) => void): void {
|
||||
this.disconnectHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听消息接收事件
|
||||
*/
|
||||
onMessage(handler: (clientId: string, data: ArrayBuffer | string) => void): void {
|
||||
this.messageHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听错误事件
|
||||
*/
|
||||
onError(handler: (error: Error) => void): void {
|
||||
this.errorHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接的客户端数量
|
||||
*/
|
||||
getClientCount(): number {
|
||||
return this.clients.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否连接
|
||||
*/
|
||||
isClientConnected(clientId: string): boolean {
|
||||
const ws = this.clients.get(clientId);
|
||||
return ws !== undefined && ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端
|
||||
*/
|
||||
disconnectClient(clientId: string, reason?: string): void {
|
||||
const ws = this.clients.get(clientId);
|
||||
if (ws) {
|
||||
ws.close(1000, reason || '服务器主动断开');
|
||||
this.handleClientDisconnect(clientId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端信息
|
||||
*/
|
||||
getClientInfo(clientId: string): ITransportClientInfo | undefined {
|
||||
return this.clientInfo.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端信息
|
||||
*/
|
||||
getAllClients(): ITransportClientInfo[] {
|
||||
return Array.from(this.clientInfo.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器事件监听
|
||||
*/
|
||||
private setupServerEvents(): void {
|
||||
if (!this.server) return;
|
||||
|
||||
this.server.on('connection', (ws, request) => {
|
||||
this.handleNewConnection(ws, request);
|
||||
});
|
||||
|
||||
this.server.on('error', (error) => {
|
||||
this.logger.error('WebSocket服务器错误:', error);
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
this.server.on('close', () => {
|
||||
this.logger.info('WebSocket服务器已关闭');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新客户端连接
|
||||
*/
|
||||
private handleNewConnection(ws: WebSocket, request: any): void {
|
||||
// 检查连接数限制
|
||||
if (this.clients.size >= this.config.maxConnections!) {
|
||||
this.logger.warn('达到最大连接数限制,拒绝新连接');
|
||||
ws.close(1013, '服务器繁忙');
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = crypto.randomUUID();
|
||||
const clientInfo: ITransportClientInfo = {
|
||||
id: clientId,
|
||||
remoteAddress: request.socket.remoteAddress || 'unknown',
|
||||
connectTime: Date.now(),
|
||||
userAgent: request.headers['user-agent'],
|
||||
headers: request.headers
|
||||
};
|
||||
|
||||
// 存储客户端连接和信息
|
||||
this.clients.set(clientId, ws);
|
||||
this.clientInfo.set(clientId, clientInfo);
|
||||
|
||||
// 设置WebSocket事件监听
|
||||
this.setupClientEvents(ws, clientId);
|
||||
|
||||
this.logger.info(`新客户端连接: ${clientId} from ${clientInfo.remoteAddress}`);
|
||||
|
||||
// 触发连接事件
|
||||
this.connectHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(clientInfo);
|
||||
} catch (error) {
|
||||
this.logger.error('连接事件处理器错误:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端WebSocket事件监听
|
||||
*/
|
||||
private setupClientEvents(ws: WebSocket, clientId: string): void {
|
||||
// 消息接收
|
||||
ws.on('message', (data) => {
|
||||
this.handleClientMessage(clientId, data);
|
||||
});
|
||||
|
||||
// 连接关闭
|
||||
ws.on('close', (code, reason) => {
|
||||
this.handleClientDisconnect(clientId, reason?.toString() || `Code: ${code}`);
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
ws.on('error', (error) => {
|
||||
this.logger.error(`客户端 ${clientId} WebSocket错误:`, error);
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
// Pong响应(心跳)
|
||||
ws.on('pong', () => {
|
||||
// 记录客户端响应心跳
|
||||
const info = this.clientInfo.get(clientId);
|
||||
if (info) {
|
||||
// 可以更新延迟信息
|
||||
}
|
||||
});
|
||||
|
||||
// 设置连接超时
|
||||
if (this.config.connectionTimeout) {
|
||||
Core.schedule(this.config.connectionTimeout / 1000, false, this, () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.ping();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端消息
|
||||
*/
|
||||
private handleClientMessage(clientId: string, data: WebSocket.Data): void {
|
||||
try {
|
||||
// 检查是否为有效的应用消息
|
||||
if (!this.isApplicationMessage(data)) {
|
||||
this.logger.debug(`忽略非应用消息 (${clientId}): ${typeof data} ${data instanceof ArrayBuffer ? data.byteLength : data.toString().length} bytes`);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = data instanceof ArrayBuffer ? data : new TextEncoder().encode(data.toString()).buffer;
|
||||
|
||||
// 触发消息事件
|
||||
this.messageHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(clientId, message);
|
||||
} catch (error) {
|
||||
this.logger.error('消息事件处理器错误:', error);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`处理客户端 ${clientId} 消息失败:`, error);
|
||||
this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为有效的应用消息
|
||||
*/
|
||||
private isApplicationMessage(data: WebSocket.Data): boolean {
|
||||
try {
|
||||
// 转换为字符串进行检查
|
||||
const jsonString = data instanceof ArrayBuffer
|
||||
? new TextDecoder().decode(data)
|
||||
: data.toString();
|
||||
|
||||
// 基本长度检查 - 空消息或过短消息通常不是应用消息
|
||||
if (!jsonString || jsonString.length < 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
// 检查是否有基本的消息结构
|
||||
return parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
(parsed.type || parsed.messageId || parsed.data);
|
||||
|
||||
} catch (error) {
|
||||
// JSON解析失败,可能是握手数据或其他非JSON消息
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接
|
||||
*/
|
||||
private handleClientDisconnect(clientId: string, reason?: string): void {
|
||||
// 清理客户端数据
|
||||
this.clients.delete(clientId);
|
||||
this.clientInfo.delete(clientId);
|
||||
|
||||
this.logger.info(`客户端断开连接: ${clientId}, 原因: ${reason || '未知'}`);
|
||||
|
||||
// 触发断开连接事件
|
||||
this.disconnectHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(clientId, reason);
|
||||
} catch (error) {
|
||||
this.logger.error('断开连接事件处理器错误:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
private handleError(error: Error): void {
|
||||
this.errorHandlers.forEach((handler) => {
|
||||
try {
|
||||
handler(error);
|
||||
} catch (handlerError) {
|
||||
this.logger.error('错误事件处理器错误:', handlerError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳到所有客户端
|
||||
*/
|
||||
public sendHeartbeat(): void {
|
||||
for (const [clientId, ws] of this.clients) {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.ping();
|
||||
} catch (error) {
|
||||
this.logger.error(`发送心跳到客户端 ${clientId} 失败:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传输层统计信息
|
||||
*/
|
||||
public getStats() {
|
||||
return {
|
||||
isRunning: this.isRunning,
|
||||
clientCount: this.clients.size,
|
||||
maxConnections: this.config.maxConnections,
|
||||
compressionEnabled: this.config.compression,
|
||||
clients: this.getAllClients()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"importHelpers": false,
|
||||
"downlevelIteration": true,
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../network-shared"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user