|
@@ -5,6 +5,7 @@ const { HttpProxyAgent } = require('http-proxy-agent');
|
|
const logger = require('../utils/logger');
|
|
const logger = require('../utils/logger');
|
|
const { TestResult } = require('../models');
|
|
const { TestResult } = require('../models');
|
|
const net = require('net');
|
|
const net = require('net');
|
|
|
|
+const { exec } = require('child_process');
|
|
|
|
|
|
class SpeedTester {
|
|
class SpeedTester {
|
|
constructor() {
|
|
constructor() {
|
|
@@ -18,19 +19,256 @@ class SpeedTester {
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
- * TCP端口连通性检测
|
|
|
|
|
|
+ * TCP端口连通性检测 - 使用tcp-ping库改进
|
|
*/
|
|
*/
|
|
async testTcpConnectivity(server, port, timeout = 5000) {
|
|
async testTcpConnectivity(server, port, timeout = 5000) {
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
+ try {
|
|
|
|
+ // 尝试使用tcp-ping库,如果不可用则回退到原生实现
|
|
|
|
+ const tcpPing = require('tcp-ping');
|
|
|
|
+ tcpPing.ping({
|
|
|
|
+ address: server,
|
|
|
|
+ port: port,
|
|
|
|
+ attempts: 3,
|
|
|
|
+ timeout: timeout
|
|
|
|
+ }, (err, data) => {
|
|
|
|
+ if (err) {
|
|
|
|
+ logger.warn(`tcp-ping库不可用,使用原生实现: ${err.message}`);
|
|
|
|
+ this.testTcpConnectivityNative(server, port, timeout).then(resolve);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (data && data.min !== undefined) {
|
|
|
|
+ // tcp-ping库返回的时间单位是秒,需要转换为毫秒
|
|
|
|
+ resolve({
|
|
|
|
+ success: true,
|
|
|
|
+ latency: Math.round(data.min * 1000),
|
|
|
|
+ avg: Math.round(data.avg * 1000),
|
|
|
|
+ max: Math.round(data.max * 1000),
|
|
|
|
+ min: Math.round(data.min * 1000)
|
|
|
|
+ });
|
|
|
|
+ } else {
|
|
|
|
+ resolve({ success: false, error: '连接失败', latency: -1 });
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ } catch (error) {
|
|
|
|
+ logger.warn(`tcp-ping库加载失败,使用原生实现: ${error.message}`);
|
|
|
|
+ this.testTcpConnectivityNative(server, port, timeout).then(resolve);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 原生TCP连通性检测(备用方案)
|
|
|
|
+ */
|
|
|
|
+ async testTcpConnectivityNative(server, port, timeout = 5000) {
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
+ const socket = new net.Socket();
|
|
|
|
+ let isConnected = false;
|
|
|
|
+ const startTime = Date.now();
|
|
|
|
+
|
|
|
|
+ socket.setTimeout(timeout);
|
|
|
|
+
|
|
|
|
+ socket.on('connect', () => {
|
|
|
|
+ isConnected = true;
|
|
|
|
+ const latency = Date.now() - startTime;
|
|
|
|
+ socket.destroy();
|
|
|
|
+ resolve({
|
|
|
|
+ success: true,
|
|
|
|
+ latency: latency,
|
|
|
|
+ avg: latency,
|
|
|
|
+ max: latency,
|
|
|
|
+ min: latency
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ socket.on('timeout', () => {
|
|
|
|
+ socket.destroy();
|
|
|
|
+ if (!isConnected) resolve({
|
|
|
|
+ success: false,
|
|
|
|
+ error: '连接超时',
|
|
|
|
+ latency: -1
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ socket.on('error', (err) => {
|
|
|
|
+ socket.destroy();
|
|
|
|
+ if (!isConnected) resolve({
|
|
|
|
+ success: false,
|
|
|
|
+ error: err.message,
|
|
|
|
+ latency: -1
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ socket.connect(port, server);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 使用系统 ping 命令测真实网络延迟(ICMP RTT)
|
|
|
|
+ * 支持 Windows 和 Linux
|
|
|
|
+ */
|
|
|
|
+ async systemPing(host, attempts = 3, timeout = 2000) {
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
+ // 判断平台
|
|
|
|
+ const isWin = process.platform === 'win32';
|
|
|
|
+ // Windows: ping -n 次数 -w 超时 host
|
|
|
|
+ // Linux/Mac: ping -c 次数 -W 超时 host
|
|
|
|
+ let cmd;
|
|
|
|
+ if (isWin) {
|
|
|
|
+ // -n 次数, -w 单次超时(毫秒)
|
|
|
|
+ cmd = `ping -n ${attempts} -w ${timeout} ${host}`;
|
|
|
|
+ } else {
|
|
|
|
+ // -c 次数, -W 单次超时(秒)
|
|
|
|
+ cmd = `ping -c ${attempts} -W ${Math.ceil(timeout/1000)} ${host}`;
|
|
|
|
+ }
|
|
|
|
+ exec(cmd, (error, stdout) => {
|
|
|
|
+ if (error) {
|
|
|
|
+ resolve({ success: false, error: error.message, raw: stdout });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ let avg, min, max, results = [];
|
|
|
|
+ if (isWin) {
|
|
|
|
+ // 解析每次延迟
|
|
|
|
+ const msMatches = [...stdout.matchAll(/时间[=<]([\d]+)ms/g)];
|
|
|
|
+ results = msMatches.map((m, i) => ({ seq: i+1, time: parseInt(m[1], 10) }));
|
|
|
|
+ // 解析统计
|
|
|
|
+ const statMatch = stdout.match(/平均 = (\d+)ms/);
|
|
|
|
+ avg = statMatch ? parseInt(statMatch[1], 10) : null;
|
|
|
|
+ const minMatch = stdout.match(/最短 = (\d+)ms/);
|
|
|
|
+ min = minMatch ? parseInt(minMatch[1], 10) : null;
|
|
|
|
+ const maxMatch = stdout.match(/最长 = (\d+)ms/);
|
|
|
|
+ max = maxMatch ? parseInt(maxMatch[1], 10) : null;
|
|
|
|
+ } else {
|
|
|
|
+ // 解析每次延迟
|
|
|
|
+ const msMatches = [...stdout.matchAll(/time=([\d.]+) ms/g)];
|
|
|
|
+ results = msMatches.map((m, i) => ({ seq: i+1, time: Math.round(parseFloat(m[1])) }));
|
|
|
|
+ // 解析统计
|
|
|
|
+ const statMatch = stdout.match(/min\/avg\/max\/.* = ([\d.]+)\/([\d.]+)\/([\d.]+)\//);
|
|
|
|
+ min = statMatch ? Math.round(parseFloat(statMatch[1])) : null;
|
|
|
|
+ avg = statMatch ? Math.round(parseFloat(statMatch[2])) : null;
|
|
|
|
+ max = statMatch ? Math.round(parseFloat(statMatch[3])) : null;
|
|
|
|
+ }
|
|
|
|
+ if (avg != null) {
|
|
|
|
+ resolve({ success: true, host, avg, min, max, attempts, results, raw: stdout });
|
|
|
|
+ } else {
|
|
|
|
+ resolve({ success: false, error: '无法解析ping输出', raw: stdout });
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 核心ping测速实现 - 参考Electron应用的方式
|
|
|
|
+ * 使用tcp-ping库进行精确的延迟测量
|
|
|
|
+ */
|
|
|
|
+ async pingHosts(host, port, attempts = 3, timeout = 2000) {
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
+ try {
|
|
|
|
+ const tcpPing = require('tcp-ping');
|
|
|
|
+ tcpPing.ping({
|
|
|
|
+ address: host,
|
|
|
|
+ port: port,
|
|
|
|
+ attempts: attempts,
|
|
|
|
+ timeout: timeout
|
|
|
|
+ }, (err, data) => {
|
|
|
|
+ if (err) {
|
|
|
|
+ logger.warn(`tcp-ping测速失败: ${host}:${port} - ${err.message}`);
|
|
|
|
+ resolve({ success: false, latency: -1, error: err.message });
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (data && data.min !== undefined) {
|
|
|
|
+ // tcp-ping返回的时间单位是秒,转换为毫秒
|
|
|
|
+ const result = {
|
|
|
|
+ success: true,
|
|
|
|
+ host: host,
|
|
|
|
+ port: port,
|
|
|
|
+ attempts: data.attempts,
|
|
|
|
+ avg: Math.round(data.avg * 1000),
|
|
|
|
+ max: Math.round(data.max * 1000),
|
|
|
|
+ min: Math.round(data.min * 1000),
|
|
|
|
+ latency: Math.round(data.min * 1000), // 使用最小延迟作为主要延迟值
|
|
|
|
+ results: data.results.map(r => ({
|
|
|
|
+ seq: r.seq,
|
|
|
|
+ time: Math.round(r.time * 1000)
|
|
|
|
+ }))
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ logger.debug(`ping测速成功: ${host}:${port} - 延迟: ${result.latency}ms (平均: ${result.avg}ms, 最大: ${result.max}ms)`);
|
|
|
|
+ resolve(result);
|
|
|
|
+ } else {
|
|
|
|
+ logger.warn(`ping测速失败: ${host}:${port} - 连接失败`);
|
|
|
|
+ resolve({ success: false, latency: -1, error: '连接失败' });
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ } catch (error) {
|
|
|
|
+ logger.warn(`tcp-ping库不可用,使用原生实现: ${error.message}`);
|
|
|
|
+ this.pingHostsNative(host, port, attempts, timeout).then(resolve);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 原生TCP ping实现(备用方案)
|
|
|
|
+ */
|
|
|
|
+ async pingHostsNative(host, port, attempts = 3, timeout = 2000) {
|
|
|
|
+ const results = [];
|
|
|
|
+
|
|
|
|
+ for (let i = 0; i < attempts; i++) {
|
|
|
|
+ try {
|
|
|
|
+ const result = await this.singlePing(host, port, timeout);
|
|
|
|
+ if (result.success) {
|
|
|
|
+ results.push(result.latency);
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ logger.debug(`单次ping失败: ${host}:${port} - 第${i+1}次尝试`);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (results.length > 0) {
|
|
|
|
+ const min = Math.min(...results);
|
|
|
|
+ const max = Math.max(...results);
|
|
|
|
+ const avg = results.reduce((a, b) => a + b, 0) / results.length;
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ success: true,
|
|
|
|
+ host: host,
|
|
|
|
+ port: port,
|
|
|
|
+ attempts: attempts,
|
|
|
|
+ avg: Math.round(avg),
|
|
|
|
+ max: max,
|
|
|
|
+ min: min,
|
|
|
|
+ latency: min, // 使用最小延迟
|
|
|
|
+ results: results.map((latency, index) => ({ seq: index + 1, time: latency }))
|
|
|
|
+ };
|
|
|
|
+ } else {
|
|
|
|
+ return {
|
|
|
|
+ success: false,
|
|
|
|
+ host: host,
|
|
|
|
+ port: port,
|
|
|
|
+ latency: -1,
|
|
|
|
+ error: '所有尝试都失败'
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 单次ping测试
|
|
|
|
+ */
|
|
|
|
+ async singlePing(host, port, timeout) {
|
|
return new Promise((resolve) => {
|
|
return new Promise((resolve) => {
|
|
const socket = new net.Socket();
|
|
const socket = new net.Socket();
|
|
let isConnected = false;
|
|
let isConnected = false;
|
|
|
|
+ const startTime = Date.now();
|
|
|
|
|
|
socket.setTimeout(timeout);
|
|
socket.setTimeout(timeout);
|
|
|
|
|
|
socket.on('connect', () => {
|
|
socket.on('connect', () => {
|
|
isConnected = true;
|
|
isConnected = true;
|
|
|
|
+ const latency = Date.now() - startTime;
|
|
socket.destroy();
|
|
socket.destroy();
|
|
- resolve({ success: true });
|
|
|
|
|
|
+ resolve({ success: true, latency: latency });
|
|
});
|
|
});
|
|
|
|
|
|
socket.on('timeout', () => {
|
|
socket.on('timeout', () => {
|
|
@@ -43,12 +281,81 @@ class SpeedTester {
|
|
if (!isConnected) resolve({ success: false, error: err.message });
|
|
if (!isConnected) resolve({ success: false, error: err.message });
|
|
});
|
|
});
|
|
|
|
|
|
- socket.connect(port, server);
|
|
|
|
|
|
+ socket.connect(port, host);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
- * 测试单个节点连通性(只做TCP端口探测)
|
|
|
|
|
|
+ * 批量ping测速 - 参考前端调用方式
|
|
|
|
+ */
|
|
|
|
+ async batchPingHosts(hosts) {
|
|
|
|
+ logger.info(`开始批量ping测速: ${hosts.length}个节点`);
|
|
|
|
+
|
|
|
|
+ const results = [];
|
|
|
|
+ const concurrency = parseInt(process.env.CONCURRENCY) || 5;
|
|
|
|
+
|
|
|
|
+ // 分批处理,控制并发数
|
|
|
|
+ for (let i = 0; i < hosts.length; i += concurrency) {
|
|
|
|
+ const batch = hosts.slice(i, i + concurrency);
|
|
|
|
+ const batchPromises = batch.map(async (hostConfig) => {
|
|
|
|
+ const { host, port, attempts = 3, timeout = 2000 } = hostConfig;
|
|
|
|
+ try {
|
|
|
|
+ const result = await this.pingHosts(host, port, attempts, timeout);
|
|
|
|
+ return { host, port, ...result };
|
|
|
|
+ } catch (error) {
|
|
|
|
+ logger.error(`ping测速异常: ${host}:${port} - ${error.message}`);
|
|
|
|
+ return {
|
|
|
|
+ host,
|
|
|
|
+ port,
|
|
|
|
+ success: false,
|
|
|
|
+ error: error.message,
|
|
|
|
+ latency: -1
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const batchResults = await Promise.allSettled(batchPromises);
|
|
|
|
+
|
|
|
|
+ batchResults.forEach((result, index) => {
|
|
|
|
+ if (result.status === 'fulfilled') {
|
|
|
|
+ results.push(result.value);
|
|
|
|
+ } else {
|
|
|
|
+ logger.error(`ping测速失败: ${batch[index].host}:${batch[index].port} - ${result.reason}`);
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ // 批次间延迟,避免过于频繁的请求
|
|
|
|
+ if (i + concurrency < hosts.length) {
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 按延迟排序
|
|
|
|
+ const sortedResults = results.sort((a, b) => {
|
|
|
|
+ if (a.success && b.success) {
|
|
|
|
+ return (a.latency || a.min) - (b.latency || b.min);
|
|
|
|
+ }
|
|
|
|
+ return a.success ? -1 : 1;
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ const successCount = sortedResults.filter(r => r.success).length;
|
|
|
|
+ logger.info(`批量ping测速完成: ${successCount}/${hosts.length}个节点连通`);
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ results: sortedResults,
|
|
|
|
+ summary: {
|
|
|
|
+ total: results.length,
|
|
|
|
+ successful: successCount,
|
|
|
|
+ failed: results.length - successCount,
|
|
|
|
+ avgLatency: sortedResults
|
|
|
|
+ .filter(r => r.success && r.latency)
|
|
|
|
+ .reduce((sum, r) => sum + r.latency, 0) / sortedResults.filter(r => r.success && r.latency).length || 0
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 测试单个节点连通性(改进版)
|
|
*/
|
|
*/
|
|
async testNode(node) {
|
|
async testNode(node) {
|
|
const startTime = Date.now();
|
|
const startTime = Date.now();
|
|
@@ -64,7 +371,8 @@ class SpeedTester {
|
|
errorMessage: null,
|
|
errorMessage: null,
|
|
testDuration: null,
|
|
testDuration: null,
|
|
ipAddress: null,
|
|
ipAddress: null,
|
|
- location: null
|
|
|
|
|
|
+ location: null,
|
|
|
|
+ pingStats: null // 新增ping统计信息
|
|
};
|
|
};
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -76,17 +384,25 @@ class SpeedTester {
|
|
throw new Error(validationResult.error);
|
|
throw new Error(validationResult.error);
|
|
}
|
|
}
|
|
|
|
|
|
- // 直接检测 server:port 的 TCP 连通性
|
|
|
|
- const tcpResult = await this.testTcpConnectivity(node.server, node.port, this.timeout);
|
|
|
|
- testResult.isSuccess = tcpResult.success;
|
|
|
|
- testResult.errorMessage = tcpResult.error || null;
|
|
|
|
|
|
+ // 使用高级ping测试
|
|
|
|
+ const pingResult = await this.pingHosts(node.server, node.port, 3, 2000);
|
|
|
|
+ testResult.isSuccess = pingResult.success;
|
|
|
|
+ testResult.latency = pingResult.min || pingResult.latency;
|
|
|
|
+ testResult.errorMessage = pingResult.error || null;
|
|
|
|
+ testResult.pingStats = pingResult.success ? {
|
|
|
|
+ attempts: pingResult.attempts,
|
|
|
|
+ avg: pingResult.avg,
|
|
|
|
+ max: pingResult.max,
|
|
|
|
+ min: pingResult.min,
|
|
|
|
+ results: pingResult.results
|
|
|
|
+ } : null;
|
|
|
|
|
|
testResult.testDuration = Date.now() - startTime;
|
|
testResult.testDuration = Date.now() - startTime;
|
|
|
|
|
|
if (testResult.isSuccess) {
|
|
if (testResult.isSuccess) {
|
|
- logger.info(`✅ 节点TCP连通性测试成功: ${node.name}`);
|
|
|
|
|
|
+ logger.info(`✅ 节点Ping测试成功: ${node.name} - 延迟: ${testResult.latency}ms (平均: ${pingResult.avg}ms, 最大: ${pingResult.max}ms)`);
|
|
} else {
|
|
} else {
|
|
- logger.warn(`❌ 节点TCP连通性测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
|
|
|
|
|
|
+ logger.warn(`❌ 节点Ping测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
} catch (error) {
|
|
testResult.errorMessage = error.message;
|
|
testResult.errorMessage = error.message;
|
|
@@ -577,4 +893,4 @@ class SpeedTester {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
-module.exports = SpeedTester;
|
|
|
|
|
|
+module.exports = SpeedTester;
|