feat(server): 添加可插拔速率限制系统 | add pluggable rate limiting system (#388)
* feat(server): 添加可插拔速率限制系统 | add pluggable rate limiting system - 新增令牌桶策略 (TokenBucketStrategy) - 推荐用于一般场景 - 新增滑动窗口策略 (SlidingWindowStrategy) - 精确跟踪 - 新增固定窗口策略 (FixedWindowStrategy) - 简单高效 - 新增房间速率限制 mixin (withRateLimit) - 新增速率限制装饰器 (@rateLimit, @noRateLimit) - 新增按消息类型限流装饰器 (@rateLimitMessage, @noRateLimitMessage) - 支持与认证系统组合使用 - 添加中英文文档 - 导出路径: @esengine/server/ratelimit * docs: 更新 README 添加新模块 | update README with new modules - 添加程序化生成 (procgen) 模块 - 添加 RPC 框架模块 - 添加游戏服务器 (server) 模块 - 添加事务系统 (transaction) 模块 - 添加世界流送 (world-streaming) 模块 - 更新网络模块描述 - 更新项目结构目录
This commit is contained in:
@@ -268,6 +268,7 @@ export default defineConfig({
|
||||
{ label: '客户端', slug: 'modules/network/client', translations: { en: 'Client' } },
|
||||
{ label: '服务器', slug: 'modules/network/server', translations: { en: 'Server' } },
|
||||
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
|
||||
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
|
||||
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
|
||||
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
|
||||
|
||||
458
docs/src/content/docs/en/modules/network/rate-limit.md
Normal file
458
docs/src/content/docs/en/modules/network/rate-limit.md
Normal file
@@ -0,0 +1,458 @@
|
||||
---
|
||||
title: "Rate Limiting"
|
||||
description: "Protect your game server from abuse with configurable rate limiting"
|
||||
---
|
||||
|
||||
The `@esengine/server` package includes a pluggable rate limiting system to protect against DDoS attacks, message flooding, and other abuse.
|
||||
|
||||
## Installation
|
||||
|
||||
Rate limiting is included in the server package:
|
||||
|
||||
```bash
|
||||
npm install @esengine/server
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { Room, onMessage } from '@esengine/server'
|
||||
import { withRateLimit, rateLimit, noRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 20,
|
||||
onLimited: (player, type, result) => {
|
||||
player.send('Error', {
|
||||
code: 'RATE_LIMITED',
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
},
|
||||
}) {
|
||||
@onMessage('Move')
|
||||
handleMove(data: { x: number; y: number }, player: Player) {
|
||||
// Protected by rate limit (10 msg/s default)
|
||||
}
|
||||
|
||||
@rateLimit({ messagesPerSecond: 1 })
|
||||
@onMessage('Trade')
|
||||
handleTrade(data: TradeData, player: Player) {
|
||||
// Stricter limit for trading
|
||||
}
|
||||
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat(data: any, player: Player) {
|
||||
// No rate limit for heartbeat
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limit Strategies
|
||||
|
||||
### Token Bucket (Default)
|
||||
|
||||
The token bucket algorithm allows burst traffic while maintaining long-term rate limits. Tokens are added at a fixed rate, and each request consumes tokens.
|
||||
|
||||
```typescript
|
||||
import { withRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
strategy: 'token-bucket',
|
||||
messagesPerSecond: 10, // Refill rate
|
||||
burstSize: 20, // Bucket capacity
|
||||
}) { }
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
```
|
||||
Config: rate=10/s, burstSize=20
|
||||
|
||||
[0s] Bucket full: 20 tokens
|
||||
[0s] 15 messages → allowed, 5 remaining
|
||||
[0.5s] Refill 5 tokens → 10 tokens
|
||||
[0.5s] 8 messages → allowed, 2 remaining
|
||||
[0.6s] Refill 1 token → 3 tokens
|
||||
[0.6s] 5 messages → 3 allowed, 2 rejected
|
||||
```
|
||||
|
||||
**Best for:** Most general use cases, balances burst tolerance with protection.
|
||||
|
||||
### Sliding Window
|
||||
|
||||
The sliding window algorithm precisely tracks requests within a time window. More accurate than fixed window but uses slightly more memory.
|
||||
|
||||
```typescript
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
strategy: 'sliding-window',
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 10,
|
||||
}) { }
|
||||
```
|
||||
|
||||
**Best for:** When you need precise rate limiting without burst tolerance.
|
||||
|
||||
### Fixed Window
|
||||
|
||||
The fixed window algorithm divides time into fixed intervals and counts requests per interval. Simple and memory-efficient but allows 2x burst at window boundaries.
|
||||
|
||||
```typescript
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
strategy: 'fixed-window',
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 10,
|
||||
}) { }
|
||||
```
|
||||
|
||||
**Best for:** Simple scenarios where boundary burst is acceptable.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Room Configuration
|
||||
|
||||
```typescript
|
||||
import { withRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
// Messages allowed per second (default: 10)
|
||||
messagesPerSecond: 10,
|
||||
|
||||
// Burst capacity / bucket size (default: 20)
|
||||
burstSize: 20,
|
||||
|
||||
// Strategy: 'token-bucket' | 'sliding-window' | 'fixed-window'
|
||||
strategy: 'token-bucket',
|
||||
|
||||
// Callback when rate limited
|
||||
onLimited: (player, messageType, result) => {
|
||||
player.send('RateLimited', {
|
||||
type: messageType,
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
},
|
||||
|
||||
// Disconnect on rate limit (default: false)
|
||||
disconnectOnLimit: false,
|
||||
|
||||
// Disconnect after N consecutive limits (0 = never)
|
||||
maxConsecutiveLimits: 10,
|
||||
|
||||
// Custom key function (default: player.id)
|
||||
getKey: (player) => player.id,
|
||||
|
||||
// Cleanup interval in ms (default: 60000)
|
||||
cleanupInterval: 60000,
|
||||
}) { }
|
||||
```
|
||||
|
||||
### Per-Message Configuration
|
||||
|
||||
Use decorators to configure rate limits for specific messages:
|
||||
|
||||
```typescript
|
||||
import { rateLimit, noRateLimit, rateLimitMessage } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room) {
|
||||
// Custom rate limit for this message
|
||||
@rateLimit({ messagesPerSecond: 1, burstSize: 2 })
|
||||
@onMessage('Trade')
|
||||
handleTrade(data: TradeData, player: Player) { }
|
||||
|
||||
// This message costs 5 tokens
|
||||
@rateLimit({ cost: 5 })
|
||||
@onMessage('ExpensiveAction')
|
||||
handleExpensive(data: any, player: Player) { }
|
||||
|
||||
// Exempt from rate limiting
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat(data: any, player: Player) { }
|
||||
|
||||
// Alternative: specify message type explicitly
|
||||
@rateLimitMessage('SpecialAction', { messagesPerSecond: 2 })
|
||||
@onMessage('SpecialAction')
|
||||
handleSpecial(data: any, player: Player) { }
|
||||
}
|
||||
```
|
||||
|
||||
## Combining with Authentication
|
||||
|
||||
Rate limiting works seamlessly with the authentication system:
|
||||
|
||||
```typescript
|
||||
import { withRoomAuth } from '@esengine/server/auth'
|
||||
import { withRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
// Apply both mixins
|
||||
class GameRoom extends withRateLimit(
|
||||
withRoomAuth(Room, { requireAuth: true }),
|
||||
{ messagesPerSecond: 10 }
|
||||
) {
|
||||
onJoin(player: AuthPlayer) {
|
||||
console.log(`${player.user?.name} joined with rate limit protection`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limit Result
|
||||
|
||||
When a message is rate limited, the callback receives a result object:
|
||||
|
||||
```typescript
|
||||
interface RateLimitResult {
|
||||
// Whether the request was allowed
|
||||
allowed: boolean
|
||||
|
||||
// Remaining quota
|
||||
remaining: number
|
||||
|
||||
// When the quota resets (timestamp)
|
||||
resetAt: number
|
||||
|
||||
// How long to wait before retrying (ms)
|
||||
retryAfter?: number
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Rate Limit Context
|
||||
|
||||
You can access the rate limit context for any player:
|
||||
|
||||
```typescript
|
||||
import { getPlayerRateLimitContext } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room) {
|
||||
someMethod(player: Player) {
|
||||
const context = this.getRateLimitContext(player)
|
||||
|
||||
// Check without consuming
|
||||
const status = context?.check()
|
||||
console.log(`Remaining: ${status?.remaining}`)
|
||||
|
||||
// Get consecutive limit count
|
||||
console.log(`Consecutive limits: ${context?.consecutiveLimitCount}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Or use the standalone function
|
||||
const context = getPlayerRateLimitContext(player)
|
||||
```
|
||||
|
||||
## Custom Strategies
|
||||
|
||||
You can use the strategies directly for custom implementations:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
TokenBucketStrategy,
|
||||
SlidingWindowStrategy,
|
||||
FixedWindowStrategy,
|
||||
createTokenBucketStrategy,
|
||||
} from '@esengine/server/ratelimit'
|
||||
|
||||
// Create strategy directly
|
||||
const strategy = createTokenBucketStrategy({
|
||||
rate: 10, // tokens per second
|
||||
capacity: 20, // max tokens
|
||||
})
|
||||
|
||||
// Check and consume
|
||||
const result = strategy.consume('player-123')
|
||||
if (result.allowed) {
|
||||
// Process message
|
||||
} else {
|
||||
// Rate limited, wait result.retryAfter ms
|
||||
}
|
||||
|
||||
// Check without consuming
|
||||
const status = strategy.getStatus('player-123')
|
||||
|
||||
// Reset a key
|
||||
strategy.reset('player-123')
|
||||
|
||||
// Cleanup expired records
|
||||
strategy.cleanup()
|
||||
```
|
||||
|
||||
## Rate Limit Context
|
||||
|
||||
The `RateLimitContext` class manages rate limiting for a single player:
|
||||
|
||||
```typescript
|
||||
import { RateLimitContext, TokenBucketStrategy } from '@esengine/server/ratelimit'
|
||||
|
||||
const strategy = new TokenBucketStrategy({ rate: 10, capacity: 20 })
|
||||
const context = new RateLimitContext('player-123', strategy)
|
||||
|
||||
// Check without consuming
|
||||
context.check()
|
||||
|
||||
// Consume quota
|
||||
context.consume()
|
||||
|
||||
// Consume with cost
|
||||
context.consume(undefined, 5)
|
||||
|
||||
// Consume for specific message type
|
||||
context.consume('Trade')
|
||||
|
||||
// Set per-message strategy
|
||||
context.setMessageStrategy('Trade', new TokenBucketStrategy({ rate: 1, capacity: 2 }))
|
||||
|
||||
// Reset
|
||||
context.reset()
|
||||
|
||||
// Get consecutive limit count
|
||||
console.log(context.consecutiveLimitCount)
|
||||
```
|
||||
|
||||
## Room Lifecycle Hook
|
||||
|
||||
You can override the `onRateLimited` hook for custom handling:
|
||||
|
||||
```typescript
|
||||
class GameRoom extends withRateLimit(Room) {
|
||||
onRateLimited(player: Player, messageType: string, result: RateLimitResult) {
|
||||
// Log the event
|
||||
console.log(`Player ${player.id} rate limited on ${messageType}`)
|
||||
|
||||
// Send custom error
|
||||
player.send('SystemMessage', {
|
||||
type: 'warning',
|
||||
message: `Slow down! Try again in ${result.retryAfter}ms`,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with token bucket**: It's the most flexible algorithm for games.
|
||||
|
||||
2. **Set appropriate limits**: Consider your game's mechanics:
|
||||
- Movement messages: Higher limits (20-60/s)
|
||||
- Chat messages: Lower limits (1-5/s)
|
||||
- Trade/purchase: Very low limits (0.5-1/s)
|
||||
|
||||
3. **Use burst capacity**: Allow short bursts for responsive gameplay:
|
||||
```typescript
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 30, // Allow 3s worth of burst
|
||||
```
|
||||
|
||||
4. **Exempt critical messages**: Don't rate limit heartbeats or system messages:
|
||||
```typescript
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat() { }
|
||||
```
|
||||
|
||||
5. **Combine with auth**: Rate limit by user ID for authenticated users:
|
||||
```typescript
|
||||
getKey: (player) => player.auth?.userId ?? player.id
|
||||
```
|
||||
|
||||
6. **Monitor and adjust**: Log rate limit events to tune your limits:
|
||||
```typescript
|
||||
onLimited: (player, type, result) => {
|
||||
metrics.increment('rate_limit', { messageType: type })
|
||||
}
|
||||
```
|
||||
|
||||
7. **Graceful degradation**: Send informative errors instead of just disconnecting:
|
||||
```typescript
|
||||
onLimited: (player, type, result) => {
|
||||
player.send('Error', {
|
||||
code: 'RATE_LIMITED',
|
||||
message: 'Too many requests',
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Room, onMessage, type Player } from '@esengine/server'
|
||||
import { withRoomAuth, type AuthPlayer } from '@esengine/server/auth'
|
||||
import {
|
||||
withRateLimit,
|
||||
rateLimit,
|
||||
noRateLimit,
|
||||
type RateLimitResult,
|
||||
} from '@esengine/server/ratelimit'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
premium: boolean
|
||||
}
|
||||
|
||||
// Combine auth and rate limit
|
||||
class GameRoom extends withRateLimit(
|
||||
withRoomAuth<User>(Room, { requireAuth: true }),
|
||||
{
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 30,
|
||||
strategy: 'token-bucket',
|
||||
|
||||
// Use user ID for rate limiting
|
||||
getKey: (player) => (player as AuthPlayer<User>).user?.id ?? player.id,
|
||||
|
||||
// Handle rate limits
|
||||
onLimited: (player, type, result) => {
|
||||
player.send('Error', {
|
||||
code: 'RATE_LIMITED',
|
||||
messageType: type,
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
},
|
||||
|
||||
// Disconnect after 20 consecutive rate limits
|
||||
maxConsecutiveLimits: 20,
|
||||
}
|
||||
) {
|
||||
onCreate() {
|
||||
console.log('Room created with auth + rate limit protection')
|
||||
}
|
||||
|
||||
onJoin(player: AuthPlayer<User>) {
|
||||
this.broadcast('PlayerJoined', { name: player.user?.name })
|
||||
}
|
||||
|
||||
// High-frequency movement (default rate limit)
|
||||
@onMessage('Move')
|
||||
handleMove(data: { x: number; y: number }, player: AuthPlayer<User>) {
|
||||
this.broadcast('PlayerMoved', { id: player.id, ...data })
|
||||
}
|
||||
|
||||
// Low-frequency trading (strict limit)
|
||||
@rateLimit({ messagesPerSecond: 0.5, burstSize: 2 })
|
||||
@onMessage('Trade')
|
||||
handleTrade(data: TradeData, player: AuthPlayer<User>) {
|
||||
// Process trade...
|
||||
}
|
||||
|
||||
// Chat with moderate limit
|
||||
@rateLimit({ messagesPerSecond: 2, burstSize: 5 })
|
||||
@onMessage('Chat')
|
||||
handleChat(data: { text: string }, player: AuthPlayer<User>) {
|
||||
this.broadcast('Chat', {
|
||||
from: player.user?.name,
|
||||
text: data.text,
|
||||
})
|
||||
}
|
||||
|
||||
// System messages - no limit
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat(data: any, player: Player) {
|
||||
player.send('Pong', { time: Date.now() })
|
||||
}
|
||||
|
||||
// Custom rate limit handling
|
||||
onRateLimited(player: Player, messageType: string, result: RateLimitResult) {
|
||||
console.warn(`[RateLimit] Player ${player.id} limited on ${messageType}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
458
docs/src/content/docs/modules/network/rate-limit.md
Normal file
458
docs/src/content/docs/modules/network/rate-limit.md
Normal file
@@ -0,0 +1,458 @@
|
||||
---
|
||||
title: "速率限制"
|
||||
description: "使用可配置的速率限制保护你的游戏服务器免受滥用"
|
||||
---
|
||||
|
||||
`@esengine/server` 包含可插拔的速率限制系统,用于防止 DDoS 攻击、消息洪水和其他滥用行为。
|
||||
|
||||
## 安装
|
||||
|
||||
速率限制包含在 server 包中:
|
||||
|
||||
```bash
|
||||
npm install @esengine/server
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
import { Room, onMessage } from '@esengine/server'
|
||||
import { withRateLimit, rateLimit, noRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 20,
|
||||
onLimited: (player, type, result) => {
|
||||
player.send('Error', {
|
||||
code: 'RATE_LIMITED',
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
},
|
||||
}) {
|
||||
@onMessage('Move')
|
||||
handleMove(data: { x: number; y: number }, player: Player) {
|
||||
// 受速率限制保护(默认 10 msg/s)
|
||||
}
|
||||
|
||||
@rateLimit({ messagesPerSecond: 1 })
|
||||
@onMessage('Trade')
|
||||
handleTrade(data: TradeData, player: Player) {
|
||||
// 交易使用更严格的限制
|
||||
}
|
||||
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat(data: any, player: Player) {
|
||||
// 心跳不限制
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 速率限制策略
|
||||
|
||||
### 令牌桶(默认)
|
||||
|
||||
令牌桶算法允许突发流量,同时保持长期速率限制。令牌以固定速率添加,每个请求消耗令牌。
|
||||
|
||||
```typescript
|
||||
import { withRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
strategy: 'token-bucket',
|
||||
messagesPerSecond: 10, // 补充速率
|
||||
burstSize: 20, // 桶容量
|
||||
}) { }
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
```
|
||||
配置: rate=10/s, burstSize=20
|
||||
|
||||
[0s] 桶满: 20 令牌
|
||||
[0s] 收到 15 条消息 → 允许,剩余 5
|
||||
[0.5s] 补充 5 令牌 → 10 令牌
|
||||
[0.5s] 收到 8 条消息 → 允许,剩余 2
|
||||
[0.6s] 补充 1 令牌 → 3 令牌
|
||||
[0.6s] 收到 5 条消息 → 允许 3,拒绝 2
|
||||
```
|
||||
|
||||
**最适合:** 大多数通用场景,平衡突发容忍度与保护。
|
||||
|
||||
### 滑动窗口
|
||||
|
||||
滑动窗口算法精确跟踪时间窗口内的请求。比固定窗口更准确,但内存使用稍多。
|
||||
|
||||
```typescript
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
strategy: 'sliding-window',
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 10,
|
||||
}) { }
|
||||
```
|
||||
|
||||
**最适合:** 需要精确限流且不需要突发容忍的场景。
|
||||
|
||||
### 固定窗口
|
||||
|
||||
固定窗口算法将时间划分为固定间隔,并计算每个间隔内的请求数。简单且内存高效,但在窗口边界允许 2 倍突发。
|
||||
|
||||
```typescript
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
strategy: 'fixed-window',
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 10,
|
||||
}) { }
|
||||
```
|
||||
|
||||
**最适合:** 简单场景,可接受边界突发。
|
||||
|
||||
## 配置
|
||||
|
||||
### 房间配置
|
||||
|
||||
```typescript
|
||||
import { withRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room, {
|
||||
// 每秒允许的消息数(默认: 10)
|
||||
messagesPerSecond: 10,
|
||||
|
||||
// 突发容量 / 桶大小(默认: 20)
|
||||
burstSize: 20,
|
||||
|
||||
// 策略: 'token-bucket' | 'sliding-window' | 'fixed-window'
|
||||
strategy: 'token-bucket',
|
||||
|
||||
// 被限流时的回调
|
||||
onLimited: (player, messageType, result) => {
|
||||
player.send('RateLimited', {
|
||||
type: messageType,
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
},
|
||||
|
||||
// 限流时断开连接(默认: false)
|
||||
disconnectOnLimit: false,
|
||||
|
||||
// 连续 N 次限流后断开(0 = 永不)
|
||||
maxConsecutiveLimits: 10,
|
||||
|
||||
// 自定义键函数(默认: player.id)
|
||||
getKey: (player) => player.id,
|
||||
|
||||
// 清理间隔(毫秒,默认: 60000)
|
||||
cleanupInterval: 60000,
|
||||
}) { }
|
||||
```
|
||||
|
||||
### 单消息配置
|
||||
|
||||
使用装饰器为特定消息配置速率限制:
|
||||
|
||||
```typescript
|
||||
import { rateLimit, noRateLimit, rateLimitMessage } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room) {
|
||||
// 此消息使用自定义速率限制
|
||||
@rateLimit({ messagesPerSecond: 1, burstSize: 2 })
|
||||
@onMessage('Trade')
|
||||
handleTrade(data: TradeData, player: Player) { }
|
||||
|
||||
// 此消息消耗 5 个令牌
|
||||
@rateLimit({ cost: 5 })
|
||||
@onMessage('ExpensiveAction')
|
||||
handleExpensive(data: any, player: Player) { }
|
||||
|
||||
// 豁免速率限制
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat(data: any, player: Player) { }
|
||||
|
||||
// 替代方案:显式指定消息类型
|
||||
@rateLimitMessage('SpecialAction', { messagesPerSecond: 2 })
|
||||
@onMessage('SpecialAction')
|
||||
handleSpecial(data: any, player: Player) { }
|
||||
}
|
||||
```
|
||||
|
||||
## 与认证系统组合
|
||||
|
||||
速率限制可与认证系统无缝配合:
|
||||
|
||||
```typescript
|
||||
import { withRoomAuth } from '@esengine/server/auth'
|
||||
import { withRateLimit } from '@esengine/server/ratelimit'
|
||||
|
||||
// 同时应用两个 mixin
|
||||
class GameRoom extends withRateLimit(
|
||||
withRoomAuth(Room, { requireAuth: true }),
|
||||
{ messagesPerSecond: 10 }
|
||||
) {
|
||||
onJoin(player: AuthPlayer) {
|
||||
console.log(`${player.user?.name} 已加入,受速率限制保护`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 速率限制结果
|
||||
|
||||
当消息被限流时,回调会收到结果对象:
|
||||
|
||||
```typescript
|
||||
interface RateLimitResult {
|
||||
// 是否允许请求
|
||||
allowed: boolean
|
||||
|
||||
// 剩余配额
|
||||
remaining: number
|
||||
|
||||
// 配额重置时间(时间戳)
|
||||
resetAt: number
|
||||
|
||||
// 重试等待时间(毫秒)
|
||||
retryAfter?: number
|
||||
}
|
||||
```
|
||||
|
||||
## 访问速率限制上下文
|
||||
|
||||
你可以访问任何玩家的速率限制上下文:
|
||||
|
||||
```typescript
|
||||
import { getPlayerRateLimitContext } from '@esengine/server/ratelimit'
|
||||
|
||||
class GameRoom extends withRateLimit(Room) {
|
||||
someMethod(player: Player) {
|
||||
const context = this.getRateLimitContext(player)
|
||||
|
||||
// 检查但不消费
|
||||
const status = context?.check()
|
||||
console.log(`剩余: ${status?.remaining}`)
|
||||
|
||||
// 获取连续限流次数
|
||||
console.log(`连续限流: ${context?.consecutiveLimitCount}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 或使用独立函数
|
||||
const context = getPlayerRateLimitContext(player)
|
||||
```
|
||||
|
||||
## 自定义策略
|
||||
|
||||
你可以直接使用策略进行自定义实现:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
TokenBucketStrategy,
|
||||
SlidingWindowStrategy,
|
||||
FixedWindowStrategy,
|
||||
createTokenBucketStrategy,
|
||||
} from '@esengine/server/ratelimit'
|
||||
|
||||
// 直接创建策略
|
||||
const strategy = createTokenBucketStrategy({
|
||||
rate: 10, // 每秒令牌数
|
||||
capacity: 20, // 最大令牌数
|
||||
})
|
||||
|
||||
// 检查并消费
|
||||
const result = strategy.consume('player-123')
|
||||
if (result.allowed) {
|
||||
// 处理消息
|
||||
} else {
|
||||
// 被限流,等待 result.retryAfter 毫秒
|
||||
}
|
||||
|
||||
// 检查但不消费
|
||||
const status = strategy.getStatus('player-123')
|
||||
|
||||
// 重置某个键
|
||||
strategy.reset('player-123')
|
||||
|
||||
// 清理过期记录
|
||||
strategy.cleanup()
|
||||
```
|
||||
|
||||
## 速率限制上下文
|
||||
|
||||
`RateLimitContext` 类管理单个玩家的速率限制:
|
||||
|
||||
```typescript
|
||||
import { RateLimitContext, TokenBucketStrategy } from '@esengine/server/ratelimit'
|
||||
|
||||
const strategy = new TokenBucketStrategy({ rate: 10, capacity: 20 })
|
||||
const context = new RateLimitContext('player-123', strategy)
|
||||
|
||||
// 检查但不消费
|
||||
context.check()
|
||||
|
||||
// 消费配额
|
||||
context.consume()
|
||||
|
||||
// 带消耗量消费
|
||||
context.consume(undefined, 5)
|
||||
|
||||
// 为特定消息类型消费
|
||||
context.consume('Trade')
|
||||
|
||||
// 设置单消息策略
|
||||
context.setMessageStrategy('Trade', new TokenBucketStrategy({ rate: 1, capacity: 2 }))
|
||||
|
||||
// 重置
|
||||
context.reset()
|
||||
|
||||
// 获取连续限流次数
|
||||
console.log(context.consecutiveLimitCount)
|
||||
```
|
||||
|
||||
## 房间生命周期钩子
|
||||
|
||||
你可以重写 `onRateLimited` 钩子进行自定义处理:
|
||||
|
||||
```typescript
|
||||
class GameRoom extends withRateLimit(Room) {
|
||||
onRateLimited(player: Player, messageType: string, result: RateLimitResult) {
|
||||
// 记录事件
|
||||
console.log(`玩家 ${player.id} 在 ${messageType} 上被限流`)
|
||||
|
||||
// 发送自定义错误
|
||||
player.send('SystemMessage', {
|
||||
type: 'warning',
|
||||
message: `请慢一点!${result.retryAfter}ms 后重试`,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **从令牌桶开始**:对于游戏来说是最灵活的算法。
|
||||
|
||||
2. **设置合适的限制**:考虑你的游戏机制:
|
||||
- 移动消息:较高限制(20-60/s)
|
||||
- 聊天消息:较低限制(1-5/s)
|
||||
- 交易/购买:非常低的限制(0.5-1/s)
|
||||
|
||||
3. **使用突发容量**:允许短暂突发以获得响应式体验:
|
||||
```typescript
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 30, // 允许 3 秒的突发
|
||||
```
|
||||
|
||||
4. **豁免关键消息**:不要限制心跳或系统消息:
|
||||
```typescript
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat() { }
|
||||
```
|
||||
|
||||
5. **与认证结合**:对已认证用户按用户 ID 限流:
|
||||
```typescript
|
||||
getKey: (player) => player.auth?.userId ?? player.id
|
||||
```
|
||||
|
||||
6. **监控和调整**:记录限流事件以调整限制:
|
||||
```typescript
|
||||
onLimited: (player, type, result) => {
|
||||
metrics.increment('rate_limit', { messageType: type })
|
||||
}
|
||||
```
|
||||
|
||||
7. **优雅降级**:发送信息性错误而不是直接断开:
|
||||
```typescript
|
||||
onLimited: (player, type, result) => {
|
||||
player.send('Error', {
|
||||
code: 'RATE_LIMITED',
|
||||
message: '请求过于频繁',
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Room, onMessage, type Player } from '@esengine/server'
|
||||
import { withRoomAuth, type AuthPlayer } from '@esengine/server/auth'
|
||||
import {
|
||||
withRateLimit,
|
||||
rateLimit,
|
||||
noRateLimit,
|
||||
type RateLimitResult,
|
||||
} from '@esengine/server/ratelimit'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
premium: boolean
|
||||
}
|
||||
|
||||
// 组合认证和速率限制
|
||||
class GameRoom extends withRateLimit(
|
||||
withRoomAuth<User>(Room, { requireAuth: true }),
|
||||
{
|
||||
messagesPerSecond: 10,
|
||||
burstSize: 30,
|
||||
strategy: 'token-bucket',
|
||||
|
||||
// 使用用户 ID 进行限流
|
||||
getKey: (player) => (player as AuthPlayer<User>).user?.id ?? player.id,
|
||||
|
||||
// 处理限流
|
||||
onLimited: (player, type, result) => {
|
||||
player.send('Error', {
|
||||
code: 'RATE_LIMITED',
|
||||
messageType: type,
|
||||
retryAfter: result.retryAfter,
|
||||
})
|
||||
},
|
||||
|
||||
// 连续 20 次限流后断开
|
||||
maxConsecutiveLimits: 20,
|
||||
}
|
||||
) {
|
||||
onCreate() {
|
||||
console.log('房间已创建,具有认证 + 速率限制保护')
|
||||
}
|
||||
|
||||
onJoin(player: AuthPlayer<User>) {
|
||||
this.broadcast('PlayerJoined', { name: player.user?.name })
|
||||
}
|
||||
|
||||
// 高频移动(默认速率限制)
|
||||
@onMessage('Move')
|
||||
handleMove(data: { x: number; y: number }, player: AuthPlayer<User>) {
|
||||
this.broadcast('PlayerMoved', { id: player.id, ...data })
|
||||
}
|
||||
|
||||
// 低频交易(严格限制)
|
||||
@rateLimit({ messagesPerSecond: 0.5, burstSize: 2 })
|
||||
@onMessage('Trade')
|
||||
handleTrade(data: TradeData, player: AuthPlayer<User>) {
|
||||
// 处理交易...
|
||||
}
|
||||
|
||||
// 聊天使用中等限制
|
||||
@rateLimit({ messagesPerSecond: 2, burstSize: 5 })
|
||||
@onMessage('Chat')
|
||||
handleChat(data: { text: string }, player: AuthPlayer<User>) {
|
||||
this.broadcast('Chat', {
|
||||
from: player.user?.name,
|
||||
text: data.text,
|
||||
})
|
||||
}
|
||||
|
||||
// 系统消息 - 不限制
|
||||
@noRateLimit()
|
||||
@onMessage('Heartbeat')
|
||||
handleHeartbeat(data: any, player: Player) {
|
||||
player.send('Pong', { time: Date.now() })
|
||||
}
|
||||
|
||||
// 自定义限流处理
|
||||
onRateLimited(player: Player, messageType: string, result: RateLimitResult) {
|
||||
console.warn(`[限流] 玩家 ${player.id} 在 ${messageType} 上被限流`)
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user