558 lines
16 KiB
Markdown
558 lines
16 KiB
Markdown
# Node.js 适配器
|
||
|
||
## 概述
|
||
|
||
Node.js 平台适配器为 Node.js 服务器环境提供支持,适用于游戏服务器、计算服务器或其他需要 ECS 架构的服务器应用。
|
||
|
||
## 特性支持
|
||
|
||
- ✅ **Worker**: 支持(通过 `worker_threads` 模块)
|
||
- ❌ **SharedArrayBuffer**: 支持(Node.js 16.17.0+)
|
||
- ✅ **Transferable Objects**: 完全支持
|
||
- ✅ **高精度时间**: 使用 `process.hrtime.bigint()`
|
||
- ✅ **设备信息**: 完整的系统和进程信息
|
||
|
||
## 完整实现
|
||
|
||
```typescript
|
||
import { worker_threads, Worker, isMainThread, parentPort } from 'worker_threads';
|
||
import * as os from 'os';
|
||
import * as process from 'process';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import type {
|
||
IPlatformAdapter,
|
||
PlatformWorker,
|
||
WorkerCreationOptions,
|
||
PlatformConfig,
|
||
NodeDeviceInfo
|
||
} from '@esengine/ecs-framework';
|
||
|
||
/**
|
||
* Node.js 平台适配器
|
||
* 支持 Node.js 服务器环境
|
||
*/
|
||
export class NodeAdapter implements IPlatformAdapter {
|
||
public readonly name = 'nodejs';
|
||
public readonly version: string;
|
||
|
||
constructor() {
|
||
this.version = process.version;
|
||
}
|
||
|
||
/**
|
||
* 检查是否支持Worker
|
||
*/
|
||
public isWorkerSupported(): boolean {
|
||
try {
|
||
// 检查 worker_threads 模块是否可用
|
||
return typeof worker_threads !== 'undefined' && typeof Worker !== 'undefined';
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查是否支持SharedArrayBuffer
|
||
*/
|
||
public isSharedArrayBufferSupported(): boolean {
|
||
// Node.js 支持 SharedArrayBuffer
|
||
return typeof SharedArrayBuffer !== 'undefined';
|
||
}
|
||
|
||
/**
|
||
* 获取硬件并发数(CPU核心数)
|
||
*/
|
||
public getHardwareConcurrency(): number {
|
||
return os.cpus().length;
|
||
}
|
||
|
||
/**
|
||
* 创建Worker
|
||
*/
|
||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||
if (!this.isWorkerSupported()) {
|
||
throw new Error('Node.js环境不支持Worker Threads');
|
||
}
|
||
|
||
try {
|
||
return new NodeWorker(script, options);
|
||
} catch (error) {
|
||
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建SharedArrayBuffer
|
||
*/
|
||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||
if (!this.isSharedArrayBufferSupported()) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
return new SharedArrayBuffer(length);
|
||
} catch (error) {
|
||
console.warn('SharedArrayBuffer创建失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取高精度时间戳(纳秒)
|
||
*/
|
||
public getHighResTimestamp(): number {
|
||
// 返回毫秒,与浏览器 performance.now() 保持一致
|
||
return Number(process.hrtime.bigint()) / 1000000;
|
||
}
|
||
|
||
/**
|
||
* 获取平台配置
|
||
*/
|
||
public getPlatformConfig(): PlatformConfig {
|
||
return {
|
||
maxWorkerCount: this.getHardwareConcurrency(),
|
||
supportsModuleWorker: true, // Node.js 支持 ES 模块
|
||
supportsTransferableObjects: true,
|
||
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
|
||
workerScriptPrefix: '',
|
||
limitations: {
|
||
noEval: false, // Node.js 支持 eval
|
||
requiresWorkerInit: false
|
||
},
|
||
extensions: {
|
||
platform: 'nodejs',
|
||
nodeVersion: process.version,
|
||
v8Version: process.versions.v8,
|
||
uvVersion: process.versions.uv,
|
||
zlibVersion: process.versions.zlib,
|
||
opensslVersion: process.versions.openssl,
|
||
architecture: process.arch,
|
||
endianness: os.endianness(),
|
||
pid: process.pid,
|
||
ppid: process.ppid
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取Node.js设备信息
|
||
*/
|
||
public getDeviceInfo(): NodeDeviceInfo {
|
||
const cpus = os.cpus();
|
||
const networkInterfaces = os.networkInterfaces();
|
||
const userInfo = os.userInfo();
|
||
|
||
return {
|
||
// 系统信息
|
||
platform: os.platform(),
|
||
arch: os.arch(),
|
||
type: os.type(),
|
||
release: os.release(),
|
||
version: os.version(),
|
||
hostname: os.hostname(),
|
||
|
||
// CPU信息
|
||
cpus: cpus.map(cpu => ({
|
||
model: cpu.model,
|
||
speed: cpu.speed,
|
||
times: cpu.times
|
||
})),
|
||
|
||
// 内存信息
|
||
totalMemory: os.totalmem(),
|
||
freeMemory: os.freemem(),
|
||
usedMemory: os.totalmem() - os.freemem(),
|
||
|
||
// 负载信息
|
||
loadAverage: os.loadavg(),
|
||
|
||
// 网络接口
|
||
networkInterfaces: Object.fromEntries(
|
||
Object.entries(networkInterfaces).map(([name, interfaces]) => [
|
||
name,
|
||
(interfaces || []).map(iface => ({
|
||
address: iface.address,
|
||
netmask: iface.netmask,
|
||
family: iface.family as 'IPv4' | 'IPv6',
|
||
mac: iface.mac,
|
||
internal: iface.internal,
|
||
cidr: iface.cidr,
|
||
scopeid: iface.scopeid
|
||
}))
|
||
])
|
||
),
|
||
|
||
// 进程信息
|
||
process: {
|
||
pid: process.pid,
|
||
ppid: process.ppid,
|
||
version: process.version,
|
||
versions: process.versions,
|
||
uptime: process.uptime()
|
||
},
|
||
|
||
// 用户信息
|
||
userInfo: {
|
||
uid: userInfo.uid,
|
||
gid: userInfo.gid,
|
||
username: userInfo.username,
|
||
homedir: userInfo.homedir,
|
||
shell: userInfo.shell
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取SharedArrayBuffer最大大小限制
|
||
*/
|
||
private getMaxSharedArrayBufferSize(): number {
|
||
const totalMemory = os.totalmem();
|
||
// 限制为系统总内存的50%
|
||
return Math.floor(totalMemory * 0.5);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Node.js Worker封装
|
||
*/
|
||
class NodeWorker implements PlatformWorker {
|
||
private _state: 'running' | 'terminated' = 'running';
|
||
private worker: Worker;
|
||
private isTemporaryFile: boolean = false;
|
||
private scriptPath: string;
|
||
|
||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||
try {
|
||
// 判断 script 是文件路径还是脚本内容
|
||
if (this.isFilePath(script)) {
|
||
// 直接使用文件路径
|
||
this.scriptPath = script;
|
||
this.isTemporaryFile = false;
|
||
} else {
|
||
// 将脚本内容写入临时文件
|
||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||
this.isTemporaryFile = true;
|
||
}
|
||
|
||
// 创建Worker
|
||
this.worker = new Worker(this.scriptPath, {
|
||
// Node.js Worker options
|
||
workerData: options.name ? { name: options.name } : undefined
|
||
});
|
||
|
||
} catch (error) {
|
||
throw new Error(`创建Node.js Worker失败: ${(error as Error).message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断是否为文件路径
|
||
*/
|
||
private isFilePath(script: string): boolean {
|
||
// 检查是否看起来像文件路径
|
||
return (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.ts')) &&
|
||
!script.includes('\n') &&
|
||
!script.includes(';') &&
|
||
script.length < 500; // 文件路径通常不会太长
|
||
}
|
||
|
||
/**
|
||
* 将脚本内容写入临时文件
|
||
*/
|
||
private writeScriptToFile(script: string, name?: string): string {
|
||
const tmpDir = os.tmpdir();
|
||
const fileName = name ? `worker-${name}-${Date.now()}.js` : `worker-${Date.now()}.js`;
|
||
const filePath = path.join(tmpDir, fileName);
|
||
|
||
try {
|
||
fs.writeFileSync(filePath, script, 'utf8');
|
||
return filePath;
|
||
} catch (error) {
|
||
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
|
||
}
|
||
}
|
||
|
||
public get state(): 'running' | 'terminated' {
|
||
return this._state;
|
||
}
|
||
|
||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||
if (this._state === 'terminated') {
|
||
throw new Error('Worker已被终止');
|
||
}
|
||
|
||
try {
|
||
if (transfer && transfer.length > 0) {
|
||
// Node.js Worker 支持 Transferable Objects
|
||
this.worker.postMessage(message, transfer);
|
||
} else {
|
||
this.worker.postMessage(message);
|
||
}
|
||
} catch (error) {
|
||
throw new Error(`发送消息到Node.js Worker失败: ${(error as Error).message}`);
|
||
}
|
||
}
|
||
|
||
public onMessage(handler: (event: { data: any }) => void): void {
|
||
this.worker.on('message', (data: any) => {
|
||
handler({ data });
|
||
});
|
||
}
|
||
|
||
public onError(handler: (error: ErrorEvent) => void): void {
|
||
this.worker.on('error', (error: Error) => {
|
||
// 将 Error 转换为 ErrorEvent 格式
|
||
const errorEvent = {
|
||
message: error.message,
|
||
filename: '',
|
||
lineno: 0,
|
||
colno: 0,
|
||
error: error
|
||
} as ErrorEvent;
|
||
handler(errorEvent);
|
||
});
|
||
}
|
||
|
||
public terminate(): void {
|
||
if (this._state === 'running') {
|
||
try {
|
||
this.worker.terminate();
|
||
this._state = 'terminated';
|
||
|
||
// 清理临时脚本文件
|
||
this.cleanupScriptFile();
|
||
} catch (error) {
|
||
console.error('终止Node.js Worker失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理临时脚本文件
|
||
*/
|
||
private cleanupScriptFile(): void {
|
||
// 只清理临时创建的文件,不清理用户提供的文件路径
|
||
if (this.scriptPath && this.isTemporaryFile) {
|
||
try {
|
||
fs.unlinkSync(this.scriptPath);
|
||
} catch (error) {
|
||
console.warn('清理Worker脚本文件失败:', error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 使用方法
|
||
|
||
### 1. 复制代码
|
||
|
||
将上述代码复制到你的项目中,例如 `src/platform/NodeAdapter.ts`。
|
||
|
||
### 2. 注册适配器
|
||
|
||
```typescript
|
||
import { PlatformManager } from '@esengine/ecs-framework';
|
||
import { NodeAdapter } from './platform/NodeAdapter';
|
||
|
||
// 检查是否在Node.js环境
|
||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||
const nodeAdapter = new NodeAdapter();
|
||
PlatformManager.getInstance().registerAdapter(nodeAdapter);
|
||
}
|
||
```
|
||
|
||
### 3. 使用 WorkerEntitySystem
|
||
|
||
Node.js 适配器与 WorkerEntitySystem 配合使用,框架会自动处理 Worker 脚本的创建:
|
||
|
||
```typescript
|
||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||
import * as os from 'os';
|
||
|
||
class PhysicsSystem extends WorkerEntitySystem {
|
||
constructor() {
|
||
super(Matcher.all(Transform, Velocity), {
|
||
enableWorker: true,
|
||
workerCount: os.cpus().length, // 使用所有CPU核心
|
||
useSharedArrayBuffer: true,
|
||
systemConfig: { gravity: 9.8 }
|
||
});
|
||
}
|
||
|
||
protected getDefaultEntityDataSize(): number {
|
||
return 6; // x, y, vx, vy, mass, radius
|
||
}
|
||
|
||
protected extractEntityData(entity: Entity): PhysicsData {
|
||
const transform = entity.getComponent(Transform);
|
||
const velocity = entity.getComponent(Velocity);
|
||
return {
|
||
x: transform.x,
|
||
y: transform.y,
|
||
vx: velocity.x,
|
||
vy: velocity.y,
|
||
mass: 1,
|
||
radius: 10
|
||
};
|
||
}
|
||
|
||
// 这个函数会被自动序列化并在Worker中执行
|
||
protected workerProcess(entities, deltaTime, config) {
|
||
return entities.map(entity => {
|
||
// 应用重力
|
||
entity.vy += config.gravity * deltaTime;
|
||
|
||
// 更新位置
|
||
entity.x += entity.vx * deltaTime;
|
||
entity.y += entity.vy * deltaTime;
|
||
|
||
return entity;
|
||
});
|
||
}
|
||
|
||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||
const transform = entity.getComponent(Transform);
|
||
const velocity = entity.getComponent(Velocity);
|
||
|
||
transform.x = result.x;
|
||
transform.y = result.y;
|
||
velocity.x = result.vx;
|
||
velocity.y = result.vy;
|
||
}
|
||
}
|
||
|
||
interface PhysicsData {
|
||
x: number;
|
||
y: number;
|
||
vx: number;
|
||
vy: number;
|
||
mass: number;
|
||
radius: number;
|
||
}
|
||
```
|
||
|
||
### 4. 获取系统信息
|
||
|
||
```typescript
|
||
const manager = PlatformManager.getInstance();
|
||
if (manager.hasAdapter()) {
|
||
const adapter = manager.getAdapter();
|
||
const deviceInfo = adapter.getDeviceInfo();
|
||
|
||
console.log('Node.js版本:', deviceInfo.process?.version);
|
||
console.log('CPU核心数:', deviceInfo.cpus?.length);
|
||
console.log('总内存:', Math.round(deviceInfo.totalMemory! / 1024 / 1024), 'MB');
|
||
console.log('可用内存:', Math.round(deviceInfo.freeMemory! / 1024 / 1024), 'MB');
|
||
}
|
||
```
|
||
|
||
## 官方文档参考
|
||
|
||
Node.js Worker Threads 相关官方文档:
|
||
|
||
- [Worker Threads 官方文档](https://nodejs.org/api/worker_threads.html)
|
||
- [SharedArrayBuffer 支持](https://nodejs.org/api/globals.html#class-sharedarraybuffer)
|
||
- [OS 模块文档](https://nodejs.org/api/os.html)
|
||
- [Process 模块文档](https://nodejs.org/api/process.html)
|
||
|
||
## 重要注意事项
|
||
|
||
### Worker Threads 要求
|
||
|
||
- **Node.js版本**: 需要 Node.js 10.5.0+ (建议 12+)
|
||
- **模块类型**: 支持 CommonJS 和 ES 模块
|
||
- **线程限制**: 理论上无限制,但建议不超过 CPU 核心数的 2 倍
|
||
|
||
### 性能优化建议
|
||
|
||
#### 1. Worker 池管理
|
||
|
||
```typescript
|
||
class ServerPhysicsSystem extends WorkerEntitySystem {
|
||
constructor() {
|
||
const cpuCount = os.cpus().length;
|
||
super(Matcher.all(Transform, Velocity), {
|
||
enableWorker: true,
|
||
workerCount: Math.min(cpuCount * 2, 16), // 最多16个Worker
|
||
entitiesPerWorker: 1000, // 每个Worker处理1000个实体
|
||
useSharedArrayBuffer: true,
|
||
systemConfig: {
|
||
gravity: 9.8,
|
||
timeStep: 1/60
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2. 内存管理
|
||
|
||
```typescript
|
||
class MemoryMonitor {
|
||
public static checkMemoryUsage(): void {
|
||
const used = process.memoryUsage();
|
||
|
||
console.log('内存使用情况:');
|
||
console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`);
|
||
console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);
|
||
console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`);
|
||
console.log(` External: ${Math.round(used.external / 1024 / 1024)} MB`);
|
||
|
||
// 内存使用率过高时触发警告
|
||
if (used.heapUsed > used.heapTotal * 0.9) {
|
||
console.warn('内存使用率过高,建议优化或重启');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 定期检查内存使用
|
||
setInterval(() => {
|
||
MemoryMonitor.checkMemoryUsage();
|
||
}, 30000); // 每30秒检查一次
|
||
```
|
||
|
||
#### 3. 服务器环境优化
|
||
|
||
```typescript
|
||
// 设置进程标题
|
||
process.title = 'ecs-game-server';
|
||
|
||
// 处理未捕获异常
|
||
process.on('uncaughtException', (error) => {
|
||
console.error('未捕获异常:', error);
|
||
process.exit(1);
|
||
});
|
||
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
console.error('未处理的Promise拒绝:', reason);
|
||
});
|
||
|
||
// 优雅关闭
|
||
process.on('SIGTERM', () => {
|
||
console.log('收到SIGTERM信号,正在关闭服务器...');
|
||
// 清理资源
|
||
process.exit(0);
|
||
});
|
||
```
|
||
|
||
## 调试技巧
|
||
|
||
```typescript
|
||
// 检查Node.js环境支持情况
|
||
const adapter = new NodeAdapter();
|
||
console.log('Node.js版本:', adapter.version);
|
||
console.log('Worker支持:', adapter.isWorkerSupported());
|
||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
||
console.log('CPU核心数:', adapter.getHardwareConcurrency());
|
||
|
||
// 获取详细配置
|
||
const config = adapter.getPlatformConfig();
|
||
console.log('平台配置:', JSON.stringify(config, null, 2));
|
||
|
||
// 系统资源监控
|
||
const deviceInfo = adapter.getDeviceInfo();
|
||
console.log('系统负载:', deviceInfo.loadAverage);
|
||
console.log('网络接口:', Object.keys(deviceInfo.networkInterfaces!));
|
||
``` |