🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import http from 'http';
|
||||
import net from 'net';
|
||||
import { EventEmitter } from 'events';
|
||||
import { FrameParser, encodeMessage } from '@network-tool/shared-protocol';
|
||||
import type { NetworkMessage } from '@network-tool/shared-types';
|
||||
|
||||
const SERVER_URL = process.env.SERVER_URL || 'http://localhost:3001';
|
||||
|
||||
export class NetworkClient extends EventEmitter {
|
||||
private token: string | null = null;
|
||||
private relaySocket: net.Socket | null = null;
|
||||
private relayParser = new FrameParser();
|
||||
|
||||
setToken(token: string): void {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Control Server 发送 HTTP 请求
|
||||
*/
|
||||
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',
|
||||
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
||||
...(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 {
|
||||
resolve({ success: false, error: 'PARSE_ERROR', message: body });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
resolve({ success: false, error: 'NETWORK_ERROR', message: err.message });
|
||||
});
|
||||
|
||||
if (data) req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 带认证的请求
|
||||
*/
|
||||
async authenticatedRequest(path: string, method = 'GET', body?: any): Promise<any> {
|
||||
if (!this.token) {
|
||||
return { success: false, error: 'NOT_AUTHENTICATED', message: 'Please login first' };
|
||||
}
|
||||
return this.apiRequest(path, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到 Relay Node
|
||||
*/
|
||||
connectToRelay(host: string, port: number, token: string, deviceId: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.disconnectFromRelay();
|
||||
|
||||
this.relaySocket = new net.Socket();
|
||||
this.relayParser.reset();
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Connection timeout'));
|
||||
this.relaySocket?.destroy();
|
||||
}, 10000);
|
||||
|
||||
this.relaySocket.connect(port, host, () => {
|
||||
clearTimeout(timeout);
|
||||
this.emit('status', 'connected');
|
||||
console.log(`[Client] 已连接到 Relay Node: ${host}:${port}`);
|
||||
|
||||
// 发送认证消息
|
||||
const authMsg: NetworkMessage = {
|
||||
type: 'auth',
|
||||
from_device_id: deviceId,
|
||||
to_device_id: 0,
|
||||
payload: JSON.stringify({
|
||||
action: 'connect',
|
||||
device_id: deviceId,
|
||||
token,
|
||||
}),
|
||||
timestamp: Date.now(),
|
||||
message_id: this.generateId(),
|
||||
};
|
||||
|
||||
this.relaySocket!.write(encodeMessage(authMsg));
|
||||
this.emit('status', 'authenticating');
|
||||
});
|
||||
|
||||
this.relaySocket.on('data', (chunk: Buffer) => {
|
||||
const messages = this.relayParser.push(chunk);
|
||||
for (const msg of messages) {
|
||||
if (msg.type === 'control') {
|
||||
try {
|
||||
const ctrl = JSON.parse(msg.payload);
|
||||
if (ctrl.action === 'connect_ok') {
|
||||
this.emit('status', 'authenticated');
|
||||
resolve();
|
||||
continue;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 转发所有消息给渲染进程
|
||||
this.emit('message', msg);
|
||||
}
|
||||
});
|
||||
|
||||
this.relaySocket.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
this.emit('status', 'error');
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.relaySocket.on('close', () => {
|
||||
this.emit('status', 'disconnected');
|
||||
this.relaySocket = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Relay 发送消息
|
||||
*/
|
||||
sendMessage(targetDeviceId: number, payload: string): void {
|
||||
if (!this.relaySocket || this.relaySocket.destroyed) {
|
||||
throw new Error('Not connected to relay');
|
||||
}
|
||||
|
||||
const msg: NetworkMessage = {
|
||||
type: 'data',
|
||||
from_device_id: 0, // 不暴露真实 device id 给对端 (relay 会处理)
|
||||
to_device_id: targetDeviceId,
|
||||
payload,
|
||||
timestamp: Date.now(),
|
||||
message_id: this.generateId(),
|
||||
};
|
||||
|
||||
this.relaySocket.write(encodeMessage(msg));
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开 Relay 连接
|
||||
*/
|
||||
disconnectFromRelay(): void {
|
||||
if (this.relaySocket) {
|
||||
this.relaySocket.destroy();
|
||||
this.relaySocket = null;
|
||||
}
|
||||
this.emit('status', 'disconnected');
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user