├── tcpmeter.png ├── logger.go ├── README.md ├── LICENSE ├── main.go ├── server.go ├── client.go ├── index.html └── admui.go /tcpmeter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/9nut/tcpmeter/HEAD/tcpmeter.png -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // Continually log any stats, provide them on an output channel 8 | // to downstream receivers (usually a CStatHandler) 9 | func LogClient(si chan Stats, so chan Stats) { 10 | log.Println("LogClient started...") 11 | for { 12 | stats, ok := <-si 13 | if !ok { 14 | log.Fatal("receive failed") 15 | } 16 | if stats.Stat == "Running" { 17 | trace.Printf("|DATA|%s|%d|\n", stats.Type, stats.Rate) 18 | } 19 | select { 20 | case so <- stats: 21 | default: 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **tcpmeter** - a tool for measuring TCP upload and download speeds and RTT latency. 2 | 3 | ### Build 4 | 5 | ```shell 6 | go build 7 | ``` 8 | 9 | #### Run 10 | 11 | * start the server on the remote machine: 12 | 13 | `tcpmeter -s -r $(hostname):8001` 14 | 15 | * start the client on the local machine: 16 | 17 | `tcpmeter -c` 18 | 19 | then navigate to `http://localhost:8080` using an HTML5 browser to interact with the client. 20 | 21 | ## Documentation 22 | 23 | `godoc` 24 | 25 | ## License 26 | MIT license (see LICENSE file). 27 | 28 | ## Contact 29 | `skip.tavakkolian@gmail.com` 30 | 31 | ## Screenshot 32 |  33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Fariborz "Skip" Tavakkolian 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "runtime/pprof" 8 | ) 9 | 10 | var trace *log.Logger 11 | 12 | func main() { 13 | var cf, sf bool 14 | var haddr, raddr string 15 | var fname, pname string 16 | cmdline := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 17 | cmdline.Usage = func() { 18 | log.Printf("usage: %s (-c|-s) [-r [host:]port] [-h [host:]port] [-l logfile]\n", os.Args[0]) 19 | } 20 | cmdline.BoolVar(&cf, "c", false, "client mode") 21 | cmdline.BoolVar(&sf, "s", false, "server mode") 22 | cmdline.StringVar(&raddr, "r", ":8001", "RPC address") 23 | cmdline.StringVar(&haddr, "h", ":8080", "Admin WebUI") 24 | cmdline.StringVar(&fname, "l", "/tmp/tcpmeter.log", "Log file name") 25 | cmdline.StringVar(&pname, "p", "", "CPU profile file") 26 | 27 | cmdline.Parse(os.Args[1:]) 28 | 29 | if pname != "" { 30 | f, err := os.Create(pname) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | if err = pprof.StartCPUProfile(f); err != nil { 35 | log.Fatal(err) 36 | } 37 | defer pprof.StopCPUProfile() 38 | } 39 | if cf == sf { 40 | cmdline.Usage() 41 | log.Fatalln("either -s or -c must be specified") 42 | } 43 | 44 | logfile, err := os.OpenFile(fname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0660) 45 | if err != nil { 46 | log.Fatal("OpenFile failed", err) 47 | } 48 | defer logfile.Close() 49 | trace = log.New(logfile, "", log.LstdFlags) 50 | log.SetFlags(log.Flags() | log.Llongfile) 51 | 52 | if cf { 53 | ClientMain(haddr) 54 | } else { 55 | TCPServer(raddr) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "net/rpc" 10 | "time" 11 | ) 12 | 13 | var SrvAddr *net.TCPAddr 14 | 15 | type NullFile struct{} 16 | type ZeroFile struct{} 17 | 18 | func (d *NullFile) Write(p []byte) (int, error) { 19 | return len(p), nil 20 | } 21 | 22 | func (d *ZeroFile) Read(p []byte) (int, error) { 23 | return len(p), nil 24 | } 25 | 26 | // TCPPerf is the receiver type for TCP Performance RPC methods 27 | type TCPPerf struct { 28 | LData *net.TCPListener // payload data listener 29 | DevNull *NullFile 30 | DevZero *ZeroFile 31 | } 32 | 33 | // TCPStart method prepares the tcp link that will be used for tcp performance testing 34 | func (p *TCPPerf) TCPStart(_ int, r *string) error { 35 | log.Println("TCPStart called") 36 | if p.LData != nil { 37 | p.LData.Close() 38 | } 39 | 40 | var err error 41 | p.DevNull = &NullFile{} 42 | p.DevZero = &ZeroFile{} 43 | 44 | addr := "" // use any available port for payload 45 | if SrvAddr.IP != nil { 46 | addr = SrvAddr.IP.String() + ":0" 47 | } else { 48 | addr = "localhost:0" 49 | } 50 | 51 | DAddr, err := net.ResolveTCPAddr("tcp", addr) 52 | if err != nil { 53 | log.Println("ResolveTCPAddr: ", err) 54 | return err 55 | } 56 | 57 | p.LData, err = net.ListenTCP("tcp", DAddr) 58 | if err != nil { 59 | log.Println("ListenTCP: ", err) 60 | return err 61 | } 62 | 63 | DAddr, ok := p.LData.Addr().(*net.TCPAddr) 64 | if ok { 65 | *r = fmt.Sprint(DAddr.Port) 66 | } 67 | 68 | log.Println("Payload port: ", *r) 69 | return nil 70 | } 71 | 72 | // TCPStop method tears down the tcp link that was used for tcp performance testing 73 | func (p *TCPPerf) TCPStop(_ int, r *bool) error { 74 | *r = false 75 | log.Println("TCPStop called") 76 | p.LData.Close() 77 | p.LData = nil 78 | *r = true 79 | return nil 80 | } 81 | 82 | // accept with deadline will do a timed accept of the payload tcp, returning a TCPConn 83 | // when possible 84 | func (p *TCPPerf) timedaccept() (conn *net.TCPConn, err error) { 85 | log.Println("timedaccept called") 86 | if p.LData == nil { 87 | err = errors.New("No Payload TCP Listener") 88 | log.Println(err) 89 | return nil, err 90 | } 91 | 92 | stop := make(chan bool) 93 | go func(stop chan bool) { 94 | select { 95 | case <-time.After(5 * time.Second): 96 | log.Println("Timeout") 97 | p.LData.Close() 98 | case <-stop: 99 | } 100 | }(stop) 101 | conn, err = p.LData.AcceptTCP() 102 | if err != nil { 103 | log.Println("AcceptTCP", err) 104 | } 105 | stop <- true // there will be a race 106 | return 107 | } 108 | 109 | // TCPRcv method tries to receive the number of bytes given by the first parameter 110 | // on a TCP host/port specified in the TCPPerf reciever. If successful, it will store 111 | // the number of bytes it actually received, at the location given by the second parameter. 112 | func (p *TCPPerf) TCPRcv(n uint64, r *uint64) error { 113 | *r = 0 114 | log.Println("TCPRcv called") 115 | conn, err := p.timedaccept() 116 | if err != nil { 117 | log.Println("timedaccept", err) 118 | return err 119 | } 120 | defer conn.Close() 121 | 122 | ncpy, err := io.CopyN(p.DevNull, conn, int64(n)) 123 | if err != nil { 124 | log.Println("CopyN error: ", err) 125 | return err 126 | } 127 | *r = uint64(ncpy) 128 | return nil 129 | } 130 | 131 | // TCPSnd method tries to send the number of bytes given by the first parameter 132 | // on a TCP host/port specified in the TCPPerf reciever. It will store 133 | // the number of bytes it actually sent, at the location given by the second parameter. 134 | func (p *TCPPerf) TCPSnd(n uint64, r *uint64) error { 135 | *r = 0 136 | log.Println("TCPSnd called") 137 | conn, err := p.timedaccept() 138 | if err != nil { 139 | log.Println("timedaccept", err) 140 | return err 141 | } 142 | defer conn.Close() 143 | 144 | ncpy, err := io.CopyN(conn, p.DevZero, int64(n)) 145 | if err != nil { 146 | log.Println("CopyN error: ", err) 147 | return err 148 | } 149 | *r = uint64(ncpy) 150 | return nil 151 | } 152 | 153 | // TCPCpy method listens on a TCP host/port specified in the TCPPerf receiver and 154 | // once established, it copies everything it recieves back to the sender. 155 | func (p *TCPPerf) TCPCpy(_ uint64, r *uint64) error { 156 | *r = 0 157 | log.Println("TCPCpy called") 158 | conn, err := p.timedaccept() 159 | if err != nil { 160 | log.Println("timedaccept", err) 161 | return err 162 | } 163 | defer conn.Close() 164 | 165 | ncpy, err := io.Copy(conn, conn) 166 | if err != nil { 167 | log.Println("Copy error: ", err) 168 | return err 169 | } 170 | *r = uint64(ncpy) 171 | return nil 172 | } 173 | 174 | // TCPServer listens and handles RPC calls from clients. 175 | func TCPServer(raddr string) { 176 | var err error 177 | 178 | perf := new(TCPPerf) 179 | rpc.Register(perf) 180 | 181 | SrvAddr, err = net.ResolveTCPAddr("tcp", raddr) 182 | if err != nil { 183 | log.Fatal(err) 184 | } 185 | 186 | l, err := net.ListenTCP("tcp", SrvAddr) 187 | if err != nil { 188 | log.Fatal(err) 189 | } 190 | log.Println("Starting server") 191 | rpc.Accept(l) 192 | } 193 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "net/rpc" 8 | "time" 9 | ) 10 | 11 | type TCPWorker interface { 12 | GetName() string 13 | Work(stop <-chan bool, stats chan<- uint64, nbytes uint64, addr string) 14 | GetRPC() string 15 | } 16 | 17 | // Type TCPSender implements TCPWorker interface for Upload speed test 18 | // It contains the name of the server side RPC function to call to initiate testing 19 | type TCPSender string 20 | 21 | func (s TCPSender) GetName() string { 22 | return "UP" 23 | } 24 | 25 | func (s TCPSender) GetRPC() string { 26 | return string(s) 27 | } 28 | 29 | // TCPSender Work method uploads nbyte bytes to tcp address addr; if it receives anything on 30 | // the stop channel sch, it exits. it periodically reports the number of bytes it 31 | // transfered since the last report on cch 32 | func (s TCPSender) Work(sch <-chan bool, cch chan<- uint64, nbytes uint64, addr string) { 33 | defer close(cch) // to signal the launcher we exited 34 | 35 | pktsize := uint64(8 * 1024) 36 | every := uint64(16 * pktsize) 37 | 38 | buf := make([]byte, pktsize) 39 | 40 | log.Println("About to dial ", addr) 41 | conn, err := net.Dial("tcp", addr) 42 | if err != nil { 43 | log.Println(err) 44 | return 45 | } 46 | defer conn.Close() 47 | conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) 48 | incr := pktsize 49 | nw := 0 50 | 51 | for n := uint64(0); n < nbytes; n += uint64(nw) { 52 | if nbytes-n < pktsize { 53 | incr = nbytes - n 54 | buf = buf[:incr] 55 | } 56 | nw, err = conn.Write(buf) 57 | if err != nil { 58 | log.Println(err) 59 | return 60 | } else if nw != len(buf) { 61 | log.Println("Short write") 62 | return 63 | } else { 64 | conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) 65 | } 66 | select { 67 | case <-sch: 68 | return 69 | default: 70 | if (n+incr)%every == 0 { 71 | cch <- every 72 | } 73 | } 74 | } 75 | cch <- incr 76 | } 77 | 78 | // Type TCPReceiver implements TCPWorker interface for Download speed test 79 | // It contains the name of the server side RPC function to call to initiate testing 80 | type TCPReceiver string 81 | 82 | func (r TCPReceiver) GetName() string { 83 | return "DOWN" 84 | } 85 | 86 | func (r TCPReceiver) GetRPC() string { 87 | return string(r) 88 | } 89 | 90 | // TCPReceiver Work method downloads nbyte bytes from tcp address addr; if it receives 91 | // anything on the stop channel sch, it exits. it periodically reports the number of bytes 92 | // it received since the last report on cch 93 | func (r TCPReceiver) Work(sch <-chan bool, cch chan<- uint64, nbytes uint64, addr string) { 94 | defer close(cch) // to signal the launcher we exited 95 | 96 | pktsize := uint64(8 * 1024) 97 | every := uint64(16 * pktsize) 98 | 99 | buf := make([]byte, pktsize) 100 | 101 | log.Println("About to dial ", addr) 102 | conn, err := net.Dial("tcp", addr) 103 | if err != nil { 104 | log.Println(err) 105 | return 106 | } 107 | defer conn.Close() 108 | conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 109 | nw := 0 110 | chkpt := uint64(0) 111 | 112 | L: 113 | for n := uint64(0); n < nbytes; n += uint64(nw) { 114 | nw, err = conn.Read(buf) 115 | if err != nil { 116 | log.Println(err) 117 | return 118 | } else { 119 | conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 120 | } 121 | chkpt += uint64(nw) 122 | select { 123 | case <-sch: 124 | break L 125 | default: 126 | if chkpt >= every { 127 | cch <- chkpt 128 | chkpt = 0 129 | } 130 | } 131 | } 132 | cch <- chkpt 133 | } 134 | 135 | func Dispatch(ch chan<- Stats, cfg SrvConfig, worker TCPWorker) error { 136 | name := worker.GetName() 137 | 138 | log.Println("Measuring ", name, " speed...") 139 | client, err := rpc.Dial("tcp", cfg.Host+":"+cfg.RPCPort) 140 | if err != nil { 141 | log.Println(err) 142 | return err 143 | } 144 | defer client.Close() 145 | 146 | Done := make(chan bool) 147 | Res := make(chan uint64) 148 | samples := make([]BitRate, 1, 21) 149 | var ( 150 | rep bool 151 | addr string 152 | tcnt uint64 153 | lcnt uint64 154 | srvtotal uint64 155 | br BitRate 156 | ) 157 | 158 | err = client.Call("TCPPerf.TCPStart", 0, &addr) 159 | if err != nil { 160 | log.Println(err) 161 | return err 162 | } 163 | 164 | log.Println("Payload address: ", addr) 165 | 166 | rpcname := worker.GetRPC() 167 | log.Println("Calling ", rpcname, " ...") 168 | aRcv := client.Go(rpcname, cfg.Count, &srvtotal, nil) 169 | 170 | go worker.Work(Done, Res, cfg.Count, cfg.Host+":"+addr) 171 | 172 | log.Println("Entering wait loop") 173 | t0 := time.Now() 174 | t1 := t0 175 | bps := func(n uint64, t0, t1 time.Time) BitRate { 176 | // n*8 bits 177 | return BitRate(n * uint64(8e9) / uint64(t1.Sub(t0).Nanoseconds())) 178 | } 179 | avg := func(s []BitRate) BitRate { 180 | t := uint64(0) 181 | for _, x := range s { 182 | t += uint64(x) 183 | } 184 | return BitRate(t / uint64(len(s))) 185 | } 186 | addsamp := func() { 187 | tn := time.Now() 188 | xr := bps(lcnt, t1, tn) 189 | lcnt = uint64(0) 190 | t1 = tn 191 | samples = append(samples, xr) 192 | if len(samples) > 20 { 193 | samples = samples[len(samples)-20:] 194 | } 195 | } 196 | timer := time.Tick(500 * time.Millisecond) 197 | 198 | L1: 199 | for { 200 | select { 201 | case <-timer: 202 | addsamp() 203 | br = avg(samples) 204 | // log.Println("Bitrate: ", br.Mbps(), " Mbps, samples:", len(samples)) 205 | if tcnt >= cfg.Count { 206 | Done <- true 207 | break L1 208 | } 209 | select { 210 | case ch <- Stats{"Running", name, br}: 211 | default: 212 | } 213 | case count, ok := <-Res: 214 | if ok { 215 | tcnt += count 216 | lcnt += count 217 | } else { 218 | addsamp() 219 | br = avg(samples) 220 | // log.Println("Bitrate: ", br.Mbps(), " Mbps") 221 | break L1 222 | } 223 | } 224 | } 225 | 226 | ch <- Stats{"Running", name, br} 227 | 228 | <-aRcv.Done 229 | br = bps(tcnt, t0, time.Now()) 230 | log.Println("My count: ", cfg.Count, " Server count: ", srvtotal, " Average: ", br.Mbps(), "Mbps") 231 | 232 | err = client.Call("TCPPerf.TCPStop", 0, &rep) 233 | if err != nil { 234 | log.Println(err) 235 | } 236 | return err 237 | 238 | } 239 | 240 | // TCPClient initiates Upload, Download or RTT measurements, based on 241 | // the instructions sent to it over the Command chan and reports back 242 | // its results over Stats chan. 243 | func TCPClient(cch <-chan Command, sch chan<- Stats) { 244 | log.Println("TCPClient started") 245 | 246 | ops := map[string]TCPWorker{ 247 | "UP": TCPSender("TCPPerf.TCPRcv"), 248 | "DOWN": TCPReceiver("TCPPerf.TCPSnd"), 249 | } 250 | 251 | timer := time.Tick(1 * time.Second) 252 | L: 253 | for { 254 | select { 255 | case c, ok := (<-cch): 256 | if !ok { 257 | close(sch) 258 | break L 259 | } 260 | log.Println("Command: ", c.Name) 261 | worker, found := ops[c.Name] 262 | if found { 263 | err := Dispatch(sch, c.Cfg, worker) 264 | if err != nil { 265 | log.Println(err) 266 | } 267 | } else if c.Name != "STOP" { 268 | log.Println("Unsupported command:", c.Name) 269 | select { 270 | case sch <- Stats{Stat: "Error", Type: "Illegal command:" + c.Name}: 271 | default: 272 | } 273 | } 274 | case <-timer: 275 | select { 276 | case sch <- Stats{Stat: "Stopped"}: 277 | default: 278 | } 279 | } 280 | } 281 | } 282 | 283 | func ClientMain(haddr string) { 284 | cch := make(chan Command) 285 | sch := make(chan Stats, 10) 286 | lch := make(chan Stats) 287 | go TCPClient(cch, sch) 288 | go LogClient(sch, lch) 289 | fmt.Printf("Open http://localhost%s in a browser\n", haddr) 290 | WebUI(haddr, cch, lch) 291 | } 292 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 9 | 134 | 135 | 136 | 137 | 138 |184 | 185 | 186 |
187 |Stopped
349 | 350 | 351 |
352 |Stopped