From 9dc7b3eecf3df749912435eb7595e1f29a1a07b2 Mon Sep 17 00:00:00 2001 From: GuanM <1-GuanM@users.noreply.gitlab.54sxh.cn> Date: Wed, 19 Jun 2024 14:13:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E7=89=88=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 17 ++- go.sum | 35 +++--- main.go | 110 ++++++++++++----- task/Yinyong.go | 125 ------------------- task/download.go | 111 +++++++++-------- task/httping.go | 143 ++++++++++++++++++++++ task/ip.go | 304 ++++++++++++++++++++++++++-------------------- task/tcping.go | 146 ++++++++++++++++++++++ task/tcpping.go | 81 ------------ utils/csv.go | 187 ++++++++++++++++++++++++++++ utils/progress.go | 25 ++++ 11 files changed, 834 insertions(+), 450 deletions(-) delete mode 100644 task/Yinyong.go create mode 100644 task/httping.go create mode 100644 task/tcping.go delete mode 100644 task/tcpping.go create mode 100644 utils/csv.go create mode 100644 utils/progress.go diff --git a/go.mod b/go.mod index b8ad0b8..c05164c 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,12 @@ module DockerST go 1.22 require ( - github.com/VividCortex/ewma v1.2.0 - github.com/schollz/progressbar/v3 v3.14.4 -) - -require ( - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect - github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/cheggaaa/pb/v3 v3.1.5 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sys v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 9a8a09f..3b4c083 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,18 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= -github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= +github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= diff --git a/main.go b/main.go index 47ebca6..4dd8ca3 100644 --- a/main.go +++ b/main.go @@ -2,50 +2,100 @@ package main import ( "DockerST/task" + "DockerST/utils" "flag" "fmt" + "os" + "runtime" + "time" ) var ( - VersionPrint bool - Version string + version, versionNew string ) func init() { - // checkUpdate() - // 定义一个字符串类型的命令行标志 - flag.IntVar(&task.TcpPort, "tcp", 443, "TCP端口") - flag.IntVar(&task.PingTimes, "t", 4, "Ping次数") - flag.IntVar(&task.Routines, "r", 200, "存活检测并发数检测") - flag.IntVar(&task.MinMS, "mis", 0, "只输出高于指定平均延迟的 IP") - flag.IntVar(&task.MaxMS, "mxs", 1000, "只输出低于指定平均延迟的 IP") - flag.IntVar(&task.DownloadNum, "dn", 10, "下载数量") - flag.StringVar(&task.URL, "url", "https://cf.xiu2.xyz/url", "默认文件下载地址") - flag.Float64Var(&task.MinSpeed, "md", 0, "最低下载速度") - flag.BoolVar(&task.TestAll, "ta", false, "测试所有 IP") - flag.BoolVar(&task.Disable, "dd", true, "禁止下载") - flag.BoolVar(&VersionPrint, "v", false, "输出版本") - flag.BoolVar(&task.IsOff, "om", false, "不下载子网列表") + var printVersion bool + var minDelay, maxDelay, downloadTime int + var maxLossRate float64 + flag.IntVar(&task.Routines, "n", 200, "延迟测速线程") + flag.IntVar(&task.PingTimes, "t", 4, "延迟测速次数") + flag.IntVar(&task.TestCount, "dn", 10, "下载测速数量") + flag.IntVar(&downloadTime, "dt", 10, "下载测速时间") + flag.IntVar(&task.TCPPort, "tp", 443, "指定测速端口") + flag.StringVar(&task.URL, "url", "https://cf.xiu2.xyz/url", "指定测速地址") + + flag.BoolVar(&task.Httping, "httping", false, "切换测速模式") + flag.IntVar(&task.HttpingStatusCode, "httping-code", 0, "有效状态代码") + flag.StringVar(&task.HttpingCFColo, "cfcolo", "", "匹配指定地区") + + flag.IntVar(&maxDelay, "tl", 9999, "平均延迟上限") + flag.IntVar(&minDelay, "tll", 0, "平均延迟下限") + flag.Float64Var(&maxLossRate, "tlr", 1, "丢包几率上限") + flag.Float64Var(&task.MinSpeed, "sl", 0, "下载速度下限") + + flag.IntVar(&utils.PrintNum, "p", 10, "显示结果数量") + flag.StringVar(&task.IPFile, "f", "ip.txt", "IP段数据文件") + flag.StringVar(&task.IPText, "ip", "", "指定IP段数据") + flag.StringVar(&utils.Output, "o", "result.csv", "输出结果文件") + + flag.BoolVar(&task.Disable, "dd", false, "禁用下载测速") + flag.BoolVar(&task.TestAll, "allip", false, "测速全部 IP") + + flag.BoolVar(&printVersion, "v", false, "打印程序版本") flag.Parse() - if VersionPrint { - fmt.Println("Version:", Version) + + if task.MinSpeed > 0 && time.Duration(maxDelay)*time.Millisecond == utils.InputMaxDelay { + fmt.Println("[小提示] 在使用 [-sl] 参数时,建议搭配 [-tl] 参数,以避免因凑不够 [-dn] 数量而一直测速...") + } + utils.InputMaxDelay = time.Duration(maxDelay) * time.Millisecond + utils.InputMinDelay = time.Duration(minDelay) * time.Millisecond + utils.InputMaxLossRate = float32(maxLossRate) + task.Timeout = time.Duration(downloadTime) * time.Second + task.HttpingCFColomap = task.MapColoMap() + + if printVersion { + println(version) + fmt.Println("检查版本更新中...") + checkUpdate() + if versionNew != "" { + fmt.Printf("*** 发现新版本 [%s]!请前往 [https://github.com/sxhoio/DockerST] 更新! ***", versionNew) + } else { + fmt.Println("当前为最新版本 [" + version + "]!") + } + os.Exit(0) } } func main() { - if VersionPrint { + task.InitRandSeed() // 置随机数种子 + + fmt.Printf("# XIU2/CloudflareSpeedTest %s \n\n", version) + + // 开始延迟测速 + 过滤延迟/丢包 + pingData := task.NewPing().Run().FilterDelay().FilterLossRate() + // 开始下载测速 + speedData := task.TestDownloadSpeed(pingData) + utils.ExportCsv(speedData) // 输出文件 + speedData.Print() // 打印结果 + + if versionNew != "" { + fmt.Printf("\n*** 发现新版本 [%s]!请前往 [https://github.com/sxhoio/DockerST] 更新! ***\n", versionNew) + } + endPrint() +} + +func endPrint() { + if utils.NoPrintResult() { return } - // InitRandSeed 初始化随机数种子 - task.InitRandSeed() - // 输出版本 - fmt.Printf("# DockerST %s \n", Version) - pingData := task.CreateData().Run().ExcludeInvalid().SortNodesDesc() - DownloadData := task.TestDownloadSpeed(pingData) - for a, v := range DownloadData { - if a == 10 { - return - } - fmt.Printf("IP: %s, 延迟: %v, 下载速度: %.2f MB/s\n", v.IP.String(), v.Delay, v.DownloadSpeed) + if runtime.GOOS == "windows" { // 如果是 Windows 系统,则需要按下 回车键 或 Ctrl+C 退出(避免通过双击运行时,测速完毕后直接关闭) + fmt.Printf("按下 回车键 或 Ctrl+C 退出。") + fmt.Scanln() } } + +// 检查更新 +func checkUpdate() { + // 暂无 +} diff --git a/task/Yinyong.go b/task/Yinyong.go deleted file mode 100644 index 1013906..0000000 --- a/task/Yinyong.go +++ /dev/null @@ -1,125 +0,0 @@ -package task - -import ( - "log" - "net" - "strconv" - "strings" -) - -type IPRanges struct { - ips []*net.IPAddr - mask string - firstIP net.IP - ipNet *net.IPNet -} - -func newIPRanges() *IPRanges { - return &IPRanges{ - ips: make([]*net.IPAddr, 0), - } -} - -// 如果是单独 IP 则加上子网掩码,反之则获取子网掩码(r.mask) -func (r *IPRanges) fixIP(ip string) string { - // 如果不含有 '/' 则代表不是 IP 段,而是一个单独的 IP,因此需要加上 /32 /128 子网掩码 - if i := strings.IndexByte(ip, '/'); i < 0 { - if IsIpv4(ip) { - r.mask = "/32" - } else { - r.mask = "/128" - } - ip += r.mask - } else { - r.mask = ip[i:] - } - return ip -} - -// 解析 IP 段,获得 IP、IP 范围、子网掩码 -func (r *IPRanges) parseCIDR(ip string) { - var err error - if r.firstIP, r.ipNet, err = net.ParseCIDR(r.fixIP(ip)); err != nil { - log.Fatalln("ParseCIDR err", err) - } -} - -func (r *IPRanges) appendIPv4(d byte) { - r.appendIP(net.IPv4(r.firstIP[12], r.firstIP[13], r.firstIP[14], d)) -} - -func (r *IPRanges) appendIP(ip net.IP) { - r.ips = append(r.ips, &net.IPAddr{IP: ip}) -} - -// 返回第四段 ip 的最小值及可用数目 -func (r *IPRanges) getIPRange() (minIP, hosts byte) { - minIP = r.firstIP[15] & r.ipNet.Mask[3] // IP 第四段最小值 - - // 根据子网掩码获取主机数量 - m := net.IPv4Mask(255, 255, 255, 255) - for i, v := range r.ipNet.Mask { - m[i] ^= v - } - total, _ := strconv.ParseInt(m.String(), 16, 32) // 总可用 IP 数 - if total > 255 { // 矫正 第四段 可用 IP 数 - hosts = 255 - return - } - hosts = byte(total) - return -} - -func (r *IPRanges) chooseIPv4() { - if r.mask == "/32" { // 单个 IP 则无需随机,直接加入自身即可 - r.appendIP(r.firstIP) - } else { - minIP, hosts := r.getIPRange() // 返回第四段 IP 的最小值及可用数目 - for r.ipNet.Contains(r.firstIP) { // 只要该 IP 没有超出 IP 网段范围,就继续循环随机 - if TestAll { // 如果是测速全部 IP - for i := 0; i <= int(hosts); i++ { // 遍历 IP 最后一段最小值到最大值 - r.appendIPv4(byte(i) + minIP) - } - } else { // 随机 IP 的最后一段 0.0.0.X - r.appendIPv4(minIP + randIPEndWith(hosts)) - } - r.firstIP[14]++ // 0.0.(X+1).X - if r.firstIP[14] == 0 { - r.firstIP[13]++ // 0.(X+1).X.X - if r.firstIP[13] == 0 { - r.firstIP[12]++ // (X+1).X.X.X - } - } - } - } -} - -func (r *IPRanges) chooseIPv6() { - if r.mask == "/128" { // 单个 IP 则无需随机,直接加入自身即可 - r.appendIP(r.firstIP) - } else { - var tempIP uint8 // 临时变量,用于记录前一位的值 - for r.ipNet.Contains(r.firstIP) { // 只要该 IP 没有超出 IP 网段范围,就继续循环随机 - r.firstIP[15] = randIPEndWith(255) // 随机 IP 的最后一段 - r.firstIP[14] = randIPEndWith(255) // 随机 IP 的最后一段 - - targetIP := make([]byte, len(r.firstIP)) - _ = copy(targetIP, r.firstIP) - r.appendIP(targetIP) // 加入 IP 地址池 - - for i := 13; i >= 0; i-- { // 从倒数第三位开始往前随机 - tempIP = r.firstIP[i] // 保存前一位的值 - r.firstIP[i] += randIPEndWith(255) // 随机 0~255,加到当前位上 - if r.firstIP[i] >= tempIP { // 如果当前位的值大于等于前一位的值,说明随机成功了,可以退出该循环 - break - } - } - } - } -} -func randIPEndWith(num byte) byte { - if num == 0 { // 对于 /32 这种单独的 IP - return byte(0) - } - return byte(randGen.Intn(int(num))) -} diff --git a/task/download.go b/task/download.go index b5d986a..87c8bfd 100644 --- a/task/download.go +++ b/task/download.go @@ -1,14 +1,15 @@ package task import ( + "DockerST/utils" "context" "fmt" "github.com/VividCortex/ewma" - "github.com/schollz/progressbar/v3" "io" "net" "net/http" "sort" + "strconv" "time" ) @@ -22,72 +23,81 @@ const ( ) var ( - URL = defaultURL - Timeout = defaultTimeout - Disable = defaultDisableDownload - DownloadNum = defaultTestNum - MinSpeed = defaultMinSpeed + URL = defaultURL + Timeout = defaultTimeout + Disable = defaultDisableDownload + + TestCount = defaultTestNum + MinSpeed = defaultMinSpeed ) -type BySpeed []IPDelay +func checkDownloadDefault() { + if URL == "" { + URL = defaultURL + } + if Timeout <= 0 { + Timeout = defaultTimeout + } + if TestCount <= 0 { + TestCount = defaultTestNum + } + if MinSpeed <= 0.0 { + MinSpeed = defaultMinSpeed + } +} -func TestDownloadSpeed(p []IPDelay) []IPDelay { +func TestDownloadSpeed(ipSet utils.PingDelaySet) (speedSet utils.DownloadSpeedSet) { + checkDownloadDefault() if Disable { - return p + return utils.DownloadSpeedSet(ipSet) } - if len(p) <= 0 { // IP数组长度(IP数量) 大于 0 时才会继续下载测速 + if len(ipSet) <= 0 { // IP数组长度(IP数量) 大于 0 时才会继续下载测速 fmt.Println("\n[信息] 延迟测速结果 IP 数量为 0,跳过下载测速。") - return p + return } - speedSet := make([]IPDelay, 0) - testNum := DownloadNum - if len(p) < DownloadNum || MinSpeed > 0 { // 如果IP数组长度(IP数量) 小于下载测速数量(-dn),则次数修正为IP数 - testNum = len(p) + testNum := TestCount + if len(ipSet) < TestCount || MinSpeed > 0 { // 如果IP数组长度(IP数量) 小于下载测速数量(-dn),则次数修正为IP数 + testNum = len(ipSet) } - if testNum < DownloadNum { - DownloadNum = testNum + if testNum < TestCount { + TestCount = testNum } - fmt.Printf("开始下载测速(下限:%.2f MB/s, 数量:%d, 队列:%d)\n", MinSpeed, DownloadNum, testNum) - // 创建进度条 - bar := progressbar.NewOptions(len(p), - progressbar.OptionSetWidth(50), - progressbar.OptionSetDescription("下载测速"), - progressbar.OptionShowCount(), - progressbar.OptionShowIts(), - ) + + fmt.Printf("开始下载测速(下限:%.2f MB/s, 数量:%d, 队列:%d)\n", MinSpeed, TestCount, testNum) + // 控制 下载测速进度条 与 延迟测速进度条 长度一致(强迫症) + bar_a := len(strconv.Itoa(len(ipSet))) + bar_b := " " + for i := 0; i < bar_a; i++ { + bar_b += " " + } + bar := utils.NewBar(TestCount, bar_b, "") for i := 0; i < testNum; i++ { - speed := downloadHandler(p[i].IP) - p[i].DownloadSpeed = speed + speed := downloadHandler(ipSet[i].IP) + ipSet[i].DownloadSpeed = speed // 在每个 IP 下载测速后,以 [下载速度下限] 条件过滤结果 if speed >= MinSpeed*1024*1024 { - p[i].DownloadSpeed = speed - err := bar.Add(1) - if err != nil { - return nil + bar.Grow(1, "") + speedSet = append(speedSet, ipSet[i]) // 高于下载速度下限时,添加到新数组中 + if len(speedSet) == TestCount { // 凑够满足条件的 IP 时(下载测速数量 -dn),就跳出循环 + break } - } else { - p = p[:i] - break } } - err := bar.Finish() - if err != nil { - return nil - } + bar.Done() if len(speedSet) == 0 { // 没有符合速度限制的数据,返回所有测试数据 - speedSet = p + speedSet = utils.DownloadSpeedSet(ipSet) } // 按速度排序 - sort.Sort(BySpeed(speedSet)) - return speedSet + sort.Sort(speedSet) + return } func getDialContext(ip *net.IPAddr) func(ctx context.Context, network, address string) (net.Conn, error) { var fakeSourceAddr string - if IsIpv4(ip.String()) { - fakeSourceAddr = fmt.Sprintf("%s:%d", ip.String(), TcpPort) + if isIPv4(ip.String()) { + fakeSourceAddr = fmt.Sprintf("%s:%d", ip.String(), TCPPort) } else { - fakeSourceAddr = fmt.Sprintf("[%s]:%d", ip.String(), TcpPort) + fakeSourceAddr = fmt.Sprintf("[%s]:%d", ip.String(), TCPPort) } return func(ctx context.Context, network, address string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, network, fakeSourceAddr) @@ -120,11 +130,7 @@ func downloadHandler(ip *net.IPAddr) float64 { if err != nil { return 0.0 } - defer func(Body io.ReadCloser) { - err = Body.Close() - if err != nil { - } - }(response.Body) + defer response.Body.Close() if response.StatusCode != 200 { return 0.0 } @@ -165,14 +171,11 @@ func downloadHandler(ip *net.IPAddr) float64 { break } // 获取上个时间片 - lastTimeSlice := timeStart.Add(timeSlice * time.Duration(timeCounter-1)) + last_time_slice := timeStart.Add(timeSlice * time.Duration(timeCounter-1)) // 下载数据量 / (用当前时间 - 上个时间片/ 时间片) - e.Add(float64(contentRead-lastContentRead) / (float64(currentTime.Sub(lastTimeSlice)) / float64(timeSlice))) + e.Add(float64(contentRead-lastContentRead) / (float64(currentTime.Sub(last_time_slice)) / float64(timeSlice))) } contentRead += int64(bufferRead) } return e.Value() / (Timeout.Seconds() / 120) } -func (a BySpeed) Len() int { return len(a) } -func (a BySpeed) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a BySpeed) Less(i, j int) bool { return a[i].DownloadSpeed > a[j].DownloadSpeed } // 这里使用 > 使其降序排序 diff --git a/task/httping.go b/task/httping.go new file mode 100644 index 0000000..8574e4b --- /dev/null +++ b/task/httping.go @@ -0,0 +1,143 @@ +package task + +import ( + //"crypto/tls" + //"fmt" + "io" + "log" + "net" + "net/http" + "regexp" + "strings" + "sync" + "time" +) + +var ( + Httping bool + HttpingStatusCode int + HttpingCFColo string + HttpingCFColomap *sync.Map + OutRegexp = regexp.MustCompile(`[A-Z]{3}`) +) + +// pingReceived pingTotalTime +func (p *Ping) httping(ip *net.IPAddr) (int, time.Duration) { + hc := http.Client{ + Timeout: time.Second * 2, + Transport: &http.Transport{ + DialContext: getDialContext(ip), + //TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 跳过证书验证 + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // 阻止重定向 + }, + } + + // 先访问一次获得 HTTP 状态码 及 Cloudflare Colo + { + requ, err := http.NewRequest(http.MethodHead, URL, nil) + if err != nil { + 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) + if err != nil { + return 0, 0 + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + //fmt.Println("IP:", ip, "StatusCode:", resp.StatusCode, resp.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 + } + } else { + if resp.StatusCode != HttpingStatusCode { + return 0, 0 + } + } + + _, _ = io.Copy(io.Discard, resp.Body) + + // 只有指定了地区才匹配机场三字码 + 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 + } + } + + } + + // 循环测速计算延迟 + success := 0 + var delay time.Duration + for i := 0; i < PingTimes; i++ { + requ, err := http.NewRequest(http.MethodHead, URL, nil) + if err != nil { + log.Fatal("意外的错误,情报告:", err) + 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") + if i == PingTimes-1 { + requ.Header.Set("Connection", "close") + } + startTime := time.Now() + resp, err := hc.Do(requ) + if err != nil { + continue + } + success++ + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + duration := time.Since(startTime) + delay += duration + + } + + return success, delay + +} + +func MapColoMap() *sync.Map { + if HttpingCFColo == "" { + return nil + } + // 将参数指定的地区三字码转为大写并格式化 + colos := strings.Split(strings.ToUpper(HttpingCFColo), ",") + colomap := &sync.Map{} + for _, colo := range colos { + colomap.Store(colo, colo) + } + return colomap +} + +func (p *Ping) getColo(b string) string { + if b == "" { + return "" + } + // 正则匹配并返回 机场三字码 + out := OutRegexp.FindString(b) + + if HttpingCFColomap == nil { + return out + } + // 匹配 机场三字码 是否为指定的地区 + _, ok := HttpingCFColomap.Load(out) + if ok { + return out + } + + return "" +} diff --git a/task/ip.go b/task/ip.go index 13dd339..768fda0 100644 --- a/task/ip.go +++ b/task/ip.go @@ -1,154 +1,196 @@ package task import ( - "encoding/json" - "fmt" - "io" + "bufio" + "log" "math/rand" "net" - "net/http" - "sort" + "os" + "strconv" "strings" "time" ) -var ( - Ipv4Cidr = []string{ - "173.245.48.0/20", - "103.21.244.0/22", - "103.22.200.0/22", - "103.31.4.0/22", - "141.101.64.0/18", - "108.162.192.0/18", - "190.93.240.0/20", - "188.114.96.0/20", - "197.234.240.0/22", - "198.41.128.0/17", - "162.158.0.0/15", - "104.16.0.0/13", - "104.24.0.0/14", - "172.64.0.0/13", - "131.0.72.0/22", - } - IPCidrApi = "https://api.cloudflare.com/client/v4/ips" - IsOff = false - MaxMS int - MinMS int - TestAll = false - randGen *rand.Rand -) +const defaultInputFile = "ip.txt" -type IPRangeList struct { - Ips []*net.IPAddr - unusedIpCount int - Delays []IPDelay -} +var ( + // TestAll test all ip + TestAll = false + // IPFile is the filename of IP Rangs + IPFile = defaultInputFile + IPText string + randGen *rand.Rand +) func InitRandSeed() { randGen = rand.New(rand.NewSource(time.Now().UnixNano())) } -// CreateData 从IP列表中选择一定数量的IP返回 -func CreateData() *IPRangeList { - ips := loadIPRanges(GetIPv4List()) - return &IPRangeList{ - Ips: ips, - unusedIpCount: 0, - Delays: []IPDelay{}, +func isIPv4(ip string) bool { + return strings.Contains(ip, ".") +} + +func randIPEndWith(num byte) byte { + if num == 0 { // 对于 /32 这种单独的 IP + return byte(0) + } + return byte(randGen.Intn(int(num))) +} + +type IPRanges struct { + ips []*net.IPAddr + mask string + firstIP net.IP + ipNet *net.IPNet +} + +func newIPRanges() *IPRanges { + return &IPRanges{ + ips: make([]*net.IPAddr, 0), } } -// loadIPRanges 从CIDR列表中加载IP地址 -func loadIPRanges(ipList []string) []*net.IPAddr { - ranges := newIPRanges() - for _, ip := range ipList { - line := strings.TrimSpace(ip) // 去除首尾的空白字符(空格、制表符、换行符等) - if line == "" { // 跳过空行 - continue - } - ranges.parseCIDR(line) // 解析 IP 段,获得 IP、IP 范围、子网掩码 - if IsIpv4(line) { // 生成要测速的所有 IPv4 / IPv6 地址(单个/随机/全部) - ranges.chooseIPv4() +// 如果是单独 IP 则加上子网掩码,反之则获取子网掩码(r.mask) +func (r *IPRanges) fixIP(ip string) string { + // 如果不含有 '/' 则代表不是 IP 段,而是一个单独的 IP,因此需要加上 /32 /128 子网掩码 + if i := strings.IndexByte(ip, '/'); i < 0 { + if isIPv4(ip) { + r.mask = "/32" } else { - ranges.chooseIPv6() + r.mask = "/128" + } + ip += r.mask + } else { + r.mask = ip[i:] + } + return ip +} + +// 解析 IP 段,获得 IP、IP 范围、子网掩码 +func (r *IPRanges) parseCIDR(ip string) { + var err error + if r.firstIP, r.ipNet, err = net.ParseCIDR(r.fixIP(ip)); err != nil { + log.Fatalln("ParseCIDR err", err) + } +} + +func (r *IPRanges) appendIPv4(d byte) { + r.appendIP(net.IPv4(r.firstIP[12], r.firstIP[13], r.firstIP[14], d)) +} + +func (r *IPRanges) appendIP(ip net.IP) { + r.ips = append(r.ips, &net.IPAddr{IP: ip}) +} + +// 返回第四段 ip 的最小值及可用数目 +func (r *IPRanges) getIPRange() (minIP, hosts byte) { + minIP = r.firstIP[15] & r.ipNet.Mask[3] // IP 第四段最小值 + + // 根据子网掩码获取主机数量 + m := net.IPv4Mask(255, 255, 255, 255) + for i, v := range r.ipNet.Mask { + m[i] ^= v + } + total, _ := strconv.ParseInt(m.String(), 16, 32) // 总可用 IP 数 + if total > 255 { // 矫正 第四段 可用 IP 数 + hosts = 255 + return + } + hosts = byte(total) + return +} + +func (r *IPRanges) chooseIPv4() { + if r.mask == "/32" { // 单个 IP 则无需随机,直接加入自身即可 + r.appendIP(r.firstIP) + } else { + minIP, hosts := r.getIPRange() // 返回第四段 IP 的最小值及可用数目 + for r.ipNet.Contains(r.firstIP) { // 只要该 IP 没有超出 IP 网段范围,就继续循环随机 + if TestAll { // 如果是测速全部 IP + for i := 0; i <= int(hosts); i++ { // 遍历 IP 最后一段最小值到最大值 + r.appendIPv4(byte(i) + minIP) + } + } else { // 随机 IP 的最后一段 0.0.0.X + r.appendIPv4(minIP + randIPEndWith(hosts)) + } + r.firstIP[14]++ // 0.0.(X+1).X + if r.firstIP[14] == 0 { + r.firstIP[13]++ // 0.(X+1).X.X + if r.firstIP[13] == 0 { + r.firstIP[12]++ // (X+1).X.X.X + } + } + } + } +} + +func (r *IPRanges) chooseIPv6() { + if r.mask == "/128" { // 单个 IP 则无需随机,直接加入自身即可 + r.appendIP(r.firstIP) + } else { + var tempIP uint8 // 临时变量,用于记录前一位的值 + for r.ipNet.Contains(r.firstIP) { // 只要该 IP 没有超出 IP 网段范围,就继续循环随机 + r.firstIP[15] = randIPEndWith(255) // 随机 IP 的最后一段 + r.firstIP[14] = randIPEndWith(255) // 随机 IP 的最后一段 + + targetIP := make([]byte, len(r.firstIP)) + _ = copy(targetIP, r.firstIP) + r.appendIP(targetIP) // 加入 IP 地址池 + + for i := 13; i >= 0; i-- { // 从倒数第三位开始往前随机 + tempIP = r.firstIP[i] // 保存前一位的值 + r.firstIP[i] += randIPEndWith(255) // 随机 0~255,加到当前位上 + if r.firstIP[i] >= tempIP { // 如果当前位的值大于等于前一位的值,说明随机成功了,可以退出该循环 + break + } + } + } + } +} + +func loadIPRanges() []*net.IPAddr { + ranges := newIPRanges() + if IPText != "" { // 从参数中获取 IP 段数据 + IPs := strings.Split(IPText, ",") // 以逗号分隔为数组并循环遍历 + for _, IP := range IPs { + IP = strings.TrimSpace(IP) // 去除首尾的空白字符(空格、制表符、换行符等) + if IP == "" { // 跳过空的(即开头、结尾或连续多个 ,, 的情况) + continue + } + ranges.parseCIDR(IP) // 解析 IP 段,获得 IP、IP 范围、子网掩码 + if isIPv4(IP) { // 生成要测速的所有 IPv4 / IPv6 地址(单个/随机/全部) + ranges.chooseIPv4() + } else { + ranges.chooseIPv6() + } + } + } else { // 从文件中获取 IP 段数据 + if IPFile == "" { + IPFile = defaultInputFile + } + file, err := os.Open(IPFile) + if err != nil { + log.Fatal(err) + } + defer func(file *os.File) { + err = file.Close() + if err != nil { + + } + }(file) + scanner := bufio.NewScanner(file) + for scanner.Scan() { // 循环遍历文件每一行 + line := strings.TrimSpace(scanner.Text()) // 去除首尾的空白字符(空格、制表符、换行符等) + if line == "" { // 跳过空行 + continue + } + ranges.parseCIDR(line) // 解析 IP 段,获得 IP、IP 范围、子网掩码 + if isIPv4(line) { // 生成要测速的所有 IPv4 / IPv6 地址(单个/随机/全部) + ranges.chooseIPv4() + } else { + ranges.chooseIPv6() + } } } return ranges.ips } - -// GetIPv4List 获取IPv4 CIDR列表 -func GetIPv4List() []string { - // 离线变量 - if IsOff { - return Ipv4Cidr - } - // 获取在线IPv4 CIDR列表 - resp, err := http.Get(IPCidrApi) - if err != nil { - fmt.Println("获取在线列表失败,正在使用内置列表") - return Ipv4Cidr - } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(resp.Body) - - // 读取响应主体 - body, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Println("获取在线列表失败,正在使用内置列表") - return Ipv4Cidr - } - - // 解析JSON数据 - var data struct { - Result struct { - IPv4CIDRs []string `json:"ipv4_cidrs"` - } `json:"result"` - Success bool `json:"success"` - } - - if err = json.Unmarshal(body, &data); err != nil || !data.Success { - fmt.Println("获取在线列表失败,正在使用内置列表") - return Ipv4Cidr - } - - fmt.Println("获取在线列表成功,正在使用在线列表") - return data.Result.IPv4CIDRs -} - -// ExcludeInvalid 排除不合格节点 -func (p *IPRangeList) ExcludeInvalid() *IPRangeList { - // 初始化一个空IPDelay切片 - var delays []IPDelay - // 遍历IPRangeList的Delays切片 - for ip, delay := range p.Delays { - // 如果最大延迟大于0且小于最大延迟 - if MaxMS > 0 && delay.Delay > time.Duration(MaxMS)*time.Millisecond { - continue - } - // 如果最小延迟大于0且大于最小延迟 - if MinMS > 0 && delay.Delay < time.Duration(MinMS)*time.Millisecond { - continue - } - // 将IPDelay的IP和Delay添加到delays切片中 - delays = append(delays, IPDelay{IP: p.Ips[ip], Delay: delay.Delay, DownloadSpeed: 0}) - } - p.Delays = delays - return p -} - -// SortNodesDesc 按延迟降序排列 -func (p *IPRangeList) SortNodesDesc() []IPDelay { - sorted := make([]IPDelay, len(p.Delays)) - _ = copy(sorted, p.Delays) - // 使用sort.Slice 对切片进行排序 - sort.Slice(sorted, func(i, j int) bool { - return sorted[i].Delay < sorted[j].Delay - }) - return sorted -} - -// IsIpv4 检查IP地址是否为IPv4 -func IsIpv4(ip string) bool { - return strings.Contains(ip, ".") -} diff --git a/task/tcping.go b/task/tcping.go new file mode 100644 index 0000000..edda6c9 --- /dev/null +++ b/task/tcping.go @@ -0,0 +1,146 @@ +package task + +import ( + "DockerST/utils" + "fmt" + "net" + "sort" + "strconv" + "sync" + "time" +) + +const ( + tcpConnectTimeout = time.Second * 1 + maxRoutine = 1000 + defaultRoutines = 200 + defaultPort = 443 + defaultPingTimes = 4 +) + +var ( + Routines = defaultRoutines + TCPPort int = defaultPort + PingTimes int = defaultPingTimes +) + +type Ping struct { + wg *sync.WaitGroup + m *sync.Mutex + ips []*net.IPAddr + csv utils.PingDelaySet + control chan bool + bar *utils.Bar +} + +func checkPingDefault() { + if Routines <= 0 { + Routines = defaultRoutines + } + if TCPPort <= 0 || TCPPort >= 65535 { + TCPPort = defaultPort + } + if PingTimes <= 0 { + PingTimes = defaultPingTimes + } +} + +func NewPing() *Ping { + checkPingDefault() + ips := loadIPRanges() + return &Ping{ + wg: &sync.WaitGroup{}, + m: &sync.Mutex{}, + ips: ips, + csv: make(utils.PingDelaySet, 0), + control: make(chan bool, Routines), + bar: utils.NewBar(len(ips), "可用:", ""), + } +} + +func (p *Ping) Run() utils.PingDelaySet { + if len(p.ips) == 0 { + return p.csv + } + if Httping { + fmt.Printf("开始延迟测速(模式:HTTP, 端口:%d, 范围:%v ~ %v ms, 丢包:%.2f)\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) + } + for _, ip := range p.ips { + p.wg.Add(1) + p.control <- false + go p.start(ip) + } + p.wg.Wait() + p.bar.Done() + sort.Sort(p.csv) + return p.csv +} + +func (p *Ping) start(ip *net.IPAddr) { + defer p.wg.Done() + p.tcpingHandler(ip) + <-p.control +} + +// bool connectionSucceed float32 time +func (p *Ping) tcping(ip *net.IPAddr) (bool, time.Duration) { + startTime := time.Now() + var fullAddress string + if isIPv4(ip.String()) { + fullAddress = fmt.Sprintf("%s:%d", ip.String(), TCPPort) + } else { + fullAddress = fmt.Sprintf("[%s]:%d", ip.String(), TCPPort) + } + conn, err := net.DialTimeout("tcp", fullAddress, tcpConnectTimeout) + if err != nil { + return false, 0 + } + defer conn.Close() + duration := time.Since(startTime) + return true, duration +} + +// pingReceived pingTotalTime +func (p *Ping) checkConnection(ip *net.IPAddr) (recv int, totalDelay time.Duration) { + if Httping { + recv, totalDelay = p.httping(ip) + return + } + for i := 0; i < PingTimes; i++ { + if ok, delay := p.tcping(ip); ok { + recv++ + totalDelay += delay + } + } + return +} + +func (p *Ping) appendIPData(data *utils.PingData) { + p.m.Lock() + defer p.m.Unlock() + p.csv = append(p.csv, utils.CloudflareIPData{ + PingData: data, + }) +} + +// handle tcping +func (p *Ping) tcpingHandler(ip *net.IPAddr) { + recv, totalDlay := p.checkConnection(ip) + nowAble := len(p.csv) + if recv != 0 { + nowAble++ + } + p.bar.Grow(1, strconv.Itoa(nowAble)) + if recv == 0 { + return + } + data := &utils.PingData{ + IP: ip, + Sended: PingTimes, + Received: recv, + Delay: totalDlay / time.Duration(recv), + } + p.appendIPData(data) +} diff --git a/task/tcpping.go b/task/tcpping.go deleted file mode 100644 index 1eecf79..0000000 --- a/task/tcpping.go +++ /dev/null @@ -1,81 +0,0 @@ -package task - -import ( - "fmt" - "github.com/schollz/progressbar/v3" - "net" - "sync" - "time" -) - -var ( - PingTimes int - Routines int - TcpPort int - TcpConnectTimeOut = time.Second * 1 -) - -type IPDelay struct { - IP *net.IPAddr - Delay time.Duration - DownloadSpeed float64 -} - -func (p *IPRangeList) Run() *IPRangeList { - var wg sync.WaitGroup - ch := make(chan struct{}, Routines) - fmt.Println("正在测试IP", "协议为:", "TCP", "端口为:", TcpPort, "并发数为:", Routines, "Ping次数为:", PingTimes) - // 创建进度条 - bar := progressbar.NewOptions(len(p.Ips), - progressbar.OptionSetWidth(50), - progressbar.OptionSetDescription("测试IP中"), - progressbar.OptionShowCount(), - progressbar.OptionShowIts(), - ) - for _, ip := range p.Ips { - wg.Add(1) - ch <- struct{}{} // 控制最大并发数 - go func(ip *net.IPAddr) { - defer wg.Done() - defer func() { <-ch }() // 释放并发控制 - success, duration := TCPing(ip) - if success && duration > 0 { - p.Delays = append(p.Delays, IPDelay{IP: ip, Delay: duration, DownloadSpeed: 0}) - } - err := bar.Add(1) - if err != nil { - return - } // 更新进度条 - }(ip) - } - wg.Wait() - err := bar.Finish() - if err != nil { - return nil - } - fmt.Printf("调用成功,可用的IP数量: %d\n", len(p.Delays)) - return p -} - -// TCPing 通过TCP连接测试IP是否可用 -func TCPing(ip *net.IPAddr) (bool, time.Duration) { - startTime := time.Now() - var fullAddress string - if IsIpv4(ip.String()) { - fullAddress = fmt.Sprintf("%s:%d", ip.String(), TcpPort) - } else { - fullAddress = fmt.Sprintf("[%s]:%d", ip.String(), TcpPort) - } - conn, err := net.DialTimeout("tcp", fullAddress, TcpConnectTimeOut) - if err != nil { - return false, 0 - } - defer func(conn net.Conn) { - err := conn.Close() - if err != nil { - - } - }(conn) - duration := time.Since(startTime) - return true, duration -} diff --git a/utils/csv.go b/utils/csv.go new file mode 100644 index 0000000..ac48e27 --- /dev/null +++ b/utils/csv.go @@ -0,0 +1,187 @@ +package utils + +import ( + "encoding/csv" + "fmt" + "log" + "net" + "os" + "strconv" + "time" +) + +const ( + defaultOutput = "result.csv" + maxDelay = 9999 * time.Millisecond + minDelay = 0 * time.Millisecond + maxLossRate float32 = 1.0 +) + +var ( + InputMaxDelay = maxDelay + InputMinDelay = minDelay + InputMaxLossRate = maxLossRate + Output = defaultOutput + PrintNum = 10 +) + +// 是否打印测试结果 +func NoPrintResult() bool { + return PrintNum == 0 +} + +// 是否输出到文件 +func noOutput() bool { + return Output == "" || Output == " " +} + +type PingData struct { + IP *net.IPAddr + Sended int + Received int + Delay time.Duration +} + +type CloudflareIPData struct { + *PingData + lossRate float32 + DownloadSpeed float64 +} + +// 计算丢包率 +func (cf *CloudflareIPData) getLossRate() float32 { + if cf.lossRate == 0 { + pingLost := cf.Sended - cf.Received + cf.lossRate = float32(pingLost) / float32(cf.Sended) + } + return cf.lossRate +} + +func (cf *CloudflareIPData) toString() []string { + result := make([]string, 6) + result[0] = cf.IP.String() + result[1] = strconv.Itoa(cf.Sended) + result[2] = strconv.Itoa(cf.Received) + result[3] = strconv.FormatFloat(float64(cf.getLossRate()), 'f', 2, 32) + result[4] = strconv.FormatFloat(cf.Delay.Seconds()*1000, 'f', 2, 32) + result[5] = strconv.FormatFloat(cf.DownloadSpeed/1024/1024, 'f', 2, 32) + return result +} + +func ExportCsv(data []CloudflareIPData) { + if noOutput() || len(data) == 0 { + return + } + fp, err := os.Create(Output) + if err != nil { + log.Fatalf("创建文件[%s]失败:%v", Output, err) + return + } + defer fp.Close() + w := csv.NewWriter(fp) //创建一个新的写入文件流 + _ = w.Write([]string{"IP 地址", "已发送", "已接收", "丢包率", "平均延迟", "下载速度 (MB/s)"}) + _ = w.WriteAll(convertToString(data)) + w.Flush() +} + +func convertToString(data []CloudflareIPData) [][]string { + result := make([][]string, 0) + for _, v := range data { + result = append(result, v.toString()) + } + return result +} + +// 延迟丢包排序 +type PingDelaySet []CloudflareIPData + +// 延迟条件过滤 +func (s PingDelaySet) FilterDelay() (data PingDelaySet) { + if InputMaxDelay > maxDelay || InputMinDelay < minDelay { // 当输入的延迟条件不在默认范围内时,不进行过滤 + return s + } + if InputMaxDelay == maxDelay && InputMinDelay == minDelay { // 当输入的延迟条件为默认值时,不进行过滤 + return s + } + for _, v := range s { + if v.Delay > InputMaxDelay { // 平均延迟上限,延迟大于条件最大值时,后面的数据都不满足条件,直接跳出循环 + break + } + if v.Delay < InputMinDelay { // 平均延迟下限,延迟小于条件最小值时,不满足条件,跳过 + continue + } + data = append(data, v) // 延迟满足条件时,添加到新数组中 + } + return +} + +// 丢包条件过滤 +func (s PingDelaySet) FilterLossRate() (data PingDelaySet) { + if InputMaxLossRate >= maxLossRate { // 当输入的丢包条件为默认值时,不进行过滤 + return s + } + for _, v := range s { + if v.getLossRate() > InputMaxLossRate { // 丢包几率上限 + break + } + data = append(data, v) // 丢包率满足条件时,添加到新数组中 + } + return +} + +func (s PingDelaySet) Len() int { + return len(s) +} +func (s PingDelaySet) Less(i, j int) bool { + iRate, jRate := s[i].getLossRate(), s[j].getLossRate() + if iRate != jRate { + return iRate < jRate + } + return s[i].Delay < s[j].Delay +} +func (s PingDelaySet) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// 下载速度排序 +type DownloadSpeedSet []CloudflareIPData + +func (s DownloadSpeedSet) Len() int { + return len(s) +} +func (s DownloadSpeedSet) Less(i, j int) bool { + return s[i].DownloadSpeed > s[j].DownloadSpeed +} +func (s DownloadSpeedSet) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s DownloadSpeedSet) Print() { + if NoPrintResult() { + return + } + if len(s) <= 0 { // IP数组长度(IP数量) 大于 0 时继续 + fmt.Println("\n[信息] 完整测速结果 IP 数量为 0,跳过输出结果。") + return + } + dateString := convertToString(s) // 转为多维数组 [][]String + if len(dateString) < PrintNum { // 如果IP数组长度(IP数量) 小于 打印次数,则次数改为IP数量 + PrintNum = len(dateString) + } + headFormat := "%-16s%-5s%-5s%-5s%-6s%-11s\n" + dataFormat := "%-18s%-8s%-8s%-8s%-10s%-15s\n" + for i := 0; i < PrintNum; i++ { // 如果要输出的 IP 中包含 IPv6,那么就需要调整一下间隔 + if len(dateString[i][0]) > 15 { + headFormat = "%-40s%-5s%-5s%-5s%-6s%-11s\n" + dataFormat = "%-42s%-8s%-8s%-8s%-10s%-15s\n" + break + } + } + fmt.Printf(headFormat, "IP 地址", "已发送", "已接收", "丢包率", "平均延迟", "下载速度 (MB/s)") + for i := 0; i < PrintNum; i++ { + fmt.Printf(dataFormat, dateString[i][0], dateString[i][1], dateString[i][2], dateString[i][3], dateString[i][4], dateString[i][5]) + } + if !noOutput() { + fmt.Printf("\n完整测速结果已写入 %v 文件,可使用记事本/表格软件查看。\n", Output) + } +} diff --git a/utils/progress.go b/utils/progress.go new file mode 100644 index 0000000..7d5b7f5 --- /dev/null +++ b/utils/progress.go @@ -0,0 +1,25 @@ +package utils + +import ( + "fmt" + + "github.com/cheggaaa/pb/v3" +) + +type Bar struct { + pb *pb.ProgressBar +} + +func NewBar(count int, MyStrStart, MyStrEnd string) *Bar { + tmpl := fmt.Sprintf(`{{counters . }} {{ bar . "[" "-" (cycle . "↖" "↗" "↘" "↙" ) "_" "]"}} %s {{string . "MyStr" | green}} %s `, MyStrStart, MyStrEnd) + bar := pb.ProgressBarTemplate(tmpl).Start(count) + return &Bar{pb: bar} +} + +func (b *Bar) Grow(num int, MyStrVal string) { + b.pb.Set("MyStr", MyStrVal).Add(num) +} + +func (b *Bar) Done() { + b.pb.Finish() +}