新增 支持显示地区码(机场三字码,仅限 Cloudflare、AWS CloudFront,HTTPing 和 下载测速(无论是哪个测速模式)过程中都会自动获取);

新增 调试模式运行参数(-debug 方便排查下载测速过程中遇到的问题);
新增 彩色输出内容;
调整 当没找到符合速度条件的 IP 时,默认不再直接忽略条件输出所有 IP 测速结果了,而是只有在调试模式下才会输出;
This commit is contained in:
xiu2
2025-06-29 00:30:05 +08:00
parent 04da9f5659
commit 66912dd657
6 changed files with 214 additions and 103 deletions

View File

@@ -54,7 +54,7 @@ func TestDownloadSpeed(ipSet utils.PingDelaySet) (speedSet utils.DownloadSpeedSe
return utils.DownloadSpeedSet(ipSet)
}
if len(ipSet) <= 0 { // IP数组长度(IP数量) 大于 0 时才会继续下载测速
fmt.Println("\n[信息] 延迟测速结果 IP 数量为 0跳过下载测速。")
fmt.Println("\n\033[33m[信息] 延迟测速结果 IP 数量为 0跳过下载测速。\033[0m")
return
}
testNum := TestCount
@@ -65,7 +65,7 @@ func TestDownloadSpeed(ipSet utils.PingDelaySet) (speedSet utils.DownloadSpeedSe
TestCount = testNum
}
fmt.Printf("开始下载测速(下限:%.2f MB/s, 数量:%d, 队列:%d\n", MinSpeed, TestCount, testNum)
fmt.Printf("\033[34m开始下载测速(下限:%.2f MB/s, 数量:%d, 队列:%d\033[0m\n", MinSpeed, TestCount, testNum)
// 控制 下载测速进度条 与 延迟测速进度条 长度一致(强迫症)
bar_a := len(strconv.Itoa(len(ipSet)))
bar_b := " "
@@ -74,8 +74,11 @@ func TestDownloadSpeed(ipSet utils.PingDelaySet) (speedSet utils.DownloadSpeedSe
}
bar := utils.NewBar(TestCount, bar_b, "")
for i := 0; i < testNum; i++ {
speed := downloadHandler(ipSet[i].IP)
speed, colo := downloadHandler(ipSet[i].IP)
ipSet[i].DownloadSpeed = speed
if ipSet[i].Colo == "" { // 只有当 Colo 是空的时候,才写入,否则代表之前是 httping 测速并获取过了
ipSet[i].Colo = colo
}
// 在每个 IP 下载测速后,以 [下载速度下限] 条件过滤结果
if speed >= MinSpeed*1024*1024 {
bar.Grow(1, "")
@@ -86,7 +89,8 @@ func TestDownloadSpeed(ipSet utils.PingDelaySet) (speedSet utils.DownloadSpeedSe
}
}
bar.Done()
if len(speedSet) == 0 { // 没有符合速度限制的数据,返回所有测数据
if utils.Debug && len(speedSet) == 0 { // 调试模式下,没有满足速度限制的数据,返回所有测数据供用户查看当前的测速结果,以便适当调低预期测速条件
fmt.Println("\033[33m[调试] 没有满足 下载速度下限 条件的 IP忽略条件返回所有测速数据方便下次测速时调整条件。\033[0m")
speedSet = utils.DownloadSpeedSet(ipSet)
}
// 按速度排序
@@ -107,12 +111,15 @@ func getDialContext(ip *net.IPAddr) func(ctx context.Context, network, address s
}
// return download Speed
func downloadHandler(ip *net.IPAddr) float64 {
func downloadHandler(ip *net.IPAddr) (float64, string) {
client := &http.Client{
Transport: &http.Transport{DialContext: getDialContext(ip)},
Timeout: Timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) > 10 { // 限制最多重定向 10 次
if utils.Debug { // 调试模式下,输出更多信息
fmt.Printf("\033[31m[调试] IP: %s, 下载测速地址重定向次数过多终止测速URL: %s\033[0m\n", ip.String(), req.URL.String())
}
return http.ErrUseLastResponse
}
if req.Header.Get("Referer") == defaultURL { // 当使用默认下载测速地址时,重定向不携带 Referer
@@ -123,19 +130,31 @@ func downloadHandler(ip *net.IPAddr) float64 {
}
req, err := http.NewRequest("GET", URL, nil)
if err != nil {
return 0.0
if utils.Debug { // 调试模式下,输出更多信息
fmt.Printf("\033[31m[调试] IP: %s, 下载测速请求创建失败,错误信息: %v, URL: %s\033[0m\n", ip.String(), err, URL)
}
return 0.0, ""
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36")
response, err := client.Do(req)
if err != nil {
return 0.0
if utils.Debug { // 调试模式下,输出更多信息
fmt.Printf("\033[31m[调试] IP: %s, 下载测速失败,错误信息: %v, URL: , 最终URL: %s%s\033[0m\n", ip.String(), err, URL, response.Request.URL.String())
}
return 0.0, ""
}
defer response.Body.Close()
if response.StatusCode != 200 {
return 0.0
if utils.Debug { // 调试模式下,输出更多信息
fmt.Printf("\033[31m[调试] IP: %s, 下载测速终止HTTP 状态码: %d, URL: %s, 最终URL: %s\033[0m\n", ip.String(), response.StatusCode, URL, response.Request.URL.String())
}
return 0.0, ""
}
// 通过头部 Server 值判断是 Cloudflare 还是 AWS CloudFront 并设置 cfRay 为各自的机场地区码完整内容
colo := getHeaderColo(response.Header)
timeStart := time.Now() // 开始时间(当前)
timeEnd := timeStart.Add(Timeout) // 加上下载测速时间得到的结束时间
@@ -179,5 +198,5 @@ func downloadHandler(ip *net.IPAddr) float64 {
}
contentRead += int64(bufferRead)
}
return e.Value() / (Timeout.Seconds() / 120)
return e.Value() / (Timeout.Seconds() / 120), colo
}

View File

@@ -18,11 +18,11 @@ var (
HttpingStatusCode int
HttpingCFColo string
HttpingCFColomap *sync.Map
OutRegexp = regexp.MustCompile(`[A-Z]{3}`)
ColoRegexp = regexp.MustCompile(`[A-Z]{3}`)
)
// pingReceived pingTotalTime
func (p *Ping) httping(ip *net.IPAddr) (int, time.Duration) {
func (p *Ping) httping(ip *net.IPAddr) (int, time.Duration, string) {
hc := http.Client{
Timeout: time.Second * 2,
Transport: &http.Transport{
@@ -35,84 +35,79 @@ func (p *Ping) httping(ip *net.IPAddr) (int, time.Duration) {
}
// 先访问一次获得 HTTP 状态码 及 Cloudflare Colo
var colo string
{
requ, err := http.NewRequest(http.MethodHead, URL, nil)
request, err := http.NewRequest(http.MethodHead, URL, nil)
if err != nil {
return 0, 0
return 0, 0, ""
}
requ.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36")
resp, err := hc.Do(requ)
request.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36")
response, err := hc.Do(request)
if err != nil {
return 0, 0
return 0, 0, ""
}
defer resp.Body.Close()
defer response.Body.Close()
//fmt.Println("IP:", ip, "StatusCode:", resp.StatusCode, resp.Request.URL)
//fmt.Println("IP:", ip, "StatusCode:", response.StatusCode, response.Request.URL)
// 如果未指定的 HTTP 状态码,或指定的状态码不合规,则默认只认为 200、301、302 才算 HTTPing 通过
if HttpingStatusCode == 0 || HttpingStatusCode < 100 && HttpingStatusCode > 599 {
if resp.StatusCode != 200 && resp.StatusCode != 301 && resp.StatusCode != 302 {
return 0, 0
if response.StatusCode != 200 && response.StatusCode != 301 && response.StatusCode != 302 {
return 0, 0, ""
}
} else {
if resp.StatusCode != HttpingStatusCode {
return 0, 0
if response.StatusCode != HttpingStatusCode {
return 0, 0, ""
}
}
io.Copy(io.Discard, resp.Body)
io.Copy(io.Discard, response.Body)
// 只有指定了地区才匹配机场三字码
// 通过头部 Server 值判断是 Cloudflare 还是 AWS CloudFront 并设置 cfRay 为各自的机场地区码完整内容
colo = getHeaderColo(response.Header)
// 只有指定了地区才匹配机场地区码
if HttpingCFColo != "" {
// 通过头部 Server 值判断是 Cloudflare 还是 AWS CloudFront 并设置 cfRay 为各自的机场三字码完整内容
cfRay := func() string {
if resp.Header.Get("Server") == "cloudflare" {
return resp.Header.Get("CF-RAY") // 示例 cf-ray: 7bd32409eda7b020-SJC
}
return resp.Header.Get("x-amz-cf-pop") // 示例 X-Amz-Cf-Pop: SIN52-P1
}()
colo := p.getColo(cfRay)
if colo == "" { // 没有匹配到三字码或不符合指定地区则直接结束该 IP 测试
return 0, 0
// 判断是否匹配指定的地区码
colo = p.filterColo(colo)
if colo == "" { // 没有匹配到地区码或不符合指定地区则直接结束该 IP 测试
return 0, 0, ""
}
}
}
// 循环测速计算延迟
success := 0
var delay time.Duration
for i := 0; i < PingTimes; i++ {
requ, err := http.NewRequest(http.MethodHead, URL, nil)
request, err := http.NewRequest(http.MethodHead, URL, nil)
if err != nil {
log.Fatal("意外的错误,情报告:", err)
return 0, 0
return 0, 0, ""
}
requ.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36")
request.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36")
if i == PingTimes-1 {
requ.Header.Set("Connection", "close")
request.Header.Set("Connection", "close")
}
startTime := time.Now()
resp, err := hc.Do(requ)
response, err := hc.Do(request)
if err != nil {
continue
}
success++
io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
duration := time.Since(startTime)
delay += duration
}
return success, delay
return success, delay, colo
}
func MapColoMap() *sync.Map {
if HttpingCFColo == "" {
return nil
}
// 将参数指定的地区三字码转为大写并格式化
// 将 -cfcolo 参数指定的地区地区码转为大写并格式化
colos := strings.Split(strings.ToUpper(HttpingCFColo), ",")
colomap := &sync.Map{}
for _, colo := range colos {
@@ -121,21 +116,36 @@ func MapColoMap() *sync.Map {
return colomap
}
func (p *Ping) getColo(b string) string {
if b == "" {
// 从响应头中获取 地区码 值
func getHeaderColo(header http.Header) (colo string) {
// 如果是 Cloudflare 的服务器,则获取 CF-RAY 头部
if header.Get("Server") == "cloudflare" {
colo = header.Get("CF-RAY") // 示例 cf-ray: 7bd32409eda7b020-SJC
} else { // 如果是 AWS CloudFront 的服务器,则获取 X-Amz-Cf-Pop 头部
colo = header.Get("x-amz-cf-pop") // 示例 X-Amz-Cf-Pop: SIN52-P1
}
// 如果没有获取到头部信息,说明不是 Cloudflare 和 AWS CloudFront则直接返回空字符串
if colo == "" {
return ""
}
// 正则匹配并返回 机场三字
out := OutRegexp.FindString(b)
// 正则匹配并返回 机场地区
return ColoRegexp.FindString(colo)
}
// 处理地区码
func (p *Ping) filterColo(colo string) string {
if colo == "" {
return ""
}
// 如果没有指定 -cfcolo 参数,则直接返回
if HttpingCFColomap == nil {
return out
return colo
}
// 匹配 机场三字码 是否为指定的地区
_, ok := HttpingCFColomap.Load(out)
// 匹配 机场地区码 是否为指定的地区
_, ok := HttpingCFColomap.Load(colo)
if ok {
return out
return colo
}
return ""
}

View File

@@ -64,9 +64,9 @@ func (p *Ping) Run() utils.PingDelaySet {
return p.csv
}
if Httping {
fmt.Printf("开始延迟测速模式HTTP, 端口:%d, 范围:%v ~ %v ms, 丢包:%.2f)\n", TCPPort, utils.InputMinDelay.Milliseconds(), utils.InputMaxDelay.Milliseconds(), utils.InputMaxLossRate)
fmt.Printf("\033[34m开始延迟测速模式HTTP, 端口:%d, 范围:%v ~ %v ms, 丢包:%.2f)\033[0m\n", TCPPort, utils.InputMinDelay.Milliseconds(), utils.InputMaxDelay.Milliseconds(), utils.InputMaxLossRate)
} else {
fmt.Printf("开始延迟测速模式TCP, 端口:%d, 范围:%v ~ %v ms, 丢包:%.2f)\n", TCPPort, utils.InputMinDelay.Milliseconds(), utils.InputMaxDelay.Milliseconds(), utils.InputMaxLossRate)
fmt.Printf("\033[34m开始延迟测速模式TCP, 端口:%d, 范围:%v ~ %v ms, 丢包:%.2f)\033[0m\n", TCPPort, utils.InputMinDelay.Milliseconds(), utils.InputMaxDelay.Milliseconds(), utils.InputMaxLossRate)
}
for _, ip := range p.ips {
p.wg.Add(1)
@@ -104,11 +104,12 @@ func (p *Ping) tcping(ip *net.IPAddr) (bool, time.Duration) {
}
// pingReceived pingTotalTime
func (p *Ping) checkConnection(ip *net.IPAddr) (recv int, totalDelay time.Duration) {
func (p *Ping) checkConnection(ip *net.IPAddr) (recv int, totalDelay time.Duration, colo string) {
if Httping {
recv, totalDelay = p.httping(ip)
recv, totalDelay, colo = p.httping(ip)
return
}
colo = "" // TCPing 不获取 colo
for i := 0; i < PingTimes; i++ {
if ok, delay := p.tcping(ip); ok {
recv++
@@ -128,7 +129,7 @@ func (p *Ping) appendIPData(data *utils.PingData) {
// handle tcping
func (p *Ping) tcpingHandler(ip *net.IPAddr) {
recv, totalDlay := p.checkConnection(ip)
recv, totalDlay, colo := p.checkConnection(ip)
nowAble := len(p.csv)
if recv != 0 {
nowAble++
@@ -142,6 +143,7 @@ func (p *Ping) tcpingHandler(ip *net.IPAddr) {
Sended: PingTimes,
Received: recv,
Delay: totalDlay / time.Duration(recv),
Colo: colo,
}
p.appendIPData(data)
}