1)feature: IP测速功能,针对请求的端口进行测速,不再固定 `443` 端口进行测速;

2)bugfix: IP测速功能,域名长时间未访问时,再访问后不会重启自动测速功能的问题修复;
3)optimize: 优化DNS获取的IP再经过IpTester校验的逻辑。
develop
王良 2025-03-14 17:12:47 +08:00
parent ab74a44e83
commit ab76d03f0b
14 changed files with 352 additions and 130 deletions

View File

@ -455,9 +455,9 @@ export default {
</a> </a>
<a-tag <a-tag
v-for="(element, index) of item.backupList" :key="index" style="margin:2px;" v-for="(element, index) of item.backupList" :key="index" style="margin:2px;"
:title="element.dns" :color="element.time ? (element.time > config.server.setting.lowSpeedDelay ? 'orange' : 'green') : 'red'" :title="element.title || `测速中:${element.host}`" :color="element.time ? (element.time > config.server.setting.lowSpeedDelay ? 'orange' : 'green') : (element.title ? 'red' : '')"
> >
{{ element.host }} {{ element.time }}{{ element.time ? 'ms' : '' }} {{ element.dns }} {{ element.host }} {{ element.time ? `${element.time}ms` : (element.title ? '' : '测速中') }} {{ element.dns }}
</a-tag> </a-tag>
</a-card> </a-card>
</a-col> </a-col>

View File

@ -152,6 +152,12 @@ $dark-input: #777; //输入框:背景色
border-color: #5a5750; border-color: #5a5750;
color: #cfa572; color: #cfa572;
} }
/* 标签:未知 */
.ant-tag:not(.ant-tag-red, .ant-tag-green, .ant-tag-orange) {
background-color: #5a5a5a;
border-color: #5a5a5a;
color: #ccc;
}
/* 按钮 */ /* 按钮 */
.ant-btn:not(.ant-btn-danger, .ant-btn-primary) { .ant-btn:not(.ant-btn-danger, .ant-btn-primary) {

View File

@ -52,13 +52,18 @@ module.exports = class BaseDNS {
} }
} }
async lookup (hostname) { async lookup (hostname, ipChecker) {
try { try {
let ipCache = this.cache.get(hostname) let ipCache = this.cache.get(hostname)
if (ipCache) { if (ipCache) {
if (ipCache.value != null) { const ip = ipCache.value
ipCache.doCount(ipCache.value, false) if (ip != null) {
return ipCache.value if (ipChecker && ipChecker(ip)) {
ipCache.doCount(ip, false)
return ip
} else {
return hostname
}
} }
} else { } else {
ipCache = new IpCache(hostname) ipCache = new IpCache(hostname)
@ -66,7 +71,7 @@ module.exports = class BaseDNS {
} }
const t = new Date() const t = new Date()
let ipList = await this._lookupInternal(hostname) let ipList = await this._lookupWithPreSetIpList(hostname)
if (ipList == null) { if (ipList == null) {
// 没有获取到ipv4地址 // 没有获取到ipv4地址
ipList = [] ipList = []
@ -74,28 +79,42 @@ module.exports = class BaseDNS {
ipList.push(hostname) // 把原域名加入到统计里去 ipList.push(hostname) // 把原域名加入到统计里去
ipCache.setBackupList(ipList) ipCache.setBackupList(ipList)
log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] ${hostname}${ipCache.value} (${new Date() - t} ms), ipList: ${JSON.stringify(ipList)}, ipCache:`, JSON.stringify(ipCache))
return ipCache.value const ip = ipCache.value
log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] ${hostname}${ip} (${new Date() - t} ms), ipList: ${JSON.stringify(ipList)}, ipCache:`, JSON.stringify(ipCache))
if (ipChecker) {
if (ip != null && ip !== hostname && ipChecker(ip)) {
return ip
}
for (const ip of ipList) {
if (ip !== hostname && ipChecker(ip)) {
return ip
}
}
}
return ip != null ? ip : hostname
} catch (error) { } catch (error) {
log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] cannot resolve hostname ${hostname}, error:`, error) log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] cannot resolve hostname ${hostname}, error:`, error)
return hostname return hostname
} }
} }
async _lookupInternal (hostname) { async _lookupWithPreSetIpList (hostname) {
// 获取当前域名的预设IP列表 // 获取当前域名的预设IP列表
let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`) let hostnamePreSetIpList = matchUtil.matchHostname(this.preSetIpList, hostname, `matched preSetIpList(${this.dnsName})`)
if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) { if (hostnamePreSetIpList && (hostnamePreSetIpList.length > 0 || hostnamePreSetIpList.length === undefined)) {
if (hostnamePreSetIpList.length > 0) { if (hostnamePreSetIpList.length > 0) {
hostnamePreSetIpList = hostnamePreSetIpList.slice() hostnamePreSetIpList = hostnamePreSetIpList.slice() // 复制一份列表数据,避免配置数据被覆盖
} else { } else {
hostnamePreSetIpList = mapToList(hostnamePreSetIpList) hostnamePreSetIpList = mapToList(hostnamePreSetIpList)
} }
if (hostnamePreSetIpList.length > 0) { if (hostnamePreSetIpList.length > 0) {
hostnamePreSetIpList.isPreSet = true hostnamePreSetIpList.isPreSet = true
log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的预设IP列表 ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`) log.info(`[DNS-over-PreSet '${this.dnsName}'] 获取到该域名的预设IP列表 ${hostname} - ${JSON.stringify(hostnamePreSetIpList)}`)
return hostnamePreSetIpList return hostnamePreSetIpList
} }
} }
@ -106,23 +125,59 @@ module.exports = class BaseDNS {
async _lookup (hostname) { async _lookup (hostname) {
const start = Date.now() const start = Date.now()
try { try {
// 执行DNS查询
log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query start: ${hostname}`)
const response = await this._doDnsQuery(hostname) const response = await this._doDnsQuery(hostname)
const cost = Date.now() - start const cost = Date.now() - start
log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query end: ${hostname}, cost: ${cost} ms, response:`, response)
if (response == null || response.answers == null || response.answers.length == null || response.answers.length === 0) { if (response == null || response.answers == null || response.answers.length == null || response.answers.length === 0) {
// 说明没有获取到ip
log.warn(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms, response:`, response) log.warn(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms, response:`, response)
return [] return []
} }
const ret = response.answers.filter(item => item.type === 'A').map(item => item.data) const ret = response.answers.filter(item => item.type === 'A').map(item => item.data)
if (ret.length === 0) { if (ret.length === 0) {
log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IPv4地址: ${hostname}, cost: ${cost} ms`) log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 没有该域名的IP地址: ${hostname}, cost: ${cost} ms`)
} else { } else {
log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的IPv4地址: ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`) log.info(`[DNS-over-${this.dnsType} '${this.dnsName}'] 获取到该域名的IP地址: ${hostname} - ${JSON.stringify(ret)}, cost: ${cost} ms`)
} }
return ret return ret
} catch (e) { } catch (e) {
log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS query error, hostname: ${hostname}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}, cost: ${Date.now() - start} ms, error:`, e) log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS query error, hostname: ${hostname}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}, cost: ${Date.now() - start} ms, error:`, e)
return [] return []
} }
} }
_doDnsQuery (hostname, type) {
return new Promise((resolve, reject) => {
// 设置超时任务
let isOver = false
const timeout = 6000
const timeoutId = setTimeout(() => {
if (!isOver) {
reject(new Error('DNS查询超时'))
}
}, timeout)
try {
this._dnsQueryPromise(hostname, type)
.then((response) => {
isOver = true
clearTimeout(timeoutId)
resolve(response)
})
.catch((e) => {
isOver = true
clearTimeout(timeoutId)
reject(e)
})
} catch (e) {
isOver = true
clearTimeout(timeoutId)
reject(e)
}
})
}
} }

View File

@ -10,7 +10,7 @@ module.exports = class DNSOverHTTPS extends BaseDNS {
this.dnsServer = dnsServer this.dnsServer = dnsServer
} }
async _doDnsQuery (hostname) { _dnsQueryPromise (hostname, type = 'A') {
return await dohQueryAsync({ url: this.dnsServer }, [{ type: 'A', name: hostname }]) return dohQueryAsync({ url: this.dnsServer }, [{ type, name: hostname }])
} }
} }

View File

@ -13,7 +13,7 @@ module.exports = class DNSOverTCP extends BaseDNS {
this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort this.dnsServerPort = Number.parseInt(dnsServerPort) || defaultPort
} }
_doDnsQuery (hostname) { _dnsQueryPromise (hostname, type = 'A') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 构造 DNS 查询报文 // 构造 DNS 查询报文
const packet = dnsPacket.encode({ const packet = dnsPacket.encode({
@ -21,7 +21,7 @@ module.exports = class DNSOverTCP extends BaseDNS {
type: 'query', type: 'query',
id: randi(0x0, 0xFFFF), id: randi(0x0, 0xFFFF),
questions: [{ questions: [{
type: 'A', type,
name: hostname, name: hostname,
}], }],
}) })

View File

@ -11,7 +11,7 @@ module.exports = class DNSOverTLS extends BaseDNS {
this.dnsServerName = dnsServerName this.dnsServerName = dnsServerName
} }
async _doDnsQuery (hostname) { _dnsQueryPromise (hostname, type = 'A') {
const options = { const options = {
host: this.dnsServer, host: this.dnsServer,
port: this.dnsServerPort, port: this.dnsServerPort,
@ -19,9 +19,9 @@ module.exports = class DNSOverTLS extends BaseDNS {
name: hostname, name: hostname,
klass: 'IN', klass: 'IN',
type: 'A', type,
} }
return await dnstls.query(options) return dnstls.query(options)
} }
} }

View File

@ -15,21 +15,28 @@ module.exports = class DNSOverUDP extends BaseDNS {
this.socketType = this.isIPv6 ? 'udp6' : 'udp4' this.socketType = this.isIPv6 ? 'udp6' : 'udp4'
} }
_doDnsQuery (hostname) { _dnsQueryPromise (hostname, type = 'A') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let isOver = false
const timeout = 5000
let timeoutId = null
// 构造 DNS 查询报文 // 构造 DNS 查询报文
const packet = dnsPacket.encode({ const packet = dnsPacket.encode({
flags: dnsPacket.RECURSION_DESIRED, flags: dnsPacket.RECURSION_DESIRED,
type: 'query', type: 'query',
id: randi(0x0, 0xFFFF), id: randi(0x0, 0xFFFF),
questions: [{ questions: [{
type: 'A', type,
name: hostname, name: hostname,
}], }],
}) })
// 创建客户端 // 创建客户端
const udpClient = dgram.createSocket(this.socketType, (msg, _rinfo) => { const udpClient = dgram.createSocket(this.socketType, (msg, _rinfo) => {
isOver = true
clearTimeout(timeoutId)
const response = dnsPacket.decode(msg) const response = dnsPacket.decode(msg)
resolve(response) resolve(response)
udpClient.close() udpClient.close()
@ -38,10 +45,20 @@ module.exports = class DNSOverUDP extends BaseDNS {
// 发送 UDP 查询 // 发送 UDP 查询
udpClient.send(packet, 0, packet.length, this.dnsServerPort, this.dnsServer, (err, _bytes) => { udpClient.send(packet, 0, packet.length, this.dnsServerPort, this.dnsServer, (err, _bytes) => {
if (err) { if (err) {
isOver = true
clearTimeout(timeoutId)
reject(err) reject(err)
udpClient.close() udpClient.close()
} }
}) })
// 设置超时任务
timeoutId = setTimeout(() => {
if (!isOver) {
reject(new Error('查询超时'))
udpClient.close()
}
}, timeout)
}) })
} }
} }

View File

@ -114,7 +114,7 @@ function connect (req, cltSocket, head, hostname, port, dnsConfig = null, isDire
if (dnsConfig && dnsConfig.dnsMap) { if (dnsConfig && dnsConfig.dnsMap) {
const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname) const dns = DnsUtil.hasDnsLookup(dnsConfig, hostname)
if (dns) { if (dns) {
options.lookup = dnsLookup.createLookupFunc(null, dns, 'connect', hostport, isDnsIntercept) options.lookup = dnsLookup.createLookupFunc(null, dns, 'connect', hostport, port, isDnsIntercept)
} }
} }
// 代理连接事件监听 // 代理连接事件监听

View File

@ -124,7 +124,7 @@ module.exports = function createRequestHandler (createIntercepts, middlewares, e
} }
} }
if (dns) { if (dns) {
rOptions.lookup = dnsLookup.createLookupFunc(res, dns, 'request url', url, isDnsIntercept) rOptions.lookup = dnsLookup.createLookupFunc(res, dns, 'request url', url, rOptions.port, isDnsIntercept)
log.debug(`域名 ${rOptions.hostname} DNS: ${dns.dnsName}`) log.debug(`域名 ${rOptions.hostname} DNS: ${dns.dnsName}`)
res.setHeader('DS-DNS', dns.dnsName) res.setHeader('DS-DNS', dns.dnsName)
} else { } else {

View File

@ -2,12 +2,35 @@ const defaultDns = require('node:dns')
const log = require('../../../utils/util.log.server') const log = require('../../../utils/util.log.server')
const speedTest = require('../../speed') const speedTest = require('../../speed')
function createIpChecker (tester) {
if (!tester || tester.backupList == null || tester.backupList.length === 0) {
return null
}
return (ip) => {
for (let i = 0; i < tester.backupList.length; i++) {
const item = tester.backupList[i]
if (item.host === ip) {
if (item.time > 0) {
return true // IP测速成功
}
if (item.status === 'failed') {
return false // IP测速失败
}
break
}
}
return true // IP测速未知
}
}
module.exports = { module.exports = {
createLookupFunc (res, dns, action, target, isDnsIntercept) { createLookupFunc (res, dns, action, target, port, isDnsIntercept) {
target = target ? (`, target: ${target}`) : '' target = target ? (`, target: ${target}`) : ''
return (hostname, options, callback) => { return (hostname, options, callback) => {
const tester = speedTest.getSpeedTester(hostname) const tester = speedTest.getSpeedTester(hostname, port)
if (tester) { if (tester) {
const aliveIpObj = tester.pickFastAliveIpObj() const aliveIpObj = tester.pickFastAliveIpObj()
if (aliveIpObj) { if (aliveIpObj) {
@ -21,7 +44,10 @@ module.exports = {
log.info(`----- ${action}: ${hostname}, no alive ip${target}, tester: { "ready": ${tester.ready}, "backupList": ${JSON.stringify(tester.backupList)} }`) log.info(`----- ${action}: ${hostname}, no alive ip${target}, tester: { "ready": ${tester.ready}, "backupList": ${JSON.stringify(tester.backupList)} }`)
} }
} }
dns.lookup(hostname).then((ip) => {
const ipChecker = createIpChecker(tester)
dns.lookup(hostname, ipChecker).then((ip) => {
if (isDnsIntercept) { if (isDnsIntercept) {
isDnsIntercept.dns = dns isDnsIntercept.dns = dns
isDnsIntercept.hostname = hostname isDnsIntercept.hostname = hostname
@ -29,35 +55,16 @@ module.exports = {
} }
if (ip !== hostname) { if (ip !== hostname) {
// 判断是否为测速失败的IP如果是则不使用当前IP
let isTestFailedIp = false
if (tester && tester.ready && tester.backupList && tester.backupList.length > 0) {
for (let i = 0; i < tester.backupList.length; i++) {
const item = tester.backupList[i]
if (item.host === ip) {
if (item.time == null) {
isTestFailedIp = true
}
break
}
}
}
if (isTestFailedIp === false) {
log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`) log.info(`----- ${action}: ${hostname}, use ip from dns '${dns.dnsName}': ${ip}${target} -----`)
if (res) { if (res) {
res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`) res.setHeader('DS-DNS-Lookup', `DNS: ${ip} ${dns.dnsName === '预设IP' ? 'PreSet' : dns.dnsName}`)
} }
callback(null, ip, 4) callback(null, ip, 4)
return
} else { } else {
// 使用默认dns // 使用默认dns
log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}, skip test failed ip from dns '${dns.dnsName}: ${ip}'${target}, options:`, options) log.info(`----- ${action}: ${hostname}, use default DNS: ${hostname}${target}, options:`, options, ', dns:', dns)
}
} else {
// 使用默认dns
log.info(`----- ${action}: ${hostname}, use hostname by default DNS: ${hostname}${target}, options:`, options, ', dns:', dns)
}
defaultDns.lookup(hostname, options, callback) defaultDns.lookup(hostname, options, callback)
}
}) })
} }
}, },

View File

@ -1,31 +1,44 @@
// 1个小时不访问取消获取 // const { exec } = require('node:child_process')
const net = require('node:net') const net = require('node:net')
const _ = require('lodash') const _ = require('lodash')
const log = require('../../utils/util.log.server') const log = require('../../utils/util.log.server')
const config = require('./config.js') const config = require('./config.js')
// const isWindows = process.platform === 'win32'
const DISABLE_TIMEOUT = 60 * 60 * 1000 const DISABLE_TIMEOUT = 60 * 60 * 1000
class SpeedTester { class SpeedTester {
constructor ({ hostname }) { constructor ({ hostname, port }) {
this.dnsMap = config.getConfig().dnsMap this.dnsMap = config.getConfig().dnsMap
this.hostname = hostname this.hostname = hostname
this.lastReadTime = Date.now() this.port = port || 443
this.ready = false this.ready = false
this.alive = [] this.alive = []
this.backupList = [] this.backupList = []
this.keepCheckId = false
this.loadingIps = false
this.loadingTest = false
this.testCount = 0 this.testCount = 0
this.test() this.lastReadTime = Date.now()
this.keepCheckIntervalId = false
this.tryTestCount = 0
this.test() // 异步:初始化完成后先测速一次
} }
pickFastAliveIpObj () { pickFastAliveIpObj () {
this.touch() this.touch()
if (this.alive.length === 0) { if (this.alive.length === 0) {
if (this.backupList.length > 0 && this.tryTestCount % 10 > 0) {
this.testBackups() // 异步
} else if (this.tryTestCount % 10 === 0) {
this.test() // 异步 this.test() // 异步
}
this.tryTestCount++
return null return null
} }
return this.alive[0] return this.alive[0]
@ -33,26 +46,27 @@ class SpeedTester {
touch () { touch () {
this.lastReadTime = Date.now() this.lastReadTime = Date.now()
if (!this.keepCheckId) { if (!this.keepCheckIntervalId) {
this.startChecker() this.startChecker()
} }
} }
startChecker () { startChecker () {
if (this.keepCheckId) { if (this.keepCheckIntervalId) {
clearInterval(this.keepCheckId) clearInterval(this.keepCheckIntervalId)
} }
this.keepCheckId = setInterval(() => { this.keepCheckIntervalId = setInterval(() => {
if (Date.now() - DISABLE_TIMEOUT > this.lastReadTime) { if (Date.now() - DISABLE_TIMEOUT > this.lastReadTime) {
// 超过很长时间没有访问,取消测试 // 超过很长时间没有访问,取消测试
clearInterval(this.keepCheckId) clearInterval(this.keepCheckIntervalId)
this.keepCheckIntervalId = false
return return
} }
if (this.alive.length > 0) { if (this.alive.length > 0) {
this.testBackups() this.testBackups() // 异步
return } else {
this.test() // 异步
} }
this.test()
}, config.getConfig().interval) }, config.getConfig().interval)
} }
@ -71,44 +85,56 @@ class SpeedTester {
promiseList.push(one) promiseList.push(one)
} }
await Promise.all(promiseList) await Promise.all(promiseList)
const items = [] const items = []
for (const ip in ips) { for (const ip in ips) {
items.push({ host: ip, port: 443, dns: ips[ip].dns }) items.push({ host: ip, dns: ips[ip].dns })
} }
return items return items
} }
async getFromOneDns (dns) { async getFromOneDns (dns) {
return await dns._lookupInternal(this.hostname) return await dns._lookupWithPreSetIpList(this.hostname)
} }
async test () { async test () {
this.testCount++
log.debug(`[speed] test start: ${this.hostname}, testCount: ${this.testCount}`)
try {
const newList = await this.getIpListFromDns(this.dnsMap) const newList = await this.getIpListFromDns(this.dnsMap)
const newBackupList = [...newList, ...this.backupList] const newBackupList = [...newList, ...this.backupList]
this.backupList = _.unionBy(newBackupList, 'host') this.backupList = _.unionBy(newBackupList, 'host')
this.testCount++
log.info('[speed]', this.hostname, '➜ ip-list:', this.backupList)
await this.testBackups() await this.testBackups()
log.info(`[speed] test end: ${this.hostname} ➜ ip-list:`, this.backupList, `, testCount: ${this.testCount}`)
if (config.notify) { if (config.notify) {
config.notify({ key: 'test' }) config.notify({ key: 'test' })
} }
} catch (e) {
log.error(`[speed] test failed: ${this.hostname}, testCount: ${this.testCount}, error:`, e)
}
} }
async testBackups () { async testBackups () {
const testAll = [] if (this.backupList.length > 0) {
const aliveList = [] const aliveList = []
const testAll = []
for (const item of this.backupList) { for (const item of this.backupList) {
testAll.push(this.doTest(item, aliveList)) testAll.push(this.doTest(item, aliveList))
} }
await Promise.all(testAll) await Promise.all(testAll)
this.alive = aliveList this.alive = aliveList
}
this.ready = true this.ready = true
} }
async doTest (item, aliveList) { async doTest (item, aliveList) {
try { try {
const ret = await this.testOne(item) const ret = await this.testOne(item)
item.title = `${ret.by}测速成功:${item.host}`
log.info(`[speed] test success: ${this.hostname}${item.host}:${this.port} from DNS '${item.dns}'`)
_.merge(item, ret) _.merge(item, ret)
aliveList.push({ ...ret, ...item }) aliveList.push({ ...ret, ...item })
aliveList.sort((a, b) => a.time - b.time) aliveList.sort((a, b) => a.time - b.time)
@ -125,48 +151,131 @@ class SpeedTester {
return a.time - b.time return a.time - b.time
}) })
} catch (e) { } catch (e) {
if (e.message !== 'timeout') { if (item.time == null) {
log.warn('[speed] test error: ', this.hostname, `${item.host}:${item.port} from DNS '${item.dns}'`, ', errorMsg:', e.message) item.title = e.message
item.status = 'failed'
}
if (!e.message.includes('timeout')) {
log.warn(`[speed] test error: ${this.hostname}${item.host}:${this.port} from DNS '${item.dns}', errorMsg: ${e.message}`)
} }
} }
} }
testOne (item) { testByTCP (item) {
const timeout = 5000
const { host, port, dns } = item
const startTime = Date.now()
let isOver = false
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const { host, dns } = item
const startTime = Date.now()
let isOver = false
const timeout = 5000
let timeoutId = null let timeoutId = null
const client = net.createConnection({ host, port }, () => {
// 'connect' 监听器 const client = net.createConnection({ host, port: this.port }, () => {
const connectionTime = Date.now()
isOver = true isOver = true
clearTimeout(timeoutId) clearTimeout(timeoutId)
resolve({ status: 'success', time: connectionTime - startTime })
const connectionTime = Date.now()
resolve({ status: 'success', by: 'TCP', time: connectionTime - startTime })
client.end() client.end()
}) })
client.on('end', () => {
})
client.on('error', (e) => { client.on('error', (e) => {
if (e.message !== 'timeout') {
log.warn('[speed] test error: ', this.hostname, `${host}:${port} from DNS '${dns}', cost: ${Date.now() - startTime} ms, errorMsg:`, e.message)
}
isOver = true isOver = true
clearTimeout(timeoutId) clearTimeout(timeoutId)
log.warn('[speed] test by TCP error: ', this.hostname, `${host}:${this.port} from DNS '${dns}', cost: ${Date.now() - startTime} ms, errorMsg:`, e.message)
reject(e) reject(e)
client.end()
}) })
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
if (isOver) { if (isOver) {
return return
} }
log.warn('[speed] test timeout:', this.hostname, `${host}:${port} from DNS '${dns}', cost: ${Date.now() - startTime} ms`)
log.warn('[speed] test by TCP timeout:', this.hostname, `${host}:${this.port} from DNS '${dns}', cost: ${Date.now() - startTime} ms`)
reject(new Error('timeout')) reject(new Error('timeout'))
client.end() client.end()
}, timeout) }, timeout)
}) })
} }
// 暂不使用
// testByPing (item) {
// return new Promise((resolve, reject) => {
// const { host, dns } = item
// const startTime = Date.now()
//
// // 设置超时程序
// let isOver = false
// const timeout = 5000
// const timeoutId = setTimeout(() => {
// if (!isOver) {
// log.warn('[speed] test by PING timeout:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms`)
// reject(new Error('timeout'))
// }
// }, timeout)
//
// // 协议选择如强制ping6
// const usePing6 = !isWindows && host.includes(':') // Windows无ping6命令
// const cmd = usePing6
// ? `ping6 -c 2 ${host}`
// : isWindows
// ? `ping -n 2 ${host}`
// : `ping -c 2 ${host}`
//
// log.debug('[speed] test by PING start:', this.hostname, `➜ ${host} from DNS '${dns}'`)
// exec(cmd, (error, stdout, _stderr) => {
// isOver = true
// clearTimeout(timeoutId)
//
// if (error) {
// log.warn('[speed] test by PING error:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms, error: 目标不可达或超时`)
// reject(new Error('目标不可达或超时'))
// return
// }
//
// // 提取延迟数据(正则匹配)
// const regex = /[=<](\d+(?:\.\d*)?)ms/gi // 适配Linux/Windows
// const times = []
// let match
// // eslint-disable-next-line no-cond-assign
// while ((match = regex.exec(stdout)) !== null) {
// times.push(Number.parseFloat(match[1]))
// }
//
// if (times.length === 0) {
// log.warn('[speed] test by PING error:', this.hostname, `➜ ${host} from DNS '${dns}', cost: ${Date.now() - startTime} ms, error: 无法解析延迟`)
// reject(new Error('无法解析延迟'))
// } else {
// // 计算平均延迟
// const avg = times.reduce((a, b) => a + b, 0) / times.length
// resolve({ status: 'success', by: 'PING', time: Math.round(avg) })
// }
// })
// })
// }
testOne (item) {
return new Promise((resolve, reject) => {
const thenFun = (ret) => {
resolve(ret)
}
// 先用TCP测速
this.testByTCP(item)
.then(thenFun)
.catch((e) => {
// // TCP测速失败再用 PING 测速
// this.testByPing(item)
// .then(thenFun)
// .catch((e2) => {
// reject(new Error(`TCP测速失败${e.message}PING测速失败${e2.message}`))
// })
reject(new Error(`TCP测速失败${e.message}`))
})
})
}
} }
module.exports = SpeedTester module.exports = SpeedTester

View File

@ -1,9 +1,9 @@
const config = { const config = {
notify () {},
dnsMap: {}, dnsMap: {},
} }
module.exports = { module.exports = {
getConfig () { getConfig () {
return config return config
}, },
notify: null,
} }

View File

@ -6,6 +6,28 @@ const SpeedTester = require('./SpeedTester.js')
const SpeedTestPool = { const SpeedTestPool = {
} }
function addSpeedTest (hostname, port) {
if (!port) {
const idx = hostname.indexOf(':')
if (idx > 0 && idx === hostname.lastIndexOf(':')) {
const arr = hostname.split(':')
hostname = arr[0]
port = Number.parseInt(arr[1]) || 443
} else {
port = 443
}
}
// 443端口不拼接在key上
const key = port === 443 ? hostname : `${hostname}:${port}`
if (SpeedTestPool[key] == null) {
return SpeedTestPool[key] = new SpeedTester({ hostname, port })
}
return SpeedTestPool[key]
}
function initSpeedTest (runtimeConfig) { function initSpeedTest (runtimeConfig) {
const { enabled, hostnameList } = runtimeConfig const { enabled, hostnameList } = runtimeConfig
const conf = config.getConfig() const conf = config.getConfig()
@ -14,45 +36,42 @@ function initSpeedTest (runtimeConfig) {
return return
} }
_.forEach(hostnameList, (hostname) => { _.forEach(hostnameList, (hostname) => {
SpeedTestPool[hostname] = new SpeedTester({ hostname }) addSpeedTest(hostname)
}) })
log.info('[speed] enabled') log.info('[speed] enabledSpeedTestPool:', SpeedTestPool)
} }
function getAllSpeedTester () { function getAllSpeedTester () {
const allSpeed = {} const allSpeed = {}
if (!config.getConfig().enabled) {
return allSpeed if (config.getConfig().enabled) {
}
_.forEach(SpeedTestPool, (item, key) => { _.forEach(SpeedTestPool, (item, key) => {
allSpeed[key] = { allSpeed[key] = {
hostname: key, hostname: item.hostname,
port: item.port,
alive: item.alive, alive: item.alive,
backupList: item.backupList, backupList: item.backupList,
} }
}) })
}
return allSpeed return allSpeed
} }
function getSpeedTester (hostname) { function getSpeedTester (hostname, port) {
if (!config.getConfig().enabled) { if (!config.getConfig().enabled) {
return return null
} }
let instance = SpeedTestPool[hostname] return addSpeedTest(hostname, port)
if (instance == null) {
instance = new SpeedTester({ hostname })
SpeedTestPool[hostname] = instance
}
return instance
} }
function registerNotify (notify) { // function registerNotify (notify) {
config.notify = notify // config.notify = notify
} // }
function reSpeedTest () { function reSpeedTest () {
_.forEach(SpeedTestPool, (item, key) => { _.forEach(SpeedTestPool, (item, _key) => {
item.test() item.test() // 异步
}) })
} }
@ -68,8 +87,8 @@ module.exports = {
SpeedTester, SpeedTester,
initSpeedTest, initSpeedTest,
getSpeedTester, getSpeedTester,
getAllSpeedTester, // getAllSpeedTester,
registerNotify, // registerNotify,
reSpeedTest, reSpeedTest,
action, action,
} }

View File

@ -26,3 +26,12 @@ assert.strictEqual(isEmpty(0), false)
assert.strictEqual(isEmpty(-1), false) assert.strictEqual(isEmpty(-1), false)
assert.strictEqual(isEmpty(''), false) assert.strictEqual(isEmpty(''), false)
assert.strictEqual(isEmpty('1'), false) assert.strictEqual(isEmpty('1'), false)
// test lodash.unionBy
const list = [
{ host: 1, port: 1, dns: 2 },
{ host: 1, port: 1, dns: 3 },
{ host: 1, port: 2, dns: 3 },
{ host: 1, port: 2, dns: 3 },
]
console.info(lodash.unionBy(list, 'host', 'port'))