🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
import net from 'net';
|
||||
import http from 'http';
|
||||
import { FrameParser, encodeMessage } from '@network-tool/shared-protocol';
|
||||
import type { NetworkMessage, ControlMessage } from '@network-tool/shared-types';
|
||||
|
||||
// ============================================================
|
||||
// 配置
|
||||
// ============================================================
|
||||
const SERVER_URL = process.env.SERVER_URL || 'http://localhost:3001';
|
||||
const NODE_NAME = process.env.NODE_NAME || 'relay-node-1';
|
||||
const NODE_HOST = process.env.NODE_HOST || '0.0.0.0';
|
||||
const NODE_PORT = parseInt(process.env.NODE_PORT || '4001', 10);
|
||||
const TCP_PORT = parseInt(process.env.TCP_PORT || '4101', 10);
|
||||
const HEARTBEAT_INTERVAL = parseInt(process.env.HEARTBEAT_INTERVAL || '15000', 10);
|
||||
|
||||
// ============================================================
|
||||
// 全局状态
|
||||
// ============================================================
|
||||
let nodeId: number | null = null;
|
||||
const connectedClients = new Map<number, net.Socket>();
|
||||
// 逻辑连接:两个设备通过同一个节点建立的对等连接
|
||||
const peerConnections = new Map<string, { deviceA: number; deviceB: number }>();
|
||||
|
||||
// ============================================================
|
||||
// HTTP 请求辅助
|
||||
// ============================================================
|
||||
function apiRequest(path: string, method = 'GET', body?: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(path, SERVER_URL);
|
||||
const data = body ? JSON.stringify(body) : undefined;
|
||||
|
||||
const options: http.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: url.pathname,
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(data ? { 'Content-Length': Buffer.byteLength(data).toString() } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => (body += chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
} catch {
|
||||
reject(new Error(`Invalid JSON response: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (data) req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 节点注册
|
||||
// ============================================================
|
||||
async function registerNode(): Promise<void> {
|
||||
try {
|
||||
console.log(`[Node] 正在向服务器注册: ${SERVER_URL}`);
|
||||
const res = await apiRequest('/api/nodes/register', 'POST', {
|
||||
name: NODE_NAME,
|
||||
host: NODE_HOST,
|
||||
port: TCP_PORT,
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
nodeId = res.data.id;
|
||||
console.log(`[Node] 注册成功! Node ID: ${nodeId}`);
|
||||
} else {
|
||||
throw new Error(res.error || 'Registration failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Node] 注册失败:', err);
|
||||
// 5 秒后重试
|
||||
setTimeout(registerNode, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 心跳
|
||||
// ============================================================
|
||||
async function sendHeartbeat(): Promise<void> {
|
||||
if (!nodeId) return;
|
||||
|
||||
try {
|
||||
const load = connectedClients.size;
|
||||
await apiRequest('/api/nodes/heartbeat', 'POST', {
|
||||
node_id: nodeId,
|
||||
load,
|
||||
status: 'online',
|
||||
});
|
||||
console.log(`[Node] 心跳 OK | 连接数: ${load}`);
|
||||
} catch (err) {
|
||||
console.error('[Node] 心跳失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Token 验证
|
||||
// ============================================================
|
||||
async function verifyToken(token: string, deviceId: number): Promise<boolean> {
|
||||
try {
|
||||
const res = await apiRequest('/api/connections/verify-token', 'POST', {
|
||||
token,
|
||||
device_id: deviceId,
|
||||
});
|
||||
return res.success && res.data?.valid === true;
|
||||
} catch (err) {
|
||||
console.error('[Node] Token 验证失败:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TCP 客户端处理
|
||||
// ============================================================
|
||||
function handleClientConnection(socket: net.Socket): void {
|
||||
const parser = new FrameParser();
|
||||
let clientDeviceId: number | null = null;
|
||||
let authenticated = false;
|
||||
|
||||
const remoteAddr = `${socket.remoteAddress}:${socket.remotePort}`;
|
||||
console.log(`[Node] 新客户端连接: ${remoteAddr}`);
|
||||
|
||||
socket.on('data', async (chunk: Buffer) => {
|
||||
const messages = parser.push(chunk);
|
||||
|
||||
for (const msg of messages) {
|
||||
// 第一条消息必须是认证消息
|
||||
if (!authenticated) {
|
||||
if (msg.type === 'auth') {
|
||||
try {
|
||||
const authData: ControlMessage = JSON.parse(msg.payload);
|
||||
if (authData.action === 'connect' && authData.token) {
|
||||
const valid = await verifyToken(authData.token, authData.device_id);
|
||||
if (valid) {
|
||||
authenticated = true;
|
||||
clientDeviceId = authData.device_id;
|
||||
connectedClients.set(clientDeviceId, socket);
|
||||
|
||||
console.log(`[Node] 客户端认证通过: Device ${clientDeviceId}`);
|
||||
socket.write(encodeMessage({
|
||||
type: 'control',
|
||||
from_device_id: 0,
|
||||
to_device_id: clientDeviceId,
|
||||
payload: JSON.stringify({ action: 'connect_ok' }),
|
||||
timestamp: Date.now(),
|
||||
message_id: generateId(),
|
||||
}));
|
||||
} else {
|
||||
socket.write(encodeMessage({
|
||||
type: 'error',
|
||||
from_device_id: 0,
|
||||
to_device_id: authData.device_id,
|
||||
payload: 'Authentication failed: invalid token',
|
||||
timestamp: Date.now(),
|
||||
message_id: generateId(),
|
||||
}));
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
socket.write(encodeMessage({
|
||||
type: 'error',
|
||||
from_device_id: 0,
|
||||
to_device_id: 0,
|
||||
payload: 'Authentication failed: invalid message',
|
||||
timestamp: Date.now(),
|
||||
message_id: generateId(),
|
||||
}));
|
||||
socket.end();
|
||||
}
|
||||
} else {
|
||||
socket.end();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 已认证的消息转发
|
||||
if (msg.type === 'data' || msg.type === 'control') {
|
||||
forwardMessage(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log(`[Node] 客户端断开: ${remoteAddr}`);
|
||||
if (clientDeviceId !== null) {
|
||||
connectedClients.delete(clientDeviceId);
|
||||
// 通知对等端
|
||||
notifyPeerDisconnect(clientDeviceId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error(`[Node] 客户端错误 ${remoteAddr}:`, err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 消息转发
|
||||
// ============================================================
|
||||
function forwardMessage(msg: NetworkMessage): void {
|
||||
const targetSocket = connectedClients.get(msg.to_device_id);
|
||||
|
||||
if (targetSocket && !targetSocket.destroyed) {
|
||||
targetSocket.write(encodeMessage(msg));
|
||||
console.log(`[Node] 转发消息: Device ${msg.from_device_id} -> Device ${msg.to_device_id}`);
|
||||
} else {
|
||||
// 目标不在线,通知发送方
|
||||
const sourceSocket = connectedClients.get(msg.from_device_id);
|
||||
if (sourceSocket && !sourceSocket.destroyed) {
|
||||
sourceSocket.write(encodeMessage({
|
||||
type: 'error',
|
||||
from_device_id: 0,
|
||||
to_device_id: msg.from_device_id,
|
||||
payload: `Target device ${msg.to_device_id} is not connected`,
|
||||
timestamp: Date.now(),
|
||||
message_id: generateId(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 对等端断开通告
|
||||
// ============================================================
|
||||
function notifyPeerDisconnect(deviceId: number): void {
|
||||
for (const [otherId, socket] of connectedClients) {
|
||||
if (otherId !== deviceId && !socket.destroyed) {
|
||||
socket.write(encodeMessage({
|
||||
type: 'control',
|
||||
from_device_id: 0,
|
||||
to_device_id: otherId,
|
||||
payload: JSON.stringify({
|
||||
action: 'peer_disconnect',
|
||||
device_id: deviceId,
|
||||
}),
|
||||
timestamp: Date.now(),
|
||||
message_id: generateId(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助
|
||||
// ============================================================
|
||||
function generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 启动
|
||||
// ============================================================
|
||||
async function main(): Promise<void> {
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log(' Network Tool - Relay Node');
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log(` Server URL: ${SERVER_URL}`);
|
||||
console.log(` Node Name: ${NODE_NAME}`);
|
||||
console.log(` TCP Port: ${TCP_PORT}`);
|
||||
console.log('═══════════════════════════════════════');
|
||||
|
||||
// 1. 注册节点
|
||||
await registerNode();
|
||||
|
||||
// 2. 启动 TCP 服务器
|
||||
const tcpServer = net.createServer(handleClientConnection);
|
||||
tcpServer.listen(TCP_PORT, NODE_HOST, () => {
|
||||
console.log(`[Node] TCP 服务启动: ${NODE_HOST}:${TCP_PORT}`);
|
||||
});
|
||||
|
||||
// 3. 心跳定时器
|
||||
setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
|
||||
// 首次心跳
|
||||
setTimeout(sendHeartbeat, 2000);
|
||||
|
||||
// 优雅退出
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n[Node] 正在关闭...');
|
||||
tcpServer.close(() => {
|
||||
console.log('[Node] TCP 服务已关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('[Node] 收到 SIGTERM,正在关闭...');
|
||||
tcpServer.close(() => process.exit(0));
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[Node] 启动失败:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user