├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── tlsping │ ├── init.go │ ├── main.go │ ├── usage.go │ └── version.go ├── ping.go └── results.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # This project 27 | cmd/tlsping/tlsping 28 | .DS_Store 29 | *.tar.gz 30 | utils/ 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Fabio Hernandez 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OS := $(shell uname -a | cut -f 1 -d ' ' | tr [:upper:] [:lower:]) 2 | ARCH := $(shell uname -m) 3 | TAG := $(shell git describe master --tags) 4 | TIMESTAMP := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') 5 | 6 | all: build 7 | 8 | build: 9 | @go install 10 | @cd cmd/tlsping && go build -ldflags="-X main.appBuildTime=$(TIMESTAMP) -X main.appVersion=$(TAG)" 11 | 12 | release: build 13 | @echo "Packaging tlsping ${TAG} for ${OS}" 14 | @cd cmd/tlsping && tar -czf tlsping-${TAG}-${OS}-${ARCH}.tar.gz tlsping 15 | 16 | clean: 17 | @rm -f cmd/tlsping/tlsping-*.tar.gz cmd/tlsping/tlsping 18 | 19 | buildall: clean build 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tlsping — a tool for measuring TLS handshake latency 2 | 3 | ## Overview 4 | `tlsping` is a command line tool to measure the time required to perform [Transport Layer Security (TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security) connections with a network server. 5 | 6 | It concurrently establishes several network connections, performs the TLS handshake on every one of them, measures the time spent handshaking and reports a summary on the observed results. 7 | 8 | ## How to use 9 | 10 | #### Examples of usage: 11 | 12 | * Measure the time to establish a TCP connection to host `mail.google.com` port `443` and perform the TLS handshaking: 13 | 14 | ```bash 15 | # The hostname 'mail.google.com' resolves to an IPv4 address 16 | $ tlsping mail.google.com:443 17 | tlsping: TLS connection to mail.google.com:443 (216.58.204.133) (10 connections) 18 | tlsping: min/avg/max/stddev = 95.95ms/96.31ms/96.63ms/218.19µs 19 | ``` 20 | 21 | * Same measurement as above but connect to host `www.cloudflare.com` when it resolves to to IPv6 address `2606:4700::6811:d209`: 22 | 23 | ```bash 24 | # The hostname 'www.cloudflare.com' resolves to an IPv6 address 25 | $ tlsping www.cloudflare.com:443 26 | tlsping: TLS connection to www.cloudflare.com:443 (2606:4700::6811:d209) (10 connections) 27 | tlsping: min/avg/max/stddev = 85.36ms/86.63ms/88.98ms/1.14ms 28 | ``` 29 | 30 | * Measure only the time to establish the TCP connection (i.e. do not perform TLS handshaking) to remote server at IPv6 address `2a00:1450:400a:800::2005` port `443`: 31 | 32 | ```bash 33 | # To specify an IPv6 address and port enclose the IP address in '[' and ']' 34 | $ tlsping -tcponly [2a00:1450:400a:800::2005]:443 35 | tlsping: TCP connection to [2a00:1450:400a:800::2005]:443 (2a00:1450:400a:800::2005) (10 connections) 36 | tlsping: min/avg/max/stddev = 5.85ms/5.97ms/6.08ms/61.55µs 37 | ``` 38 | 39 | #### Synopsis 40 | 41 | ```bash 42 | tlsping [-c count] [-tcponly] [-json] [-ca=] [-insecure] 43 | tlsping -help 44 | tlsping -version 45 | ``` 46 | 47 | #### Getting help 48 | 49 | ```bahs 50 | tlsping -help 51 | ``` 52 | 53 | ## Installation 54 | Download a **binary release** for your target operating system from the [releases page](https://github.com/airnandez/tlsping/releases). 55 | 56 | Alternatively, if you prefer to **build from sources**, you need the [Go programming environment](https://golang.org). Do: 57 | 58 | ``` 59 | go get -u github.com/airnandez/tlsping/... 60 | ``` 61 | 62 | ## Credits 63 | 64 | This tool was developed and is maintained by Fabio Hernandez at [IN2P3 / CNRS computing center](http://cc.in2p3.fr) (Lyon, France). 65 | 66 | ## License 67 | Copyright 2016-2020 Fabio Hernandez 68 | 69 | Licensed under the Apache License, Version 2.0 (the "License"); 70 | you may not use this file except in compliance with the License. 71 | You may obtain a copy of the License at 72 | 73 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 74 | 75 | Unless required by applicable law or agreed to in writing, software 76 | distributed under the License is distributed on an "AS IS" BASIS, 77 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 78 | See the License for the specific language governing permissions and 79 | limitations under the License. 80 | -------------------------------------------------------------------------------- /cmd/tlsping/init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | defaultServerAddr string = "" 14 | defaultIterations int = 10 15 | maxCount int = 100 16 | ) 17 | 18 | var ( 19 | // application name 20 | appName string 21 | 22 | // application version: set at build time based on git tag 23 | appVersion = "dev" 24 | 25 | // application build time stamp: set at build time 26 | appBuildTime = "unknown" 27 | 28 | // application error logger 29 | errlog *log.Logger 30 | outlog *log.Logger 31 | 32 | // fields used in the help templates 33 | tmplFields = map[string]string{ 34 | "Sp2": " ", 35 | "Sp3": " ", 36 | "Sp4": " ", 37 | "Sp5": " ", 38 | "Sp6": " ", 39 | "Sp7": " ", 40 | "Sp8": " ", 41 | "Tab1": "\t", 42 | "Tab2": "\t\t", 43 | "Tab3": "\t\t\t", 44 | "Tab4": "\t\t\t\t", 45 | "Tab5": "\t\t\t\t\t", 46 | "Tab6": "\t\t\t\t\t\t", 47 | "UsageVersion": "short", 48 | "AppVersion": appVersion, 49 | "BuildTime": appBuildTime, 50 | "Os": runtime.GOOS, 51 | "Arch": runtime.GOARCH, 52 | "GoVersion": runtime.Version(), 53 | } 54 | ) 55 | 56 | func init() { 57 | appName = filepath.Base(os.Args[0]) 58 | errlog = log.New(os.Stderr, fmt.Sprintf("%s: ", appName), 0) 59 | outlog = log.New(os.Stdout, fmt.Sprintf("%s: ", appName), 0) 60 | tmplFields["AppName"] = appName 61 | tmplFields["AppNameFiller"] = strings.Repeat(" ", len(appName)) 62 | return 63 | } 64 | -------------------------------------------------------------------------------- /cmd/tlsping/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | 11 | "github.com/airnandez/tlsping" 12 | ) 13 | 14 | func main() { 15 | fset := flag.NewFlagSet(appName, flag.ExitOnError) 16 | fset.Usage = func() { 17 | printUsage(os.Stderr, usageShort) 18 | } 19 | tcpOnly := fset.Bool("tcponly", false, "") 20 | count := fset.Int("c", defaultIterations, "") 21 | jsonOutput := fset.Bool("json", false, "") 22 | insecure := fset.Bool("insecure", false, "") 23 | ca := fset.String("ca", "", "") 24 | version := fset.Bool("version", false, "") 25 | help := fset.Bool("help", false, "") 26 | fset.Parse(os.Args[1:]) 27 | 28 | if *version { 29 | printVersion(os.Stderr) 30 | os.Exit(0) 31 | } 32 | if *help { 33 | printUsage(os.Stderr, usageLong) 34 | os.Exit(0) 35 | } 36 | args := fset.Args() 37 | if len(args) != 1 { 38 | errlog.Printf("missing server address\n") 39 | printUsage(os.Stderr, usageShort) 40 | os.Exit(1) 41 | } 42 | serverAddr := args[0] 43 | if *count <= 0 { 44 | *count = 1 45 | } 46 | if *count > maxCount { 47 | errlog.Printf("number of allowed connections cannot exceed %d\n", maxCount) 48 | printUsage(os.Stderr, usageShort) 49 | os.Exit(1) 50 | } 51 | caCerts, err := loadCaCerts(*ca) 52 | if err != nil { 53 | errlog.Printf("%s\n", err) 54 | printUsage(os.Stderr, usageShort) 55 | os.Exit(1) 56 | } 57 | config := tlsping.Config{ 58 | Count: *count, 59 | AvoidTLSHandshake: *tcpOnly, 60 | InsecureSkipVerify: *insecure, 61 | RootCAs: caCerts, 62 | } 63 | result, err := tlsping.Ping(serverAddr, &config) 64 | if err != nil { 65 | errlog.Printf("error connecting to '%s': %s\n", serverAddr, err) 66 | os.Exit(1) 67 | } 68 | s := "TLS" 69 | if *tcpOnly { 70 | s = "TCP" 71 | } 72 | if !*jsonOutput { 73 | outlog.Printf("%s connection to %s (%s) (%d connections)\n", s, serverAddr, result.IPAddr, *count) 74 | outlog.Printf("min/avg/max/stddev = %s/%s/%s/%s\n", result.MinStr(), result.AvgStr(), result.MaxStr(), result.StdStr()) 75 | os.Exit(0) 76 | } 77 | 78 | // Format the result in JSON 79 | jsonRes := JsonResult{ 80 | Host: result.Host, 81 | IPAddr: result.IPAddr, 82 | ServerAddr: result.Address, 83 | Connection: s, 84 | Min: result.Min, 85 | Max: result.Max, 86 | Count: result.Count, 87 | Avg: result.Avg, 88 | Std: result.Std, 89 | } 90 | if err != nil { 91 | jsonRes.Error = fmt.Sprintf("%s", err) 92 | } 93 | b, err := json.Marshal(jsonRes) 94 | if err != nil { 95 | errlog.Printf("error producing JSON: %s\n", err) 96 | os.Exit(1) 97 | } 98 | os.Stdout.Write(b) 99 | os.Exit(0) 100 | } 101 | 102 | type JsonResult struct { 103 | Host string `json:"host"` 104 | IPAddr string `json:"ip"` 105 | ServerAddr string `json:"address"` 106 | Connection string `json:"connection"` 107 | Count int `json:"count"` 108 | Min float64 `json:"min"` 109 | Max float64 `json:"max"` 110 | Avg float64 `json:"average"` 111 | Std float64 `json:"stddev"` 112 | Error string `json:"error"` 113 | } 114 | 115 | func loadCaCerts(path string) (*x509.CertPool, error) { 116 | if path == "" { 117 | return nil, nil 118 | } 119 | caCerts, err := ioutil.ReadFile(path) 120 | if err != nil { 121 | return nil, fmt.Errorf("error loading CA certficates from '%s': %s", path, err) 122 | } 123 | pool := x509.NewCertPool() 124 | if !pool.AppendCertsFromPEM(caCerts) { 125 | return nil, fmt.Errorf("error creating pool of CA certficates: %s", err) 126 | } 127 | return pool, nil 128 | } 129 | -------------------------------------------------------------------------------- /cmd/tlsping/usage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "text/tabwriter" 8 | "text/template" 9 | ) 10 | 11 | type usageType uint32 12 | 13 | const ( 14 | usageShort usageType = iota 15 | usageLong 16 | ) 17 | 18 | func printUsage(f *os.File, kind usageType) { 19 | const template = ` 20 | USAGE: 21 | {{.Tab1}}{{.AppName}} [-c count] [-tcponly] [-json] [-ca=] 22 | {{.Tab1}}{{.AppNameFiller}} [-insecure] 23 | {{.Tab1}}{{.AppName}} -help 24 | {{.Tab1}}{{.AppName}} -version 25 | {{if eq .UsageVersion "short"}} 26 | Use '{{.AppName}} -help' to get detailed information about options and examples 27 | of usage.{{else}} 28 | 29 | DESCRIPTION: 30 | {{.Tab1}}{{.AppName}} is a basic tool to measure the time required to establish a 31 | {{.Tab1}}TCP connection and perform the TLS handshake with a remote server. 32 | {{.Tab1}}It reports summary statistics of the measurements obtained over a number 33 | {{.Tab1}}of successful connections. 34 | 35 | {{.Tab1}}The address of the remote server, i.e. , is of the form 36 | {{.Tab1}}'host:port', for instance 'mail.google.com:443', '216.58.215.37:443' or 37 | {{.Tab1}}'[2a00:1450:400a:800::2005]:443'. 38 | 39 | OPTIONS: 40 | {{.Tab1}}-c count 41 | {{.Tab2}}Perform count concurrent measurements. 42 | {{.Tab2}}Default: {{.DefaultCount}} 43 | 44 | {{.Tab1}}-tcponly 45 | {{.Tab2}}Establish the TCP connection with the remote server but do not perform 46 | {{.Tab2}}the TLS handshake. 47 | 48 | {{.Tab1}}-insecure 49 | {{.Tab2}}Don't verify the validity of the server certificate. Only relevant when 50 | {{.Tab2}}TLS handshake is performed (see '-tcponly' option). 51 | {{.Tab2}}This option is intended to be used for measuring times for connecting 52 | {{.Tab2}}to servers which use custom not widely trusted certificates, for 53 | {{.Tab2}}instance, development servers using self-signed certificates. 54 | 55 | {{.Tab1}}-ca 56 | {{.Tab2}}PEM-formatted file containing the CA certificates this program trusts. 57 | {{.Tab2}}Default: use this host's CA certificate store. 58 | 59 | {{.Tab1}}-json 60 | {{.Tab2}}Format the result in JSON format and print to standard output. Reported 61 | {{.Tab2}}times are understood in seconds. 62 | 63 | {{.Tab1}}-help 64 | {{.Tab2}}Prints this help 65 | 66 | {{.Tab1}}-version 67 | {{.Tab2}}Show detailed version information about this application 68 | 69 | EXAMPLES: 70 | {{.Tab1}}To measure the time to establish a TCP connection and perform TLS 71 | {{.Tab1}}handshaking with host 'mail.google.com' port 443 use: 72 | 73 | {{.Tab3}}{{.AppName}} mail.google.com:443 74 | 75 | {{.Tab1}}To measure the time to establishing a TCP connection (i.e. without 76 | {{.Tab1}}performing TLS handshaking) to host at IPv6 address 77 | {{.Tab1}}'2606:4700::6811:d209' port 443 use: 78 | 79 | {{.Tab3}}{{.AppName}} -tcponly [2a00:1450:400a:800::2005]:443 80 | {{end}} 81 | ` 82 | if kind == usageLong { 83 | tmplFields["UsageVersion"] = "long" 84 | } 85 | tmplFields["DefaultCount"] = fmt.Sprintf("%d", defaultIterations) 86 | render(template, tmplFields, f) 87 | } 88 | 89 | func render(tpl string, fields map[string]string, out io.Writer) { 90 | minWidth, tabWidth, padding := 4, 4, 0 91 | tabwriter := tabwriter.NewWriter(out, minWidth, tabWidth, padding, byte(' '), 0) 92 | templ := template.Must(template.New("").Parse(tpl)) 93 | templ.Execute(tabwriter, fields) 94 | tabwriter.Flush() 95 | } 96 | -------------------------------------------------------------------------------- /cmd/tlsping/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // printVersion prints the version information about this application 8 | func printVersion(f *os.File) { 9 | const template = ` 10 | {{.AppName}} {{.AppVersion}} 11 | 12 | Compiler: 13 | {{.Tab1}}{{.GoVersion}} ({{.Os}}/{{.Arch}}) 14 | 15 | Built on: 16 | {{.Tab1}}{{.BuildTime}} 17 | 18 | Author: 19 | {{.Tab1}}Fabio Hernandez 20 | {{.Tab1}}IN2P3/CNRS computing center, Lyon (France) 21 | 22 | Source code and documentation: 23 | {{.Tab1}}https://github.com/airnandez/{{.AppName}} 24 | ` 25 | render(template, tmplFields, f) 26 | } 27 | -------------------------------------------------------------------------------- /ping.go: -------------------------------------------------------------------------------- 1 | // Package tlsping measures the time needed for establishing TLS connections 2 | package tlsping 3 | 4 | import ( 5 | "crypto/tls" 6 | "crypto/x509" 7 | "net" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // Config is used to configure how to time the TLS connection 13 | type Config struct { 14 | // Dont perform TLS handshake. Only measure the time for 15 | // estasblishing the TCP connection 16 | AvoidTLSHandshake bool 17 | 18 | // Don't verify server certificate. Used relevant if 19 | // the TLS handshake is performed 20 | InsecureSkipVerify bool 21 | 22 | // Set of root certificate authorities to use to verify the server 23 | // certificate. This is only relevant when measuring the time spent 24 | // in the TLS handshake. 25 | // If nil, the host's set of root certificate authorities is used. 26 | RootCAs *x509.CertPool 27 | 28 | // Number of times to connect. The time spent by every connection will 29 | // be measured and the results will be summarized. 30 | Count int 31 | } 32 | 33 | // Ping establishes network connections to the specified network addr 34 | // and returns summary statistics of the time spent establishing those 35 | // connections. The operation is governed by the provided configuration. 36 | // It returns an error if at least one of the connections fails. 37 | // addr is of the form 'hostname:port' 38 | // The returned results do not include the time spent calling the 39 | // DNS for translating the host name to IP address. This resolution 40 | // is performed once and a single of retrieved IP addresses is used for all 41 | // connections. 42 | func Ping(addr string, config *Config) (PingResult, error) { 43 | if config.Count == 0 { 44 | config.Count = 1 45 | } 46 | host, ipAddr, port, err := resolveAddr(addr) 47 | if err != nil { 48 | return PingResult{}, err 49 | } 50 | result := PingResult{ 51 | Host: host, 52 | IPAddr: ipAddr, 53 | Address: addr, 54 | } 55 | target := net.JoinHostPort(ipAddr, port) 56 | var f func() error 57 | d := &net.Dialer{ 58 | Timeout: 5 * time.Second, 59 | } 60 | if config.AvoidTLSHandshake { 61 | f = func() error { 62 | conn, err := d.Dial("tcp", target) 63 | if err == nil { 64 | conn.Close() 65 | } 66 | return err 67 | } 68 | } else { 69 | tlsConfig := tls.Config{ 70 | ServerName: host, 71 | InsecureSkipVerify: config.InsecureSkipVerify, 72 | RootCAs: config.RootCAs, 73 | } 74 | f = func() error { 75 | conn, err := tls.DialWithDialer(d, "tcp", target, &tlsConfig) 76 | if err == nil { 77 | conn.Close() 78 | } 79 | return err 80 | } 81 | } 82 | 83 | // Launch workers to perform the timing 84 | results := make(chan connectDuration, config.Count) 85 | var wg sync.WaitGroup 86 | wg.Add(config.Count) 87 | for i := 0; i < config.Count; i++ { 88 | go func() { 89 | defer wg.Done() 90 | d, err := timeit(f) 91 | results <- connectDuration{ 92 | seconds: d, 93 | err: err, 94 | } 95 | }() 96 | } 97 | 98 | // Wait for workers to finish 99 | go func() { 100 | wg.Wait() 101 | close(results) 102 | }() 103 | 104 | // Collect workers' results 105 | durations := make([]float64, 0, config.Count) 106 | for res := range results { 107 | if res.err != nil { 108 | return result, res.err 109 | } 110 | durations = append(durations, res.seconds) 111 | } 112 | result.setSummaryStats(summarize(durations)) 113 | return result, nil 114 | } 115 | 116 | type connectDuration struct { 117 | seconds float64 118 | err error 119 | } 120 | 121 | // timeit measures the time spent executing the argument function f 122 | // It returns the elapsed time spent as a floating point number of seconds 123 | func timeit(f func() error) (float64, error) { 124 | start := time.Now() 125 | err := f() 126 | end := time.Now() 127 | if err != nil { 128 | return 0, err 129 | } 130 | return end.Sub(start).Seconds(), nil 131 | } 132 | 133 | // resolveAddr queries the DNS to resolve the name of the host 134 | // in addr and returns the hostname, IP address and port. 135 | // If the DNS responds with more than one IP address associated 136 | // to the given host, the first address to which a TCP connection 137 | // can be established is returned. 138 | func resolveAddr(addr string) (string, string, string, error) { 139 | host, port, err := net.SplitHostPort(addr) 140 | if err != nil { 141 | return "", "", "", err 142 | } 143 | if len(host) == 0 { 144 | host = "localhost" 145 | } 146 | addrs, err := net.LookupHost(host) 147 | if err != nil { 148 | return "", "", "", err 149 | } 150 | d := net.Dialer{Timeout: 3 * time.Second} 151 | for _, a := range addrs { 152 | conn, err := d.Dial("tcp", net.JoinHostPort(a, port)) 153 | if err == nil { 154 | conn.Close() 155 | return host, a, port, nil 156 | } 157 | } 158 | return host, addrs[0], port, nil 159 | } 160 | -------------------------------------------------------------------------------- /results.go: -------------------------------------------------------------------------------- 1 | package tlsping 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // PingResult contains summary statistics of the measured connection 9 | // times 10 | type PingResult struct { 11 | // Target host name 12 | Host string 13 | 14 | // IP Address of the host used for these measurements 15 | IPAddr string 16 | 17 | // Address of the target, in the form hostname:port 18 | Address string 19 | 20 | // Number of measurements summarized in this result 21 | Count int 22 | 23 | // Minimum and maximum observed connection times, in seconds 24 | Min, Max float64 25 | 26 | // Average and standard deviation of the observed connection 27 | // times, in seconds 28 | Avg, Std float64 29 | } 30 | 31 | // setSummaryStats sets the summary stats passed as arguments to the ping 32 | // result 33 | func (r *PingResult) setSummaryStats(count int, min, max, avg, std float64) { 34 | r.Count = count 35 | r.Min, r.Max = min, max 36 | r.Avg, r.Std = avg, std 37 | } 38 | 39 | // summarize summarizes the measurements of time durations given as 40 | // argument. The result argument is populated with summary statistics 41 | // of the durations. The argument values and the returned values are understood 42 | // in seconds 43 | func summarize(durations []float64) (count int, min, max, avg, std float64) { 44 | count = len(durations) 45 | min, max = math.MaxFloat64, math.SmallestNonzeroFloat64 46 | sum := float64(0) 47 | for _, d := range durations { 48 | sum += d 49 | if d < min { 50 | min = d 51 | } 52 | if d > max { 53 | max = d 54 | } 55 | } 56 | n := float64(count) 57 | avg = sum / n 58 | std = float64(0) 59 | for _, d := range durations { 60 | dev := d - avg 61 | std += dev * dev 62 | } 63 | std = math.Sqrt(std / n) 64 | return 65 | } 66 | 67 | func (r *PingResult) MinStr() string { 68 | return secsToString(r.Min) 69 | } 70 | 71 | func (r *PingResult) MaxStr() string { 72 | return secsToString(r.Max) 73 | } 74 | 75 | func (r *PingResult) AvgStr() string { 76 | return secsToString(r.Avg) 77 | } 78 | 79 | func (r *PingResult) StdStr() string { 80 | return secsToString(r.Std) 81 | } 82 | 83 | func secsToString(secs float64) string { 84 | if secs >= 1.0 { 85 | // unit is seconds 86 | return fmt.Sprintf("%.2fs", secs) 87 | } 88 | secs = secs * 1000.0 89 | if secs >= 1.0 { 90 | // unit is milliseconds 91 | return fmt.Sprintf("%.2fms", secs) 92 | } 93 | secs = secs * 1000.0 94 | if secs >= 1.0 { 95 | // unit is µ seconds 96 | return fmt.Sprintf("%.2fµs", secs) 97 | } 98 | // unit is nano seconds 99 | secs = secs * 1000.0 100 | return fmt.Sprintf("%.2fns", secs) 101 | } 102 | --------------------------------------------------------------------------------