第一版修改基础框架

This commit is contained in:
GuanM 2024-06-19 14:13:06 +08:00
parent b2e862d651
commit 9dc7b3eecf
11 changed files with 834 additions and 450 deletions

17
go.mod
View File

@ -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
)

35
go.sum
View File

@ -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=

110
main.go
View File

@ -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() {
// 暂无
}

View File

@ -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)))
}

View File

@ -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 } // 这里使用 > 使其降序排序

143
task/httping.go Normal file
View File

@ -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 ""
}

View File

@ -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, ".")
}

146
task/tcping.go Normal file
View File

@ -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)
}

View File

@ -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
}

187
utils/csv.go Normal file
View File

@ -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)
}
}

25
utils/progress.go Normal file
View File

@ -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()
}