|
@@ -286,32 +286,71 @@ class SpeedTester {
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
- * 批量ping测速 - 参考前端调用方式
|
|
|
|
|
|
+ * 批量ping测速 - 改进版
|
|
|
|
+ * 提供更精细的并发控制和更好的错误处理
|
|
*/
|
|
*/
|
|
- async batchPingHosts(hosts) {
|
|
|
|
- logger.info(`开始批量ping测速: ${hosts.length}个节点`);
|
|
|
|
|
|
+ async batchPingHosts(hosts, options = {}) {
|
|
|
|
+ const {
|
|
|
|
+ concurrency = 3, // 降低默认并发数
|
|
|
|
+ batchDelay = 2000, // 增加批次间延迟
|
|
|
|
+ retryAttempts = 1, // 失败重试次数
|
|
|
|
+ timeout = 3000, // 增加超时时间
|
|
|
|
+ attempts = 3 // 每次ping的尝试次数
|
|
|
|
+ } = options;
|
|
|
|
+
|
|
|
|
+ logger.info(`开始批量ping测速: ${hosts.length}个节点,并发数: ${concurrency}`);
|
|
|
|
|
|
const results = [];
|
|
const results = [];
|
|
- const concurrency = parseInt(process.env.CONCURRENCY) || 5;
|
|
|
|
|
|
+ const startTime = Date.now();
|
|
|
|
|
|
// 分批处理,控制并发数
|
|
// 分批处理,控制并发数
|
|
for (let i = 0; i < hosts.length; i += concurrency) {
|
|
for (let i = 0; i < hosts.length; i += concurrency) {
|
|
const batch = hosts.slice(i, 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
|
|
|
|
- };
|
|
|
|
|
|
+ logger.debug(`处理批次 ${Math.floor(i/concurrency) + 1}/${Math.ceil(hosts.length/concurrency)}: ${batch.length}个节点`);
|
|
|
|
+
|
|
|
|
+ const batchPromises = batch.map(async (hostConfig, batchIndex) => {
|
|
|
|
+ const { host, port } = hostConfig;
|
|
|
|
+ const nodeName = `${host}:${port}`;
|
|
|
|
+
|
|
|
|
+ // 添加随机延迟,避免同时发起请求
|
|
|
|
+ const randomDelay = Math.random() * 500;
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, randomDelay));
|
|
|
|
+
|
|
|
|
+ let lastError = null;
|
|
|
|
+
|
|
|
|
+ // 重试机制
|
|
|
|
+ for (let retry = 0; retry <= retryAttempts; retry++) {
|
|
|
|
+ try {
|
|
|
|
+ const result = await this.pingHosts(host, port, attempts, timeout);
|
|
|
|
+ return {
|
|
|
|
+ host,
|
|
|
|
+ port,
|
|
|
|
+ ...result,
|
|
|
|
+ retryCount: retry,
|
|
|
|
+ batchIndex: batchIndex
|
|
|
|
+ };
|
|
|
|
+ } catch (error) {
|
|
|
|
+ lastError = error;
|
|
|
|
+ logger.debug(`ping测速失败 (重试 ${retry + 1}/${retryAttempts + 1}): ${nodeName} - ${error.message}`);
|
|
|
|
+
|
|
|
|
+ if (retry < retryAttempts) {
|
|
|
|
+ // 重试前等待
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1000 * (retry + 1)));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // 所有重试都失败
|
|
|
|
+ logger.warn(`ping测速最终失败: ${nodeName} - ${lastError?.message || '未知错误'}`);
|
|
|
|
+ return {
|
|
|
|
+ host,
|
|
|
|
+ port,
|
|
|
|
+ success: false,
|
|
|
|
+ error: lastError?.message || '所有重试都失败',
|
|
|
|
+ latency: -1,
|
|
|
|
+ retryCount: retryAttempts,
|
|
|
|
+ batchIndex: batchIndex
|
|
|
|
+ };
|
|
});
|
|
});
|
|
|
|
|
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
@@ -320,26 +359,38 @@ class SpeedTester {
|
|
if (result.status === 'fulfilled') {
|
|
if (result.status === 'fulfilled') {
|
|
results.push(result.value);
|
|
results.push(result.value);
|
|
} else {
|
|
} else {
|
|
- logger.error(`ping测速失败: ${batch[index].host}:${batch[index].port} - ${result.reason}`);
|
|
|
|
|
|
+ logger.error(`ping测速异常: ${batch[index].host}:${batch[index].port} - ${result.reason}`);
|
|
|
|
+ results.push({
|
|
|
|
+ host: batch[index].host,
|
|
|
|
+ port: batch[index].port,
|
|
|
|
+ success: false,
|
|
|
|
+ error: result.reason,
|
|
|
|
+ latency: -1,
|
|
|
|
+ retryCount: 0,
|
|
|
|
+ batchIndex: index
|
|
|
|
+ });
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
// 批次间延迟,避免过于频繁的请求
|
|
// 批次间延迟,避免过于频繁的请求
|
|
if (i + concurrency < hosts.length) {
|
|
if (i + concurrency < hosts.length) {
|
|
- await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
|
|
+ logger.debug(`批次间延迟: ${batchDelay}ms`);
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, batchDelay));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 按延迟排序
|
|
// 按延迟排序
|
|
const sortedResults = results.sort((a, b) => {
|
|
const sortedResults = results.sort((a, b) => {
|
|
if (a.success && b.success) {
|
|
if (a.success && b.success) {
|
|
- return (a.latency || a.min) - (b.latency || b.min);
|
|
|
|
|
|
+ return (a.latency || a.min || 9999) - (b.latency || b.min || 9999);
|
|
}
|
|
}
|
|
return a.success ? -1 : 1;
|
|
return a.success ? -1 : 1;
|
|
});
|
|
});
|
|
|
|
|
|
const successCount = sortedResults.filter(r => r.success).length;
|
|
const successCount = sortedResults.filter(r => r.success).length;
|
|
- logger.info(`批量ping测速完成: ${successCount}/${hosts.length}个节点连通`);
|
|
|
|
|
|
+ const totalTime = Date.now() - startTime;
|
|
|
|
+
|
|
|
|
+ logger.info(`批量ping测速完成: ${successCount}/${hosts.length}个节点连通,总耗时: ${totalTime}ms`);
|
|
|
|
|
|
return {
|
|
return {
|
|
results: sortedResults,
|
|
results: sortedResults,
|
|
@@ -349,11 +400,85 @@ class SpeedTester {
|
|
failed: results.length - successCount,
|
|
failed: results.length - successCount,
|
|
avgLatency: sortedResults
|
|
avgLatency: sortedResults
|
|
.filter(r => r.success && r.latency)
|
|
.filter(r => r.success && r.latency)
|
|
- .reduce((sum, r) => sum + r.latency, 0) / sortedResults.filter(r => r.success && r.latency).length || 0
|
|
|
|
|
|
+ .reduce((sum, r) => sum + r.latency, 0) / sortedResults.filter(r => r.success && r.latency).length || 0,
|
|
|
|
+ totalTime: totalTime,
|
|
|
|
+ concurrency: concurrency,
|
|
|
|
+ batchDelay: batchDelay
|
|
}
|
|
}
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * 单次测速 - 优化版
|
|
|
|
+ * 提供更精确的单节点测速
|
|
|
|
+ */
|
|
|
|
+ async singlePingOptimized(host, port, options = {}) {
|
|
|
|
+ const {
|
|
|
|
+ attempts = 5, // 增加尝试次数
|
|
|
|
+ timeout = 5000, // 增加超时时间
|
|
|
|
+ interval = 500 // 尝试间隔
|
|
|
|
+ } = options;
|
|
|
|
+
|
|
|
|
+ logger.info(`开始单次优化测速: ${host}:${port}`);
|
|
|
|
+
|
|
|
|
+ const results = [];
|
|
|
|
+ const startTime = Date.now();
|
|
|
|
+
|
|
|
|
+ for (let i = 0; i < attempts; i++) {
|
|
|
|
+ try {
|
|
|
|
+ const result = await this.singlePing(host, port, timeout);
|
|
|
|
+ if (result.success) {
|
|
|
|
+ results.push(result.latency);
|
|
|
|
+ logger.debug(`第${i + 1}次尝试成功: ${result.latency}ms`);
|
|
|
|
+ } else {
|
|
|
|
+ logger.debug(`第${i + 1}次尝试失败: ${result.error}`);
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ logger.debug(`第${i + 1}次尝试异常: ${error.message}`);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 尝试间隔
|
|
|
|
+ if (i < attempts - 1) {
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, interval));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ 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;
|
|
|
|
+ const totalTime = Date.now() - startTime;
|
|
|
|
+
|
|
|
|
+ logger.info(`单次优化测速完成: ${host}:${port} - 延迟: ${min}ms (平均: ${Math.round(avg)}ms, 最大: ${max}ms)`);
|
|
|
|
+
|
|
|
|
+ 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 })),
|
|
|
|
+ totalTime: totalTime
|
|
|
|
+ };
|
|
|
|
+ } else {
|
|
|
|
+ const totalTime = Date.now() - startTime;
|
|
|
|
+ logger.warn(`单次优化测速失败: ${host}:${port} - 所有尝试都失败`);
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ success: false,
|
|
|
|
+ host: host,
|
|
|
|
+ port: port,
|
|
|
|
+ attempts: attempts,
|
|
|
|
+ latency: -1,
|
|
|
|
+ error: '所有尝试都失败',
|
|
|
|
+ totalTime: totalTime
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* 测试单个节点连通性(改进版)
|
|
* 测试单个节点连通性(改进版)
|
|
*/
|
|
*/
|
|
@@ -440,10 +565,9 @@ class SpeedTester {
|
|
// 验证server字段是否为有效的域名或IP地址
|
|
// 验证server字段是否为有效的域名或IP地址
|
|
const server = node.server.trim();
|
|
const server = node.server.trim();
|
|
|
|
|
|
- // 只检查明显无效的情况,而不是过于严格的验证
|
|
|
|
- // 检查是否包含中文字符(通常表示这是描述而不是有效地址)
|
|
|
|
- if (/[\u4e00-\u9fa5]/.test(server)) {
|
|
|
|
- return { isValid: false, error: `服务器地址包含中文字符,无效: ${server}` };
|
|
|
|
|
|
+ // 检查是否为空或只包含空白字符
|
|
|
|
+ if (!server || server.trim() === '') {
|
|
|
|
+ return { isValid: false, error: '服务器地址不能为空' };
|
|
}
|
|
}
|
|
|
|
|
|
// 检查是否是URL格式(不应该作为server地址)
|
|
// 检查是否是URL格式(不应该作为server地址)
|
|
@@ -456,9 +580,15 @@ class SpeedTester {
|
|
return { isValid: false, error: `服务器地址包含无效字符: ${server}` };
|
|
return { isValid: false, error: `服务器地址包含无效字符: ${server}` };
|
|
}
|
|
}
|
|
|
|
|
|
- // 检查是否为空或只包含空白字符
|
|
|
|
- if (!server || server.trim() === '') {
|
|
|
|
- return { isValid: false, error: '服务器地址不能为空' };
|
|
|
|
|
|
+ // 允许包含中文的服务器地址
|
|
|
|
+ if (/[\u4e00-\u9fa5]/.test(server)) {
|
|
|
|
+ // 包含中文的地址直接通过验证
|
|
|
|
+ logger.debug(`检测到包含中文的服务器地址: ${server}`);
|
|
|
|
+ } else {
|
|
|
|
+ // 对于不包含中文的域名,进行基本的域名格式验证
|
|
|
|
+ if (!this.isValidBasicDomain(server)) {
|
|
|
|
+ return { isValid: false, error: `服务器地址格式无效: ${server}` };
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
// 对于SS/SSR节点,检查密码
|
|
// 对于SS/SSR节点,检查密码
|
|
@@ -479,6 +609,99 @@ class SpeedTester {
|
|
return { isValid: true, error: null };
|
|
return { isValid: true, error: null };
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
+ * 验证是否为合法的中文域名
|
|
|
|
+ * 支持以下格式:
|
|
|
|
+ * - 纯中文域名:中文.域名.com
|
|
|
|
+ * - 混合域名:中文.example.com
|
|
|
|
+ * - 包含中文的域名:test.中文.com
|
|
|
|
+ */
|
|
|
|
+ isValidChineseDomain(domain) {
|
|
|
|
+ // 移除前后空格
|
|
|
|
+ domain = domain.trim();
|
|
|
|
+
|
|
|
|
+ // 检查是否包含中文字符
|
|
|
|
+ if (!/[\u4e00-\u9fa5]/.test(domain)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否包含域名分隔符(点号)
|
|
|
|
+ if (!domain.includes('.')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否以点号开头或结尾
|
|
|
|
+ if (domain.startsWith('.') || domain.endsWith('.')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否包含连续的点号
|
|
|
|
+ if (domain.includes('..')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查每个标签的长度(域名标签不能超过63个字符)
|
|
|
|
+ const labels = domain.split('.');
|
|
|
|
+ for (const label of labels) {
|
|
|
|
+ if (label.length === 0 || label.length > 63) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查顶级域名(最后一部分)是否至少包含一个非中文字符
|
|
|
|
+ const tld = labels[labels.length - 1];
|
|
|
|
+ if (!/[a-zA-Z0-9]/.test(tld)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否包含有效的域名字符(中文、英文、数字、连字符)
|
|
|
|
+ const validDomainRegex = /^[a-zA-Z0-9\u4e00-\u9fa5\-\.]+$/;
|
|
|
|
+ if (!validDomainRegex.test(domain)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 验证是否为合法的基本域名格式(不包含中文)
|
|
|
|
+ */
|
|
|
|
+ isValidBasicDomain(domain) {
|
|
|
|
+ // 移除前后空格
|
|
|
|
+ domain = domain.trim();
|
|
|
|
+
|
|
|
|
+ // 检查是否包含域名分隔符(点号)
|
|
|
|
+ if (!domain.includes('.')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否以点号开头或结尾
|
|
|
|
+ if (domain.startsWith('.') || domain.endsWith('.')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否包含连续的点号
|
|
|
|
+ if (domain.includes('..')) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查每个标签的长度(域名标签不能超过63个字符)
|
|
|
|
+ const labels = domain.split('.');
|
|
|
|
+ for (const label of labels) {
|
|
|
|
+ if (label.length === 0 || label.length > 63) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否包含有效的域名字符(英文、数字、连字符)
|
|
|
|
+ const validDomainRegex = /^[a-zA-Z0-9\-\.]+$/;
|
|
|
|
+ if (!validDomainRegex.test(domain)) {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* 创建代理配置
|
|
* 创建代理配置
|
|
*/
|
|
*/
|