重构network库(mvp版本)搭建基础设施和核心接口
定义ITransport/ISerializer/INetworkMessage接口 NetworkIdentity组件 基础事件定义
This commit is contained in:
@@ -1,622 +0,0 @@
|
||||
/**
|
||||
* 身份验证管理器
|
||||
*
|
||||
* 处理客户端身份验证、令牌验证等功能
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
|
||||
/**
|
||||
* 认证配置
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
/** 令牌过期时间(毫秒) */
|
||||
tokenExpirationTime?: number;
|
||||
/** 最大登录尝试次数 */
|
||||
maxLoginAttempts?: number;
|
||||
/** 登录尝试重置时间(毫秒) */
|
||||
loginAttemptResetTime?: number;
|
||||
/** 是否启用令牌刷新 */
|
||||
enableTokenRefresh?: boolean;
|
||||
/** 令牌刷新阈值(毫秒) */
|
||||
tokenRefreshThreshold?: number;
|
||||
/** 是否启用IP限制 */
|
||||
enableIpRestriction?: boolean;
|
||||
/** 密码哈希算法 */
|
||||
passwordHashAlgorithm?: 'sha256' | 'sha512';
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/** 用户ID */
|
||||
id: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 密码哈希 */
|
||||
passwordHash: string;
|
||||
/** 用户角色 */
|
||||
roles: string[];
|
||||
/** 用户元数据 */
|
||||
metadata: Record<string, NetworkValue>;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
/** 最后登录时间 */
|
||||
lastLoginAt?: Date;
|
||||
/** 是否激活 */
|
||||
isActive: boolean;
|
||||
/** 允许的IP地址列表 */
|
||||
allowedIps?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证令牌
|
||||
*/
|
||||
export interface AuthToken {
|
||||
/** 令牌ID */
|
||||
id: string;
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 令牌值 */
|
||||
token: string;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
/** 过期时间 */
|
||||
expiresAt: Date;
|
||||
/** 是否已撤销 */
|
||||
isRevoked: boolean;
|
||||
/** 令牌元数据 */
|
||||
metadata: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录尝试记录
|
||||
*/
|
||||
interface LoginAttempt {
|
||||
/** IP地址 */
|
||||
ip: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 尝试次数 */
|
||||
attempts: number;
|
||||
/** 最后尝试时间 */
|
||||
lastAttempt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证结果
|
||||
*/
|
||||
export interface AuthResult {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 用户信息 */
|
||||
user?: UserInfo;
|
||||
/** 认证令牌 */
|
||||
token?: AuthToken;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 错误代码 */
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器事件
|
||||
*/
|
||||
export interface AuthManagerEvents {
|
||||
/** 用户登录成功 */
|
||||
'login-success': (user: UserInfo, token: AuthToken, clientId: string) => void;
|
||||
/** 用户登录失败 */
|
||||
'login-failed': (username: string, reason: string, clientId: string) => void;
|
||||
/** 用户注销 */
|
||||
'logout': (userId: string, clientId: string) => void;
|
||||
/** 令牌过期 */
|
||||
'token-expired': (userId: string, tokenId: string) => void;
|
||||
/** 令牌刷新 */
|
||||
'token-refreshed': (userId: string, oldTokenId: string, newTokenId: string) => void;
|
||||
/** 认证错误 */
|
||||
'auth-error': (error: Error, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份验证管理器
|
||||
*/
|
||||
export class AuthenticationManager extends EventEmitter {
|
||||
private config: AuthConfig;
|
||||
private users = new Map<string, UserInfo>();
|
||||
private tokens = new Map<string, AuthToken>();
|
||||
private loginAttempts = new Map<string, LoginAttempt>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: AuthConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
tokenExpirationTime: 24 * 60 * 60 * 1000, // 24小时
|
||||
maxLoginAttempts: 5,
|
||||
loginAttemptResetTime: 15 * 60 * 1000, // 15分钟
|
||||
enableTokenRefresh: true,
|
||||
tokenRefreshThreshold: 60 * 60 * 1000, // 1小时
|
||||
enableIpRestriction: false,
|
||||
passwordHashAlgorithm: 'sha256',
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用户
|
||||
*/
|
||||
async registerUser(userData: {
|
||||
username: string;
|
||||
password: string;
|
||||
roles?: string[];
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
allowedIps?: string[];
|
||||
}): Promise<UserInfo> {
|
||||
const { username, password, roles = ['user'], metadata = {}, allowedIps } = userData;
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (this.findUserByUsername(username)) {
|
||||
throw new Error('Username already exists');
|
||||
}
|
||||
|
||||
const userId = this.generateId();
|
||||
const passwordHash = this.hashPassword(password);
|
||||
|
||||
const user: UserInfo = {
|
||||
id: userId,
|
||||
username,
|
||||
passwordHash,
|
||||
roles,
|
||||
metadata,
|
||||
createdAt: new Date(),
|
||||
isActive: true,
|
||||
allowedIps
|
||||
};
|
||||
|
||||
this.users.set(userId, user);
|
||||
|
||||
console.log(`User registered: ${username} (${userId})`);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
async login(
|
||||
username: string,
|
||||
password: string,
|
||||
client: ClientConnection
|
||||
): Promise<AuthResult> {
|
||||
try {
|
||||
const clientIp = client.remoteAddress;
|
||||
const attemptKey = `${clientIp}-${username}`;
|
||||
|
||||
// 检查登录尝试次数
|
||||
if (this.isLoginBlocked(attemptKey)) {
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Too many login attempts. Please try again later.',
|
||||
errorCode: 'LOGIN_BLOCKED'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = this.findUserByUsername(username);
|
||||
if (!user || !user.isActive) {
|
||||
this.recordLoginAttempt(attemptKey);
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Invalid username or password',
|
||||
errorCode: 'INVALID_CREDENTIALS'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const passwordHash = this.hashPassword(password);
|
||||
if (user.passwordHash !== passwordHash) {
|
||||
this.recordLoginAttempt(attemptKey);
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Invalid username or password',
|
||||
errorCode: 'INVALID_CREDENTIALS'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
// IP限制检查
|
||||
if (this.config.enableIpRestriction && user.allowedIps && user.allowedIps.length > 0) {
|
||||
if (!user.allowedIps.includes(clientIp)) {
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: 'Access denied from this IP address',
|
||||
errorCode: 'IP_RESTRICTED'
|
||||
};
|
||||
this.emit('login-failed', username, result.error!, client.id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建认证令牌
|
||||
const token = this.createToken(user.id);
|
||||
|
||||
// 更新用户最后登录时间
|
||||
user.lastLoginAt = new Date();
|
||||
|
||||
// 清除登录尝试记录
|
||||
this.loginAttempts.delete(attemptKey);
|
||||
|
||||
const result: AuthResult = {
|
||||
success: true,
|
||||
user,
|
||||
token
|
||||
};
|
||||
|
||||
console.log(`User logged in: ${username} (${user.id}) from ${clientIp}`);
|
||||
this.emit('login-success', user, token, client.id);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
const result: AuthResult = {
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'INTERNAL_ERROR'
|
||||
};
|
||||
this.emit('auth-error', error as Error, client.id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注销
|
||||
*/
|
||||
async logout(tokenValue: string, client: ClientConnection): Promise<boolean> {
|
||||
try {
|
||||
const token = this.findTokenByValue(tokenValue);
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 撤销令牌
|
||||
token.isRevoked = true;
|
||||
|
||||
console.log(`User logged out: ${token.userId} from ${client.remoteAddress}`);
|
||||
this.emit('logout', token.userId, client.id);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.emit('auth-error', error as Error, client.id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌
|
||||
*/
|
||||
async validateToken(tokenValue: string): Promise<AuthResult> {
|
||||
try {
|
||||
const token = this.findTokenByValue(tokenValue);
|
||||
|
||||
if (!token || token.isRevoked) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid token',
|
||||
errorCode: 'INVALID_TOKEN'
|
||||
};
|
||||
}
|
||||
|
||||
if (token.expiresAt < new Date()) {
|
||||
token.isRevoked = true;
|
||||
this.emit('token-expired', token.userId, token.id);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Token expired',
|
||||
errorCode: 'TOKEN_EXPIRED'
|
||||
};
|
||||
}
|
||||
|
||||
const user = this.users.get(token.userId);
|
||||
if (!user || !user.isActive) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found or inactive',
|
||||
errorCode: 'USER_NOT_FOUND'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user,
|
||||
token
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.emit('auth-error', error as Error);
|
||||
return {
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'INTERNAL_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
async refreshToken(tokenValue: string): Promise<AuthResult> {
|
||||
try {
|
||||
const validationResult = await this.validateToken(tokenValue);
|
||||
if (!validationResult.success || !validationResult.user || !validationResult.token) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const token = validationResult.token;
|
||||
const timeUntilExpiration = token.expiresAt.getTime() - Date.now();
|
||||
|
||||
// 检查是否需要刷新
|
||||
if (timeUntilExpiration > this.config.tokenRefreshThreshold!) {
|
||||
return validationResult; // 不需要刷新
|
||||
}
|
||||
|
||||
// 创建新令牌
|
||||
const newToken = this.createToken(token.userId, token.metadata);
|
||||
|
||||
// 撤销旧令牌
|
||||
token.isRevoked = true;
|
||||
|
||||
console.log(`Token refreshed for user: ${token.userId}`);
|
||||
this.emit('token-refreshed', token.userId, token.id, newToken.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: validationResult.user,
|
||||
token: newToken
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.emit('auth-error', error as Error);
|
||||
return {
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'INTERNAL_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
getUserById(userId: string): UserInfo | undefined {
|
||||
return this.users.get(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息(通过用户名)
|
||||
*/
|
||||
getUserByUsername(username: string): UserInfo | undefined {
|
||||
return this.findUserByUsername(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
async updateUser(userId: string, updates: Partial<UserInfo>): Promise<boolean> {
|
||||
const user = this.users.get(userId);
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 不允许更新某些字段
|
||||
const { id, createdAt, ...allowedUpdates } = updates as any;
|
||||
Object.assign(user, allowedUpdates);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销所有用户令牌
|
||||
*/
|
||||
async revokeAllUserTokens(userId: string): Promise<number> {
|
||||
let revokedCount = 0;
|
||||
|
||||
for (const token of this.tokens.values()) {
|
||||
if (token.userId === userId && !token.isRevoked) {
|
||||
token.isRevoked = true;
|
||||
revokedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return revokedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃令牌数量
|
||||
*/
|
||||
getActiveTokenCount(): number {
|
||||
return Array.from(this.tokens.values())
|
||||
.filter(token => !token.isRevoked && token.expiresAt > new Date()).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期令牌和登录尝试记录
|
||||
*/
|
||||
cleanup(): void {
|
||||
const now = new Date();
|
||||
let cleanedTokens = 0;
|
||||
let cleanedAttempts = 0;
|
||||
|
||||
// 清理过期令牌
|
||||
for (const [tokenId, token] of this.tokens.entries()) {
|
||||
if (token.expiresAt < now || token.isRevoked) {
|
||||
this.tokens.delete(tokenId);
|
||||
cleanedTokens++;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过期的登录尝试记录
|
||||
const resetTime = this.config.loginAttemptResetTime!;
|
||||
for (const [attemptKey, attempt] of this.loginAttempts.entries()) {
|
||||
if (now.getTime() - attempt.lastAttempt.getTime() > resetTime) {
|
||||
this.loginAttempts.delete(attemptKey);
|
||||
cleanedAttempts++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedTokens > 0 || cleanedAttempts > 0) {
|
||||
console.log(`Auth cleanup: ${cleanedTokens} tokens, ${cleanedAttempts} login attempts`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁认证管理器
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
this.users.clear();
|
||||
this.tokens.clear();
|
||||
this.loginAttempts.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器(每小时清理一次)
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanup();
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户(通过用户名)
|
||||
*/
|
||||
private findUserByUsername(username: string): UserInfo | undefined {
|
||||
return Array.from(this.users.values())
|
||||
.find(user => user.username === username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找令牌(通过令牌值)
|
||||
*/
|
||||
private findTokenByValue(tokenValue: string): AuthToken | undefined {
|
||||
return Array.from(this.tokens.values())
|
||||
.find(token => token.token === tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 哈希密码
|
||||
*/
|
||||
private hashPassword(password: string): string {
|
||||
return createHash(this.config.passwordHashAlgorithm!)
|
||||
.update(password)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建认证令牌
|
||||
*/
|
||||
private createToken(userId: string, metadata: Record<string, NetworkValue> = {}): AuthToken {
|
||||
const tokenId = this.generateId();
|
||||
const tokenValue = randomBytes(32).toString('hex');
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + this.config.tokenExpirationTime!);
|
||||
|
||||
const token: AuthToken = {
|
||||
id: tokenId,
|
||||
userId,
|
||||
token: tokenValue,
|
||||
createdAt: now,
|
||||
expiresAt,
|
||||
isRevoked: false,
|
||||
metadata
|
||||
};
|
||||
|
||||
this.tokens.set(tokenId, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查登录是否被阻止
|
||||
*/
|
||||
private isLoginBlocked(attemptKey: string): boolean {
|
||||
const attempt = this.loginAttempts.get(attemptKey);
|
||||
if (!attempt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const resetTime = this.config.loginAttemptResetTime!;
|
||||
|
||||
// 检查重置时间
|
||||
if (now.getTime() - attempt.lastAttempt.getTime() > resetTime) {
|
||||
this.loginAttempts.delete(attemptKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
return attempt.attempts >= this.config.maxLoginAttempts!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录尝试
|
||||
*/
|
||||
private recordLoginAttempt(attemptKey: string): void {
|
||||
const now = new Date();
|
||||
const [ip, username] = attemptKey.split('-', 2);
|
||||
|
||||
const existingAttempt = this.loginAttempts.get(attemptKey);
|
||||
if (existingAttempt) {
|
||||
// 检查是否需要重置
|
||||
if (now.getTime() - existingAttempt.lastAttempt.getTime() > this.config.loginAttemptResetTime!) {
|
||||
existingAttempt.attempts = 1;
|
||||
} else {
|
||||
existingAttempt.attempts++;
|
||||
}
|
||||
existingAttempt.lastAttempt = now;
|
||||
} else {
|
||||
this.loginAttempts.set(attemptKey, {
|
||||
ip,
|
||||
username,
|
||||
attempts: 1,
|
||||
lastAttempt: now
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof AuthManagerEvents>(event: K, listener: AuthManagerEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof AuthManagerEvents>(event: K, ...args: Parameters<AuthManagerEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,684 +0,0 @@
|
||||
/**
|
||||
* 权限管理器
|
||||
*
|
||||
* 处理用户权限、角色管理、访问控制等功能
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { UserInfo } from './AuthenticationManager';
|
||||
|
||||
/**
|
||||
* 权限类型
|
||||
*/
|
||||
export type Permission = string;
|
||||
|
||||
/**
|
||||
* 角色定义
|
||||
*/
|
||||
export interface Role {
|
||||
/** 角色ID */
|
||||
id: string;
|
||||
/** 角色名称 */
|
||||
name: string;
|
||||
/** 角色描述 */
|
||||
description?: string;
|
||||
/** 权限列表 */
|
||||
permissions: Permission[];
|
||||
/** 父角色ID */
|
||||
parentRoleId?: string;
|
||||
/** 是否系统角色 */
|
||||
isSystemRole: boolean;
|
||||
/** 角色元数据 */
|
||||
metadata: Record<string, NetworkValue>;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查上下文
|
||||
*/
|
||||
export interface PermissionContext {
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 用户角色 */
|
||||
userRoles: string[];
|
||||
/** 请求的权限 */
|
||||
permission: Permission;
|
||||
/** 资源ID(可选) */
|
||||
resourceId?: string;
|
||||
/** 附加上下文数据 */
|
||||
context?: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查结果
|
||||
*/
|
||||
export interface PermissionResult {
|
||||
/** 是否允许 */
|
||||
granted: boolean;
|
||||
/** 原因 */
|
||||
reason?: string;
|
||||
/** 匹配的角色 */
|
||||
matchingRole?: string;
|
||||
/** 使用的权限 */
|
||||
usedPermission?: Permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限管理器配置
|
||||
*/
|
||||
export interface AuthorizationConfig {
|
||||
/** 是否启用权限继承 */
|
||||
enableInheritance?: boolean;
|
||||
/** 是否启用权限缓存 */
|
||||
enableCache?: boolean;
|
||||
/** 缓存过期时间(毫秒) */
|
||||
cacheExpirationTime?: number;
|
||||
/** 默认权限策略 */
|
||||
defaultPolicy?: 'deny' | 'allow';
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限管理器事件
|
||||
*/
|
||||
export interface AuthorizationEvents {
|
||||
/** 权限被授予 */
|
||||
'permission-granted': (context: PermissionContext, result: PermissionResult) => void;
|
||||
/** 权限被拒绝 */
|
||||
'permission-denied': (context: PermissionContext, result: PermissionResult) => void;
|
||||
/** 角色创建 */
|
||||
'role-created': (role: Role) => void;
|
||||
/** 角色更新 */
|
||||
'role-updated': (roleId: string, updates: Partial<Role>) => void;
|
||||
/** 角色删除 */
|
||||
'role-deleted': (roleId: string) => void;
|
||||
/** 权限错误 */
|
||||
'authorization-error': (error: Error, context?: PermissionContext) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限缓存项
|
||||
*/
|
||||
interface CacheItem {
|
||||
result: PermissionResult;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义权限
|
||||
*/
|
||||
export const Permissions = {
|
||||
// 系统权限
|
||||
SYSTEM_ADMIN: 'system:admin',
|
||||
SYSTEM_CONFIG: 'system:config',
|
||||
|
||||
// 用户管理权限
|
||||
USER_CREATE: 'user:create',
|
||||
USER_READ: 'user:read',
|
||||
USER_UPDATE: 'user:update',
|
||||
USER_DELETE: 'user:delete',
|
||||
USER_MANAGE_ROLES: 'user:manage-roles',
|
||||
|
||||
// 房间权限
|
||||
ROOM_CREATE: 'room:create',
|
||||
ROOM_JOIN: 'room:join',
|
||||
ROOM_LEAVE: 'room:leave',
|
||||
ROOM_MANAGE: 'room:manage',
|
||||
ROOM_KICK_PLAYERS: 'room:kick-players',
|
||||
|
||||
// 网络权限
|
||||
NETWORK_SEND_RPC: 'network:send-rpc',
|
||||
NETWORK_SYNC_VARS: 'network:sync-vars',
|
||||
NETWORK_BROADCAST: 'network:broadcast',
|
||||
|
||||
// 聊天权限
|
||||
CHAT_SEND: 'chat:send',
|
||||
CHAT_MODERATE: 'chat:moderate',
|
||||
CHAT_PRIVATE: 'chat:private',
|
||||
|
||||
// 文件权限
|
||||
FILE_UPLOAD: 'file:upload',
|
||||
FILE_DOWNLOAD: 'file:download',
|
||||
FILE_DELETE: 'file:delete'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 预定义角色
|
||||
*/
|
||||
export const SystemRoles = {
|
||||
ADMIN: 'admin',
|
||||
MODERATOR: 'moderator',
|
||||
USER: 'user',
|
||||
GUEST: 'guest'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 权限管理器
|
||||
*/
|
||||
export class AuthorizationManager extends EventEmitter {
|
||||
private config: AuthorizationConfig;
|
||||
private roles = new Map<string, Role>();
|
||||
private permissionCache = new Map<string, CacheItem>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: AuthorizationConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
enableInheritance: true,
|
||||
enableCache: true,
|
||||
cacheExpirationTime: 5 * 60 * 1000, // 5分钟
|
||||
defaultPolicy: 'deny',
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
*/
|
||||
async createRole(roleData: {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: Permission[];
|
||||
parentRoleId?: string;
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
}): Promise<Role> {
|
||||
const { id, name, description, permissions, parentRoleId, metadata = {} } = roleData;
|
||||
|
||||
if (this.roles.has(id)) {
|
||||
throw new Error(`Role with id "${id}" already exists`);
|
||||
}
|
||||
|
||||
// 验证父角色是否存在
|
||||
if (parentRoleId && !this.roles.has(parentRoleId)) {
|
||||
throw new Error(`Parent role "${parentRoleId}" not found`);
|
||||
}
|
||||
|
||||
const role: Role = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
permissions: [...permissions],
|
||||
parentRoleId,
|
||||
isSystemRole: false,
|
||||
metadata,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
this.roles.set(id, role);
|
||||
this.clearPermissionCache(); // 清除缓存
|
||||
|
||||
console.log(`Role created: ${name} (${id})`);
|
||||
this.emit('role-created', role);
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色
|
||||
*/
|
||||
getRole(roleId: string): Role | undefined {
|
||||
return this.roles.get(roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有角色
|
||||
*/
|
||||
getAllRoles(): Role[] {
|
||||
return Array.from(this.roles.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
async updateRole(roleId: string, updates: Partial<Role>): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 系统角色不允许修改某些字段
|
||||
if (role.isSystemRole) {
|
||||
const { permissions, parentRoleId, ...allowedUpdates } = updates;
|
||||
Object.assign(role, allowedUpdates);
|
||||
} else {
|
||||
// 不允许更新某些字段
|
||||
const { id, createdAt, isSystemRole, ...allowedUpdates } = updates as any;
|
||||
Object.assign(role, allowedUpdates);
|
||||
}
|
||||
|
||||
this.clearPermissionCache(); // 清除缓存
|
||||
|
||||
console.log(`Role updated: ${role.name} (${roleId})`);
|
||||
this.emit('role-updated', roleId, updates);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
async deleteRole(roleId: string): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role.isSystemRole) {
|
||||
throw new Error('Cannot delete system role');
|
||||
}
|
||||
|
||||
// 检查是否有子角色依赖此角色
|
||||
const childRoles = Array.from(this.roles.values())
|
||||
.filter(r => r.parentRoleId === roleId);
|
||||
|
||||
if (childRoles.length > 0) {
|
||||
throw new Error(`Cannot delete role "${roleId}": ${childRoles.length} child roles depend on it`);
|
||||
}
|
||||
|
||||
this.roles.delete(roleId);
|
||||
this.clearPermissionCache(); // 清除缓存
|
||||
|
||||
console.log(`Role deleted: ${role.name} (${roleId})`);
|
||||
this.emit('role-deleted', roleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限
|
||||
*/
|
||||
async checkPermission(context: PermissionContext): Promise<PermissionResult> {
|
||||
try {
|
||||
// 检查缓存
|
||||
const cacheKey = this.getCacheKey(context);
|
||||
if (this.config.enableCache) {
|
||||
const cached = this.permissionCache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > new Date()) {
|
||||
return cached.result;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.performPermissionCheck(context);
|
||||
|
||||
// 缓存结果
|
||||
if (this.config.enableCache) {
|
||||
const expiresAt = new Date(Date.now() + this.config.cacheExpirationTime!);
|
||||
this.permissionCache.set(cacheKey, { result, expiresAt });
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
if (result.granted) {
|
||||
this.emit('permission-granted', context, result);
|
||||
} else {
|
||||
this.emit('permission-denied', context, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.emit('authorization-error', error as Error, context);
|
||||
|
||||
return {
|
||||
granted: this.config.defaultPolicy === 'allow',
|
||||
reason: `Authorization error: ${(error as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限
|
||||
*/
|
||||
async hasPermission(user: UserInfo, permission: Permission, resourceId?: string): Promise<boolean> {
|
||||
const context: PermissionContext = {
|
||||
userId: user.id,
|
||||
userRoles: user.roles,
|
||||
permission,
|
||||
resourceId
|
||||
};
|
||||
|
||||
const result = await this.checkPermission(context);
|
||||
return result.granted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有权限
|
||||
*/
|
||||
async getUserPermissions(user: UserInfo): Promise<Permission[]> {
|
||||
const permissions = new Set<Permission>();
|
||||
|
||||
for (const roleId of user.roles) {
|
||||
const rolePermissions = await this.getRolePermissions(roleId);
|
||||
rolePermissions.forEach(p => permissions.add(p));
|
||||
}
|
||||
|
||||
return Array.from(permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色的所有权限(包括继承的权限)
|
||||
*/
|
||||
async getRolePermissions(roleId: string): Promise<Permission[]> {
|
||||
const permissions = new Set<Permission>();
|
||||
const visited = new Set<string>();
|
||||
|
||||
const collectPermissions = (currentRoleId: string) => {
|
||||
if (visited.has(currentRoleId)) {
|
||||
return; // 防止循环引用
|
||||
}
|
||||
visited.add(currentRoleId);
|
||||
|
||||
const role = this.roles.get(currentRoleId);
|
||||
if (!role) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加当前角色的权限
|
||||
role.permissions.forEach(p => permissions.add(p));
|
||||
|
||||
// 递归添加父角色的权限
|
||||
if (this.config.enableInheritance && role.parentRoleId) {
|
||||
collectPermissions(role.parentRoleId);
|
||||
}
|
||||
};
|
||||
|
||||
collectPermissions(roleId);
|
||||
return Array.from(permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为角色添加权限
|
||||
*/
|
||||
async addPermissionToRole(roleId: string, permission: Permission): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!role.permissions.includes(permission)) {
|
||||
role.permissions.push(permission);
|
||||
this.clearPermissionCache();
|
||||
console.log(`Permission "${permission}" added to role "${roleId}"`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从角色移除权限
|
||||
*/
|
||||
async removePermissionFromRole(roleId: string, permission: Permission): Promise<boolean> {
|
||||
const role = this.roles.get(roleId);
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const index = role.permissions.indexOf(permission);
|
||||
if (index !== -1) {
|
||||
role.permissions.splice(index, 1);
|
||||
this.clearPermissionCache();
|
||||
console.log(`Permission "${permission}" removed from role "${roleId}"`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定角色
|
||||
*/
|
||||
hasRole(user: UserInfo, roleId: string): boolean {
|
||||
return user.roles.includes(roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户添加角色
|
||||
*/
|
||||
async addRoleToUser(user: UserInfo, roleId: string): Promise<boolean> {
|
||||
if (!this.roles.has(roleId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.roles.includes(roleId)) {
|
||||
user.roles.push(roleId);
|
||||
this.clearUserPermissionCache(user.id);
|
||||
console.log(`Role "${roleId}" added to user "${user.id}"`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户移除角色
|
||||
*/
|
||||
async removeRoleFromUser(user: UserInfo, roleId: string): Promise<boolean> {
|
||||
const index = user.roles.indexOf(roleId);
|
||||
if (index !== -1) {
|
||||
user.roles.splice(index, 1);
|
||||
this.clearUserPermissionCache(user.id);
|
||||
console.log(`Role "${roleId}" removed from user "${user.id}"`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除权限缓存
|
||||
*/
|
||||
clearPermissionCache(): void {
|
||||
this.permissionCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定用户的权限缓存
|
||||
*/
|
||||
clearUserPermissionCache(userId: string): void {
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
for (const [key] of this.permissionCache) {
|
||||
if (key.startsWith(`${userId}:`)) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToDelete.forEach(key => this.permissionCache.delete(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁权限管理器
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
this.roles.clear();
|
||||
this.permissionCache.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 创建系统角色
|
||||
this.createSystemRoles();
|
||||
|
||||
// 启动缓存清理定时器(每30分钟清理一次)
|
||||
if (this.config.enableCache) {
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupCache();
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统角色
|
||||
*/
|
||||
private createSystemRoles(): void {
|
||||
// 管理员角色
|
||||
const adminRole: Role = {
|
||||
id: SystemRoles.ADMIN,
|
||||
name: 'Administrator',
|
||||
description: 'Full system access',
|
||||
permissions: Object.values(Permissions),
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// 版主角色
|
||||
const moderatorRole: Role = {
|
||||
id: SystemRoles.MODERATOR,
|
||||
name: 'Moderator',
|
||||
description: 'Room and user management',
|
||||
permissions: [
|
||||
Permissions.USER_READ,
|
||||
Permissions.ROOM_CREATE,
|
||||
Permissions.ROOM_JOIN,
|
||||
Permissions.ROOM_MANAGE,
|
||||
Permissions.ROOM_KICK_PLAYERS,
|
||||
Permissions.NETWORK_SEND_RPC,
|
||||
Permissions.NETWORK_SYNC_VARS,
|
||||
Permissions.CHAT_SEND,
|
||||
Permissions.CHAT_MODERATE,
|
||||
Permissions.CHAT_PRIVATE
|
||||
],
|
||||
parentRoleId: SystemRoles.USER,
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// 普通用户角色
|
||||
const userRole: Role = {
|
||||
id: SystemRoles.USER,
|
||||
name: 'User',
|
||||
description: 'Basic user permissions',
|
||||
permissions: [
|
||||
Permissions.ROOM_JOIN,
|
||||
Permissions.ROOM_LEAVE,
|
||||
Permissions.NETWORK_SEND_RPC,
|
||||
Permissions.NETWORK_SYNC_VARS,
|
||||
Permissions.CHAT_SEND,
|
||||
Permissions.FILE_DOWNLOAD
|
||||
],
|
||||
parentRoleId: SystemRoles.GUEST,
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// 访客角色
|
||||
const guestRole: Role = {
|
||||
id: SystemRoles.GUEST,
|
||||
name: 'Guest',
|
||||
description: 'Limited access for guests',
|
||||
permissions: [
|
||||
Permissions.ROOM_JOIN
|
||||
],
|
||||
isSystemRole: true,
|
||||
metadata: {},
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
this.roles.set(adminRole.id, adminRole);
|
||||
this.roles.set(moderatorRole.id, moderatorRole);
|
||||
this.roles.set(userRole.id, userRole);
|
||||
this.roles.set(guestRole.id, guestRole);
|
||||
|
||||
console.log('System roles created');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行权限检查
|
||||
*/
|
||||
private async performPermissionCheck(context: PermissionContext): Promise<PermissionResult> {
|
||||
// 获取用户的所有角色权限
|
||||
const userPermissions = new Set<Permission>();
|
||||
|
||||
for (const roleId of context.userRoles) {
|
||||
const rolePermissions = await this.getRolePermissions(roleId);
|
||||
rolePermissions.forEach(p => userPermissions.add(p));
|
||||
}
|
||||
|
||||
// 直接权限匹配
|
||||
if (userPermissions.has(context.permission)) {
|
||||
return {
|
||||
granted: true,
|
||||
reason: 'Direct permission match',
|
||||
usedPermission: context.permission
|
||||
};
|
||||
}
|
||||
|
||||
// 通配符权限匹配
|
||||
const wildcardPermissions = Array.from(userPermissions)
|
||||
.filter(p => p.endsWith('*'));
|
||||
|
||||
for (const wildcardPerm of wildcardPermissions) {
|
||||
const prefix = wildcardPerm.slice(0, -1);
|
||||
if (context.permission.startsWith(prefix)) {
|
||||
return {
|
||||
granted: true,
|
||||
reason: 'Wildcard permission match',
|
||||
usedPermission: wildcardPerm
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配的权限
|
||||
return {
|
||||
granted: this.config.defaultPolicy === 'allow',
|
||||
reason: this.config.defaultPolicy === 'allow'
|
||||
? 'Default allow policy'
|
||||
: 'No matching permissions found'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存键
|
||||
*/
|
||||
private getCacheKey(context: PermissionContext): string {
|
||||
const roleString = context.userRoles.sort().join(',');
|
||||
const resourcePart = context.resourceId ? `:${context.resourceId}` : '';
|
||||
return `${context.userId}:${roleString}:${context.permission}${resourcePart}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
private cleanupCache(): void {
|
||||
const now = new Date();
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [key, item] of this.permissionCache.entries()) {
|
||||
if (item.expiresAt < now) {
|
||||
this.permissionCache.delete(key);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
console.log(`Permission cache cleanup: ${cleanedCount} entries removed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof AuthorizationEvents>(event: K, listener: AuthorizationEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof AuthorizationEvents>(event: K, ...args: Parameters<AuthorizationEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 认证系统导出
|
||||
*/
|
||||
|
||||
export * from './AuthenticationManager';
|
||||
export * from './AuthorizationManager';
|
||||
@@ -1,478 +0,0 @@
|
||||
/**
|
||||
* 客户端连接管理
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { NetworkValue, NetworkMessage } from '@esengine/ecs-framework-network-shared';
|
||||
import { TransportMessage } from './Transport';
|
||||
|
||||
/**
|
||||
* 客户端连接状态
|
||||
*/
|
||||
export enum ClientConnectionState {
|
||||
/** 连接中 */
|
||||
CONNECTING = 'connecting',
|
||||
/** 已连接 */
|
||||
CONNECTED = 'connected',
|
||||
/** 认证中 */
|
||||
AUTHENTICATING = 'authenticating',
|
||||
/** 已认证 */
|
||||
AUTHENTICATED = 'authenticated',
|
||||
/** 断开连接中 */
|
||||
DISCONNECTING = 'disconnecting',
|
||||
/** 已断开 */
|
||||
DISCONNECTED = 'disconnected',
|
||||
/** 错误状态 */
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端权限
|
||||
*/
|
||||
export interface ClientPermissions {
|
||||
/** 是否可以加入房间 */
|
||||
canJoinRooms?: boolean;
|
||||
/** 是否可以创建房间 */
|
||||
canCreateRooms?: boolean;
|
||||
/** 是否可以发送RPC */
|
||||
canSendRpc?: boolean;
|
||||
/** 是否可以同步变量 */
|
||||
canSyncVars?: boolean;
|
||||
/** 自定义权限 */
|
||||
customPermissions?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接事件
|
||||
*/
|
||||
export interface ClientConnectionEvents {
|
||||
/** 状态变化 */
|
||||
'state-changed': (oldState: ClientConnectionState, newState: ClientConnectionState) => void;
|
||||
/** 收到消息 */
|
||||
'message': (message: TransportMessage) => void;
|
||||
/** 连接错误 */
|
||||
'error': (error: Error) => void;
|
||||
/** 连接超时 */
|
||||
'timeout': () => void;
|
||||
/** 身份验证成功 */
|
||||
'authenticated': (userData: Record<string, NetworkValue>) => void;
|
||||
/** 身份验证失败 */
|
||||
'authentication-failed': (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端统计信息
|
||||
*/
|
||||
export interface ClientStats {
|
||||
/** 消息发送数 */
|
||||
messagesSent: number;
|
||||
/** 消息接收数 */
|
||||
messagesReceived: number;
|
||||
/** 字节发送数 */
|
||||
bytesSent: number;
|
||||
/** 字节接收数 */
|
||||
bytesReceived: number;
|
||||
/** 最后活跃时间 */
|
||||
lastActivity: Date;
|
||||
/** 连接时长(毫秒) */
|
||||
connectionDuration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接管理类
|
||||
*/
|
||||
export class ClientConnection extends EventEmitter {
|
||||
/** 连接ID */
|
||||
public readonly id: string;
|
||||
|
||||
/** 客户端IP地址 */
|
||||
public readonly remoteAddress: string;
|
||||
|
||||
/** 连接创建时间 */
|
||||
public readonly connectedAt: Date;
|
||||
|
||||
/** 当前状态 */
|
||||
private _state: ClientConnectionState = ClientConnectionState.CONNECTING;
|
||||
|
||||
/** 用户数据 */
|
||||
private _userData: Record<string, NetworkValue> = {};
|
||||
|
||||
/** 权限信息 */
|
||||
private _permissions: ClientPermissions = {};
|
||||
|
||||
/** 所在房间ID */
|
||||
private _currentRoomId: string | null = null;
|
||||
|
||||
/** 统计信息 */
|
||||
private _stats: ClientStats;
|
||||
|
||||
/** 最后活跃时间 */
|
||||
private _lastActivity: Date;
|
||||
|
||||
/** 超时定时器 */
|
||||
private _timeoutTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** 连接超时时间(毫秒) */
|
||||
private _connectionTimeout: number;
|
||||
|
||||
/** 发送消息回调 */
|
||||
private _sendMessageCallback: (message: TransportMessage) => Promise<boolean>;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
remoteAddress: string,
|
||||
sendMessageCallback: (message: TransportMessage) => Promise<boolean>,
|
||||
options: {
|
||||
connectionTimeout?: number;
|
||||
userData?: Record<string, NetworkValue>;
|
||||
permissions?: ClientPermissions;
|
||||
} = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
this.id = id;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.connectedAt = new Date();
|
||||
this._lastActivity = new Date();
|
||||
this._connectionTimeout = options.connectionTimeout || 60000; // 1分钟
|
||||
this._sendMessageCallback = sendMessageCallback;
|
||||
|
||||
if (options.userData) {
|
||||
this._userData = { ...options.userData };
|
||||
}
|
||||
|
||||
if (options.permissions) {
|
||||
this._permissions = { ...options.permissions };
|
||||
}
|
||||
|
||||
this._stats = {
|
||||
messagesSent: 0,
|
||||
messagesReceived: 0,
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
lastActivity: this._lastActivity,
|
||||
connectionDuration: 0
|
||||
};
|
||||
|
||||
this.setState(ClientConnectionState.CONNECTED);
|
||||
this.startTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
*/
|
||||
get state(): ClientConnectionState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户数据
|
||||
*/
|
||||
get userData(): Readonly<Record<string, NetworkValue>> {
|
||||
return this._userData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限信息
|
||||
*/
|
||||
get permissions(): Readonly<ClientPermissions> {
|
||||
return this._permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前房间ID
|
||||
*/
|
||||
get currentRoomId(): string | null {
|
||||
return this._currentRoomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
get stats(): Readonly<ClientStats> {
|
||||
this._stats.connectionDuration = Date.now() - this.connectedAt.getTime();
|
||||
this._stats.lastActivity = this._lastActivity;
|
||||
return this._stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后活跃时间
|
||||
*/
|
||||
get lastActivity(): Date {
|
||||
return this._lastActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this._state === ClientConnectionState.CONNECTED ||
|
||||
this._state === ClientConnectionState.AUTHENTICATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已认证
|
||||
*/
|
||||
get isAuthenticated(): boolean {
|
||||
return this._state === ClientConnectionState.AUTHENTICATED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(message: TransportMessage): Promise<boolean> {
|
||||
if (!this.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await this._sendMessageCallback(message);
|
||||
if (success) {
|
||||
this._stats.messagesSent++;
|
||||
const messageSize = JSON.stringify(message).length;
|
||||
this._stats.bytesSent += messageSize;
|
||||
this.updateActivity();
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
handleMessage(message: TransportMessage): void {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._stats.messagesReceived++;
|
||||
const messageSize = JSON.stringify(message).length;
|
||||
this._stats.bytesReceived += messageSize;
|
||||
this.updateActivity();
|
||||
|
||||
this.emit('message', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户数据
|
||||
*/
|
||||
setUserData(key: string, value: NetworkValue): void {
|
||||
this._userData[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户数据
|
||||
*/
|
||||
getUserData<T extends NetworkValue = NetworkValue>(key: string): T | undefined {
|
||||
return this._userData[key] as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置用户数据
|
||||
*/
|
||||
setUserDataBatch(data: Record<string, NetworkValue>): void {
|
||||
Object.assign(this._userData, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置权限
|
||||
*/
|
||||
setPermission(permission: keyof ClientPermissions, value: boolean): void {
|
||||
(this._permissions as any)[permission] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限
|
||||
*/
|
||||
hasPermission(permission: keyof ClientPermissions): boolean {
|
||||
return (this._permissions as any)[permission] || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义权限
|
||||
*/
|
||||
setCustomPermission(permission: string, value: boolean): void {
|
||||
if (!this._permissions.customPermissions) {
|
||||
this._permissions.customPermissions = {};
|
||||
}
|
||||
this._permissions.customPermissions[permission] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查自定义权限
|
||||
*/
|
||||
hasCustomPermission(permission: string): boolean {
|
||||
return this._permissions.customPermissions?.[permission] || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行身份认证
|
||||
*/
|
||||
async authenticate(credentials: Record<string, NetworkValue>): Promise<boolean> {
|
||||
if (this._state !== ClientConnectionState.CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState(ClientConnectionState.AUTHENTICATING);
|
||||
|
||||
try {
|
||||
// 这里可以添加实际的认证逻辑
|
||||
// 目前简单地认为所有认证都成功
|
||||
|
||||
this.setUserDataBatch(credentials);
|
||||
this.setState(ClientConnectionState.AUTHENTICATED);
|
||||
this.emit('authenticated', credentials);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.setState(ClientConnectionState.CONNECTED);
|
||||
this.emit('authentication-failed', (error as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间
|
||||
*/
|
||||
joinRoom(roomId: string): void {
|
||||
this._currentRoomId = roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间
|
||||
*/
|
||||
leaveRoom(): void {
|
||||
this._currentRoomId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(reason?: string): void {
|
||||
if (this._state === ClientConnectionState.DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(ClientConnectionState.DISCONNECTING);
|
||||
this.stopTimeout();
|
||||
|
||||
// 发送断开连接消息
|
||||
this.sendMessage({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'disconnect',
|
||||
reason: reason || 'server-disconnect'
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState(ClientConnectionState.DISCONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活跃时间
|
||||
*/
|
||||
updateActivity(): void {
|
||||
this._lastActivity = new Date();
|
||||
this.resetTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接状态
|
||||
*/
|
||||
private setState(newState: ClientConnectionState): void {
|
||||
const oldState = this._state;
|
||||
if (oldState !== newState) {
|
||||
this._state = newState;
|
||||
this.emit('state-changed', oldState, newState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
private handleError(error: Error): void {
|
||||
this.setState(ClientConnectionState.ERROR);
|
||||
this.emit('error', error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动超时检测
|
||||
*/
|
||||
private startTimeout(): void {
|
||||
this.resetTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置超时定时器
|
||||
*/
|
||||
private resetTimeout(): void {
|
||||
this.stopTimeout();
|
||||
|
||||
if (this._connectionTimeout > 0) {
|
||||
this._timeoutTimer = setTimeout(() => {
|
||||
this.handleTimeout();
|
||||
}, this._connectionTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止超时检测
|
||||
*/
|
||||
private stopTimeout(): void {
|
||||
if (this._timeoutTimer) {
|
||||
clearTimeout(this._timeoutTimer);
|
||||
this._timeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理超时
|
||||
*/
|
||||
private handleTimeout(): void {
|
||||
this.emit('timeout');
|
||||
this.disconnect('timeout');
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁连接
|
||||
*/
|
||||
destroy(): void {
|
||||
this.stopTimeout();
|
||||
this.removeAllListeners();
|
||||
this.setState(ClientConnectionState.DISCONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof ClientConnectionEvents>(event: K, listener: ClientConnectionEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof ClientConnectionEvents>(event: K, ...args: Parameters<ClientConnectionEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化连接信息
|
||||
*/
|
||||
toJSON(): object {
|
||||
return {
|
||||
id: this.id,
|
||||
remoteAddress: this.remoteAddress,
|
||||
state: this._state,
|
||||
connectedAt: this.connectedAt.toISOString(),
|
||||
lastActivity: this._lastActivity.toISOString(),
|
||||
currentRoomId: this._currentRoomId,
|
||||
userData: this._userData,
|
||||
permissions: this._permissions,
|
||||
stats: this.stats
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
/**
|
||||
* HTTP 传输层实现
|
||||
*
|
||||
* 用于处理 REST API 请求和长轮询连接
|
||||
*/
|
||||
|
||||
import { createServer, IncomingMessage, ServerResponse, Server as HttpServer } from 'http';
|
||||
import { parse as parseUrl } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Transport, TransportConfig, ClientConnectionInfo, TransportMessage } from './Transport';
|
||||
|
||||
/**
|
||||
* HTTP 传输配置
|
||||
*/
|
||||
export interface HttpTransportConfig extends TransportConfig {
|
||||
/** API 路径前缀 */
|
||||
apiPrefix?: string;
|
||||
/** 最大请求大小(字节) */
|
||||
maxRequestSize?: number;
|
||||
/** 长轮询超时(毫秒) */
|
||||
longPollTimeout?: number;
|
||||
/** 是否启用 CORS */
|
||||
enableCors?: boolean;
|
||||
/** 允许的域名 */
|
||||
corsOrigins?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 请求上下文
|
||||
*/
|
||||
interface HttpRequestContext {
|
||||
/** 请求ID */
|
||||
id: string;
|
||||
/** HTTP 请求 */
|
||||
request: IncomingMessage;
|
||||
/** HTTP 响应 */
|
||||
response: ServerResponse;
|
||||
/** 解析后的URL */
|
||||
parsedUrl: any;
|
||||
/** 请求体数据 */
|
||||
body?: string;
|
||||
/** 查询参数 */
|
||||
query: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 客户端连接信息(用于长轮询)
|
||||
*/
|
||||
interface HttpConnectionInfo extends ClientConnectionInfo {
|
||||
/** 长轮询响应对象 */
|
||||
longPollResponse?: ServerResponse;
|
||||
/** 消息队列 */
|
||||
messageQueue: TransportMessage[];
|
||||
/** 长轮询超时定时器 */
|
||||
longPollTimer?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 传输层实现
|
||||
*/
|
||||
export class HttpTransport extends Transport {
|
||||
private httpServer: HttpServer | null = null;
|
||||
private httpConnections = new Map<string, HttpConnectionInfo>();
|
||||
|
||||
protected override config: HttpTransportConfig;
|
||||
|
||||
constructor(config: HttpTransportConfig) {
|
||||
super(config);
|
||||
this.config = {
|
||||
apiPrefix: '/api',
|
||||
maxRequestSize: 1024 * 1024, // 1MB
|
||||
longPollTimeout: 30000, // 30秒
|
||||
enableCors: true,
|
||||
corsOrigins: ['*'],
|
||||
heartbeatInterval: 60000,
|
||||
connectionTimeout: 120000,
|
||||
maxConnections: 1000,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 HTTP 服务器
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
throw new Error('HTTP transport is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
this.httpServer = createServer((req, res) => {
|
||||
this.handleHttpRequest(req, res);
|
||||
});
|
||||
|
||||
this.httpServer.on('error', (error: Error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.httpServer!.listen(this.config.port, this.config.host, (error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
this.isRunning = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.emit('server-started', this.config);
|
||||
} catch (error) {
|
||||
await this.cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 HTTP 服务器
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
// 断开所有长轮询连接
|
||||
for (const [connectionId] of this.httpConnections) {
|
||||
this.disconnectClient(connectionId, 'server-shutdown');
|
||||
}
|
||||
|
||||
await this.cleanup();
|
||||
this.emit('server-stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
async sendToClient(connectionId: string, message: TransportMessage): Promise<boolean> {
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有长轮询连接,直接发送
|
||||
if (connection.longPollResponse && !connection.longPollResponse.headersSent) {
|
||||
this.sendLongPollResponse(connection, [message]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 否则加入消息队列
|
||||
connection.messageQueue.push(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
let sentCount = 0;
|
||||
|
||||
for (const [connectionId, connection] of this.httpConnections) {
|
||||
if (excludeId && connectionId === excludeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await this.sendToClient(connectionId, message)) {
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端列表
|
||||
*/
|
||||
async sendToClients(connectionIds: string[], message: TransportMessage): Promise<number> {
|
||||
let sentCount = 0;
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
if (await this.sendToClient(connectionId, message)) {
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
async disconnectClient(connectionId: string, reason?: string): Promise<void> {
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (connection) {
|
||||
this.cleanupConnection(connectionId);
|
||||
this.removeConnection(connectionId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 请求
|
||||
*/
|
||||
private async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
try {
|
||||
// 设置 CORS 头
|
||||
if (this.config.enableCors) {
|
||||
this.setCorsHeaders(res);
|
||||
}
|
||||
|
||||
// 处理 OPTIONS 请求
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrl = parseUrl(req.url || '', true);
|
||||
const pathname = parsedUrl.pathname || '';
|
||||
|
||||
// 检查是否为 API 请求
|
||||
if (!pathname.startsWith(this.config.apiPrefix!)) {
|
||||
this.sendErrorResponse(res, 404, 'Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
const context: HttpRequestContext = {
|
||||
id: uuidv4(),
|
||||
request: req,
|
||||
response: res,
|
||||
parsedUrl,
|
||||
query: parsedUrl.query as Record<string, string>,
|
||||
};
|
||||
|
||||
// 读取请求体
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
context.body = await this.readRequestBody(req);
|
||||
}
|
||||
|
||||
// 路由处理
|
||||
const apiPath = pathname.substring(this.config.apiPrefix!.length);
|
||||
await this.routeApiRequest(context, apiPath);
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
this.sendErrorResponse(res, 500, 'Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 路由处理
|
||||
*/
|
||||
private async routeApiRequest(context: HttpRequestContext, apiPath: string): Promise<void> {
|
||||
const { request, response } = context;
|
||||
|
||||
switch (apiPath) {
|
||||
case '/connect':
|
||||
if (request.method === 'POST') {
|
||||
await this.handleConnect(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/disconnect':
|
||||
if (request.method === 'POST') {
|
||||
await this.handleDisconnect(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/poll':
|
||||
if (request.method === 'GET') {
|
||||
await this.handleLongPoll(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/send':
|
||||
if (request.method === 'POST') {
|
||||
await this.handleSendMessage(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
case '/status':
|
||||
if (request.method === 'GET') {
|
||||
await this.handleStatus(context);
|
||||
} else {
|
||||
this.sendErrorResponse(response, 405, 'Method Not Allowed');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this.sendErrorResponse(response, 404, 'API endpoint not found');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接请求
|
||||
*/
|
||||
private async handleConnect(context: HttpRequestContext): Promise<void> {
|
||||
const { request, response } = context;
|
||||
|
||||
try {
|
||||
// 检查连接数限制
|
||||
if (this.config.maxConnections && this.httpConnections.size >= this.config.maxConnections) {
|
||||
this.sendErrorResponse(response, 429, 'Too many connections');
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionId = uuidv4();
|
||||
const remoteAddress = request.socket.remoteAddress || request.headers['x-forwarded-for'] || 'unknown';
|
||||
|
||||
const connectionInfo: HttpConnectionInfo = {
|
||||
id: connectionId,
|
||||
remoteAddress: Array.isArray(remoteAddress) ? remoteAddress[0] : remoteAddress,
|
||||
connectedAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
userData: {},
|
||||
messageQueue: []
|
||||
};
|
||||
|
||||
this.httpConnections.set(connectionId, connectionInfo);
|
||||
this.addConnection(connectionInfo);
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
connectionId,
|
||||
serverTime: Date.now()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
this.sendErrorResponse(response, 500, 'Failed to create connection');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断开连接请求
|
||||
*/
|
||||
private async handleDisconnect(context: HttpRequestContext): Promise<void> {
|
||||
const { response, query } = context;
|
||||
|
||||
const connectionId = query.connectionId;
|
||||
if (!connectionId) {
|
||||
this.sendErrorResponse(response, 400, 'Missing connectionId');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.disconnectClient(connectionId, 'client-disconnect');
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
message: 'Disconnected successfully'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理长轮询请求
|
||||
*/
|
||||
private async handleLongPoll(context: HttpRequestContext): Promise<void> {
|
||||
const { response, query } = context;
|
||||
|
||||
const connectionId = query.connectionId;
|
||||
if (!connectionId) {
|
||||
this.sendErrorResponse(response, 400, 'Missing connectionId');
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
this.sendErrorResponse(response, 404, 'Connection not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateClientActivity(connectionId);
|
||||
|
||||
// 如果有排队的消息,立即返回
|
||||
if (connection.messageQueue.length > 0) {
|
||||
const messages = connection.messageQueue.splice(0);
|
||||
this.sendLongPollResponse(connection, messages);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置长轮询
|
||||
connection.longPollResponse = response;
|
||||
|
||||
// 设置超时
|
||||
connection.longPollTimer = setTimeout(() => {
|
||||
this.sendLongPollResponse(connection, []);
|
||||
}, this.config.longPollTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理发送消息请求
|
||||
*/
|
||||
private async handleSendMessage(context: HttpRequestContext): Promise<void> {
|
||||
const { response, query, body } = context;
|
||||
|
||||
const connectionId = query.connectionId;
|
||||
if (!connectionId) {
|
||||
this.sendErrorResponse(response, 400, 'Missing connectionId');
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (!connection) {
|
||||
this.sendErrorResponse(response, 404, 'Connection not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
this.sendErrorResponse(response, 400, 'Missing message body');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(body) as TransportMessage;
|
||||
message.senderId = connectionId;
|
||||
|
||||
this.handleMessage(connectionId, message);
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
message: 'Message sent successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.sendErrorResponse(response, 400, 'Invalid message format');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态请求
|
||||
*/
|
||||
private async handleStatus(context: HttpRequestContext): Promise<void> {
|
||||
const { response } = context;
|
||||
|
||||
this.sendJsonResponse(response, 200, {
|
||||
success: true,
|
||||
status: 'running',
|
||||
connections: this.httpConnections.size,
|
||||
uptime: process.uptime(),
|
||||
serverTime: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取请求体
|
||||
*/
|
||||
private readRequestBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
let totalSize = 0;
|
||||
|
||||
req.on('data', (chunk: Buffer) => {
|
||||
totalSize += chunk.length;
|
||||
if (totalSize > this.config.maxRequestSize!) {
|
||||
reject(new Error('Request body too large'));
|
||||
return;
|
||||
}
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
resolve(body);
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送长轮询响应
|
||||
*/
|
||||
private sendLongPollResponse(connection: HttpConnectionInfo, messages: TransportMessage[]): void {
|
||||
if (!connection.longPollResponse || connection.longPollResponse.headersSent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清理定时器
|
||||
if (connection.longPollTimer) {
|
||||
clearTimeout(connection.longPollTimer);
|
||||
connection.longPollTimer = undefined;
|
||||
}
|
||||
|
||||
this.sendJsonResponse(connection.longPollResponse, 200, {
|
||||
success: true,
|
||||
messages
|
||||
});
|
||||
|
||||
connection.longPollResponse = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 CORS 头
|
||||
*/
|
||||
private setCorsHeaders(res: ServerResponse): void {
|
||||
const origins = this.config.corsOrigins!;
|
||||
const origin = origins.includes('*') ? '*' : origins[0];
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 JSON 响应
|
||||
*/
|
||||
private sendJsonResponse(res: ServerResponse, statusCode: number, data: any): void {
|
||||
if (res.headersSent) return;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.writeHead(statusCode);
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误响应
|
||||
*/
|
||||
private sendErrorResponse(res: ServerResponse, statusCode: number, message: string): void {
|
||||
if (res.headersSent) return;
|
||||
|
||||
this.sendJsonResponse(res, statusCode, {
|
||||
success: false,
|
||||
error: message,
|
||||
code: statusCode
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接资源
|
||||
*/
|
||||
private cleanupConnection(connectionId: string): void {
|
||||
const connection = this.httpConnections.get(connectionId);
|
||||
if (connection) {
|
||||
if (connection.longPollTimer) {
|
||||
clearTimeout(connection.longPollTimer);
|
||||
}
|
||||
if (connection.longPollResponse && !connection.longPollResponse.headersSent) {
|
||||
this.sendJsonResponse(connection.longPollResponse, 200, {
|
||||
success: true,
|
||||
messages: [],
|
||||
disconnected: true
|
||||
});
|
||||
}
|
||||
this.httpConnections.delete(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有资源
|
||||
*/
|
||||
private async cleanup(): Promise<void> {
|
||||
// 清理所有连接
|
||||
for (const connectionId of this.httpConnections.keys()) {
|
||||
this.cleanupConnection(connectionId);
|
||||
}
|
||||
this.clearConnections();
|
||||
|
||||
// 关闭 HTTP 服务器
|
||||
if (this.httpServer) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.httpServer!.close(() => resolve());
|
||||
});
|
||||
this.httpServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 连接统计信息
|
||||
*/
|
||||
getHttpStats(): {
|
||||
totalConnections: number;
|
||||
activeLongPolls: number;
|
||||
queuedMessages: number;
|
||||
} {
|
||||
let activeLongPolls = 0;
|
||||
let queuedMessages = 0;
|
||||
|
||||
for (const connection of this.httpConnections.values()) {
|
||||
if (connection.longPollResponse && !connection.longPollResponse.headersSent) {
|
||||
activeLongPolls++;
|
||||
}
|
||||
queuedMessages += connection.messageQueue.length;
|
||||
}
|
||||
|
||||
return {
|
||||
totalConnections: this.httpConnections.size,
|
||||
activeLongPolls,
|
||||
queuedMessages
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
/**
|
||||
* 网络服务器主类
|
||||
*
|
||||
* 整合 WebSocket 和 HTTP 传输,提供统一的网络服务接口
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Transport, TransportConfig, TransportMessage } from './Transport';
|
||||
import { WebSocketTransport, WebSocketTransportConfig } from './WebSocketTransport';
|
||||
import { HttpTransport, HttpTransportConfig } from './HttpTransport';
|
||||
import { ClientConnection, ClientConnectionState, ClientPermissions } from './ClientConnection';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 网络服务器配置
|
||||
*/
|
||||
export interface NetworkServerConfig {
|
||||
/** 服务器名称 */
|
||||
name?: string;
|
||||
/** WebSocket 配置 */
|
||||
websocket?: WebSocketTransportConfig;
|
||||
/** HTTP 配置 */
|
||||
http?: HttpTransportConfig;
|
||||
/** 默认客户端权限 */
|
||||
defaultPermissions?: ClientPermissions;
|
||||
/** 最大客户端连接数 */
|
||||
maxConnections?: number;
|
||||
/** 客户端认证超时(毫秒) */
|
||||
authenticationTimeout?: number;
|
||||
/** 是否启用统计 */
|
||||
enableStats?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器统计信息
|
||||
*/
|
||||
export interface ServerStats {
|
||||
/** 总连接数 */
|
||||
totalConnections: number;
|
||||
/** 当前活跃连接数 */
|
||||
activeConnections: number;
|
||||
/** 已认证连接数 */
|
||||
authenticatedConnections: number;
|
||||
/** 消息总数 */
|
||||
totalMessages: number;
|
||||
/** 错误总数 */
|
||||
totalErrors: number;
|
||||
/** 服务器启动时间 */
|
||||
startTime: Date;
|
||||
/** 服务器运行时间(毫秒) */
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络服务器事件
|
||||
*/
|
||||
export interface NetworkServerEvents {
|
||||
/** 服务器启动 */
|
||||
'server-started': () => void;
|
||||
/** 服务器停止 */
|
||||
'server-stopped': () => void;
|
||||
/** 客户端连接 */
|
||||
'client-connected': (client: ClientConnection) => void;
|
||||
/** 客户端断开连接 */
|
||||
'client-disconnected': (clientId: string, reason?: string) => void;
|
||||
/** 客户端认证成功 */
|
||||
'client-authenticated': (client: ClientConnection) => void;
|
||||
/** 收到消息 */
|
||||
'message': (client: ClientConnection, message: TransportMessage) => void;
|
||||
/** 服务器错误 */
|
||||
'error': (error: Error, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络服务器主类
|
||||
*/
|
||||
export class NetworkServer extends EventEmitter {
|
||||
private config: NetworkServerConfig;
|
||||
private wsTransport: WebSocketTransport | null = null;
|
||||
private httpTransport: HttpTransport | null = null;
|
||||
private clients = new Map<string, ClientConnection>();
|
||||
private isRunning = false;
|
||||
private stats: ServerStats;
|
||||
|
||||
constructor(config: NetworkServerConfig) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
name: 'NetworkServer',
|
||||
maxConnections: 1000,
|
||||
authenticationTimeout: 30000, // 30秒
|
||||
enableStats: true,
|
||||
defaultPermissions: {
|
||||
canJoinRooms: true,
|
||||
canCreateRooms: false,
|
||||
canSendRpc: true,
|
||||
canSyncVars: true
|
||||
},
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
totalConnections: 0,
|
||||
activeConnections: 0,
|
||||
authenticatedConnections: 0,
|
||||
totalMessages: 0,
|
||||
totalErrors: 0,
|
||||
startTime: new Date(),
|
||||
uptime: 0
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务器
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
throw new Error('Server is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// 启动 WebSocket 传输
|
||||
if (this.config.websocket && this.wsTransport) {
|
||||
promises.push(this.wsTransport.start());
|
||||
}
|
||||
|
||||
// 启动 HTTP 传输
|
||||
if (this.config.http && this.httpTransport) {
|
||||
promises.push(this.httpTransport.start());
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
throw new Error('No transport configured. Please configure at least one transport (WebSocket or HTTP)');
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.isRunning = true;
|
||||
this.stats.startTime = new Date();
|
||||
|
||||
console.log(`Network Server "${this.config.name}" started successfully`);
|
||||
if (this.config.websocket) {
|
||||
console.log(`- WebSocket: ws://${this.config.websocket.host || 'localhost'}:${this.config.websocket.port}${this.config.websocket.path || '/ws'}`);
|
||||
}
|
||||
if (this.config.http) {
|
||||
console.log(`- HTTP: http://${this.config.http.host || 'localhost'}:${this.config.http.port}${this.config.http.apiPrefix || '/api'}`);
|
||||
}
|
||||
|
||||
this.emit('server-started');
|
||||
|
||||
} catch (error) {
|
||||
await this.stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务器
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
// 断开所有客户端
|
||||
const clients = Array.from(this.clients.values());
|
||||
for (const client of clients) {
|
||||
client.disconnect('server-shutdown');
|
||||
}
|
||||
|
||||
// 停止传输层
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (this.wsTransport) {
|
||||
promises.push(this.wsTransport.stop());
|
||||
}
|
||||
|
||||
if (this.httpTransport) {
|
||||
promises.push(this.httpTransport.stop());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
console.log(`Network Server "${this.config.name}" stopped`);
|
||||
this.emit('server-stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器配置
|
||||
*/
|
||||
getConfig(): Readonly<NetworkServerConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器统计信息
|
||||
*/
|
||||
getStats(): ServerStats {
|
||||
this.stats.uptime = Date.now() - this.stats.startTime.getTime();
|
||||
this.stats.activeConnections = this.clients.size;
|
||||
this.stats.authenticatedConnections = Array.from(this.clients.values())
|
||||
.filter(client => client.isAuthenticated).length;
|
||||
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有客户端连接
|
||||
*/
|
||||
getClients(): ClientConnection[] {
|
||||
return Array.from(this.clients.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定客户端连接
|
||||
*/
|
||||
getClient(clientId: string): ClientConnection | undefined {
|
||||
return this.clients.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否存在
|
||||
*/
|
||||
hasClient(clientId: string): boolean {
|
||||
return this.clients.has(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端数量
|
||||
*/
|
||||
getClientCount(): number {
|
||||
return this.clients.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
async sendToClient(clientId: string, message: TransportMessage): Promise<boolean> {
|
||||
const client = this.clients.get(clientId);
|
||||
if (!client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
const promises = Array.from(this.clients.entries())
|
||||
.filter(([clientId]) => clientId !== excludeId)
|
||||
.map(([, client]) => client.sendMessage(message));
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
return results.filter(result => result.status === 'fulfilled' && result.value).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定房间的所有客户端
|
||||
*/
|
||||
async broadcastToRoom(roomId: string, message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
const roomClients = Array.from(this.clients.values())
|
||||
.filter(client => client.currentRoomId === roomId && client.id !== excludeId);
|
||||
|
||||
const promises = roomClients.map(client => client.sendMessage(message));
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
return results.filter(result => result.status === 'fulfilled' && result.value).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
async disconnectClient(clientId: string, reason?: string): Promise<void> {
|
||||
const client = this.clients.get(clientId);
|
||||
if (client) {
|
||||
client.disconnect(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在指定房间的客户端列表
|
||||
*/
|
||||
getClientsInRoom(roomId: string): ClientConnection[] {
|
||||
return Array.from(this.clients.values())
|
||||
.filter(client => client.currentRoomId === roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务器是否正在运行
|
||||
*/
|
||||
isServerRunning(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务器
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 初始化 WebSocket 传输
|
||||
if (this.config.websocket) {
|
||||
this.wsTransport = new WebSocketTransport(this.config.websocket);
|
||||
this.setupTransportEvents(this.wsTransport);
|
||||
}
|
||||
|
||||
// 初始化 HTTP 传输
|
||||
if (this.config.http) {
|
||||
this.httpTransport = new HttpTransport(this.config.http);
|
||||
this.setupTransportEvents(this.httpTransport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置传输层事件监听
|
||||
*/
|
||||
private setupTransportEvents(transport: Transport): void {
|
||||
transport.on('client-connected', (connectionInfo) => {
|
||||
this.handleClientConnected(connectionInfo.id, connectionInfo.remoteAddress || 'unknown', transport);
|
||||
});
|
||||
|
||||
transport.on('client-disconnected', (connectionId, reason) => {
|
||||
this.handleClientDisconnected(connectionId, reason);
|
||||
});
|
||||
|
||||
transport.on('message', (connectionId, message) => {
|
||||
this.handleTransportMessage(connectionId, message);
|
||||
});
|
||||
|
||||
transport.on('error', (error, connectionId) => {
|
||||
this.handleTransportError(error, connectionId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端连接
|
||||
*/
|
||||
private handleClientConnected(connectionId: string, remoteAddress: string, transport: Transport): void {
|
||||
// 检查连接数限制
|
||||
if (this.config.maxConnections && this.clients.size >= this.config.maxConnections) {
|
||||
transport.disconnectClient(connectionId, 'Max connections reached');
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new ClientConnection(
|
||||
connectionId,
|
||||
remoteAddress,
|
||||
(message) => transport.sendToClient(connectionId, message),
|
||||
{
|
||||
connectionTimeout: this.config.authenticationTimeout,
|
||||
permissions: this.config.defaultPermissions
|
||||
}
|
||||
);
|
||||
|
||||
// 设置客户端事件监听
|
||||
this.setupClientEvents(client);
|
||||
|
||||
this.clients.set(connectionId, client);
|
||||
this.stats.totalConnections++;
|
||||
|
||||
console.log(`Client connected: ${connectionId} from ${remoteAddress}`);
|
||||
this.emit('client-connected', client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接
|
||||
*/
|
||||
private handleClientDisconnected(connectionId: string, reason?: string): void {
|
||||
const client = this.clients.get(connectionId);
|
||||
if (client) {
|
||||
client.destroy();
|
||||
this.clients.delete(connectionId);
|
||||
|
||||
console.log(`Client disconnected: ${connectionId}, reason: ${reason || 'unknown'}`);
|
||||
this.emit('client-disconnected', connectionId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传输层消息
|
||||
*/
|
||||
private handleTransportMessage(connectionId: string, message: TransportMessage): void {
|
||||
const client = this.clients.get(connectionId);
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.handleMessage(message);
|
||||
this.stats.totalMessages++;
|
||||
|
||||
this.emit('message', client, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传输层错误
|
||||
*/
|
||||
private handleTransportError(error: Error, connectionId?: string): void {
|
||||
this.stats.totalErrors++;
|
||||
|
||||
console.error(`Transport error${connectionId ? ` (client: ${connectionId})` : ''}:`, error.message);
|
||||
this.emit('error', error, connectionId);
|
||||
|
||||
// 如果是特定客户端的错误,断开该客户端
|
||||
if (connectionId) {
|
||||
this.disconnectClient(connectionId, 'transport-error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置客户端事件监听
|
||||
*/
|
||||
private setupClientEvents(client: ClientConnection): void {
|
||||
client.on('authenticated', (userData) => {
|
||||
console.log(`Client authenticated: ${client.id}`, userData);
|
||||
this.emit('client-authenticated', client);
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
console.error(`Client error (${client.id}):`, error.message);
|
||||
this.emit('error', error, client.id);
|
||||
});
|
||||
|
||||
client.on('timeout', () => {
|
||||
console.log(`Client timeout: ${client.id}`);
|
||||
this.disconnectClient(client.id, 'timeout');
|
||||
});
|
||||
|
||||
client.on('state-changed', (oldState, newState) => {
|
||||
console.log(`Client ${client.id} state changed: ${oldState} -> ${newState}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof NetworkServerEvents>(event: K, listener: NetworkServerEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof NetworkServerEvents>(event: K, ...args: Parameters<NetworkServerEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
/**
|
||||
* 网络传输层抽象接口
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { NetworkMessage, NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 传输层配置
|
||||
*/
|
||||
export interface TransportConfig {
|
||||
/** 服务器端口 */
|
||||
port: number;
|
||||
/** 主机地址 */
|
||||
host?: string;
|
||||
/** 最大连接数 */
|
||||
maxConnections?: number;
|
||||
/** 心跳间隔(毫秒) */
|
||||
heartbeatInterval?: number;
|
||||
/** 连接超时(毫秒) */
|
||||
connectionTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接信息
|
||||
*/
|
||||
export interface ClientConnectionInfo {
|
||||
/** 连接ID */
|
||||
id: string;
|
||||
/** 客户端IP */
|
||||
remoteAddress?: string;
|
||||
/** 连接时间 */
|
||||
connectedAt: Date;
|
||||
/** 最后活跃时间 */
|
||||
lastActivity: Date;
|
||||
/** 用户数据 */
|
||||
userData?: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络消息包装
|
||||
*/
|
||||
export interface TransportMessage {
|
||||
/** 消息类型 */
|
||||
type: 'rpc' | 'syncvar' | 'system' | 'custom';
|
||||
/** 消息数据 */
|
||||
data: NetworkValue;
|
||||
/** 发送者ID */
|
||||
senderId?: string;
|
||||
/** 目标客户端ID(可选,用于单播) */
|
||||
targetId?: string;
|
||||
/** 是否可靠传输 */
|
||||
reliable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络传输层事件
|
||||
*/
|
||||
export interface TransportEvents {
|
||||
/** 客户端连接 */
|
||||
'client-connected': (connectionInfo: ClientConnectionInfo) => void;
|
||||
/** 客户端断开连接 */
|
||||
'client-disconnected': (connectionId: string, reason?: string) => void;
|
||||
/** 收到消息 */
|
||||
'message': (connectionId: string, message: TransportMessage) => void;
|
||||
/** 传输错误 */
|
||||
'error': (error: Error, connectionId?: string) => void;
|
||||
/** 服务器启动 */
|
||||
'server-started': (config: TransportConfig) => void;
|
||||
/** 服务器关闭 */
|
||||
'server-stopped': () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络传输层抽象类
|
||||
*/
|
||||
export abstract class Transport extends EventEmitter {
|
||||
protected config: TransportConfig;
|
||||
protected isRunning = false;
|
||||
protected connections = new Map<string, ClientConnectionInfo>();
|
||||
|
||||
constructor(config: TransportConfig) {
|
||||
super();
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动传输层服务
|
||||
*/
|
||||
abstract start(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 停止传输层服务
|
||||
*/
|
||||
abstract stop(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
abstract sendToClient(connectionId: string, message: TransportMessage): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
abstract broadcast(message: TransportMessage, excludeId?: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 广播消息给指定客户端列表
|
||||
*/
|
||||
abstract sendToClients(connectionIds: string[], message: TransportMessage): Promise<number>;
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
abstract disconnectClient(connectionId: string, reason?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取在线客户端数量
|
||||
*/
|
||||
getConnectionCount(): number {
|
||||
return this.connections.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有连接信息
|
||||
*/
|
||||
getConnections(): ClientConnectionInfo[] {
|
||||
return Array.from(this.connections.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定连接信息
|
||||
*/
|
||||
getConnection(connectionId: string): ClientConnectionInfo | undefined {
|
||||
return this.connections.get(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接是否存在
|
||||
*/
|
||||
hasConnection(connectionId: string): boolean {
|
||||
return this.connections.has(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器是否正在运行
|
||||
*/
|
||||
isServerRunning(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传输层配置
|
||||
*/
|
||||
getConfig(): TransportConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户端最后活跃时间
|
||||
*/
|
||||
protected updateClientActivity(connectionId: string): void {
|
||||
const connection = this.connections.get(connectionId);
|
||||
if (connection) {
|
||||
connection.lastActivity = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加客户端连接
|
||||
*/
|
||||
protected addConnection(connectionInfo: ClientConnectionInfo): void {
|
||||
this.connections.set(connectionInfo.id, connectionInfo);
|
||||
this.emit('client-connected', connectionInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除客户端连接
|
||||
*/
|
||||
protected removeConnection(connectionId: string, reason?: string): void {
|
||||
if (this.connections.delete(connectionId)) {
|
||||
this.emit('client-disconnected', connectionId, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*/
|
||||
protected handleMessage(connectionId: string, message: TransportMessage): void {
|
||||
this.updateClientActivity(connectionId);
|
||||
this.emit('message', connectionId, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传输错误
|
||||
*/
|
||||
protected handleError(error: Error, connectionId?: string): void {
|
||||
this.emit('error', error, connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有连接
|
||||
*/
|
||||
protected clearConnections(): void {
|
||||
const connectionIds = Array.from(this.connections.keys());
|
||||
for (const id of connectionIds) {
|
||||
this.removeConnection(id, 'server-shutdown');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof TransportEvents>(event: K, listener: TransportEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof TransportEvents>(event: K, ...args: Parameters<TransportEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
/**
|
||||
* WebSocket 传输层实现
|
||||
*/
|
||||
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { createServer, Server as HttpServer } from 'http';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Transport, TransportConfig, ClientConnectionInfo, TransportMessage } from './Transport';
|
||||
|
||||
/**
|
||||
* WebSocket 传输配置
|
||||
*/
|
||||
export interface WebSocketTransportConfig extends TransportConfig {
|
||||
/** WebSocket 路径 */
|
||||
path?: string;
|
||||
/** 是否启用压缩 */
|
||||
compression?: boolean;
|
||||
/** 最大消息大小(字节) */
|
||||
maxMessageSize?: number;
|
||||
/** ping 间隔(毫秒) */
|
||||
pingInterval?: number;
|
||||
/** pong 超时(毫秒) */
|
||||
pongTimeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 客户端连接扩展信息
|
||||
*/
|
||||
interface WebSocketConnectionInfo extends ClientConnectionInfo {
|
||||
/** WebSocket 实例 */
|
||||
socket: WebSocket;
|
||||
/** ping 定时器 */
|
||||
pingTimer?: NodeJS.Timeout;
|
||||
/** pong 超时定时器 */
|
||||
pongTimer?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 传输层实现
|
||||
*/
|
||||
export class WebSocketTransport extends Transport {
|
||||
private httpServer: HttpServer | null = null;
|
||||
private wsServer: WebSocketServer | null = null;
|
||||
private wsConnections = new Map<string, WebSocketConnectionInfo>();
|
||||
|
||||
protected override config: WebSocketTransportConfig;
|
||||
|
||||
constructor(config: WebSocketTransportConfig) {
|
||||
super(config);
|
||||
this.config = {
|
||||
path: '/ws',
|
||||
compression: true,
|
||||
maxMessageSize: 1024 * 1024, // 1MB
|
||||
pingInterval: 30000, // 30秒
|
||||
pongTimeout: 5000, // 5秒
|
||||
heartbeatInterval: 30000,
|
||||
connectionTimeout: 60000,
|
||||
maxConnections: 1000,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 WebSocket 服务器
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
throw new Error('WebSocket transport is already running');
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建 HTTP 服务器
|
||||
this.httpServer = createServer();
|
||||
|
||||
// 创建 WebSocket 服务器
|
||||
this.wsServer = new WebSocketServer({
|
||||
server: this.httpServer,
|
||||
path: this.config.path,
|
||||
maxPayload: this.config.maxMessageSize,
|
||||
perMessageDeflate: this.config.compression
|
||||
});
|
||||
|
||||
// 设置事件监听
|
||||
this.setupEventListeners();
|
||||
|
||||
// 启动服务器
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.httpServer!.listen(this.config.port, this.config.host, (error?: Error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
this.isRunning = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.emit('server-started', this.config);
|
||||
} catch (error) {
|
||||
await this.cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 WebSocket 服务器
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
// 断开所有客户端连接
|
||||
for (const [connectionId, connection] of this.wsConnections) {
|
||||
this.disconnectClient(connectionId, 'server-shutdown');
|
||||
}
|
||||
|
||||
await this.cleanup();
|
||||
this.emit('server-stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端
|
||||
*/
|
||||
async sendToClient(connectionId: string, message: TransportMessage): Promise<boolean> {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (!connection || connection.socket.readyState !== WebSocket.OPEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.stringify(message);
|
||||
connection.socket.send(data);
|
||||
this.updateClientActivity(connectionId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error, connectionId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有客户端
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeId?: string): Promise<number> {
|
||||
const data = JSON.stringify(message);
|
||||
let sentCount = 0;
|
||||
|
||||
for (const [connectionId, connection] of this.wsConnections) {
|
||||
if (excludeId && connectionId === excludeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (connection.socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
connection.socket.send(data);
|
||||
sentCount++;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error, connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定客户端列表
|
||||
*/
|
||||
async sendToClients(connectionIds: string[], message: TransportMessage): Promise<number> {
|
||||
const data = JSON.stringify(message);
|
||||
let sentCount = 0;
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection && connection.socket.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
connection.socket.send(data);
|
||||
sentCount++;
|
||||
} catch (error) {
|
||||
this.handleError(error as Error, connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定客户端连接
|
||||
*/
|
||||
async disconnectClient(connectionId: string, reason?: string): Promise<void> {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection) {
|
||||
this.cleanupConnection(connectionId);
|
||||
connection.socket.close(1000, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件监听器
|
||||
*/
|
||||
private setupEventListeners(): void {
|
||||
if (!this.wsServer) return;
|
||||
|
||||
this.wsServer.on('connection', (socket: WebSocket, request) => {
|
||||
this.handleNewConnection(socket, request);
|
||||
});
|
||||
|
||||
this.wsServer.on('error', (error: Error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
|
||||
if (this.httpServer) {
|
||||
this.httpServer.on('error', (error: Error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新连接
|
||||
*/
|
||||
private handleNewConnection(socket: WebSocket, request: any): void {
|
||||
// 检查连接数限制
|
||||
if (this.config.maxConnections && this.wsConnections.size >= this.config.maxConnections) {
|
||||
socket.close(1013, 'Too many connections');
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionId = uuidv4();
|
||||
const remoteAddress = request.socket.remoteAddress || request.headers['x-forwarded-for'] || 'unknown';
|
||||
|
||||
const connectionInfo: WebSocketConnectionInfo = {
|
||||
id: connectionId,
|
||||
socket,
|
||||
remoteAddress: Array.isArray(remoteAddress) ? remoteAddress[0] : remoteAddress,
|
||||
connectedAt: new Date(),
|
||||
lastActivity: new Date(),
|
||||
userData: {}
|
||||
};
|
||||
|
||||
this.wsConnections.set(connectionId, connectionInfo);
|
||||
this.addConnection(connectionInfo);
|
||||
|
||||
// 设置 socket 事件监听
|
||||
socket.on('message', (data: Buffer) => {
|
||||
this.handleClientMessage(connectionId, data);
|
||||
});
|
||||
|
||||
socket.on('close', (code: number, reason: Buffer) => {
|
||||
this.handleClientDisconnect(connectionId, code, reason.toString());
|
||||
});
|
||||
|
||||
socket.on('error', (error: Error) => {
|
||||
this.handleError(error, connectionId);
|
||||
this.handleClientDisconnect(connectionId, 1006, 'Socket error');
|
||||
});
|
||||
|
||||
socket.on('pong', () => {
|
||||
this.handlePong(connectionId);
|
||||
});
|
||||
|
||||
// 启动心跳检测
|
||||
this.startHeartbeat(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端消息
|
||||
*/
|
||||
private handleClientMessage(connectionId: string, data: Buffer): void {
|
||||
try {
|
||||
const message = JSON.parse(data.toString()) as TransportMessage;
|
||||
message.senderId = connectionId;
|
||||
this.handleMessage(connectionId, message);
|
||||
} catch (error) {
|
||||
this.handleError(new Error(`Invalid message format from client ${connectionId}`), connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接
|
||||
*/
|
||||
private handleClientDisconnect(connectionId: string, code: number, reason: string): void {
|
||||
this.cleanupConnection(connectionId);
|
||||
this.removeConnection(connectionId, `${code}: ${reason}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳检测
|
||||
*/
|
||||
private startHeartbeat(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (!connection) return;
|
||||
|
||||
if (this.config.pingInterval && this.config.pingInterval > 0) {
|
||||
connection.pingTimer = setInterval(() => {
|
||||
this.sendPing(connectionId);
|
||||
}, this.config.pingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 ping
|
||||
*/
|
||||
private sendPing(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (!connection || connection.socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
connection.socket.ping();
|
||||
|
||||
// 设置 pong 超时
|
||||
if (this.config.pongTimeout && this.config.pongTimeout > 0) {
|
||||
if (connection.pongTimer) {
|
||||
clearTimeout(connection.pongTimer);
|
||||
}
|
||||
|
||||
connection.pongTimer = setTimeout(() => {
|
||||
this.disconnectClient(connectionId, 'Pong timeout');
|
||||
}, this.config.pongTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 pong 响应
|
||||
*/
|
||||
private handlePong(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection && connection.pongTimer) {
|
||||
clearTimeout(connection.pongTimer);
|
||||
connection.pongTimer = undefined;
|
||||
}
|
||||
this.updateClientActivity(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理连接资源
|
||||
*/
|
||||
private cleanupConnection(connectionId: string): void {
|
||||
const connection = this.wsConnections.get(connectionId);
|
||||
if (connection) {
|
||||
if (connection.pingTimer) {
|
||||
clearInterval(connection.pingTimer);
|
||||
}
|
||||
if (connection.pongTimer) {
|
||||
clearTimeout(connection.pongTimer);
|
||||
}
|
||||
this.wsConnections.delete(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有资源
|
||||
*/
|
||||
private async cleanup(): Promise<void> {
|
||||
// 清理所有连接
|
||||
for (const connectionId of this.wsConnections.keys()) {
|
||||
this.cleanupConnection(connectionId);
|
||||
}
|
||||
this.clearConnections();
|
||||
|
||||
// 关闭 WebSocket 服务器
|
||||
if (this.wsServer) {
|
||||
this.wsServer.close();
|
||||
this.wsServer = null;
|
||||
}
|
||||
|
||||
// 关闭 HTTP 服务器
|
||||
if (this.httpServer) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.httpServer!.close(() => resolve());
|
||||
});
|
||||
this.httpServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket 连接统计信息
|
||||
*/
|
||||
getWebSocketStats(): {
|
||||
totalConnections: number;
|
||||
activeConnections: number;
|
||||
inactiveConnections: number;
|
||||
} {
|
||||
let activeConnections = 0;
|
||||
let inactiveConnections = 0;
|
||||
|
||||
for (const connection of this.wsConnections.values()) {
|
||||
if (connection.socket.readyState === WebSocket.OPEN) {
|
||||
activeConnections++;
|
||||
} else {
|
||||
inactiveConnections++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalConnections: this.wsConnections.size,
|
||||
activeConnections,
|
||||
inactiveConnections
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* 核心模块导出
|
||||
*/
|
||||
|
||||
export * from './Transport';
|
||||
export * from './WebSocketTransport';
|
||||
export * from './HttpTransport';
|
||||
export * from './ClientConnection';
|
||||
export * from './NetworkServer';
|
||||
@@ -1,79 +1,26 @@
|
||||
/**
|
||||
* ECS Framework Network Server
|
||||
*
|
||||
* 提供完整的网络服务端功能,包括:
|
||||
* - WebSocket 和 HTTP 传输层
|
||||
* - 客户端连接管理
|
||||
* - 房间系统
|
||||
* - 身份验证和权限管理
|
||||
* - SyncVar 和 RPC 系统
|
||||
* - 消息验证
|
||||
* @esengine/network-server
|
||||
* ECS Framework网络层 - 服务端实现
|
||||
*/
|
||||
|
||||
// 核心模块
|
||||
export * from './core';
|
||||
// 核心服务器 (待实现)
|
||||
// export * from './core/NetworkServer';
|
||||
// export * from './core/ClientConnection';
|
||||
|
||||
// 房间系统
|
||||
export * from './rooms';
|
||||
// 传输层 (待实现)
|
||||
// export * from './transport/WebSocketTransport';
|
||||
// export * from './transport/HttpTransport';
|
||||
|
||||
// 认证系统
|
||||
export * from './auth';
|
||||
// 系统层 (待实现)
|
||||
// export * from './systems/SyncVarSystem';
|
||||
// export * from './systems/RpcSystem';
|
||||
|
||||
// 网络系统
|
||||
export * from './systems';
|
||||
// 房间管理 (待实现)
|
||||
// export * from './rooms/Room';
|
||||
// export * from './rooms/RoomManager';
|
||||
|
||||
// 验证系统
|
||||
export * from './validation';
|
||||
// 认证授权 (待实现)
|
||||
// export * from './auth/AuthManager';
|
||||
|
||||
// 版本信息
|
||||
export const VERSION = '1.0.0';
|
||||
|
||||
// 导出常用组合配置
|
||||
export interface ServerConfigPreset {
|
||||
/** 服务器名称 */
|
||||
name: string;
|
||||
/** WebSocket 端口 */
|
||||
wsPort: number;
|
||||
/** HTTP 端口(可选) */
|
||||
httpPort?: number;
|
||||
/** 最大连接数 */
|
||||
maxConnections: number;
|
||||
/** 是否启用认证 */
|
||||
enableAuth: boolean;
|
||||
/** 是否启用房间系统 */
|
||||
enableRooms: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义服务器配置
|
||||
*/
|
||||
export const ServerPresets = {
|
||||
/** 开发环境配置 */
|
||||
Development: {
|
||||
name: 'Development Server',
|
||||
wsPort: 8080,
|
||||
httpPort: 3000,
|
||||
maxConnections: 100,
|
||||
enableAuth: false,
|
||||
enableRooms: true
|
||||
} as ServerConfigPreset,
|
||||
|
||||
/** 生产环境配置 */
|
||||
Production: {
|
||||
name: 'Production Server',
|
||||
wsPort: 443,
|
||||
httpPort: 80,
|
||||
maxConnections: 10000,
|
||||
enableAuth: true,
|
||||
enableRooms: true
|
||||
} as ServerConfigPreset,
|
||||
|
||||
/** 测试环境配置 */
|
||||
Testing: {
|
||||
name: 'Test Server',
|
||||
wsPort: 9090,
|
||||
maxConnections: 10,
|
||||
enableAuth: false,
|
||||
enableRooms: false
|
||||
} as ServerConfigPreset
|
||||
};
|
||||
// 重新导出shared包的类型
|
||||
export * from '@esengine/network-shared';
|
||||
@@ -1,637 +0,0 @@
|
||||
/**
|
||||
* 房间管理
|
||||
*
|
||||
* 类似于 Unity Mirror 的 Scene 概念,管理一组客户端和网络对象
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Entity, Scene } from '@esengine/ecs-framework';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* 房间状态
|
||||
*/
|
||||
export enum RoomState {
|
||||
/** 创建中 */
|
||||
CREATING = 'creating',
|
||||
/** 活跃状态 */
|
||||
ACTIVE = 'active',
|
||||
/** 暂停状态 */
|
||||
PAUSED = 'paused',
|
||||
/** 关闭中 */
|
||||
CLOSING = 'closing',
|
||||
/** 已关闭 */
|
||||
CLOSED = 'closed'
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间配置
|
||||
*/
|
||||
export interface RoomConfig {
|
||||
/** 房间ID */
|
||||
id: string;
|
||||
/** 房间名称 */
|
||||
name: string;
|
||||
/** 房间描述 */
|
||||
description?: string;
|
||||
/** 最大玩家数 */
|
||||
maxPlayers: number;
|
||||
/** 是否私有房间 */
|
||||
isPrivate?: boolean;
|
||||
/** 房间密码 */
|
||||
password?: string;
|
||||
/** 房间元数据 */
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
/** 是否持久化 */
|
||||
persistent?: boolean;
|
||||
/** 房间过期时间(毫秒) */
|
||||
expirationTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家数据
|
||||
*/
|
||||
export interface PlayerData {
|
||||
/** 客户端连接 */
|
||||
client: ClientConnection;
|
||||
/** 加入时间 */
|
||||
joinedAt: Date;
|
||||
/** 是否为房主 */
|
||||
isOwner: boolean;
|
||||
/** 玩家自定义数据 */
|
||||
customData: Record<string, NetworkValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间统计信息
|
||||
*/
|
||||
export interface RoomStats {
|
||||
/** 当前玩家数 */
|
||||
currentPlayers: number;
|
||||
/** 最大玩家数 */
|
||||
maxPlayers: number;
|
||||
/** 总加入过的玩家数 */
|
||||
totalPlayersJoined: number;
|
||||
/** 消息总数 */
|
||||
totalMessages: number;
|
||||
/** 创建时间 */
|
||||
createdAt: Date;
|
||||
/** 房间存活时间(毫秒) */
|
||||
lifetime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间事件
|
||||
*/
|
||||
export interface RoomEvents {
|
||||
/** 玩家加入 */
|
||||
'player-joined': (player: PlayerData) => void;
|
||||
/** 玩家离开 */
|
||||
'player-left': (clientId: string, reason?: string) => void;
|
||||
/** 房主变更 */
|
||||
'owner-changed': (newOwnerId: string, oldOwnerId?: string) => void;
|
||||
/** 房间状态变化 */
|
||||
'state-changed': (oldState: RoomState, newState: RoomState) => void;
|
||||
/** 收到消息 */
|
||||
'message': (clientId: string, message: TransportMessage) => void;
|
||||
/** 房间更新 */
|
||||
'room-updated': (updatedFields: Partial<RoomConfig>) => void;
|
||||
/** 房间错误 */
|
||||
'error': (error: Error, clientId?: string) => void;
|
||||
/** 房间即将关闭 */
|
||||
'closing': (reason: string) => void;
|
||||
/** 房间已关闭 */
|
||||
'closed': (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间类
|
||||
*/
|
||||
export class Room extends EventEmitter {
|
||||
private config: RoomConfig;
|
||||
private state: RoomState = RoomState.CREATING;
|
||||
private players = new Map<string, PlayerData>();
|
||||
private ownerId: string | null = null;
|
||||
private ecsScene: Scene | null = null;
|
||||
private stats: RoomStats;
|
||||
private expirationTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RoomConfig) {
|
||||
super();
|
||||
|
||||
this.config = { ...config };
|
||||
this.stats = {
|
||||
currentPlayers: 0,
|
||||
maxPlayers: config.maxPlayers,
|
||||
totalPlayersJoined: 0,
|
||||
totalMessages: 0,
|
||||
createdAt: new Date(),
|
||||
lifetime: 0
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间ID
|
||||
*/
|
||||
get id(): string {
|
||||
return this.config.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间名称
|
||||
*/
|
||||
get name(): string {
|
||||
return this.config.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间状态
|
||||
*/
|
||||
get currentState(): RoomState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间配置
|
||||
*/
|
||||
getConfig(): Readonly<RoomConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间统计信息
|
||||
*/
|
||||
getStats(): RoomStats {
|
||||
this.stats.lifetime = Date.now() - this.stats.createdAt.getTime();
|
||||
this.stats.currentPlayers = this.players.size;
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家
|
||||
*/
|
||||
getPlayers(): PlayerData[] {
|
||||
return Array.from(this.players.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定玩家
|
||||
*/
|
||||
getPlayer(clientId: string): PlayerData | undefined {
|
||||
return this.players.get(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否在房间中
|
||||
*/
|
||||
hasPlayer(clientId: string): boolean {
|
||||
return this.players.has(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前玩家数量
|
||||
*/
|
||||
getPlayerCount(): number {
|
||||
return this.players.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否已满
|
||||
*/
|
||||
isFull(): boolean {
|
||||
return this.players.size >= this.config.maxPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否为空
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this.players.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房主
|
||||
*/
|
||||
getOwner(): PlayerData | undefined {
|
||||
return this.ownerId ? this.players.get(this.ownerId) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ECS 场景
|
||||
*/
|
||||
getEcsScene(): Scene | null {
|
||||
return this.ecsScene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入房间
|
||||
*/
|
||||
async addPlayer(client: ClientConnection, customData: Record<string, NetworkValue> = {}): Promise<boolean> {
|
||||
if (this.state !== RoomState.ACTIVE) {
|
||||
throw new Error(`Cannot join room in state: ${this.state}`);
|
||||
}
|
||||
|
||||
if (this.hasPlayer(client.id)) {
|
||||
throw new Error(`Player ${client.id} is already in the room`);
|
||||
}
|
||||
|
||||
if (this.isFull()) {
|
||||
throw new Error('Room is full');
|
||||
}
|
||||
|
||||
// 检查房间密码
|
||||
if (this.config.isPrivate && this.config.password) {
|
||||
const providedPassword = customData.password as string;
|
||||
if (providedPassword !== this.config.password) {
|
||||
throw new Error('Invalid room password');
|
||||
}
|
||||
}
|
||||
|
||||
const isFirstPlayer = this.isEmpty();
|
||||
const playerData: PlayerData = {
|
||||
client,
|
||||
joinedAt: new Date(),
|
||||
isOwner: isFirstPlayer,
|
||||
customData: { ...customData }
|
||||
};
|
||||
|
||||
this.players.set(client.id, playerData);
|
||||
client.joinRoom(this.id);
|
||||
|
||||
// 设置房主
|
||||
if (isFirstPlayer) {
|
||||
this.ownerId = client.id;
|
||||
}
|
||||
|
||||
this.stats.totalPlayersJoined++;
|
||||
|
||||
// 通知其他玩家
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'player-joined',
|
||||
playerId: client.id,
|
||||
playerData: {
|
||||
id: client.id,
|
||||
joinedAt: playerData.joinedAt.toISOString(),
|
||||
isOwner: playerData.isOwner,
|
||||
customData: playerData.customData
|
||||
}
|
||||
}
|
||||
}, client.id);
|
||||
|
||||
console.log(`Player ${client.id} joined room ${this.id}`);
|
||||
this.emit('player-joined', playerData);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开房间
|
||||
*/
|
||||
async removePlayer(clientId: string, reason?: string): Promise<boolean> {
|
||||
const player = this.players.get(clientId);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.players.delete(clientId);
|
||||
player.client.leaveRoom();
|
||||
|
||||
// 如果离开的是房主,转移房主权限
|
||||
if (this.ownerId === clientId) {
|
||||
await this.transferOwnership();
|
||||
}
|
||||
|
||||
// 通知其他玩家
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'player-left',
|
||||
playerId: clientId,
|
||||
reason: reason || 'unknown'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Player ${clientId} left room ${this.id}, reason: ${reason || 'unknown'}`);
|
||||
this.emit('player-left', clientId, reason);
|
||||
|
||||
// 如果房间为空,考虑关闭
|
||||
if (this.isEmpty() && !this.config.persistent) {
|
||||
await this.close('empty-room');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转移房主权限
|
||||
*/
|
||||
async transferOwnership(newOwnerId?: string): Promise<boolean> {
|
||||
const oldOwnerId = this.ownerId;
|
||||
|
||||
if (newOwnerId) {
|
||||
const newOwner = this.players.get(newOwnerId);
|
||||
if (!newOwner) {
|
||||
return false;
|
||||
}
|
||||
this.ownerId = newOwnerId;
|
||||
newOwner.isOwner = true;
|
||||
} else {
|
||||
// 自动选择下一个玩家作为房主
|
||||
const players = Array.from(this.players.values());
|
||||
if (players.length > 0) {
|
||||
const newOwner = players[0];
|
||||
this.ownerId = newOwner.client.id;
|
||||
newOwner.isOwner = true;
|
||||
} else {
|
||||
this.ownerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新旧房主状态
|
||||
if (oldOwnerId) {
|
||||
const oldOwner = this.players.get(oldOwnerId);
|
||||
if (oldOwner) {
|
||||
oldOwner.isOwner = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 通知所有玩家房主变更
|
||||
if (this.ownerId) {
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'owner-changed',
|
||||
newOwnerId: this.ownerId,
|
||||
oldOwnerId: oldOwnerId || ''
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Room ${this.id} ownership transferred from ${oldOwnerId || 'none'} to ${this.ownerId}`);
|
||||
this.emit('owner-changed', this.ownerId, oldOwnerId || undefined);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给房间内所有玩家
|
||||
*/
|
||||
async broadcast(message: TransportMessage, excludeClientId?: string): Promise<number> {
|
||||
const players = Array.from(this.players.values())
|
||||
.filter(player => player.client.id !== excludeClientId);
|
||||
|
||||
const promises = players.map(player => player.client.sendMessage(message));
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
return results.filter(result => result.status === 'fulfilled' && result.value).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定玩家
|
||||
*/
|
||||
async sendToPlayer(clientId: string, message: TransportMessage): Promise<boolean> {
|
||||
const player = this.players.get(clientId);
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await player.client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家消息
|
||||
*/
|
||||
async handleMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
if (!this.hasPlayer(clientId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stats.totalMessages++;
|
||||
this.emit('message', clientId, message);
|
||||
|
||||
// 根据消息类型进行处理
|
||||
switch (message.type) {
|
||||
case 'rpc':
|
||||
await this.handleRpcMessage(clientId, message);
|
||||
break;
|
||||
case 'syncvar':
|
||||
await this.handleSyncVarMessage(clientId, message);
|
||||
break;
|
||||
case 'system':
|
||||
await this.handleSystemMessage(clientId, message);
|
||||
break;
|
||||
default:
|
||||
// 转发自定义消息
|
||||
await this.broadcast(message, clientId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新房间配置
|
||||
*/
|
||||
async updateConfig(updates: Partial<RoomConfig>): Promise<void> {
|
||||
// 验证更新
|
||||
if (updates.maxPlayers !== undefined && updates.maxPlayers < this.players.size) {
|
||||
throw new Error('Cannot reduce maxPlayers below current player count');
|
||||
}
|
||||
|
||||
const oldConfig = { ...this.config };
|
||||
Object.assign(this.config, updates);
|
||||
|
||||
// 通知所有玩家房间更新
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-updated',
|
||||
updates
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('room-updated', updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停房间
|
||||
*/
|
||||
async pause(): Promise<void> {
|
||||
if (this.state === RoomState.ACTIVE) {
|
||||
this.setState(RoomState.PAUSED);
|
||||
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-paused'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复房间
|
||||
*/
|
||||
async resume(): Promise<void> {
|
||||
if (this.state === RoomState.PAUSED) {
|
||||
this.setState(RoomState.ACTIVE);
|
||||
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-resumed'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭房间
|
||||
*/
|
||||
async close(reason: string = 'server-shutdown'): Promise<void> {
|
||||
if (this.state === RoomState.CLOSED || this.state === RoomState.CLOSING) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(RoomState.CLOSING);
|
||||
this.emit('closing', reason);
|
||||
|
||||
// 通知所有玩家房间即将关闭
|
||||
await this.broadcast({
|
||||
type: 'system',
|
||||
data: {
|
||||
action: 'room-closing',
|
||||
reason
|
||||
}
|
||||
});
|
||||
|
||||
// 移除所有玩家
|
||||
const playerIds = Array.from(this.players.keys());
|
||||
for (const clientId of playerIds) {
|
||||
await this.removePlayer(clientId, 'room-closed');
|
||||
}
|
||||
|
||||
this.cleanup();
|
||||
this.setState(RoomState.CLOSED);
|
||||
|
||||
console.log(`Room ${this.id} closed, reason: ${reason}`);
|
||||
this.emit('closed', reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化房间
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 创建 ECS 场景
|
||||
this.ecsScene = new Scene();
|
||||
|
||||
// 设置过期定时器
|
||||
if (this.config.expirationTime && this.config.expirationTime > 0) {
|
||||
this.expirationTimer = setTimeout(() => {
|
||||
this.close('expired');
|
||||
}, this.config.expirationTime);
|
||||
}
|
||||
|
||||
this.setState(RoomState.ACTIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 RPC 消息
|
||||
*/
|
||||
private async handleRpcMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
// RPC 消息处理逻辑
|
||||
// 这里可以添加权限检查、速率限制等
|
||||
await this.broadcast(message, clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SyncVar 消息
|
||||
*/
|
||||
private async handleSyncVarMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
// SyncVar 消息处理逻辑
|
||||
// 这里可以添加权限检查、数据验证等
|
||||
await this.broadcast(message, clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统消息
|
||||
*/
|
||||
private async handleSystemMessage(clientId: string, message: TransportMessage): Promise<void> {
|
||||
const data = message.data as any;
|
||||
|
||||
switch (data.action) {
|
||||
case 'request-ownership':
|
||||
// 处理房主权限转移请求
|
||||
if (this.ownerId === clientId) {
|
||||
await this.transferOwnership(data.newOwnerId);
|
||||
}
|
||||
break;
|
||||
// 其他系统消息处理...
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间状态
|
||||
*/
|
||||
private setState(newState: RoomState): void {
|
||||
const oldState = this.state;
|
||||
if (oldState !== newState) {
|
||||
this.state = newState;
|
||||
this.emit('state-changed', oldState, newState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
private cleanup(): void {
|
||||
if (this.expirationTimer) {
|
||||
clearTimeout(this.expirationTimer);
|
||||
this.expirationTimer = null;
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
|
||||
if (this.ecsScene) {
|
||||
this.ecsScene = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof RoomEvents>(event: K, listener: RoomEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof RoomEvents>(event: K, ...args: Parameters<RoomEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化房间信息
|
||||
*/
|
||||
toJSON(): object {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
state: this.state,
|
||||
config: this.config,
|
||||
stats: this.getStats(),
|
||||
players: this.getPlayers().map(player => ({
|
||||
id: player.client.id,
|
||||
joinedAt: player.joinedAt.toISOString(),
|
||||
isOwner: player.isOwner,
|
||||
customData: player.customData
|
||||
})),
|
||||
ownerId: this.ownerId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
/**
|
||||
* 房间管理器
|
||||
*
|
||||
* 管理所有房间的创建、销毁、查找等操作
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Room, RoomConfig, RoomState, PlayerData } from './Room';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
|
||||
/**
|
||||
* 房间管理器配置
|
||||
*/
|
||||
export interface RoomManagerConfig {
|
||||
/** 最大房间数量 */
|
||||
maxRooms?: number;
|
||||
/** 默认房间过期时间(毫秒) */
|
||||
defaultExpirationTime?: number;
|
||||
/** 是否启用房间统计 */
|
||||
enableStats?: boolean;
|
||||
/** 房间清理间隔(毫秒) */
|
||||
cleanupInterval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间查询选项
|
||||
*/
|
||||
export interface RoomQueryOptions {
|
||||
/** 房间名称模糊搜索 */
|
||||
namePattern?: string;
|
||||
/** 房间状态过滤 */
|
||||
state?: RoomState;
|
||||
/** 是否私有房间 */
|
||||
isPrivate?: boolean;
|
||||
/** 最小空位数 */
|
||||
minAvailableSlots?: number;
|
||||
/** 最大空位数 */
|
||||
maxAvailableSlots?: number;
|
||||
/** 元数据过滤 */
|
||||
metadata?: Record<string, NetworkValue>;
|
||||
/** 限制结果数量 */
|
||||
limit?: number;
|
||||
/** 跳过条数 */
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器统计信息
|
||||
*/
|
||||
export interface RoomManagerStats {
|
||||
/** 总房间数 */
|
||||
totalRooms: number;
|
||||
/** 活跃房间数 */
|
||||
activeRooms: number;
|
||||
/** 总玩家数 */
|
||||
totalPlayers: number;
|
||||
/** 私有房间数 */
|
||||
privateRooms: number;
|
||||
/** 持久化房间数 */
|
||||
persistentRooms: number;
|
||||
/** 创建的房间总数 */
|
||||
roomsCreated: number;
|
||||
/** 关闭的房间总数 */
|
||||
roomsClosed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器事件
|
||||
*/
|
||||
export interface RoomManagerEvents {
|
||||
/** 房间创建 */
|
||||
'room-created': (room: Room) => void;
|
||||
/** 房间关闭 */
|
||||
'room-closed': (roomId: string, reason: string) => void;
|
||||
/** 玩家加入房间 */
|
||||
'player-joined-room': (roomId: string, player: PlayerData) => void;
|
||||
/** 玩家离开房间 */
|
||||
'player-left-room': (roomId: string, clientId: string, reason?: string) => void;
|
||||
/** 房间管理器错误 */
|
||||
'error': (error: Error, roomId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 房间管理器
|
||||
*/
|
||||
export class RoomManager extends EventEmitter {
|
||||
private config: RoomManagerConfig;
|
||||
private rooms = new Map<string, Room>();
|
||||
private stats: RoomManagerStats;
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RoomManagerConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
maxRooms: 1000,
|
||||
defaultExpirationTime: 0, // 0 = 不过期
|
||||
enableStats: true,
|
||||
cleanupInterval: 60000, // 1分钟
|
||||
...config
|
||||
};
|
||||
|
||||
this.stats = {
|
||||
totalRooms: 0,
|
||||
activeRooms: 0,
|
||||
totalPlayers: 0,
|
||||
privateRooms: 0,
|
||||
persistentRooms: 0,
|
||||
roomsCreated: 0,
|
||||
roomsClosed: 0
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间管理器配置
|
||||
*/
|
||||
getConfig(): Readonly<RoomManagerConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间管理器统计信息
|
||||
*/
|
||||
getStats(): RoomManagerStats {
|
||||
this.updateStats();
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*/
|
||||
async createRoom(config: RoomConfig, creatorClient?: ClientConnection): Promise<Room> {
|
||||
// 检查房间数量限制
|
||||
if (this.config.maxRooms && this.rooms.size >= this.config.maxRooms) {
|
||||
throw new Error('Maximum number of rooms reached');
|
||||
}
|
||||
|
||||
// 检查房间ID是否已存在
|
||||
if (this.rooms.has(config.id)) {
|
||||
throw new Error(`Room with id "${config.id}" already exists`);
|
||||
}
|
||||
|
||||
// 应用默认过期时间
|
||||
const roomConfig: RoomConfig = {
|
||||
expirationTime: this.config.defaultExpirationTime,
|
||||
...config
|
||||
};
|
||||
|
||||
const room = new Room(roomConfig);
|
||||
|
||||
// 设置房间事件监听
|
||||
this.setupRoomEvents(room);
|
||||
|
||||
this.rooms.set(room.id, room);
|
||||
this.stats.roomsCreated++;
|
||||
|
||||
console.log(`Room created: ${room.id} by ${creatorClient?.id || 'system'}`);
|
||||
this.emit('room-created', room);
|
||||
|
||||
// 如果有创建者,自动加入房间
|
||||
if (creatorClient) {
|
||||
try {
|
||||
await room.addPlayer(creatorClient);
|
||||
} catch (error) {
|
||||
console.error(`Failed to add creator to room ${room.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间
|
||||
*/
|
||||
getRoom(roomId: string): Room | undefined {
|
||||
return this.rooms.get(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否存在
|
||||
*/
|
||||
hasRoom(roomId: string): boolean {
|
||||
return this.rooms.has(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有房间
|
||||
*/
|
||||
getAllRooms(): Room[] {
|
||||
return Array.from(this.rooms.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询房间
|
||||
*/
|
||||
findRooms(options: RoomQueryOptions = {}): Room[] {
|
||||
let rooms = Array.from(this.rooms.values());
|
||||
|
||||
// 状态过滤
|
||||
if (options.state !== undefined) {
|
||||
rooms = rooms.filter(room => room.currentState === options.state);
|
||||
}
|
||||
|
||||
// 私有房间过滤
|
||||
if (options.isPrivate !== undefined) {
|
||||
rooms = rooms.filter(room => room.getConfig().isPrivate === options.isPrivate);
|
||||
}
|
||||
|
||||
// 名称模糊搜索
|
||||
if (options.namePattern) {
|
||||
const pattern = options.namePattern.toLowerCase();
|
||||
rooms = rooms.filter(room =>
|
||||
room.getConfig().name.toLowerCase().includes(pattern)
|
||||
);
|
||||
}
|
||||
|
||||
// 空位数过滤
|
||||
if (options.minAvailableSlots !== undefined) {
|
||||
rooms = rooms.filter(room => {
|
||||
const available = room.getConfig().maxPlayers - room.getPlayerCount();
|
||||
return available >= options.minAvailableSlots!;
|
||||
});
|
||||
}
|
||||
|
||||
if (options.maxAvailableSlots !== undefined) {
|
||||
rooms = rooms.filter(room => {
|
||||
const available = room.getConfig().maxPlayers - room.getPlayerCount();
|
||||
return available <= options.maxAvailableSlots!;
|
||||
});
|
||||
}
|
||||
|
||||
// 元数据过滤
|
||||
if (options.metadata) {
|
||||
rooms = rooms.filter(room => {
|
||||
const roomMetadata = room.getConfig().metadata || {};
|
||||
return Object.entries(options.metadata!).every(([key, value]) =>
|
||||
roomMetadata[key] === value
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 排序(按创建时间,最新的在前)
|
||||
rooms.sort((a, b) =>
|
||||
b.getStats().createdAt.getTime() - a.getStats().createdAt.getTime()
|
||||
);
|
||||
|
||||
// 分页
|
||||
const offset = options.offset || 0;
|
||||
const limit = options.limit || rooms.length;
|
||||
return rooms.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭房间
|
||||
*/
|
||||
async closeRoom(roomId: string, reason: string = 'manual'): Promise<boolean> {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await room.close(reason);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.emit('error', error as Error, roomId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入房间
|
||||
*/
|
||||
async joinRoom(
|
||||
roomId: string,
|
||||
client: ClientConnection,
|
||||
customData: Record<string, NetworkValue> = {}
|
||||
): Promise<boolean> {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Room "${roomId}" not found`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await room.addPlayer(client, customData);
|
||||
} catch (error) {
|
||||
this.emit('error', error as Error, roomId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开房间
|
||||
*/
|
||||
async leaveRoom(roomId: string, clientId: string, reason?: string): Promise<boolean> {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await room.removePlayer(clientId, reason);
|
||||
} catch (error) {
|
||||
this.emit('error', error as Error, roomId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开所有房间
|
||||
*/
|
||||
async leaveAllRooms(clientId: string, reason?: string): Promise<number> {
|
||||
let leftCount = 0;
|
||||
|
||||
for (const room of this.rooms.values()) {
|
||||
if (room.hasPlayer(clientId)) {
|
||||
try {
|
||||
await room.removePlayer(clientId, reason);
|
||||
leftCount++;
|
||||
} catch (error) {
|
||||
console.error(`Error removing player ${clientId} from room ${room.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return leftCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家所在的房间
|
||||
*/
|
||||
getPlayerRooms(clientId: string): Room[] {
|
||||
return Array.from(this.rooms.values())
|
||||
.filter(room => room.hasPlayer(clientId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间数量
|
||||
*/
|
||||
getRoomCount(): number {
|
||||
return this.rooms.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总玩家数量
|
||||
*/
|
||||
getTotalPlayerCount(): number {
|
||||
return Array.from(this.rooms.values())
|
||||
.reduce((total, room) => total + room.getPlayerCount(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空闲房间
|
||||
*/
|
||||
async cleanupRooms(): Promise<number> {
|
||||
let cleanedCount = 0;
|
||||
const now = Date.now();
|
||||
|
||||
for (const room of this.rooms.values()) {
|
||||
const config = room.getConfig();
|
||||
const stats = room.getStats();
|
||||
|
||||
// 清理条件:
|
||||
// 1. 非持久化的空房间
|
||||
// 2. 已过期的房间
|
||||
// 3. 已关闭的房间
|
||||
let shouldClean = false;
|
||||
let reason = '';
|
||||
|
||||
if (room.currentState === RoomState.CLOSED) {
|
||||
shouldClean = true;
|
||||
reason = 'room-closed';
|
||||
} else if (!config.persistent && room.isEmpty()) {
|
||||
shouldClean = true;
|
||||
reason = 'empty-room';
|
||||
} else if (config.expirationTime && config.expirationTime > 0) {
|
||||
const expireTime = stats.createdAt.getTime() + config.expirationTime;
|
||||
if (now >= expireTime) {
|
||||
shouldClean = true;
|
||||
reason = 'expired';
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldClean) {
|
||||
try {
|
||||
if (room.currentState !== RoomState.CLOSED) {
|
||||
await room.close(reason);
|
||||
}
|
||||
this.rooms.delete(room.id);
|
||||
cleanedCount++;
|
||||
console.log(`Cleaned up room: ${room.id}, reason: ${reason}`);
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning up room ${room.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有房间
|
||||
*/
|
||||
async closeAllRooms(reason: string = 'shutdown'): Promise<void> {
|
||||
const rooms = Array.from(this.rooms.values());
|
||||
const promises = rooms.map(room => room.close(reason));
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
this.rooms.clear();
|
||||
|
||||
console.log(`Closed ${rooms.length} rooms, reason: ${reason}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁房间管理器
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
// 停止清理定时器
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
// 关闭所有房间
|
||||
await this.closeAllRooms('manager-destroyed');
|
||||
|
||||
// 移除所有事件监听器
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化房间管理器
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器
|
||||
if (this.config.cleanupInterval && this.config.cleanupInterval > 0) {
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupRooms().catch(error => {
|
||||
console.error('Error during room cleanup:', error);
|
||||
});
|
||||
}, this.config.cleanupInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置房间事件监听
|
||||
*/
|
||||
private setupRoomEvents(room: Room): void {
|
||||
room.on('player-joined', (player) => {
|
||||
this.emit('player-joined-room', room.id, player);
|
||||
});
|
||||
|
||||
room.on('player-left', (clientId, reason) => {
|
||||
this.emit('player-left-room', room.id, clientId, reason);
|
||||
});
|
||||
|
||||
room.on('closed', (reason) => {
|
||||
this.rooms.delete(room.id);
|
||||
this.stats.roomsClosed++;
|
||||
console.log(`Room ${room.id} removed from manager, reason: ${reason}`);
|
||||
this.emit('room-closed', room.id, reason);
|
||||
});
|
||||
|
||||
room.on('error', (error) => {
|
||||
this.emit('error', error, room.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
private updateStats(): void {
|
||||
this.stats.totalRooms = this.rooms.size;
|
||||
this.stats.activeRooms = Array.from(this.rooms.values())
|
||||
.filter(room => room.currentState === RoomState.ACTIVE).length;
|
||||
this.stats.totalPlayers = this.getTotalPlayerCount();
|
||||
this.stats.privateRooms = Array.from(this.rooms.values())
|
||||
.filter(room => room.getConfig().isPrivate).length;
|
||||
this.stats.persistentRooms = Array.from(this.rooms.values())
|
||||
.filter(room => room.getConfig().persistent).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof RoomManagerEvents>(event: K, listener: RoomManagerEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof RoomManagerEvents>(event: K, ...args: Parameters<RoomManagerEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 房间系统导出
|
||||
*/
|
||||
|
||||
export * from './Room';
|
||||
export * from './RoomManager';
|
||||
@@ -1,762 +0,0 @@
|
||||
/**
|
||||
* RPC 系统
|
||||
*
|
||||
* 处理服务端的 RPC 调用、权限验证、参数验证等
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
NetworkValue,
|
||||
RpcMetadata
|
||||
} from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { Room } from '../rooms/Room';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* RPC 调用记录
|
||||
*/
|
||||
export interface RpcCall {
|
||||
/** 调用ID */
|
||||
id: string;
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 参数 */
|
||||
parameters: NetworkValue[];
|
||||
/** 元数据 */
|
||||
metadata: RpcMetadata;
|
||||
/** 发送者客户端ID */
|
||||
senderId: string;
|
||||
/** 目标客户端IDs(用于 ClientRpc) */
|
||||
targetClientIds?: string[];
|
||||
/** 是否需要响应 */
|
||||
requiresResponse: boolean;
|
||||
/** 时间戳 */
|
||||
timestamp: Date;
|
||||
/** 过期时间 */
|
||||
expiresAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 响应
|
||||
*/
|
||||
export interface RpcResponse {
|
||||
/** 调用ID */
|
||||
callId: string;
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 返回值 */
|
||||
result?: NetworkValue;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 错误代码 */
|
||||
errorCode?: string;
|
||||
/** 时间戳 */
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 系统配置
|
||||
*/
|
||||
export interface RpcSystemConfig {
|
||||
/** RPC 调用超时时间(毫秒) */
|
||||
callTimeout?: number;
|
||||
/** 最大并发 RPC 调用数 */
|
||||
maxConcurrentCalls?: number;
|
||||
/** 是否启用权限检查 */
|
||||
enablePermissionCheck?: boolean;
|
||||
/** 是否启用参数验证 */
|
||||
enableParameterValidation?: boolean;
|
||||
/** 是否启用频率限制 */
|
||||
enableRateLimit?: boolean;
|
||||
/** 最大 RPC 频率(调用/秒) */
|
||||
maxRpcRate?: number;
|
||||
/** 单个参数最大大小(字节) */
|
||||
maxParameterSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 系统事件
|
||||
*/
|
||||
export interface RpcSystemEvents {
|
||||
/** ClientRpc 调用 */
|
||||
'client-rpc-called': (call: RpcCall) => void;
|
||||
/** ServerRpc 调用 */
|
||||
'server-rpc-called': (call: RpcCall) => void;
|
||||
/** RPC 调用完成 */
|
||||
'rpc-completed': (call: RpcCall, response?: RpcResponse) => void;
|
||||
/** RPC 调用超时 */
|
||||
'rpc-timeout': (callId: string) => void;
|
||||
/** 权限验证失败 */
|
||||
'permission-denied': (clientId: string, call: RpcCall) => void;
|
||||
/** 参数验证失败 */
|
||||
'parameter-validation-failed': (clientId: string, call: RpcCall, reason: string) => void;
|
||||
/** 频率限制触发 */
|
||||
'rate-limit-exceeded': (clientId: string) => void;
|
||||
/** RPC 错误 */
|
||||
'rpc-error': (error: Error, callId?: string, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端 RPC 状态
|
||||
*/
|
||||
interface ClientRpcState {
|
||||
/** 客户端ID */
|
||||
clientId: string;
|
||||
/** 活跃的调用 */
|
||||
activeCalls: Map<string, RpcCall>;
|
||||
/** RPC 调用计数 */
|
||||
rpcCount: number;
|
||||
/** 频率重置时间 */
|
||||
rateResetTime: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 待处理的 RPC 响应
|
||||
*/
|
||||
interface PendingRpcResponse {
|
||||
/** 调用信息 */
|
||||
call: RpcCall;
|
||||
/** 超时定时器 */
|
||||
timeoutTimer: NodeJS.Timeout;
|
||||
/** 响应回调 */
|
||||
responseCallback: (response: RpcResponse) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 系统
|
||||
*/
|
||||
export class RpcSystem extends EventEmitter {
|
||||
private config: RpcSystemConfig;
|
||||
private clientStates = new Map<string, ClientRpcState>();
|
||||
private pendingCalls = new Map<string, PendingRpcResponse>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RpcSystemConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
callTimeout: 30000, // 30秒
|
||||
maxConcurrentCalls: 10,
|
||||
enablePermissionCheck: true,
|
||||
enableParameterValidation: true,
|
||||
enableRateLimit: true,
|
||||
maxRpcRate: 30, // 30次/秒
|
||||
maxParameterSize: 65536, // 64KB
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ClientRpc 调用
|
||||
*/
|
||||
async handleClientRpcCall(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = message.data as any;
|
||||
const {
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters = [],
|
||||
metadata,
|
||||
targetFilter = 'all'
|
||||
} = data;
|
||||
|
||||
// 创建 RPC 调用记录
|
||||
const rpcCall: RpcCall = {
|
||||
id: uuidv4(),
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters,
|
||||
metadata,
|
||||
senderId: client.id,
|
||||
requiresResponse: metadata?.requiresResponse || false,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 权限检查
|
||||
if (this.config.enablePermissionCheck) {
|
||||
if (!this.checkRpcPermission(client, rpcCall, 'client-rpc')) {
|
||||
this.emit('permission-denied', client.id, rpcCall);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 频率限制检查
|
||||
if (this.config.enableRateLimit && !this.checkRpcRate(client.id)) {
|
||||
this.emit('rate-limit-exceeded', client.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if (this.config.enableParameterValidation) {
|
||||
const validationResult = this.validateRpcParameters(rpcCall);
|
||||
if (!validationResult.valid) {
|
||||
this.emit('parameter-validation-failed', client.id, rpcCall, validationResult.reason!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确定目标客户端
|
||||
const targetClientIds = this.getClientRpcTargets(room, client.id, targetFilter);
|
||||
rpcCall.targetClientIds = targetClientIds;
|
||||
|
||||
// 记录活跃调用
|
||||
this.recordActiveCall(client.id, rpcCall);
|
||||
|
||||
// 触发事件
|
||||
this.emit('client-rpc-called', rpcCall);
|
||||
|
||||
// 发送到目标客户端
|
||||
await this.sendClientRpc(room, rpcCall, targetClientIds);
|
||||
|
||||
// 如果不需要响应,立即标记完成
|
||||
if (!rpcCall.requiresResponse) {
|
||||
this.completeRpcCall(rpcCall);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.emit('rpc-error', error as Error, undefined, client.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ServerRpc 调用
|
||||
*/
|
||||
async handleServerRpcCall(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage,
|
||||
room: Room
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = message.data as any;
|
||||
const {
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters = [],
|
||||
metadata
|
||||
} = data;
|
||||
|
||||
// 创建 RPC 调用记录
|
||||
const rpcCall: RpcCall = {
|
||||
id: uuidv4(),
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters,
|
||||
metadata,
|
||||
senderId: client.id,
|
||||
requiresResponse: metadata?.requiresResponse || false,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 权限检查
|
||||
if (this.config.enablePermissionCheck) {
|
||||
if (!this.checkRpcPermission(client, rpcCall, 'server-rpc')) {
|
||||
this.emit('permission-denied', client.id, rpcCall);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 频率限制检查
|
||||
if (this.config.enableRateLimit && !this.checkRpcRate(client.id)) {
|
||||
this.emit('rate-limit-exceeded', client.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if (this.config.enableParameterValidation) {
|
||||
const validationResult = this.validateRpcParameters(rpcCall);
|
||||
if (!validationResult.valid) {
|
||||
this.emit('parameter-validation-failed', client.id, rpcCall, validationResult.reason!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 记录活跃调用
|
||||
this.recordActiveCall(client.id, rpcCall);
|
||||
|
||||
// 触发事件
|
||||
this.emit('server-rpc-called', rpcCall);
|
||||
|
||||
// ServerRpc 在服务端执行,这里需要实际的执行逻辑
|
||||
// 在实际使用中,应该通过事件或回调来执行具体的方法
|
||||
const response = await this.executeServerRpc(rpcCall);
|
||||
|
||||
// 发送响应(如果需要)
|
||||
if (rpcCall.requiresResponse && response) {
|
||||
await this.sendRpcResponse(client, response);
|
||||
}
|
||||
|
||||
this.completeRpcCall(rpcCall, response || undefined);
|
||||
|
||||
} catch (error) {
|
||||
this.emit('rpc-error', error as Error, undefined, client.id);
|
||||
|
||||
// 发送错误响应
|
||||
if (message.data && (message.data as any).requiresResponse) {
|
||||
const errorResponse: RpcResponse = {
|
||||
callId: (message.data as any).callId || uuidv4(),
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'EXECUTION_ERROR',
|
||||
timestamp: new Date()
|
||||
};
|
||||
await this.sendRpcResponse(client, errorResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 RPC 响应
|
||||
*/
|
||||
async handleRpcResponse(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = message.data as any as RpcResponse;
|
||||
const pendingCall = this.pendingCalls.get(response.callId);
|
||||
|
||||
if (pendingCall) {
|
||||
// 清除超时定时器
|
||||
clearTimeout(pendingCall.timeoutTimer);
|
||||
this.pendingCalls.delete(response.callId);
|
||||
|
||||
// 调用响应回调
|
||||
pendingCall.responseCallback(response);
|
||||
|
||||
// 完成调用
|
||||
this.completeRpcCall(pendingCall.call, response);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.emit('rpc-error', error as Error, undefined, client.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 ClientRpc(从服务端向客户端发送)
|
||||
*/
|
||||
async callClientRpc(
|
||||
room: Room,
|
||||
networkId: number,
|
||||
componentType: string,
|
||||
methodName: string,
|
||||
parameters: NetworkValue[] = [],
|
||||
options: {
|
||||
targetFilter?: 'all' | 'others' | 'owner' | string[];
|
||||
requiresResponse?: boolean;
|
||||
timeout?: number;
|
||||
} = {}
|
||||
): Promise<RpcResponse[]> {
|
||||
const rpcCall: RpcCall = {
|
||||
id: uuidv4(),
|
||||
networkId,
|
||||
componentType,
|
||||
methodName,
|
||||
parameters,
|
||||
metadata: {
|
||||
methodName,
|
||||
rpcType: 'client-rpc',
|
||||
requiresAuth: false,
|
||||
reliable: true,
|
||||
requiresResponse: options.requiresResponse || false
|
||||
},
|
||||
senderId: 'server',
|
||||
requiresResponse: options.requiresResponse || false,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 确定目标客户端
|
||||
const targetClientIds = typeof options.targetFilter === 'string'
|
||||
? this.getClientRpcTargets(room, 'server', options.targetFilter)
|
||||
: options.targetFilter || [];
|
||||
|
||||
rpcCall.targetClientIds = targetClientIds;
|
||||
|
||||
// 发送到目标客户端
|
||||
await this.sendClientRpc(room, rpcCall, targetClientIds);
|
||||
|
||||
// 如果需要响应,等待响应
|
||||
if (options.requiresResponse) {
|
||||
return await this.waitForRpcResponses(rpcCall, targetClientIds, options.timeout);
|
||||
}
|
||||
|
||||
this.completeRpcCall(rpcCall);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端统计信息
|
||||
*/
|
||||
getClientRpcStats(clientId: string): {
|
||||
activeCalls: number;
|
||||
totalCalls: number;
|
||||
} {
|
||||
const state = this.clientStates.get(clientId);
|
||||
return {
|
||||
activeCalls: state?.activeCalls.size || 0,
|
||||
totalCalls: state?.rpcCount || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有客户端的 RPC 调用
|
||||
*/
|
||||
cancelClientRpcs(clientId: string): number {
|
||||
const state = this.clientStates.get(clientId);
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const cancelledCount = state.activeCalls.size;
|
||||
|
||||
// 取消所有活跃调用
|
||||
for (const call of state.activeCalls.values()) {
|
||||
this.completeRpcCall(call);
|
||||
}
|
||||
|
||||
state.activeCalls.clear();
|
||||
return cancelledCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁 RPC 系统
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
// 清除所有待处理的调用
|
||||
for (const pending of this.pendingCalls.values()) {
|
||||
clearTimeout(pending.timeoutTimer);
|
||||
}
|
||||
|
||||
this.clientStates.clear();
|
||||
this.pendingCalls.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器(每分钟清理一次)
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanup();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 RPC 权限
|
||||
*/
|
||||
private checkRpcPermission(
|
||||
client: ClientConnection,
|
||||
call: RpcCall,
|
||||
rpcType: 'client-rpc' | 'server-rpc'
|
||||
): boolean {
|
||||
// 基本权限检查
|
||||
if (!client.hasPermission('canSendRpc')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ServerRpc 额外权限检查
|
||||
if (rpcType === 'server-rpc' && call.metadata.requiresAuth) {
|
||||
if (!client.isAuthenticated) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 可以添加更多特定的权限检查逻辑
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 RPC 频率
|
||||
*/
|
||||
private checkRpcRate(clientId: string): boolean {
|
||||
if (!this.config.maxRpcRate || this.config.maxRpcRate <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let state = this.clientStates.get(clientId);
|
||||
|
||||
if (!state) {
|
||||
state = {
|
||||
clientId,
|
||||
activeCalls: new Map(),
|
||||
rpcCount: 1,
|
||||
rateResetTime: new Date(now.getTime() + 1000)
|
||||
};
|
||||
this.clientStates.set(clientId, state);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否需要重置计数
|
||||
if (now >= state.rateResetTime) {
|
||||
state.rpcCount = 1;
|
||||
state.rateResetTime = new Date(now.getTime() + 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
if (state.rpcCount >= this.config.maxRpcRate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.rpcCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 RPC 参数
|
||||
*/
|
||||
private validateRpcParameters(call: RpcCall): { valid: boolean; reason?: string } {
|
||||
// 检查参数数量
|
||||
if (call.parameters.length > 10) {
|
||||
return { valid: false, reason: 'Too many parameters' };
|
||||
}
|
||||
|
||||
// 检查每个参数的大小
|
||||
for (let i = 0; i < call.parameters.length; i++) {
|
||||
const param = call.parameters[i];
|
||||
try {
|
||||
const serialized = JSON.stringify(param);
|
||||
if (serialized.length > this.config.maxParameterSize!) {
|
||||
return { valid: false, reason: `Parameter ${i} is too large` };
|
||||
}
|
||||
} catch (error) {
|
||||
return { valid: false, reason: `Parameter ${i} is not serializable` };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ClientRpc 目标客户端
|
||||
*/
|
||||
private getClientRpcTargets(
|
||||
room: Room,
|
||||
senderId: string,
|
||||
targetFilter: string
|
||||
): string[] {
|
||||
const players = room.getPlayers();
|
||||
|
||||
switch (targetFilter) {
|
||||
case 'all':
|
||||
return players.map(p => p.client.id);
|
||||
|
||||
case 'others':
|
||||
return players
|
||||
.filter(p => p.client.id !== senderId)
|
||||
.map(p => p.client.id);
|
||||
|
||||
case 'owner':
|
||||
const owner = room.getOwner();
|
||||
return owner ? [owner.client.id] : [];
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 ClientRpc
|
||||
*/
|
||||
private async sendClientRpc(
|
||||
room: Room,
|
||||
call: RpcCall,
|
||||
targetClientIds: string[]
|
||||
): Promise<void> {
|
||||
const message: TransportMessage = {
|
||||
type: 'rpc',
|
||||
data: {
|
||||
action: 'client-rpc',
|
||||
callId: call.id,
|
||||
networkId: call.networkId,
|
||||
componentType: call.componentType,
|
||||
methodName: call.methodName,
|
||||
parameters: call.parameters,
|
||||
metadata: call.metadata as any,
|
||||
requiresResponse: call.requiresResponse,
|
||||
timestamp: call.timestamp.getTime()
|
||||
} as any
|
||||
};
|
||||
|
||||
// 发送给目标客户端
|
||||
const promises = targetClientIds.map(clientId =>
|
||||
room.sendToPlayer(clientId, message)
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 ServerRpc
|
||||
*/
|
||||
private async executeServerRpc(call: RpcCall): Promise<RpcResponse | null> {
|
||||
// 这里应该是实际的服务端方法执行逻辑
|
||||
// 在实际实现中,可能需要通过事件或回调来执行具体的方法
|
||||
|
||||
// 示例响应
|
||||
const response: RpcResponse = {
|
||||
callId: call.id,
|
||||
success: true,
|
||||
result: undefined, // 实际执行结果
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 RPC 响应
|
||||
*/
|
||||
private async sendRpcResponse(
|
||||
client: ClientConnection,
|
||||
response: RpcResponse
|
||||
): Promise<void> {
|
||||
const message: TransportMessage = {
|
||||
type: 'rpc',
|
||||
data: {
|
||||
action: 'rpc-response',
|
||||
...response
|
||||
} as any
|
||||
};
|
||||
|
||||
await client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 RPC 响应
|
||||
*/
|
||||
private async waitForRpcResponses(
|
||||
call: RpcCall,
|
||||
targetClientIds: string[],
|
||||
timeout?: number
|
||||
): Promise<RpcResponse[]> {
|
||||
return new Promise((resolve) => {
|
||||
const responses: RpcResponse[] = [];
|
||||
const responseTimeout = timeout || this.config.callTimeout!;
|
||||
let responseCount = 0;
|
||||
|
||||
const responseCallback = (response: RpcResponse) => {
|
||||
responses.push(response);
|
||||
responseCount++;
|
||||
|
||||
// 如果收到所有响应,立即resolve
|
||||
if (responseCount >= targetClientIds.length) {
|
||||
resolve(responses);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置超时
|
||||
const timeoutTimer = setTimeout(() => {
|
||||
resolve(responses); // 返回已收到的响应
|
||||
this.emit('rpc-timeout', call.id);
|
||||
}, responseTimeout);
|
||||
|
||||
// 注册待处理的响应
|
||||
this.pendingCalls.set(call.id, {
|
||||
call,
|
||||
timeoutTimer,
|
||||
responseCallback
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录活跃调用
|
||||
*/
|
||||
private recordActiveCall(clientId: string, call: RpcCall): void {
|
||||
let state = this.clientStates.get(clientId);
|
||||
if (!state) {
|
||||
state = {
|
||||
clientId,
|
||||
activeCalls: new Map(),
|
||||
rpcCount: 0,
|
||||
rateResetTime: new Date()
|
||||
};
|
||||
this.clientStates.set(clientId, state);
|
||||
}
|
||||
|
||||
state.activeCalls.set(call.id, call);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成 RPC 调用
|
||||
*/
|
||||
private completeRpcCall(call: RpcCall, response?: RpcResponse): void {
|
||||
// 从活跃调用中移除
|
||||
const state = this.clientStates.get(call.senderId);
|
||||
if (state) {
|
||||
state.activeCalls.delete(call.id);
|
||||
}
|
||||
|
||||
// 触发完成事件
|
||||
this.emit('rpc-completed', call, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的调用和状态
|
||||
*/
|
||||
private cleanup(): void {
|
||||
const now = new Date();
|
||||
let cleanedCalls = 0;
|
||||
let cleanedStates = 0;
|
||||
|
||||
// 清理过期的待处理调用
|
||||
for (const [callId, pending] of this.pendingCalls.entries()) {
|
||||
if (pending.call.expiresAt && pending.call.expiresAt < now) {
|
||||
clearTimeout(pending.timeoutTimer);
|
||||
this.pendingCalls.delete(callId);
|
||||
cleanedCalls++;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空的客户端状态
|
||||
for (const [clientId, state] of this.clientStates.entries()) {
|
||||
if (state.activeCalls.size === 0 &&
|
||||
now.getTime() - state.rateResetTime.getTime() > 60000) {
|
||||
this.clientStates.delete(clientId);
|
||||
cleanedStates++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCalls > 0 || cleanedStates > 0) {
|
||||
console.log(`RPC cleanup: ${cleanedCalls} calls, ${cleanedStates} states`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof RpcSystemEvents>(event: K, listener: RpcSystemEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof RpcSystemEvents>(event: K, ...args: Parameters<RpcSystemEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,587 +0,0 @@
|
||||
/**
|
||||
* SyncVar 同步系统
|
||||
*
|
||||
* 处理服务端的 SyncVar 同步逻辑、权限验证、数据传播等
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
NetworkValue,
|
||||
SyncVarMetadata,
|
||||
NetworkSerializer
|
||||
} from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { Room } from '../rooms/Room';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* SyncVar 更改记录
|
||||
*/
|
||||
export interface SyncVarChange {
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 属性名 */
|
||||
propertyName: string;
|
||||
/** 旧值 */
|
||||
oldValue: NetworkValue;
|
||||
/** 新值 */
|
||||
newValue: NetworkValue;
|
||||
/** 元数据 */
|
||||
metadata: SyncVarMetadata;
|
||||
/** 发送者客户端ID */
|
||||
senderId: string;
|
||||
/** 时间戳 */
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 同步配置
|
||||
*/
|
||||
export interface SyncVarSystemConfig {
|
||||
/** 批量同步间隔(毫秒) */
|
||||
batchInterval?: number;
|
||||
/** 单次批量最大数量 */
|
||||
maxBatchSize?: number;
|
||||
/** 是否启用增量同步 */
|
||||
enableDeltaSync?: boolean;
|
||||
/** 是否启用权限检查 */
|
||||
enablePermissionCheck?: boolean;
|
||||
/** 是否启用数据验证 */
|
||||
enableDataValidation?: boolean;
|
||||
/** 最大同步频率(次/秒) */
|
||||
maxSyncRate?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络对象状态
|
||||
*/
|
||||
export interface NetworkObjectState {
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 拥有者客户端ID */
|
||||
ownerId: string;
|
||||
/** 组件状态 */
|
||||
components: Map<string, Map<string, NetworkValue>>;
|
||||
/** 最后更新时间 */
|
||||
lastUpdateTime: Date;
|
||||
/** 权威状态 */
|
||||
hasAuthority: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 系统事件
|
||||
*/
|
||||
export interface SyncVarSystemEvents {
|
||||
/** SyncVar 值变化 */
|
||||
'syncvar-changed': (change: SyncVarChange) => void;
|
||||
/** 同步批次完成 */
|
||||
'batch-synced': (changes: SyncVarChange[], targetClients: string[]) => void;
|
||||
/** 权限验证失败 */
|
||||
'permission-denied': (clientId: string, change: SyncVarChange) => void;
|
||||
/** 数据验证失败 */
|
||||
'validation-failed': (clientId: string, change: SyncVarChange, reason: string) => void;
|
||||
/** 同步错误 */
|
||||
'sync-error': (error: Error, clientId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端同步状态
|
||||
*/
|
||||
interface ClientSyncState {
|
||||
/** 客户端ID */
|
||||
clientId: string;
|
||||
/** 待同步的变化列表 */
|
||||
pendingChanges: SyncVarChange[];
|
||||
/** 最后同步时间 */
|
||||
lastSyncTime: Date;
|
||||
/** 同步频率限制 */
|
||||
syncCount: number;
|
||||
/** 频率重置时间 */
|
||||
rateResetTime: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncVar 同步系统
|
||||
*/
|
||||
export class SyncVarSystem extends EventEmitter {
|
||||
private config: SyncVarSystemConfig;
|
||||
private networkObjects = new Map<number, NetworkObjectState>();
|
||||
private clientSyncStates = new Map<string, ClientSyncState>();
|
||||
private serializer: NetworkSerializer;
|
||||
private batchTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: SyncVarSystemConfig = {}) {
|
||||
super();
|
||||
|
||||
this.config = {
|
||||
batchInterval: 50, // 50ms批量间隔
|
||||
maxBatchSize: 100,
|
||||
enableDeltaSync: true,
|
||||
enablePermissionCheck: true,
|
||||
enableDataValidation: true,
|
||||
maxSyncRate: 60, // 60次/秒
|
||||
...config
|
||||
};
|
||||
|
||||
this.serializer = new NetworkSerializer();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册网络对象
|
||||
*/
|
||||
registerNetworkObject(
|
||||
networkId: number,
|
||||
ownerId: string,
|
||||
hasAuthority: boolean = true
|
||||
): void {
|
||||
if (this.networkObjects.has(networkId)) {
|
||||
console.warn(`Network object ${networkId} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
const networkObject: NetworkObjectState = {
|
||||
networkId,
|
||||
ownerId,
|
||||
components: new Map(),
|
||||
lastUpdateTime: new Date(),
|
||||
hasAuthority
|
||||
};
|
||||
|
||||
this.networkObjects.set(networkId, networkObject);
|
||||
console.log(`Network object registered: ${networkId} owned by ${ownerId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销网络对象
|
||||
*/
|
||||
unregisterNetworkObject(networkId: number): boolean {
|
||||
const removed = this.networkObjects.delete(networkId);
|
||||
if (removed) {
|
||||
console.log(`Network object unregistered: ${networkId}`);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象
|
||||
*/
|
||||
getNetworkObject(networkId: number): NetworkObjectState | undefined {
|
||||
return this.networkObjects.get(networkId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SyncVar 变化消息
|
||||
*/
|
||||
async handleSyncVarChange(
|
||||
client: ClientConnection,
|
||||
message: TransportMessage,
|
||||
room?: Room
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data = message.data as any;
|
||||
const {
|
||||
networkId,
|
||||
componentType,
|
||||
propertyName,
|
||||
oldValue,
|
||||
newValue,
|
||||
metadata
|
||||
} = data;
|
||||
|
||||
// 创建变化记录
|
||||
const change: SyncVarChange = {
|
||||
networkId,
|
||||
componentType,
|
||||
propertyName,
|
||||
oldValue,
|
||||
newValue,
|
||||
metadata,
|
||||
senderId: client.id,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
// 权限检查
|
||||
if (this.config.enablePermissionCheck) {
|
||||
if (!this.checkSyncVarPermission(client, change)) {
|
||||
this.emit('permission-denied', client.id, change);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 频率限制检查
|
||||
if (!this.checkSyncRate(client.id)) {
|
||||
console.warn(`SyncVar rate limit exceeded for client ${client.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据验证
|
||||
if (this.config.enableDataValidation) {
|
||||
const validationResult = this.validateSyncVarData(change);
|
||||
if (!validationResult.valid) {
|
||||
this.emit('validation-failed', client.id, change, validationResult.reason!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新网络对象状态
|
||||
this.updateNetworkObjectState(change);
|
||||
|
||||
// 触发变化事件
|
||||
this.emit('syncvar-changed', change);
|
||||
|
||||
// 添加到待同步列表
|
||||
if (room) {
|
||||
this.addToBatchSync(change, room);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.emit('sync-error', error as Error, client.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象的完整状态
|
||||
*/
|
||||
getNetworkObjectSnapshot(networkId: number): Record<string, any> | null {
|
||||
const networkObject = this.networkObjects.get(networkId);
|
||||
if (!networkObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot: Record<string, any> = {};
|
||||
|
||||
for (const [componentType, componentData] of networkObject.components) {
|
||||
snapshot[componentType] = {};
|
||||
for (const [propertyName, value] of componentData) {
|
||||
snapshot[componentType][propertyName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向客户端发送网络对象快照
|
||||
*/
|
||||
async sendNetworkObjectSnapshot(
|
||||
client: ClientConnection,
|
||||
networkId: number
|
||||
): Promise<boolean> {
|
||||
const snapshot = this.getNetworkObjectSnapshot(networkId);
|
||||
if (!snapshot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const message: TransportMessage = {
|
||||
type: 'syncvar',
|
||||
data: {
|
||||
action: 'snapshot',
|
||||
networkId,
|
||||
snapshot
|
||||
}
|
||||
};
|
||||
|
||||
return await client.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步所有网络对象给新客户端
|
||||
*/
|
||||
async syncAllNetworkObjects(client: ClientConnection, room: Room): Promise<number> {
|
||||
let syncedCount = 0;
|
||||
|
||||
for (const networkObject of this.networkObjects.values()) {
|
||||
// 检查客户端是否有权限看到这个网络对象
|
||||
if (this.canClientSeeNetworkObject(client.id, networkObject)) {
|
||||
const success = await this.sendNetworkObjectSnapshot(client, networkObject.networkId);
|
||||
if (success) {
|
||||
syncedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Synced ${syncedCount} network objects to client ${client.id}`);
|
||||
return syncedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网络对象拥有者
|
||||
*/
|
||||
setNetworkObjectOwner(networkId: number, newOwnerId: string): boolean {
|
||||
const networkObject = this.networkObjects.get(networkId);
|
||||
if (!networkObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldOwnerId = networkObject.ownerId;
|
||||
networkObject.ownerId = newOwnerId;
|
||||
networkObject.lastUpdateTime = new Date();
|
||||
|
||||
console.log(`Network object ${networkId} ownership changed: ${oldOwnerId} -> ${newOwnerId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络对象拥有者
|
||||
*/
|
||||
getNetworkObjectOwner(networkId: number): string | undefined {
|
||||
const networkObject = this.networkObjects.get(networkId);
|
||||
return networkObject?.ownerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁 SyncVar 系统
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.batchTimer) {
|
||||
clearInterval(this.batchTimer);
|
||||
this.batchTimer = null;
|
||||
}
|
||||
|
||||
this.networkObjects.clear();
|
||||
this.clientSyncStates.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动批量同步定时器
|
||||
if (this.config.batchInterval && this.config.batchInterval > 0) {
|
||||
this.batchTimer = setInterval(() => {
|
||||
this.processBatchSync();
|
||||
}, this.config.batchInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 SyncVar 权限
|
||||
*/
|
||||
private checkSyncVarPermission(client: ClientConnection, change: SyncVarChange): boolean {
|
||||
// 检查客户端是否有网络同步权限
|
||||
if (!client.hasPermission('canSyncVars')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取网络对象
|
||||
const networkObject = this.networkObjects.get(change.networkId);
|
||||
if (!networkObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查权威权限
|
||||
if (change.metadata.authorityOnly) {
|
||||
// 只有网络对象拥有者或有权威权限的客户端可以修改
|
||||
return networkObject.ownerId === client.id || networkObject.hasAuthority;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查同步频率
|
||||
*/
|
||||
private checkSyncRate(clientId: string): boolean {
|
||||
if (!this.config.maxSyncRate || this.config.maxSyncRate <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let syncState = this.clientSyncStates.get(clientId);
|
||||
|
||||
if (!syncState) {
|
||||
syncState = {
|
||||
clientId,
|
||||
pendingChanges: [],
|
||||
lastSyncTime: now,
|
||||
syncCount: 1,
|
||||
rateResetTime: new Date(now.getTime() + 1000) // 1秒后重置
|
||||
};
|
||||
this.clientSyncStates.set(clientId, syncState);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否需要重置计数
|
||||
if (now >= syncState.rateResetTime) {
|
||||
syncState.syncCount = 1;
|
||||
syncState.rateResetTime = new Date(now.getTime() + 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
if (syncState.syncCount >= this.config.maxSyncRate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
syncState.syncCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 SyncVar 数据
|
||||
*/
|
||||
private validateSyncVarData(change: SyncVarChange): { valid: boolean; reason?: string } {
|
||||
// 基本类型检查
|
||||
if (change.newValue === null || change.newValue === undefined) {
|
||||
return { valid: false, reason: 'Value cannot be null or undefined' };
|
||||
}
|
||||
|
||||
// 检查数据大小(防止过大的数据)
|
||||
try {
|
||||
const serialized = JSON.stringify(change.newValue);
|
||||
if (serialized.length > 65536) { // 64KB限制
|
||||
return { valid: false, reason: 'Data too large' };
|
||||
}
|
||||
} catch (error) {
|
||||
return { valid: false, reason: 'Data is not serializable' };
|
||||
}
|
||||
|
||||
// 可以添加更多特定的验证逻辑
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网络对象状态
|
||||
*/
|
||||
private updateNetworkObjectState(change: SyncVarChange): void {
|
||||
let networkObject = this.networkObjects.get(change.networkId);
|
||||
|
||||
if (!networkObject) {
|
||||
// 如果网络对象不存在,创建一个新的(可能是客户端创建的)
|
||||
networkObject = {
|
||||
networkId: change.networkId,
|
||||
ownerId: change.senderId,
|
||||
components: new Map(),
|
||||
lastUpdateTime: new Date(),
|
||||
hasAuthority: true
|
||||
};
|
||||
this.networkObjects.set(change.networkId, networkObject);
|
||||
}
|
||||
|
||||
// 获取或创建组件数据
|
||||
let componentData = networkObject.components.get(change.componentType);
|
||||
if (!componentData) {
|
||||
componentData = new Map();
|
||||
networkObject.components.set(change.componentType, componentData);
|
||||
}
|
||||
|
||||
// 更新属性值
|
||||
componentData.set(change.propertyName, change.newValue);
|
||||
networkObject.lastUpdateTime = change.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到批量同步
|
||||
*/
|
||||
private addToBatchSync(change: SyncVarChange, room: Room): void {
|
||||
// 获取房间内需要同步的客户端
|
||||
const roomPlayers = room.getPlayers();
|
||||
const targetClientIds = roomPlayers
|
||||
.filter(player => player.client.id !== change.senderId) // 不发送给发送者
|
||||
.map(player => player.client.id);
|
||||
|
||||
// 为每个目标客户端添加变化记录
|
||||
for (const clientId of targetClientIds) {
|
||||
let syncState = this.clientSyncStates.get(clientId);
|
||||
if (!syncState) {
|
||||
syncState = {
|
||||
clientId,
|
||||
pendingChanges: [],
|
||||
lastSyncTime: new Date(),
|
||||
syncCount: 0,
|
||||
rateResetTime: new Date()
|
||||
};
|
||||
this.clientSyncStates.set(clientId, syncState);
|
||||
}
|
||||
|
||||
syncState.pendingChanges.push(change);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理批量同步
|
||||
*/
|
||||
private async processBatchSync(): Promise<void> {
|
||||
const syncPromises: Promise<void>[] = [];
|
||||
|
||||
for (const [clientId, syncState] of this.clientSyncStates.entries()) {
|
||||
if (syncState.pendingChanges.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取要同步的变化(限制批量大小)
|
||||
const changesToSync = syncState.pendingChanges.splice(
|
||||
0,
|
||||
this.config.maxBatchSize
|
||||
);
|
||||
|
||||
if (changesToSync.length > 0) {
|
||||
syncPromises.push(this.sendBatchChanges(clientId, changesToSync));
|
||||
}
|
||||
}
|
||||
|
||||
if (syncPromises.length > 0) {
|
||||
await Promise.allSettled(syncPromises);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送批量变化
|
||||
*/
|
||||
private async sendBatchChanges(clientId: string, changes: SyncVarChange[]): Promise<void> {
|
||||
try {
|
||||
// 这里需要获取客户端连接,实际实现中可能需要从外部传入
|
||||
// 为了简化,这里假设有一个方法可以获取客户端连接
|
||||
// 实际使用时,可能需要通过回调或事件来发送消息
|
||||
|
||||
const message: TransportMessage = {
|
||||
type: 'syncvar',
|
||||
data: {
|
||||
action: 'batch-update',
|
||||
changes: changes.map(change => ({
|
||||
networkId: change.networkId,
|
||||
componentType: change.componentType,
|
||||
propertyName: change.propertyName,
|
||||
newValue: change.newValue,
|
||||
metadata: change.metadata as any,
|
||||
timestamp: change.timestamp.getTime()
|
||||
}))
|
||||
} as any
|
||||
};
|
||||
|
||||
// 这里需要实际的发送逻辑
|
||||
// 在实际使用中,应该通过事件或回调来发送消息
|
||||
this.emit('batch-synced', changes, [clientId]);
|
||||
|
||||
} catch (error) {
|
||||
this.emit('sync-error', error as Error, clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否可以看到网络对象
|
||||
*/
|
||||
private canClientSeeNetworkObject(clientId: string, networkObject: NetworkObjectState): boolean {
|
||||
// 基本实现:客户端可以看到自己拥有的对象和公共对象
|
||||
// 实际实现中可能需要更复杂的可见性逻辑
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件监听
|
||||
*/
|
||||
override on<K extends keyof SyncVarSystemEvents>(event: K, listener: SyncVarSystemEvents[K]): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型安全的事件触发
|
||||
*/
|
||||
override emit<K extends keyof SyncVarSystemEvents>(event: K, ...args: Parameters<SyncVarSystemEvents[K]>): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 系统模块导出
|
||||
*/
|
||||
|
||||
export * from './SyncVarSystem';
|
||||
export * from './RpcSystem';
|
||||
@@ -1,572 +0,0 @@
|
||||
/**
|
||||
* 消息验证器
|
||||
*
|
||||
* 验证网络消息的格式、大小、内容等
|
||||
*/
|
||||
|
||||
import { NetworkValue } from '@esengine/ecs-framework-network-shared';
|
||||
import { TransportMessage } from '../core/Transport';
|
||||
|
||||
/**
|
||||
* 验证结果
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
/** 是否有效 */
|
||||
valid: boolean;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 错误代码 */
|
||||
errorCode?: string;
|
||||
/** 详细信息 */
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
*/
|
||||
export interface ValidationConfig {
|
||||
/** 最大消息大小(字节) */
|
||||
maxMessageSize?: number;
|
||||
/** 最大数组长度 */
|
||||
maxArrayLength?: number;
|
||||
/** 最大对象深度 */
|
||||
maxObjectDepth?: number;
|
||||
/** 最大字符串长度 */
|
||||
maxStringLength?: number;
|
||||
/** 允许的消息类型 */
|
||||
allowedMessageTypes?: string[];
|
||||
/** 是否允许null值 */
|
||||
allowNullValues?: boolean;
|
||||
/** 是否允许undefined值 */
|
||||
allowUndefinedValues?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
export interface ValidationRule {
|
||||
/** 规则名称 */
|
||||
name: string;
|
||||
/** 验证函数 */
|
||||
validate: (value: any, context: ValidationContext) => ValidationResult;
|
||||
/** 是否必需 */
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证上下文
|
||||
*/
|
||||
export interface ValidationContext {
|
||||
/** 当前路径 */
|
||||
path: string[];
|
||||
/** 当前深度 */
|
||||
depth: number;
|
||||
/** 配置 */
|
||||
config: ValidationConfig;
|
||||
/** 消息类型 */
|
||||
messageType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息验证器
|
||||
*/
|
||||
export class MessageValidator {
|
||||
private config: ValidationConfig;
|
||||
private customRules = new Map<string, ValidationRule>();
|
||||
|
||||
constructor(config: ValidationConfig = {}) {
|
||||
this.config = {
|
||||
maxMessageSize: 1024 * 1024, // 1MB
|
||||
maxArrayLength: 1000,
|
||||
maxObjectDepth: 10,
|
||||
maxStringLength: 10000,
|
||||
allowedMessageTypes: ['rpc', 'syncvar', 'system', 'custom'],
|
||||
allowNullValues: true,
|
||||
allowUndefinedValues: false,
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证传输消息
|
||||
*/
|
||||
validateMessage(message: TransportMessage): ValidationResult {
|
||||
try {
|
||||
// 基本结构验证
|
||||
const structureResult = this.validateMessageStructure(message);
|
||||
if (!structureResult.valid) {
|
||||
return structureResult;
|
||||
}
|
||||
|
||||
// 消息大小验证
|
||||
const sizeResult = this.validateMessageSize(message);
|
||||
if (!sizeResult.valid) {
|
||||
return sizeResult;
|
||||
}
|
||||
|
||||
// 消息类型验证
|
||||
const typeResult = this.validateMessageType(message);
|
||||
if (!typeResult.valid) {
|
||||
return typeResult;
|
||||
}
|
||||
|
||||
// 数据内容验证
|
||||
const dataResult = this.validateMessageData(message);
|
||||
if (!dataResult.valid) {
|
||||
return dataResult;
|
||||
}
|
||||
|
||||
// 自定义规则验证
|
||||
const customResult = this.validateCustomRules(message);
|
||||
if (!customResult.valid) {
|
||||
return customResult;
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'VALIDATION_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证网络值
|
||||
*/
|
||||
validateNetworkValue(value: NetworkValue, context?: Partial<ValidationContext>): ValidationResult {
|
||||
const fullContext: ValidationContext = {
|
||||
path: [],
|
||||
depth: 0,
|
||||
config: this.config,
|
||||
...context
|
||||
};
|
||||
|
||||
return this.validateValue(value, fullContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义验证规则
|
||||
*/
|
||||
addValidationRule(rule: ValidationRule): void {
|
||||
this.customRules.set(rule.name, rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除自定义验证规则
|
||||
*/
|
||||
removeValidationRule(ruleName: string): boolean {
|
||||
return this.customRules.delete(ruleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有自定义规则
|
||||
*/
|
||||
getCustomRules(): ValidationRule[] {
|
||||
return Array.from(this.customRules.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息结构
|
||||
*/
|
||||
private validateMessageStructure(message: TransportMessage): ValidationResult {
|
||||
// 检查必需字段
|
||||
if (!message.type) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Message type is required',
|
||||
errorCode: 'MISSING_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
if (message.data === undefined) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Message data is required',
|
||||
errorCode: 'MISSING_DATA'
|
||||
};
|
||||
}
|
||||
|
||||
// 检查字段类型
|
||||
if (typeof message.type !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Message type must be a string',
|
||||
errorCode: 'INVALID_TYPE_FORMAT'
|
||||
};
|
||||
}
|
||||
|
||||
// 检查可选字段
|
||||
if (message.senderId && typeof message.senderId !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Sender ID must be a string',
|
||||
errorCode: 'INVALID_SENDER_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (message.targetId && typeof message.targetId !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Target ID must be a string',
|
||||
errorCode: 'INVALID_TARGET_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (message.reliable !== undefined && typeof message.reliable !== 'boolean') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Reliable flag must be a boolean',
|
||||
errorCode: 'INVALID_RELIABLE_FLAG'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息大小
|
||||
*/
|
||||
private validateMessageSize(message: TransportMessage): ValidationResult {
|
||||
try {
|
||||
const serialized = JSON.stringify(message);
|
||||
const size = new TextEncoder().encode(serialized).length;
|
||||
|
||||
if (size > this.config.maxMessageSize!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Message size (${size} bytes) exceeds maximum (${this.config.maxMessageSize} bytes)`,
|
||||
errorCode: 'MESSAGE_TOO_LARGE',
|
||||
details: { actualSize: size, maxSize: this.config.maxMessageSize }
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Failed to serialize message for size validation',
|
||||
errorCode: 'SERIALIZATION_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息类型
|
||||
*/
|
||||
private validateMessageType(message: TransportMessage): ValidationResult {
|
||||
if (!this.config.allowedMessageTypes!.includes(message.type)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Message type '${message.type}' is not allowed`,
|
||||
errorCode: 'INVALID_MESSAGE_TYPE',
|
||||
details: {
|
||||
messageType: message.type,
|
||||
allowedTypes: this.config.allowedMessageTypes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息数据
|
||||
*/
|
||||
private validateMessageData(message: TransportMessage): ValidationResult {
|
||||
const context: ValidationContext = {
|
||||
path: ['data'],
|
||||
depth: 0,
|
||||
config: this.config,
|
||||
messageType: message.type
|
||||
};
|
||||
|
||||
return this.validateValue(message.data, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证值
|
||||
*/
|
||||
private validateValue(value: any, context: ValidationContext): ValidationResult {
|
||||
// 深度检查
|
||||
if (context.depth > this.config.maxObjectDepth!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Object depth (${context.depth}) exceeds maximum (${this.config.maxObjectDepth})`,
|
||||
errorCode: 'OBJECT_TOO_DEEP',
|
||||
details: { path: context.path.join('.'), depth: context.depth }
|
||||
};
|
||||
}
|
||||
|
||||
// null/undefined 检查
|
||||
if (value === null) {
|
||||
if (!this.config.allowNullValues) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Null values are not allowed',
|
||||
errorCode: 'NULL_NOT_ALLOWED',
|
||||
details: { path: context.path.join('.') }
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
if (!this.config.allowUndefinedValues) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Undefined values are not allowed',
|
||||
errorCode: 'UNDEFINED_NOT_ALLOWED',
|
||||
details: { path: context.path.join('.') }
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 根据类型验证
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return this.validateString(value, context);
|
||||
|
||||
case 'number':
|
||||
return this.validateNumber(value, context);
|
||||
|
||||
case 'boolean':
|
||||
return { valid: true };
|
||||
|
||||
case 'object':
|
||||
if (Array.isArray(value)) {
|
||||
return this.validateArray(value, context);
|
||||
} else {
|
||||
return this.validateObject(value, context);
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
valid: false,
|
||||
error: `Unsupported value type: ${typeof value}`,
|
||||
errorCode: 'UNSUPPORTED_TYPE',
|
||||
details: { path: context.path.join('.'), type: typeof value }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证字符串
|
||||
*/
|
||||
private validateString(value: string, context: ValidationContext): ValidationResult {
|
||||
if (value.length > this.config.maxStringLength!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `String length (${value.length}) exceeds maximum (${this.config.maxStringLength})`,
|
||||
errorCode: 'STRING_TOO_LONG',
|
||||
details: {
|
||||
path: context.path.join('.'),
|
||||
actualLength: value.length,
|
||||
maxLength: this.config.maxStringLength
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数字
|
||||
*/
|
||||
private validateNumber(value: number, context: ValidationContext): ValidationResult {
|
||||
// 检查是否为有效数字
|
||||
if (!Number.isFinite(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Number must be finite',
|
||||
errorCode: 'INVALID_NUMBER',
|
||||
details: { path: context.path.join('.'), value }
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数组
|
||||
*/
|
||||
private validateArray(value: any[], context: ValidationContext): ValidationResult {
|
||||
// 长度检查
|
||||
if (value.length > this.config.maxArrayLength!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Array length (${value.length}) exceeds maximum (${this.config.maxArrayLength})`,
|
||||
errorCode: 'ARRAY_TOO_LONG',
|
||||
details: {
|
||||
path: context.path.join('.'),
|
||||
actualLength: value.length,
|
||||
maxLength: this.config.maxArrayLength
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 验证每个元素
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const elementContext: ValidationContext = {
|
||||
...context,
|
||||
path: [...context.path, `[${i}]`],
|
||||
depth: context.depth + 1
|
||||
};
|
||||
|
||||
const result = this.validateValue(value[i], elementContext);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证对象
|
||||
*/
|
||||
private validateObject(value: Record<string, any>, context: ValidationContext): ValidationResult {
|
||||
// 验证每个属性
|
||||
for (const [key, propertyValue] of Object.entries(value)) {
|
||||
const propertyContext: ValidationContext = {
|
||||
...context,
|
||||
path: [...context.path, key],
|
||||
depth: context.depth + 1
|
||||
};
|
||||
|
||||
const result = this.validateValue(propertyValue, propertyContext);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证自定义规则
|
||||
*/
|
||||
private validateCustomRules(message: TransportMessage): ValidationResult {
|
||||
for (const rule of this.customRules.values()) {
|
||||
const context: ValidationContext = {
|
||||
path: [],
|
||||
depth: 0,
|
||||
config: this.config,
|
||||
messageType: message.type
|
||||
};
|
||||
|
||||
const result = rule.validate(message, context);
|
||||
if (!result.valid) {
|
||||
return {
|
||||
...result,
|
||||
details: {
|
||||
...result.details,
|
||||
rule: rule.name
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义验证规则
|
||||
*/
|
||||
export const DefaultValidationRules = {
|
||||
/**
|
||||
* RPC 消息验证规则
|
||||
*/
|
||||
RpcMessage: {
|
||||
name: 'RpcMessage',
|
||||
validate: (message: TransportMessage, context: ValidationContext): ValidationResult => {
|
||||
if (message.type !== 'rpc') {
|
||||
return { valid: true }; // 不是 RPC 消息,跳过验证
|
||||
}
|
||||
|
||||
const data = message.data as any;
|
||||
|
||||
// 检查必需字段
|
||||
if (!data.networkId || typeof data.networkId !== 'number') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC message must have a valid networkId',
|
||||
errorCode: 'RPC_INVALID_NETWORK_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.componentType || typeof data.componentType !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC message must have a valid componentType',
|
||||
errorCode: 'RPC_INVALID_COMPONENT_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.methodName || typeof data.methodName !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC message must have a valid methodName',
|
||||
errorCode: 'RPC_INVALID_METHOD_NAME'
|
||||
};
|
||||
}
|
||||
|
||||
// 检查参数数组
|
||||
if (data.parameters && !Array.isArray(data.parameters)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'RPC parameters must be an array',
|
||||
errorCode: 'RPC_INVALID_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
} as ValidationRule,
|
||||
|
||||
/**
|
||||
* SyncVar 消息验证规则
|
||||
*/
|
||||
SyncVarMessage: {
|
||||
name: 'SyncVarMessage',
|
||||
validate: (message: TransportMessage, context: ValidationContext): ValidationResult => {
|
||||
if (message.type !== 'syncvar') {
|
||||
return { valid: true }; // 不是 SyncVar 消息,跳过验证
|
||||
}
|
||||
|
||||
const data = message.data as any;
|
||||
|
||||
// 检查必需字段
|
||||
if (!data.networkId || typeof data.networkId !== 'number') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'SyncVar message must have a valid networkId',
|
||||
errorCode: 'SYNCVAR_INVALID_NETWORK_ID'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.componentType || typeof data.componentType !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'SyncVar message must have a valid componentType',
|
||||
errorCode: 'SYNCVAR_INVALID_COMPONENT_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.propertyName || typeof data.propertyName !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'SyncVar message must have a valid propertyName',
|
||||
errorCode: 'SYNCVAR_INVALID_PROPERTY_NAME'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
} as ValidationRule
|
||||
};
|
||||
@@ -1,776 +0,0 @@
|
||||
/**
|
||||
* RPC 验证器
|
||||
*
|
||||
* 专门用于验证 RPC 调用的参数、权限、频率等
|
||||
*/
|
||||
|
||||
import { NetworkValue, RpcMetadata } from '@esengine/ecs-framework-network-shared';
|
||||
import { ClientConnection } from '../core/ClientConnection';
|
||||
import { ValidationResult } from './MessageValidator';
|
||||
|
||||
/**
|
||||
* RPC 验证配置
|
||||
*/
|
||||
export interface RpcValidationConfig {
|
||||
/** 最大参数数量 */
|
||||
maxParameterCount?: number;
|
||||
/** 单个参数最大大小(字节) */
|
||||
maxParameterSize?: number;
|
||||
/** 允许的参数类型 */
|
||||
allowedParameterTypes?: string[];
|
||||
/** 方法名黑名单 */
|
||||
blacklistedMethods?: string[];
|
||||
/** 方法名白名单 */
|
||||
whitelistedMethods?: string[];
|
||||
/** 是否启用参数类型检查 */
|
||||
enableTypeCheck?: boolean;
|
||||
/** 是否启用参数内容过滤 */
|
||||
enableContentFilter?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 调用上下文
|
||||
*/
|
||||
export interface RpcCallContext {
|
||||
/** 客户端连接 */
|
||||
client: ClientConnection;
|
||||
/** 网络对象ID */
|
||||
networkId: number;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 参数列表 */
|
||||
parameters: NetworkValue[];
|
||||
/** RPC 元数据 */
|
||||
metadata: RpcMetadata;
|
||||
/** RPC 类型 */
|
||||
rpcType: 'client-rpc' | 'server-rpc';
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数类型定义
|
||||
*/
|
||||
export interface ParameterTypeDefinition {
|
||||
/** 参数名 */
|
||||
name: string;
|
||||
/** 参数类型 */
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
|
||||
/** 是否必需 */
|
||||
required?: boolean;
|
||||
/** 最小值/长度 */
|
||||
min?: number;
|
||||
/** 最大值/长度 */
|
||||
max?: number;
|
||||
/** 允许的值列表 */
|
||||
allowedValues?: NetworkValue[];
|
||||
/** 正则表达式(仅用于字符串) */
|
||||
pattern?: RegExp;
|
||||
/** 自定义验证函数 */
|
||||
customValidator?: (value: NetworkValue) => ValidationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法签名定义
|
||||
*/
|
||||
export interface MethodSignature {
|
||||
/** 方法名 */
|
||||
methodName: string;
|
||||
/** 组件类型 */
|
||||
componentType: string;
|
||||
/** 参数定义 */
|
||||
parameters: ParameterTypeDefinition[];
|
||||
/** 返回值类型 */
|
||||
returnType?: string;
|
||||
/** 是否需要权限验证 */
|
||||
requiresAuth?: boolean;
|
||||
/** 所需权限 */
|
||||
requiredPermissions?: string[];
|
||||
/** 频率限制(调用/分钟) */
|
||||
rateLimit?: number;
|
||||
/** 自定义验证函数 */
|
||||
customValidator?: (context: RpcCallContext) => ValidationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 频率跟踪
|
||||
*/
|
||||
interface RpcRateTracker {
|
||||
/** 客户端ID */
|
||||
clientId: string;
|
||||
/** 方法调用计数 */
|
||||
methodCalls: Map<string, { count: number; resetTime: Date }>;
|
||||
/** 最后更新时间 */
|
||||
lastUpdate: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 验证器
|
||||
*/
|
||||
export class RpcValidator {
|
||||
private config: RpcValidationConfig;
|
||||
private methodSignatures = new Map<string, MethodSignature>();
|
||||
private rateTrackers = new Map<string, RpcRateTracker>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(config: RpcValidationConfig = {}) {
|
||||
this.config = {
|
||||
maxParameterCount: 10,
|
||||
maxParameterSize: 65536, // 64KB
|
||||
allowedParameterTypes: ['string', 'number', 'boolean', 'object', 'array'],
|
||||
blacklistedMethods: [],
|
||||
whitelistedMethods: [],
|
||||
enableTypeCheck: true,
|
||||
enableContentFilter: true,
|
||||
...config
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 RPC 调用
|
||||
*/
|
||||
validateRpcCall(context: RpcCallContext): ValidationResult {
|
||||
try {
|
||||
// 基本验证
|
||||
const basicResult = this.validateBasicRpcCall(context);
|
||||
if (!basicResult.valid) {
|
||||
return basicResult;
|
||||
}
|
||||
|
||||
// 方法名验证
|
||||
const methodResult = this.validateMethodName(context);
|
||||
if (!methodResult.valid) {
|
||||
return methodResult;
|
||||
}
|
||||
|
||||
// 权限验证
|
||||
const permissionResult = this.validateRpcPermissions(context);
|
||||
if (!permissionResult.valid) {
|
||||
return permissionResult;
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
const parameterResult = this.validateParameters(context);
|
||||
if (!parameterResult.valid) {
|
||||
return parameterResult;
|
||||
}
|
||||
|
||||
// 频率限制验证
|
||||
const rateResult = this.validateRateLimit(context);
|
||||
if (!rateResult.valid) {
|
||||
return rateResult;
|
||||
}
|
||||
|
||||
// 签名验证(如果有定义)
|
||||
const signatureResult = this.validateMethodSignature(context);
|
||||
if (!signatureResult.valid) {
|
||||
return signatureResult;
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: (error as Error).message,
|
||||
errorCode: 'RPC_VALIDATION_ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册方法签名
|
||||
*/
|
||||
registerMethodSignature(signature: MethodSignature): void {
|
||||
const key = `${signature.componentType}.${signature.methodName}`;
|
||||
this.methodSignatures.set(key, signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除方法签名
|
||||
*/
|
||||
removeMethodSignature(componentType: string, methodName: string): boolean {
|
||||
const key = `${componentType}.${methodName}`;
|
||||
return this.methodSignatures.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方法签名
|
||||
*/
|
||||
getMethodSignature(componentType: string, methodName: string): MethodSignature | undefined {
|
||||
const key = `${componentType}.${methodName}`;
|
||||
return this.methodSignatures.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加方法到黑名单
|
||||
*/
|
||||
addToBlacklist(methodName: string): void {
|
||||
if (!this.config.blacklistedMethods!.includes(methodName)) {
|
||||
this.config.blacklistedMethods!.push(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从黑名单移除方法
|
||||
*/
|
||||
removeFromBlacklist(methodName: string): boolean {
|
||||
const index = this.config.blacklistedMethods!.indexOf(methodName);
|
||||
if (index !== -1) {
|
||||
this.config.blacklistedMethods!.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加方法到白名单
|
||||
*/
|
||||
addToWhitelist(methodName: string): void {
|
||||
if (!this.config.whitelistedMethods!.includes(methodName)) {
|
||||
this.config.whitelistedMethods!.push(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端的 RPC 统计
|
||||
*/
|
||||
getClientRpcStats(clientId: string): {
|
||||
totalCalls: number;
|
||||
methodStats: Record<string, number>;
|
||||
} {
|
||||
const tracker = this.rateTrackers.get(clientId);
|
||||
if (!tracker) {
|
||||
return { totalCalls: 0, methodStats: {} };
|
||||
}
|
||||
|
||||
let totalCalls = 0;
|
||||
const methodStats: Record<string, number> = {};
|
||||
|
||||
for (const [method, data] of tracker.methodCalls) {
|
||||
totalCalls += data.count;
|
||||
methodStats[method] = data.count;
|
||||
}
|
||||
|
||||
return { totalCalls, methodStats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置客户端的频率限制
|
||||
*/
|
||||
resetClientRateLimit(clientId: string): boolean {
|
||||
const tracker = this.rateTrackers.get(clientId);
|
||||
if (!tracker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tracker.methodCalls.clear();
|
||||
tracker.lastUpdate = new Date();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁验证器
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
this.methodSignatures.clear();
|
||||
this.rateTrackers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化验证器
|
||||
*/
|
||||
private initialize(): void {
|
||||
// 启动清理定时器(每5分钟清理一次)
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupRateTrackers();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基本 RPC 调用验证
|
||||
*/
|
||||
private validateBasicRpcCall(context: RpcCallContext): ValidationResult {
|
||||
// 网络对象ID验证
|
||||
if (!Number.isInteger(context.networkId) || context.networkId <= 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid network object ID',
|
||||
errorCode: 'INVALID_NETWORK_ID'
|
||||
};
|
||||
}
|
||||
|
||||
// 组件类型验证
|
||||
if (!context.componentType || typeof context.componentType !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid component type',
|
||||
errorCode: 'INVALID_COMPONENT_TYPE'
|
||||
};
|
||||
}
|
||||
|
||||
// 方法名验证
|
||||
if (!context.methodName || typeof context.methodName !== 'string') {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid method name',
|
||||
errorCode: 'INVALID_METHOD_NAME'
|
||||
};
|
||||
}
|
||||
|
||||
// 参数数组验证
|
||||
if (!Array.isArray(context.parameters)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Parameters must be an array',
|
||||
errorCode: 'INVALID_PARAMETERS_FORMAT'
|
||||
};
|
||||
}
|
||||
|
||||
// 参数数量检查
|
||||
if (context.parameters.length > this.config.maxParameterCount!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Too many parameters: ${context.parameters.length} (max: ${this.config.maxParameterCount})`,
|
||||
errorCode: 'TOO_MANY_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法名验证
|
||||
*/
|
||||
private validateMethodName(context: RpcCallContext): ValidationResult {
|
||||
const methodName = context.methodName;
|
||||
|
||||
// 黑名单检查
|
||||
if (this.config.blacklistedMethods!.length > 0) {
|
||||
if (this.config.blacklistedMethods!.includes(methodName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Method '${methodName}' is blacklisted`,
|
||||
errorCode: 'METHOD_BLACKLISTED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 白名单检查
|
||||
if (this.config.whitelistedMethods!.length > 0) {
|
||||
if (!this.config.whitelistedMethods!.includes(methodName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Method '${methodName}' is not whitelisted`,
|
||||
errorCode: 'METHOD_NOT_WHITELISTED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 危险方法名检查
|
||||
const dangerousPatterns = [
|
||||
/^__/, // 私有方法
|
||||
/constructor/i,
|
||||
/prototype/i,
|
||||
/eval/i,
|
||||
/function/i
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(methodName)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Potentially dangerous method name: '${methodName}'`,
|
||||
errorCode: 'DANGEROUS_METHOD_NAME'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC 权限验证
|
||||
*/
|
||||
private validateRpcPermissions(context: RpcCallContext): ValidationResult {
|
||||
// 基本 RPC 权限检查
|
||||
if (!context.client.hasPermission('canSendRpc')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Client does not have RPC permission',
|
||||
errorCode: 'RPC_PERMISSION_DENIED'
|
||||
};
|
||||
}
|
||||
|
||||
// ServerRpc 特殊权限检查
|
||||
if (context.rpcType === 'server-rpc') {
|
||||
if (context.metadata.requiresAuth && !context.client.isAuthenticated) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Authentication required for this RPC',
|
||||
errorCode: 'AUTHENTICATION_REQUIRED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 检查方法签名中的权限要求
|
||||
const signature = this.getMethodSignature(context.componentType, context.methodName);
|
||||
if (signature && signature.requiredPermissions) {
|
||||
for (const permission of signature.requiredPermissions) {
|
||||
if (!context.client.hasCustomPermission(permission)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Required permission '${permission}' not found`,
|
||||
errorCode: 'INSUFFICIENT_PERMISSIONS'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证
|
||||
*/
|
||||
private validateParameters(context: RpcCallContext): ValidationResult {
|
||||
// 参数大小检查
|
||||
for (let i = 0; i < context.parameters.length; i++) {
|
||||
const param = context.parameters[i];
|
||||
|
||||
try {
|
||||
const serialized = JSON.stringify(param);
|
||||
const size = new TextEncoder().encode(serialized).length;
|
||||
|
||||
if (size > this.config.maxParameterSize!) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} is too large: ${size} bytes (max: ${this.config.maxParameterSize})`,
|
||||
errorCode: 'PARAMETER_TOO_LARGE'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} is not serializable`,
|
||||
errorCode: 'PARAMETER_NOT_SERIALIZABLE'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 参数类型检查
|
||||
if (this.config.enableTypeCheck) {
|
||||
const typeResult = this.validateParameterTypes(context);
|
||||
if (!typeResult.valid) {
|
||||
return typeResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 参数内容过滤
|
||||
if (this.config.enableContentFilter) {
|
||||
const contentResult = this.validateParameterContent(context);
|
||||
if (!contentResult.valid) {
|
||||
return contentResult;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数类型验证
|
||||
*/
|
||||
private validateParameterTypes(context: RpcCallContext): ValidationResult {
|
||||
for (let i = 0; i < context.parameters.length; i++) {
|
||||
const param = context.parameters[i];
|
||||
const paramType = this.getParameterType(param);
|
||||
|
||||
if (!this.config.allowedParameterTypes!.includes(paramType)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} type '${paramType}' is not allowed`,
|
||||
errorCode: 'INVALID_PARAMETER_TYPE'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数内容验证
|
||||
*/
|
||||
private validateParameterContent(context: RpcCallContext): ValidationResult {
|
||||
for (let i = 0; i < context.parameters.length; i++) {
|
||||
const param = context.parameters[i];
|
||||
|
||||
// 检查危险内容
|
||||
if (typeof param === 'string') {
|
||||
const dangerousPatterns = [
|
||||
/<script/i,
|
||||
/javascript:/i,
|
||||
/eval\(/i,
|
||||
/function\(/i,
|
||||
/__proto__/i
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(param)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${i} contains potentially dangerous content`,
|
||||
errorCode: 'DANGEROUS_PARAMETER_CONTENT'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率限制验证
|
||||
*/
|
||||
private validateRateLimit(context: RpcCallContext): ValidationResult {
|
||||
const signature = this.getMethodSignature(context.componentType, context.methodName);
|
||||
if (!signature || !signature.rateLimit) {
|
||||
return { valid: true }; // 没有频率限制
|
||||
}
|
||||
|
||||
const clientId = context.client.id;
|
||||
const methodKey = `${context.componentType}.${context.methodName}`;
|
||||
|
||||
let tracker = this.rateTrackers.get(clientId);
|
||||
if (!tracker) {
|
||||
tracker = {
|
||||
clientId,
|
||||
methodCalls: new Map(),
|
||||
lastUpdate: new Date()
|
||||
};
|
||||
this.rateTrackers.set(clientId, tracker);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let methodData = tracker.methodCalls.get(methodKey);
|
||||
|
||||
if (!methodData) {
|
||||
methodData = {
|
||||
count: 1,
|
||||
resetTime: new Date(now.getTime() + 60000) // 1分钟后重置
|
||||
};
|
||||
tracker.methodCalls.set(methodKey, methodData);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 检查是否需要重置
|
||||
if (now >= methodData.resetTime) {
|
||||
methodData.count = 1;
|
||||
methodData.resetTime = new Date(now.getTime() + 60000);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 检查频率限制
|
||||
if (methodData.count >= signature.rateLimit) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Rate limit exceeded for method '${methodKey}': ${methodData.count}/${signature.rateLimit} per minute`,
|
||||
errorCode: 'RATE_LIMIT_EXCEEDED'
|
||||
};
|
||||
}
|
||||
|
||||
methodData.count++;
|
||||
tracker.lastUpdate = now;
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法签名验证
|
||||
*/
|
||||
private validateMethodSignature(context: RpcCallContext): ValidationResult {
|
||||
const signature = this.getMethodSignature(context.componentType, context.methodName);
|
||||
if (!signature) {
|
||||
return { valid: true }; // 没有定义签名,跳过验证
|
||||
}
|
||||
|
||||
// 参数数量检查
|
||||
const requiredParams = signature.parameters.filter(p => p.required !== false);
|
||||
if (context.parameters.length < requiredParams.length) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Not enough parameters: expected at least ${requiredParams.length}, got ${context.parameters.length}`,
|
||||
errorCode: 'INSUFFICIENT_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
if (context.parameters.length > signature.parameters.length) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Too many parameters: expected at most ${signature.parameters.length}, got ${context.parameters.length}`,
|
||||
errorCode: 'EXCESS_PARAMETERS'
|
||||
};
|
||||
}
|
||||
|
||||
// 参数类型和值验证
|
||||
for (let i = 0; i < Math.min(context.parameters.length, signature.parameters.length); i++) {
|
||||
const param = context.parameters[i];
|
||||
const paramDef = signature.parameters[i];
|
||||
|
||||
const paramResult = this.validateParameterDefinition(param, paramDef, i);
|
||||
if (!paramResult.valid) {
|
||||
return paramResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证
|
||||
if (signature.customValidator) {
|
||||
const customResult = signature.customValidator(context);
|
||||
if (!customResult.valid) {
|
||||
return customResult;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证参数定义
|
||||
*/
|
||||
private validateParameterDefinition(
|
||||
value: NetworkValue,
|
||||
definition: ParameterTypeDefinition,
|
||||
index: number
|
||||
): ValidationResult {
|
||||
// 类型检查
|
||||
const actualType = this.getParameterType(value);
|
||||
if (definition.type !== 'any' && actualType !== definition.type) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} type mismatch: expected '${definition.type}', got '${actualType}'`,
|
||||
errorCode: 'PARAMETER_TYPE_MISMATCH'
|
||||
};
|
||||
}
|
||||
|
||||
// 值范围检查
|
||||
if (typeof value === 'number' && (definition.min !== undefined || definition.max !== undefined)) {
|
||||
if (definition.min !== undefined && value < definition.min) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} value ${value} is less than minimum ${definition.min}`,
|
||||
errorCode: 'PARAMETER_BELOW_MINIMUM'
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.max !== undefined && value > definition.max) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} value ${value} is greater than maximum ${definition.max}`,
|
||||
errorCode: 'PARAMETER_ABOVE_MAXIMUM'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串长度检查
|
||||
if (typeof value === 'string' && (definition.min !== undefined || definition.max !== undefined)) {
|
||||
if (definition.min !== undefined && value.length < definition.min) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} string length ${value.length} is less than minimum ${definition.min}`,
|
||||
errorCode: 'STRING_TOO_SHORT'
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.max !== undefined && value.length > definition.max) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} string length ${value.length} is greater than maximum ${definition.max}`,
|
||||
errorCode: 'STRING_TOO_LONG'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 允许值检查
|
||||
if (definition.allowedValues && definition.allowedValues.length > 0) {
|
||||
if (!definition.allowedValues.includes(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} value '${value}' is not in allowed values: ${definition.allowedValues.join(', ')}`,
|
||||
errorCode: 'VALUE_NOT_ALLOWED'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 正则表达式检查(字符串)
|
||||
if (typeof value === 'string' && definition.pattern) {
|
||||
if (!definition.pattern.test(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Parameter ${index} string '${value}' does not match required pattern`,
|
||||
errorCode: 'PATTERN_MISMATCH'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证
|
||||
if (definition.customValidator) {
|
||||
const customResult = definition.customValidator(value);
|
||||
if (!customResult.valid) {
|
||||
return {
|
||||
...customResult,
|
||||
error: `Parameter ${index} validation failed: ${customResult.error}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数类型
|
||||
*/
|
||||
private getParameterType(value: any): string {
|
||||
if (value === null || value === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return 'array';
|
||||
}
|
||||
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的频率跟踪器
|
||||
*/
|
||||
private cleanupRateTrackers(): void {
|
||||
const now = new Date();
|
||||
const expireTime = 10 * 60 * 1000; // 10分钟
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [clientId, tracker] of this.rateTrackers.entries()) {
|
||||
if (now.getTime() - tracker.lastUpdate.getTime() > expireTime) {
|
||||
this.rateTrackers.delete(clientId);
|
||||
cleanedCount++;
|
||||
} else {
|
||||
// 清理过期的方法调用记录
|
||||
for (const [methodKey, methodData] of tracker.methodCalls.entries()) {
|
||||
if (now >= methodData.resetTime) {
|
||||
tracker.methodCalls.delete(methodKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
console.log(`RPC validator cleanup: ${cleanedCount} rate trackers removed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 验证系统导出
|
||||
*/
|
||||
|
||||
export * from './MessageValidator';
|
||||
export * from './RpcValidator';
|
||||
Reference in New Issue
Block a user