修復 too many connection 問題
This commit is contained in:
251
lib/ip-based-connection-manager.ts
Normal file
251
lib/ip-based-connection-manager.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
// =====================================================
|
||||
// 基於 IP 的連線管理器
|
||||
// =====================================================
|
||||
|
||||
import mysql from 'mysql2/promise';
|
||||
import { db } from './database';
|
||||
|
||||
export class IPBasedConnectionManager {
|
||||
private static instance: IPBasedConnectionManager;
|
||||
private clientIP: string | null = null;
|
||||
private connectionTracker = new Map<string, {
|
||||
connectionId: string;
|
||||
createdAt: number;
|
||||
lastUsed: number;
|
||||
userAgent?: string;
|
||||
}>();
|
||||
|
||||
private constructor() {
|
||||
this.detectClientIP();
|
||||
}
|
||||
|
||||
public static getInstance(): IPBasedConnectionManager {
|
||||
if (!IPBasedConnectionManager.instance) {
|
||||
IPBasedConnectionManager.instance = new IPBasedConnectionManager();
|
||||
}
|
||||
return IPBasedConnectionManager.instance;
|
||||
}
|
||||
|
||||
// 檢測客戶端 IP
|
||||
private detectClientIP() {
|
||||
// 在瀏覽器環境中,我們無法直接獲取真實 IP
|
||||
// 但我們可以通過其他方式來識別連線
|
||||
if (typeof window !== 'undefined') {
|
||||
// 客戶端:生成一個唯一標識符
|
||||
this.clientIP = this.generateClientId();
|
||||
console.log('🖥️ 客戶端標識符:', this.clientIP);
|
||||
} else {
|
||||
// 服務端:嘗試從環境變數或請求中獲取
|
||||
this.clientIP = process.env.CLIENT_IP || 'server-side';
|
||||
console.log('🖥️ 服務端標識符:', this.clientIP);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成客戶端唯一標識符
|
||||
private generateClientId(): string {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2);
|
||||
return `client_${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
// 設置客戶端 IP(從請求中獲取)
|
||||
public setClientIP(ip: string) {
|
||||
this.clientIP = ip;
|
||||
console.log('🖥️ 設置客戶端 IP:', ip);
|
||||
}
|
||||
|
||||
// 獲取客戶端 IP
|
||||
public getClientIP(): string | null {
|
||||
return this.clientIP;
|
||||
}
|
||||
|
||||
// 註冊連線(帶 IP 標識)
|
||||
public registerConnection(connectionId: string, metadata?: {
|
||||
userAgent?: string;
|
||||
requestId?: string;
|
||||
}) {
|
||||
if (!this.clientIP) return;
|
||||
|
||||
this.connectionTracker.set(connectionId, {
|
||||
connectionId,
|
||||
createdAt: Date.now(),
|
||||
lastUsed: Date.now(),
|
||||
userAgent: metadata?.userAgent,
|
||||
});
|
||||
|
||||
console.log(`📝 註冊連線: ${connectionId} (IP: ${this.clientIP})`);
|
||||
}
|
||||
|
||||
// 更新連線使用時間
|
||||
public updateConnectionUsage(connectionId: string) {
|
||||
const conn = this.connectionTracker.get(connectionId);
|
||||
if (conn) {
|
||||
conn.lastUsed = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// 釋放連線
|
||||
public releaseConnection(connectionId: string) {
|
||||
const conn = this.connectionTracker.get(connectionId);
|
||||
if (conn) {
|
||||
this.connectionTracker.delete(connectionId);
|
||||
console.log(`🗑️ 釋放連線: ${connectionId} (IP: ${this.clientIP})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 根據 IP 清理連線
|
||||
public async cleanupConnectionsByIP(targetIP?: string): Promise<{
|
||||
success: boolean;
|
||||
killedCount: number;
|
||||
message: string;
|
||||
}> {
|
||||
const ipToClean = targetIP || this.clientIP;
|
||||
|
||||
if (!ipToClean) {
|
||||
return {
|
||||
success: false,
|
||||
killedCount: 0,
|
||||
message: '無法識別客戶端 IP'
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`🧹 開始清理 IP ${ipToClean} 的連線...`);
|
||||
|
||||
let connection = null;
|
||||
let killedCount = 0;
|
||||
|
||||
try {
|
||||
// 建立臨時連線
|
||||
connection = await db.getConnection();
|
||||
|
||||
// 獲取指定 IP 的所有連線(使用更寬鬆的匹配)
|
||||
const [connections] = await connection.execute(`
|
||||
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||||
FROM information_schema.PROCESSLIST
|
||||
WHERE USER = ? AND (HOST LIKE ? OR HOST LIKE ? OR HOST LIKE ?)
|
||||
ORDER BY TIME DESC
|
||||
`, [
|
||||
process.env.DB_USER || 'A999',
|
||||
`%${ipToClean}%`, // 包含 IP 的 HOST
|
||||
`%${ipToClean}.%`, // IP 開頭的 HOST
|
||||
`%${ipToClean}-%` // IP 帶連字符的 HOST
|
||||
]);
|
||||
|
||||
console.log(`🔍 找到 ${connections.length} 個來自 ${ipToClean} 的連線`);
|
||||
|
||||
// 顯示連線詳情
|
||||
connections.forEach((conn: any, index: number) => {
|
||||
console.log(`${index + 1}. ID: ${conn.ID}, HOST: ${conn.HOST}, 時間: ${conn.TIME}s, 狀態: ${conn.STATE}`);
|
||||
});
|
||||
|
||||
// 殺死指定 IP 的連線(除了當前連線)
|
||||
for (const conn of connections) {
|
||||
if (conn.ID !== connection.threadId) {
|
||||
try {
|
||||
await connection.execute(`KILL ${conn.ID}`);
|
||||
console.log(`💀 已殺死連線 ${conn.ID} (HOST: ${conn.HOST})`);
|
||||
killedCount++;
|
||||
} catch (error: any) {
|
||||
console.log(`⚠️ 無法殺死連線 ${conn.ID}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理本地追蹤的連線
|
||||
this.connectionTracker.clear();
|
||||
|
||||
console.log(`✅ 已清理 ${killedCount} 個來自 ${ipToClean} 的連線`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
killedCount,
|
||||
message: `已清理 ${killedCount} 個來自 ${ipToClean} 的連線`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 清理連線失敗:', error);
|
||||
return {
|
||||
success: false,
|
||||
killedCount: 0,
|
||||
message: `清理失敗: ${error instanceof Error ? error.message : '未知錯誤'}`
|
||||
};
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取指定 IP 的連線狀態
|
||||
public async getConnectionsByIP(targetIP?: string): Promise<{
|
||||
ip: string;
|
||||
connectionCount: number;
|
||||
connections: any[];
|
||||
}> {
|
||||
const ipToCheck = targetIP || this.clientIP;
|
||||
|
||||
if (!ipToCheck) {
|
||||
return {
|
||||
ip: 'unknown',
|
||||
connectionCount: 0,
|
||||
connections: []
|
||||
};
|
||||
}
|
||||
|
||||
let connection = null;
|
||||
|
||||
try {
|
||||
connection = await db.getConnection();
|
||||
|
||||
const [connections] = await connection.execute(`
|
||||
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||||
FROM information_schema.PROCESSLIST
|
||||
WHERE USER = ? AND HOST LIKE ?
|
||||
ORDER BY TIME DESC
|
||||
`, [process.env.DB_USER || 'A999', `%${ipToCheck}%`]);
|
||||
|
||||
return {
|
||||
ip: ipToCheck,
|
||||
connectionCount: connections.length,
|
||||
connections: connections
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 獲取連線狀態失敗:', error);
|
||||
return {
|
||||
ip: ipToCheck,
|
||||
connectionCount: 0,
|
||||
connections: []
|
||||
};
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理所有本地追蹤的連線
|
||||
public cleanupLocalConnections() {
|
||||
const count = this.connectionTracker.size;
|
||||
this.connectionTracker.clear();
|
||||
console.log(`🧹 已清理 ${count} 個本地追蹤的連線`);
|
||||
return count;
|
||||
}
|
||||
|
||||
// 獲取本地連線統計
|
||||
public getLocalConnectionStats() {
|
||||
return {
|
||||
clientIP: this.clientIP,
|
||||
trackedConnections: this.connectionTracker.size,
|
||||
connections: Array.from(this.connectionTracker.values()).map(conn => ({
|
||||
connectionId: conn.connectionId,
|
||||
createdAt: new Date(conn.createdAt).toISOString(),
|
||||
lastUsed: new Date(conn.lastUsed).toISOString(),
|
||||
userAgent: conn.userAgent
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 導出單例實例
|
||||
export const ipConnectionManager = IPBasedConnectionManager.getInstance();
|
Reference in New Issue
Block a user