diff --git a/.gitignore b/.gitignore index 859fd68..7970fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ dist Releases +*.exe +*.csv diff --git a/IPRangeLoader.go b/IPRangeLoader.go deleted file mode 100644 index 1d6899e..0000000 --- a/IPRangeLoader.go +++ /dev/null @@ -1,176 +0,0 @@ -package main - -import ( - "bufio" - "log" - "net" - "os" - "strconv" - "strings" -) - -// 根据子网掩码获取主机数量 -func getCidrHostNum(maskLen int) int { - cidrIPNum := int(0) - if maskLen < 32 { - var i int = int(32 - maskLen - 1) - for ; i >= 1; i-- { - cidrIPNum += 1 << i - } - cidrIPNum += 2 - } else { - cidrIPNum = 1 - } - if cidrIPNum > 255 { - cidrIPNum = 255 - } - return cidrIPNum -} - -// 获取 IP 最后一段最小值和最大值 -func getCidrIPRange(cidr string) (uint8, uint8) { - ip := strings.Split(cidr, "/")[0] - ipSegs := strings.Split(ip, ".") - maskLen, _ := strconv.Atoi(strings.Split(cidr, "/")[1]) - seg4MinIP, seg4MaxIP := getIPSeg4Range(ipSegs, maskLen) - //ipPrefix := ipSegs[0] + "." + ipSegs[1] + "." + ipSegs[2] + "." - - return seg4MinIP, - seg4MaxIP -} - -// 获取 IP 最后一段的区间 -func getIPSeg4Range(ipSegs []string, maskLen int) (uint8, uint8) { - ipSeg, _ := strconv.Atoi(ipSegs[3]) - return getIPSegRange(uint8(ipSeg), uint8(32-maskLen)) -} - -// 根据输入的基础IP地址和CIDR掩码计算一个IP片段的区间 -func getIPSegRange(userSegIP, offset uint8) (uint8, uint8) { - var ipSegMax uint8 = 255 - netSegIP := ipSegMax << offset - segMinIP := netSegIP & userSegIP - segMaxIP := userSegIP&(255< 0 && time.Duration(maxDelay)*time.Millisecond == utils.InputMaxDelay { + fmt.Println("[警告] '-sl' 参数建议和 '-tl' 参数一起使用") + } + utils.InputMaxDelay = time.Duration(maxDelay) * time.Millisecond + utils.InputMinDelay = time.Duration(minDelay) * time.Millisecond + task.Timeout = time.Duration(downloadTime) * time.Second + if printVersion { println(version) fmt.Println("检查版本更新中...") @@ -94,195 +98,53 @@ https://github.com/XIU2/CloudflareSpeedTest } os.Exit(0) } - if pingRoutine <= 0 { - pingRoutine = 200 - } - if pingTime <= 0 { - pingTime = 4 - } - if tcpPort < 1 || tcpPort > 65535 { - tcpPort = 443 - } - if downloadTestCount <= 0 { - downloadTestCount = 20 - } - if downloadSecond <= 0 { - downloadSecond = 10 - } - if url == "" { - url = "https://cf.xiu2.xyz/Github/CloudflareSpeedTest.png" - } - if timeLimit <= 0 || timeLimit > 9999 { - timeLimit = 9999 - } - if timeLimitLow < 0 || timeLimitLow > 9999 { - timeLimitLow = 0 - } - if speedLimit < 0 { - speedLimit = 0 - } - if printResultNum < 0 { - printResultNum = 20 - } - if ipFile == "" { - ipFile = "ip.txt" - } - if outputFile == " " { - outputFile = "" - } } func main() { - go checkUpdate() // 检查版本更新 - initRandSeed() // 置随机数种子 - failTime = pingTime // 设置接收次数 - ips := loadFirstIPOfRangeFromFile(ipFile) // 读入IP - pingCount := len(ips) * pingTime // 计算进度条总数(IP*测试次数) - bar := pb.Simple.Start(pingCount) // 进度条总数 - var wg sync.WaitGroup - var mu sync.Mutex - var data = make([]CloudflareIPData, 0) - var data2 = make([]CloudflareIPData, 0) - downloadTestTime = time.Duration(downloadSecond) * time.Second + go checkUpdate() // 检查版本更新 + + fmt.Printf("# XIU2/CloudflareSpeedTest %s \n", version) // 开始延迟测速 - fmt.Println("# XIU2/CloudflareSpeedTest " + version + "\n") - if ipv6Mode { // IPv6 模式判断 - fmt.Println("开始延迟测速(模式:TCP IPv6,端口:" + strconv.Itoa(tcpPort) + ",平均延迟上限:" + fmt.Sprintf("%.2f", timeLimit) + " ms" + ",平均延迟下限:" + fmt.Sprintf("%.2f", timeLimitLow) + " ms):") - } else { - fmt.Println("开始延迟测速(模式:TCP IPv4,端口:" + strconv.Itoa(tcpPort) + ",平均延迟上限:" + fmt.Sprintf("%.2f", timeLimit) + " ms" + ",平均延迟下限:" + fmt.Sprintf("%.2f", timeLimitLow) + " ms):") - } - control := make(chan bool, pingRoutine) - for _, ip := range ips { - wg.Add(1) - control <- false - handleProgress := handleProgressGenerator(bar) // 多线程进度条 - go tcpingGoroutine(&wg, &mu, ip, tcpPort, pingTime, &data, control, handleProgress) - } - wg.Wait() - bar.Finish() - - sort.Sort(CloudflareIPDataSet(data)) // 排序(按延迟,从低到高,不同丢包率会分开单独按延迟和丢包率排序) - - // 延迟测速完毕后,以 [平均延迟上限] + [平均延迟下限] 条件过滤结果 - if timeLimit != 9999 || timeLimitLow != 0 { - for i := 0; i < len(data); i++ { - if float64(data[i].pingTime) <= timeLimit { // 平均延迟上限 - if float64(data[i].pingTime) > timeLimitLow { // 平均延迟下限 - data2 = append(data2, data[i]) // 延迟满足条件时,添加到新数组中 - } else { - continue - } - } else { - break - } - } - data = data2 - data2 = []CloudflareIPData{} - } - + pingData := task.NewPing().Run().FilterDelay() // 开始下载测速 - if !disableDownload { // 如果禁用下载测速就跳过 - if len(data) > 0 { // IP数组长度(IP数量) 大于 0 时才会继续下载测速 - if len(data) < downloadTestCount { // 如果IP数组长度(IP数量) 小于下载测速数量(-dn),则次数修正为IP数 - downloadTestCount = len(data) - } - var downloadTestCount2 int // 临时的下载测速次数,即实际的下载测速数量 - if speedLimit > 0 { - downloadTestCount2 = len(data) // 如果指定了 [下载速度下限] 条件,则临时变量改为总数量(即一直测速下去,直到凑够下载测速数量 -dn) - } else { - downloadTestCount2 = downloadTestCount // 如果没有指定 [下载速度下限] 条件,则临时变量为下载测速数量(-dn) - } - fmt.Println("开始下载测速(下载速度下限:" + fmt.Sprintf("%.2f", speedLimit) + " MB/s,下载测速数量:" + strconv.Itoa(downloadTestCount) + ",下载测速队列:" + strconv.Itoa(downloadTestCount2) + "):") - bar = pb.Simple.Start(downloadTestCount) - for i := 0; i < downloadTestCount2; i++ { - _, speed := DownloadSpeedHandler(data[i].ip) - data[i].downloadSpeed = speed - // 在每个 IP 下载测速后,以 [下载速度下限] 条件过滤结果 - if float64(speed)/1024/1024 >= speedLimit { - data2 = append(data2, data[i]) // 高于下载速度下限时,添加到新数组中 - bar.Add(1) - if len(data2) == downloadTestCount { // 凑够满足条件的 IP 时(下载测速数量 -dn),就跳出循环 - break - } - } - } - bar.Finish() - } else { - fmt.Println("\n[信息] 延迟测速结果 IP 数量为 0,跳过下载测速。") - } - } + speedData := task.TestDownloadSpeed(pingData) + utils.ExportCsv(speedData) + speedData.Print(task.IPv6) - if len(data2) > 0 { // 如果该数组有内容,说明指定了 [下载测速下限] 条件,且最少有 1 个满足条件的 IP - data = data2 + if versionNew != "" { + fmt.Printf("\n*** 发现新版本 [%s]!请前往 [https://github.com/XIU2/CloudflareSpeedTest] 更新! ***\n", versionNew) } - sort.Sort(CloudflareIPDataSetD(data)) // 排序(按下载速度,从高到低) - if outputFile != "" { - ExportCsv(outputFile, data) // 输出结果到文件 - } - printResult(data) // 显示最快结果 + endPrint() } -// 显示最快结果 -func printResult(data []CloudflareIPData) { - sysType := runtime.GOOS - if printResultNum > 0 { // 如果禁止直接输出结果就跳过 - dateString := convertToString(data) // 转为多维数组 [][]String - if len(dateString) > 0 { // IP数组长度(IP数量) 大于 0 时继续 - if len(dateString) < printResultNum { // 如果IP数组长度(IP数量) 小于 打印次数,则次数改为IP数量 - printResultNum = len(dateString) - } - if ipv6Mode { // IPv6 太长了,所以需要调整一下间隔 - fmt.Printf("%-40s%-5s%-5s%-5s%-6s%-11s\n", "IP 地址", "已发送", "已接收", "丢包率", "平均延迟", "下载速度 (MB/s)") - for i := 0; i < printResultNum; i++ { - fmt.Printf("%-42s%-8s%-8s%-8s%-10s%-15s\n", ipPadding(dateString[i][0]), dateString[i][1], dateString[i][2], dateString[i][3], dateString[i][4], dateString[i][5]) - } - } else { - fmt.Printf("%-16s%-5s%-5s%-5s%-6s%-11s\n", "IP 地址", "已发送", "已接收", "丢包率", "平均延迟", "下载速度 (MB/s)") - for i := 0; i < printResultNum; i++ { - fmt.Printf("%-18s%-8s%-8s%-8s%-10s%-15s\n", ipPadding(dateString[i][0]), dateString[i][1], dateString[i][2], dateString[i][3], dateString[i][4], dateString[i][5]) - } - } - - if versionNew != "" { - fmt.Println("\n*** 发现新版本 [" + versionNew + "]!请前往 [https://github.com/XIU2/CloudflareSpeedTest] 更新! ***") - } - - if sysType == "windows" { // 如果是 Windows 系统,则需要按下 回车键 或 Ctrl+C 退出(避免通过双击运行时,测速完毕后直接关闭) - if outputFile != "" { - fmt.Printf("\n完整测速结果已写入 %v 文件,请使用记事本/表格软件查看。\n按下 回车键 或 Ctrl+C 退出。", outputFile) - } else { - fmt.Printf("\n按下 回车键 或 Ctrl+C 退出。") - } - var pause int - fmt.Scanln(&pause) - } else { // 其它系统直接退出 - if outputFile != "" { - fmt.Println("\n完整测速结果已写入 " + outputFile + " 文件,请使用记事本/表格软件查看。") - } - } - } else { - fmt.Println("\n[信息] 完整测速结果 IP 数量为 0,跳过输出结果。") - } - } else { - fmt.Println("\n完整测速结果已写入 " + outputFile + " 文件,请使用记事本/表格软件查看。") +func endPrint() { + if utils.NoPrintResult() { + return + } + if runtime.GOOS == "windows" { // 如果是 Windows 系统,则需要按下 回车键 或 Ctrl+C 退出(避免通过双击运行时,测速完毕后直接关闭) + fmt.Println("\n按下 回车键 或 Ctrl+C 退出。") + var pause int + fmt.Scanln(&pause) } } // 检查更新 func checkUpdate() { - timeout := time.Duration(10 * time.Second) + timeout := 10 * time.Second client := http.Client{Timeout: timeout} res, err := client.Get("https://api.xiuer.pw/ver/cloudflarespeedtest.txt") - if err == nil { - // 读取资源数据 body: []byte - body, err := ioutil.ReadAll(res.Body) - // 关闭资源流 - res.Body.Close() - if err == nil { - if string(body) != version { - versionNew = string(body) - } - } + if err != nil { + return + } + // 读取资源数据 body: []byte + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return + } + // 关闭资源流 + defer res.Body.Close() + if string(body) != version { + versionNew = string(body) } } diff --git a/task/download.go b/task/download.go new file mode 100644 index 0000000..a16a4bd --- /dev/null +++ b/task/download.go @@ -0,0 +1,152 @@ +package task + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "sort" + "time" + + "CloudflareSpeedTest/utils" + + "github.com/VividCortex/ewma" +) + +const ( + bufferSize = 1024 + defaultURL = "https://cf.xiu2.xyz/Github/CloudflareSpeedTest.png" + defaultTimeout = 10 * time.Second + defaultDisableDownload = false + defaultTestNum = 10 + defaultMinSpeed float64 = 0.0 +) + +var ( + // download test url + URL = defaultURL + // download timeout + Timeout = defaultTimeout + // disable download + Disable = defaultDisableDownload + + TestCount = defaultTestNum + MinSpeed = defaultMinSpeed +) + +func checkDownloadDefault() { + if URL == "" { + URL = defaultURL + } + if Timeout <= 0 { + Timeout = defaultTimeout + } + if TestCount <= 0 { + TestCount = defaultTestNum + } + if MinSpeed <= 0.0 { + MinSpeed = defaultMinSpeed + } +} + +func TestDownloadSpeed(ipSet utils.PingDelaySet) (speedSet utils.DownloadSpeedSet) { + checkDownloadDefault() + if Disable { + return utils.DownloadSpeedSet(ipSet) + } + if len(ipSet) <= 0 { // IP数组长度(IP数量) 大于 0 时才会继续下载测速 + fmt.Println("\n[信息] 延迟测速结果 IP 数量为 0,跳过下载测速。") + return + } + testNum := TestCount + if len(ipSet) < TestCount || MinSpeed > 0 { // 如果IP数组长度(IP数量) 小于下载测速数量(-dn),则次数修正为IP数 + testNum = len(ipSet) + } + + fmt.Printf("开始下载测速(下载速度下限:%.2f MB/s,下载测速数量:%d,下载测速队列:%d):\n", MinSpeed, TestCount, testNum) + bar := utils.NewBar(TestCount) + for i := 0; i < testNum; i++ { + speed := downloadHandler(ipSet[i].IP) + ipSet[i].DownloadSpeed = speed + // 在每个 IP 下载测速后,以 [下载速度下限] 条件过滤结果 + if speed >= MinSpeed*1024*1024 { + bar.Grow(1) + speedSet = append(speedSet, ipSet[i]) // 高于下载速度下限时,添加到新数组中 + if len(speedSet) == TestCount { // 凑够满足条件的 IP 时(下载测速数量 -dn),就跳出循环 + break + } + } + } + bar.Done() + if len(speedSet) == 0 { // 没有符合速度限制的数据,返回所有测试数据 + speedSet = utils.DownloadSpeedSet(ipSet) + } + // 按速度排序 + sort.Sort(speedSet) + return +} + +func getDialContext(ip *net.IPAddr) func(ctx context.Context, network, address string) (net.Conn, error) { + fakeSourceAddr := ip.String() + ":443" + if IPv6 { // IPv6 需要加上 [] + fakeSourceAddr = "[" + ip.String() + "]:443" + } + return func(ctx context.Context, network, address string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, network, fakeSourceAddr) + } +} + +// return download Speed +func downloadHandler(ip *net.IPAddr) float64 { + client := &http.Client{ + Transport: &http.Transport{DialContext: getDialContext(ip)}, + Timeout: Timeout, + } + response, err := client.Get(URL) + if err != nil { + return 0.0 + } + defer response.Body.Close() + if response.StatusCode != 200 { + return 0.0 + } + timeStart := time.Now() + timeEnd := timeStart.Add(Timeout) + + contentLength := response.ContentLength + buffer := make([]byte, bufferSize) + + var ( + contentRead int64 = 0 + timeSlice = Timeout / 100 + timeCounter = 1 + lastContentRead int64 = 0 + ) + + var nextTime = timeStart.Add(timeSlice * time.Duration(timeCounter)) + e := ewma.NewMovingAverage() + + for contentLength != contentRead { + currentTime := time.Now() + if currentTime.After(nextTime) { + timeCounter++ + nextTime = timeStart.Add(timeSlice * time.Duration(timeCounter)) + e.Add(float64(contentRead - lastContentRead)) + lastContentRead = contentRead + } + if currentTime.After(timeEnd) { + break + } + bufferRead, err := response.Body.Read(buffer) + if err != nil { + if err != io.EOF { + break + } + e.Add(float64(contentRead-lastContentRead) / (float64(nextTime.Sub(currentTime)) / float64(timeSlice))) + } + contentRead += int64(bufferRead) + } + return e.Value() / (Timeout.Seconds() / 120) + +} diff --git a/task/ip.go b/task/ip.go new file mode 100644 index 0000000..14ac6bb --- /dev/null +++ b/task/ip.go @@ -0,0 +1,156 @@ +package task + +import ( + "bufio" + "log" + "math/rand" + "net" + "os" + "strconv" + "strings" + "time" +) + +const defaultInputFile = "ip.txt" + +var ( + // IPv6 IP version is 6 + IPv6 = false + // TestAll test all ip + TestAll = false + // IPFile is the filename of IP Rangs + IPFile = defaultInputFile +) + +func randIPEndWith(num byte) byte { + rand.Seed(time.Now().UnixNano()) + return byte(rand.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), + } +} + +func (r *IPRanges) fixIP(ip string) string { + // 如果不含有 '/' 则代表不是 IP 段,而是一个单独的 IP,因此需要加上 /32 /128 子网掩码 + if i := strings.IndexByte(ip, '/'); i < 0 { + r.mask = "/32" + if IPv6 { + r.mask = "/128" + } + ip += r.mask + } else { + r.mask = ip[i:] + } + return 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 + } + if total == 0 { + hosts = 1 + return + } + hosts = byte(total) + return +} + +func (r *IPRanges) chooseIPv4() { + minIP, hosts := r.getIPRange() + for r.ipNet.Contains(r.firstIP) { + 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() { + var tempIP uint8 + for r.ipNet.Contains(r.firstIP) { + //fmt.Println(firstIP) + //fmt.Println(firstIP[0], firstIP[1], firstIP[2], firstIP[3], firstIP[4], firstIP[5], firstIP[6], firstIP[7], firstIP[8], firstIP[9], firstIP[10], firstIP[11], firstIP[12], firstIP[13], firstIP[14], firstIP[15]) + if r.mask != "/128" { + 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) + for i := 13; i >= 0; i-- { + tempIP = r.firstIP[i] + r.firstIP[i] += randIPEndWith(255) + if r.firstIP[i] >= tempIP { + break + } + } + } +} + +func loadIPRanges() []*net.IPAddr { + if IPFile == "" { + IPFile = defaultInputFile + } + file, err := os.Open(IPFile) + if err != nil { + log.Fatal(err) + } + defer file.Close() + ranges := newIPRanges() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + ranges.parseCIDR(scanner.Text()) + if IPv6 { + ranges.chooseIPv6() + continue + } + ranges.chooseIPv4() + } + return ranges.ips +} diff --git a/task/tcping.go b/task/tcping.go new file mode 100644 index 0000000..fc5f9de --- /dev/null +++ b/task/tcping.go @@ -0,0 +1,137 @@ +package task + +import ( + "fmt" + "net" + "sort" + "sync" + "time" + + "CloudflareSpeedTest/utils" +) + +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 + } + ipVersion := "IPv4" + if IPv6 { // IPv6 模式判断 + ipVersion = "IPv6" + } + fmt.Printf("开始延迟测速(模式:TCP %s,端口:%d,平均延迟上限:%vms,平均延迟下限:%vms)\n", ipVersion, TCPPort, utils.InputMaxDelay.Milliseconds(), utils.InputMinDelay.Milliseconds()) + 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() + fullAddress := fmt.Sprintf("%s:%d", ip.String(), TCPPort) + //fmt.Println(ip.String()) + if IPv6 { // IPv6 需要加上 [] + 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) { + 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) + p.bar.Grow(1) + if recv == 0 { + return + } + data := &utils.PingData{ + IP: ip, + Sended: PingTimes, + Received: recv, + Delay: totalDlay / time.Duration(recv), + } + p.appendIPData(data) +} diff --git a/tcping.go b/tcping.go deleted file mode 100644 index 9c38df8..0000000 --- a/tcping.go +++ /dev/null @@ -1,172 +0,0 @@ -package main - -import ( - "context" - "io" - "net" - "net/http" - "strconv" - "sync" - "time" - - "github.com/VividCortex/ewma" -) - -//bool connectionSucceed float32 time -func tcping(ip net.IPAddr, tcpPort int) (bool, float32) { - startTime := time.Now() - var fullAddress string - //fmt.Println(ip.String()) - if ipv6Mode { // IPv6 需要加上 [] - fullAddress = "[" + ip.String() + "]:" + strconv.Itoa(tcpPort) - } else { - fullAddress = ip.String() + ":" + strconv.Itoa(tcpPort) - } - conn, err := net.DialTimeout("tcp", fullAddress, tcpConnectTimeout) - if err != nil { - return false, 0 - } else { - var endTime = time.Since(startTime) - var duration = float32(endTime.Microseconds()) / 1000.0 - _ = conn.Close() - return true, duration - } -} - -//pingReceived pingTotalTime -func checkConnection(ip net.IPAddr, tcpPort int) (int, float32) { - pingRecv := 0 - var pingTime float32 = 0.0 - for i := 1; i <= failTime; i++ { - pingSucceed, pingTimeCurrent := tcping(ip, tcpPort) - if pingSucceed { - pingRecv++ - pingTime += pingTimeCurrent - } - } - return pingRecv, pingTime -} - -//return Success packetRecv averagePingTime specificIPAddr -func tcpingHandler(ip net.IPAddr, tcpPort int, pingCount int, progressHandler func(e progressEvent)) (bool, int, float32, net.IPAddr) { - ipCanConnect := false - pingRecv := 0 - var pingTime float32 = 0.0 - for !ipCanConnect { - pingRecvCurrent, pingTimeCurrent := checkConnection(ip, tcpPort) - if pingRecvCurrent != 0 { - ipCanConnect = true - pingRecv = pingRecvCurrent - pingTime = pingTimeCurrent - } else { - ip.IP[15]++ - if ip.IP[15] == 0 { - break - } - break - } - } - if ipCanConnect { - progressHandler(AvailableIPFound) - for i := failTime; i < pingCount; i++ { - pingSuccess, pingTimeCurrent := tcping(ip, tcpPort) - progressHandler(NormalPing) - if pingSuccess { - pingRecv++ - pingTime += pingTimeCurrent - } - } - return true, pingRecv, pingTime / float32(pingRecv), ip - } else { - progressHandler(NoAvailableIPFound) - return false, 0, 0, net.IPAddr{} - } -} - -func tcpingGoroutine(wg *sync.WaitGroup, mutex *sync.Mutex, ip net.IPAddr, tcpPort int, pingCount int, csv *[]CloudflareIPData, control chan bool, progressHandler func(e progressEvent)) { - defer wg.Done() - success, pingRecv, pingTimeAvg, currentIP := tcpingHandler(ip, tcpPort, pingCount, progressHandler) - if success { - mutex.Lock() - var cfdata CloudflareIPData - cfdata.ip = currentIP - cfdata.pingReceived = pingRecv - cfdata.pingTime = pingTimeAvg - cfdata.pingCount = pingCount - *csv = append(*csv, cfdata) - mutex.Unlock() - } - <-control -} - -func GetDialContextByAddr(fakeSourceAddr string) func(ctx context.Context, network, address string) (net.Conn, error) { - return func(ctx context.Context, network, address string) (net.Conn, error) { - c, e := (&net.Dialer{}).DialContext(ctx, network, fakeSourceAddr) - return c, e - } -} - -//bool : can download,float32 downloadSpeed -func DownloadSpeedHandler(ip net.IPAddr) (bool, float32) { - var client = http.Client{ - Transport: nil, - CheckRedirect: nil, - Jar: nil, - Timeout: downloadTestTime, - } - var fullAddress string - if ipv6Mode { // IPv6 需要加上 [] - fullAddress = "[" + ip.String() + "]:443" - } else { - fullAddress = ip.String() + ":443" - } - client.Transport = &http.Transport{ - DialContext: GetDialContextByAddr(fullAddress), - } - response, err := client.Get(url) - if err != nil { - return false, 0 - } else { - defer func() { _ = response.Body.Close() }() - if response.StatusCode == 200 { - timeStart := time.Now() - timeEnd := timeStart.Add(downloadTestTime) - - contentLength := response.ContentLength - buffer := make([]byte, downloadBufferSize) - - var contentRead int64 = 0 - var timeSlice = downloadTestTime / 100 - var timeCounter = 1 - var lastContentRead int64 = 0 - - var nextTime = timeStart.Add(timeSlice * time.Duration(timeCounter)) - e := ewma.NewMovingAverage() - - for contentLength != contentRead { - var currentTime = time.Now() - if currentTime.After(nextTime) { - timeCounter += 1 - nextTime = timeStart.Add(timeSlice * time.Duration(timeCounter)) - e.Add(float64(contentRead - lastContentRead)) - lastContentRead = contentRead - } - if currentTime.After(timeEnd) { - break - } - bufferRead, err := response.Body.Read(buffer) - contentRead += int64(bufferRead) - if err != nil { - if err != io.EOF { - break - } else { - e.Add(float64(contentRead-lastContentRead) / (float64(nextTime.Sub(currentTime)) / float64(timeSlice))) - } - } - } - return true, float32(e.Value()) / (float32(downloadTestTime.Seconds()) / 120) - } else { - return false, 0 - } - } -} diff --git a/util.go b/util.go deleted file mode 100644 index fb20cd5..0000000 --- a/util.go +++ /dev/null @@ -1,164 +0,0 @@ -package main - -import ( - "encoding/csv" - "log" - "math/rand" - "net" - "os" - "strconv" - "time" - - "github.com/cheggaaa/pb/v3" -) - -type CloudflareIPData struct { - ip net.IPAddr - pingCount int - pingReceived int - recvRate float32 - downloadSpeed float32 - pingTime float32 -} - -func (cf *CloudflareIPData) getRecvRate() float32 { - if cf.recvRate == 0 { - pingLost := cf.pingCount - cf.pingReceived - cf.recvRate = float32(pingLost) / float32(cf.pingCount) - } - return cf.recvRate -} - -func ExportCsv(filePath string, data []CloudflareIPData) { - fp, err := os.Create(filePath) - if err != nil { - log.Fatalf("创建文件["+filePath+"]句柄失败,%v", err) - return - } - defer fp.Close() - w := csv.NewWriter(fp) //创建一个新的写入文件流 - w.Write([]string{"IP 地址", "已发送", "已接收", "丢包率", "平均延迟", "下载速度 (MB/s)"}) - w.WriteAll(convertToString(data)) - w.Flush() -} - -func (cf *CloudflareIPData) toString() []string { - result := make([]string, 6) - result[0] = cf.ip.String() - result[1] = strconv.Itoa(cf.pingCount) - result[2] = strconv.Itoa(cf.pingReceived) - result[3] = strconv.FormatFloat(float64(cf.getRecvRate()), 'f', 2, 32) - result[4] = strconv.FormatFloat(float64(cf.pingTime), 'f', 2, 32) - result[5] = strconv.FormatFloat(float64(cf.downloadSpeed)/1024/1024, 'f', 2, 32) - return result -} - -func convertToString(data []CloudflareIPData) [][]string { - result := make([][]string, 0) - for _, v := range data { - result = append(result, v.toString()) - } - return result -} - -var pingTime int -var pingRoutine int - -type progressEvent int - -const ( - NoAvailableIPFound progressEvent = iota - AvailableIPFound - NormalPing -) - -var url string - -var downloadTestTime time.Duration - -const downloadBufferSize = 1024 - -var downloadTestCount int - -//const defaultTcpPort = 443 -const tcpConnectTimeout = time.Second * 1 - -var failTime int - -// 平均延迟排序(丢包另算) -type CloudflareIPDataSet []CloudflareIPData - -// 下载速度排序 -type CloudflareIPDataSetD []CloudflareIPData - -func initRandSeed() { - rand.Seed(time.Now().UnixNano()) -} - -func randipEndWith(num int) uint8 { - return uint8(rand.Intn(num)) -} - -func GetRandomString() string { - str := "0123456789abcdef" - bytes := []byte(str) - result := []byte{} - r := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < 4; i++ { - result = append(result, bytes[r.Intn(len(bytes))]) - } - return string(result) -} - -func ipPadding(ip string) string { - var ipLength int - var ipPrint string - ipPrint = ip - ipLength = len(ipPrint) - if ipLength < 15 { - for i := 0; i <= 15-ipLength; i++ { - ipPrint += " " - } - } - return ipPrint -} - -func handleProgressGenerator(pb *pb.ProgressBar) func(e progressEvent) { - return func(e progressEvent) { - switch e { - case NoAvailableIPFound: - pb.Add(pingTime) - case AvailableIPFound: - pb.Add(failTime) - case NormalPing: - pb.Increment() - } - } -} - -func (cfs CloudflareIPDataSet) Len() int { - return len(cfs) -} - -func (cfs CloudflareIPDataSet) Less(i, j int) bool { - if (cfs)[i].getRecvRate() != cfs[j].getRecvRate() { - return cfs[i].getRecvRate() < cfs[j].getRecvRate() - } - return cfs[i].pingTime < cfs[j].pingTime -} - -func (cfs CloudflareIPDataSet) Swap(i, j int) { - cfs[i], cfs[j] = cfs[j], cfs[i] -} - -func (cfs CloudflareIPDataSetD) Len() int { - return len(cfs) -} - -func (cfs CloudflareIPDataSetD) Less(i, j int) bool { - return cfs[i].downloadSpeed > cfs[j].downloadSpeed -} - -func (cfs CloudflareIPDataSetD) Swap(i, j int) { - cfs[i], cfs[j] = cfs[j], cfs[i] -} diff --git a/utils/csv.go b/utils/csv.go new file mode 100644 index 0000000..42a8f8d --- /dev/null +++ b/utils/csv.go @@ -0,0 +1,166 @@ +package utils + +import ( + "encoding/csv" + "fmt" + "log" + "net" + "os" + "strconv" + "time" +) + +const ( + defaultOutput = "result.csv" + maxDelay = 9999 * time.Millisecond + minDelay = 0 * time.Millisecond +) + +var ( + InputMaxDelay = maxDelay + InputMinDelay = minDelay + 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 + recvRate float32 + DownloadSpeed float64 +} + +func (cf *CloudflareIPData) getRecvRate() float32 { + if cf.recvRate == 0 { + pingLost := cf.Sended - cf.Received + cf.recvRate = float32(pingLost) / float32(cf.Sended) + } + return cf.recvRate +} + +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.getRecvRate()), '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 + } + for _, v := range s { + if v.Delay > InputMaxDelay { // 平均延迟上限 + break + } + if v.Delay < InputMinDelay { // 平均延迟下限 + continue + } + 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].getRecvRate(), s[j].getRecvRate() + 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(ipv6 bool) { + 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" + if ipv6 { // IPv6 太长了,所以需要调整一下间隔 + headFormat = "%-40s%-5s%-5s%-5s%-6s%-11s\n" + dataFormat = "%-42s%-8s%-8s%-8s%-10s%-15s\n" + } + 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..85010c3 --- /dev/null +++ b/utils/progress.go @@ -0,0 +1,19 @@ +package utils + +import "github.com/cheggaaa/pb/v3" + +type Bar struct { + pb *pb.ProgressBar +} + +func NewBar(count int) *Bar { + return &Bar{pb.Simple.Start(count)} +} + +func (b *Bar) Grow(num int) { + b.pb.Add(num) +} + +func (b *Bar) Done() { + b.pb.Finish() +} \ No newline at end of file