Files
FunConnect/server/src/relay.ts
FunMC b17679cec6 feat: FunConnect v1.0.0 - Minecraft联机平台完整版
- server: Node.js TCP中继服务器,支持多节点集群
- web: React管理面板(仪表盘、房间管理、节点管理)
- client: Electron桌面客户端(连接、创建/加入房间、本地代理)
- deploy: Ubuntu一键部署脚本
2026-02-22 23:33:00 +08:00

206 lines
6.2 KiB
TypeScript

import net from 'net';
import logger from './logger';
import { Room, RoomManager } from './room';
import config from './config';
export class RelayEngine {
private server: net.Server | null = null;
private roomManager: RoomManager;
private hostConnections: Map<string, net.Socket> = new Map();
private pendingConnections: Map<string, { roomId: string; playerName: string; resolve: (socket: net.Socket) => void }> = new Map();
constructor(roomManager: RoomManager) {
this.roomManager = roomManager;
}
start(port: number): Promise<void> {
return new Promise((resolve, reject) => {
this.server = net.createServer((clientSocket) => {
this.handleConnection(clientSocket);
});
this.server.on('error', (err) => {
logger.error(`Relay server error: ${err.message}`);
reject(err);
});
this.server.listen(port, '0.0.0.0', () => {
logger.info(`Relay TCP server listening on port ${port}`);
resolve();
});
});
}
private handleConnection(clientSocket: net.Socket): void {
const remoteAddr = `${clientSocket.remoteAddress}:${clientSocket.remotePort}`;
logger.info(`New TCP connection from ${remoteAddr}`);
let buffer = Buffer.alloc(0);
let identified = false;
const timeout = setTimeout(() => {
if (!identified) {
logger.warn(`Connection from ${remoteAddr} timed out waiting for identification`);
clientSocket.destroy();
}
}, 15000);
clientSocket.once('data', (data) => {
clearTimeout(timeout);
identified = true;
buffer = Buffer.concat([buffer, data]);
this.identifyAndRoute(clientSocket, buffer, remoteAddr);
});
clientSocket.on('error', (err) => {
logger.debug(`Socket error from ${remoteAddr}: ${err.message}`);
});
}
private identifyAndRoute(clientSocket: net.Socket, initialData: Buffer, remoteAddr: string): void {
try {
const headerStr = initialData.toString('utf8', 0, Math.min(initialData.length, 512));
if (headerStr.startsWith('FUNMC_HOST:')) {
this.handleHostConnection(clientSocket, headerStr, remoteAddr);
} else if (headerStr.startsWith('FUNMC_JOIN:')) {
this.handlePlayerJoin(clientSocket, headerStr, initialData, remoteAddr);
} else {
logger.debug(`Unknown protocol from ${remoteAddr}, closing`);
clientSocket.destroy();
}
} catch (err: any) {
logger.error(`Error routing connection from ${remoteAddr}: ${err.message}`);
clientSocket.destroy();
}
}
private handleHostConnection(hostSocket: net.Socket, header: string, remoteAddr: string): void {
const parts = header.split('\n')[0].replace('FUNMC_HOST:', '').split('|');
if (parts.length < 2) {
hostSocket.write('ERROR:INVALID_FORMAT\n');
hostSocket.destroy();
return;
}
const roomId = parts[0].trim();
const hostPort = parseInt(parts[1].trim());
const room = this.roomManager.getRoom(roomId);
if (!room) {
hostSocket.write('ERROR:ROOM_NOT_FOUND\n');
hostSocket.destroy();
return;
}
room.hostSocket = hostSocket;
this.hostConnections.set(roomId, hostSocket);
hostSocket.write('OK:HOST_REGISTERED\n');
logger.info(`Host registered for room ${roomId} from ${remoteAddr}, target port ${hostPort}`);
hostSocket.on('close', () => {
logger.info(`Host disconnected from room ${roomId}`);
this.hostConnections.delete(roomId);
this.roomManager.deleteRoom(roomId);
});
hostSocket.on('error', (err) => {
logger.error(`Host socket error for room ${roomId}: ${err.message}`);
});
}
private handlePlayerJoin(playerSocket: net.Socket, header: string, initialData: Buffer, remoteAddr: string): void {
const parts = header.split('\n')[0].replace('FUNMC_JOIN:', '').split('|');
if (parts.length < 2) {
playerSocket.write('ERROR:INVALID_FORMAT\n');
playerSocket.destroy();
return;
}
const roomId = parts[0].trim();
const playerName = parts[1].trim();
const password = parts[2]?.trim();
const room = this.roomManager.getRoom(roomId);
if (!room) {
playerSocket.write('ERROR:ROOM_NOT_FOUND\n');
playerSocket.destroy();
return;
}
if (room.password && room.password !== password) {
playerSocket.write('ERROR:WRONG_PASSWORD\n');
playerSocket.destroy();
return;
}
if (room.isFull) {
playerSocket.write('ERROR:ROOM_FULL\n');
playerSocket.destroy();
return;
}
const hostSocket = this.hostConnections.get(roomId);
if (!hostSocket || hostSocket.destroyed) {
playerSocket.write('ERROR:HOST_OFFLINE\n');
playerSocket.destroy();
return;
}
const player = room.addPlayer(playerName, playerSocket);
playerSocket.write('OK:CONNECTED\n');
this.setupRelay(roomId, player.id, playerSocket, hostSocket, room);
}
private setupRelay(roomId: string, playerId: string, playerSocket: net.Socket, hostSocket: net.Socket, room: Room): void {
const hostAddr = hostSocket.remoteAddress;
const hostPort = room.hostPort;
const targetSocket = new net.Socket();
targetSocket.connect(hostPort, hostAddr || '127.0.0.1', () => {
logger.info(`Relay established: player ${playerId} <-> host ${hostAddr}:${hostPort} in room ${roomId}`);
playerSocket.pipe(targetSocket);
targetSocket.pipe(playerSocket);
});
targetSocket.on('error', (err) => {
logger.error(`Target connection error for player ${playerId}: ${err.message}`);
playerSocket.destroy();
});
targetSocket.on('close', () => {
playerSocket.destroy();
room.removePlayer(playerId);
});
playerSocket.on('close', () => {
targetSocket.destroy();
room.removePlayer(playerId);
});
playerSocket.on('error', (err) => {
logger.debug(`Player socket error: ${err.message}`);
targetSocket.destroy();
});
}
registerHostDirect(roomId: string, hostSocket: net.Socket): void {
this.hostConnections.set(roomId, hostSocket);
}
stop(): void {
if (this.server) {
this.server.close();
logger.info('Relay TCP server stopped');
}
for (const [, socket] of this.hostConnections) {
socket.destroy();
}
this.hostConnections.clear();
}
}