├── 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 | ![alt text](https://github.com/9nut/tcpmeter/raw/master/tcpmeter.png "tcpmeter client web UI") 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 |
139 |

tcpmeter - TCP Speedometer

140 |
141 |
142 |
143 |
144 |
145 | Server Information 146 |

147 |
148 | 149 |

150 |
151 |

152 |

153 | Packet Type 154 |

155 | UDP 156 | TCP 157 |

158 |
159 |

160 |

161 |

162 | Type of Measurement 163 |

164 | Upload 165 | Download 166 | Round Trip
167 | Continuous 168 |

169 |
170 |

171 |

172 |

173 | Size of Dataset 174 |

175 |
176 | KB 177 | MB 178 | GB 179 |

180 |
181 |

182 |
183 |

184 | 185 | 186 |

187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |

Stopped

198 |
199 |
200 |
201 |
202 | 203 | 204 | -------------------------------------------------------------------------------- /admui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | // SrvConfig defines the far-end server, and its command and payload ports 12 | type SrvConfig struct { 13 | Host string 14 | RPCPort string 15 | Count uint64 16 | Repeat bool 17 | } 18 | 19 | // Command controls the type of function that TCPClient should perform 20 | type Command struct { 21 | Name string 22 | Cfg SrvConfig 23 | } 24 | 25 | type BitRate uint64 26 | 27 | // Returns bitrate in Mega-bits per second 28 | func (b BitRate) Mbps() float32 { 29 | return float32(b) / float32(1000000) 30 | } 31 | 32 | // Returns bitrate in Mega-bytes per second 33 | func (b BitRate) MBps() float32 { 34 | return float32(b) / float32(8*1000000) 35 | } 36 | 37 | // Returns bitrate in Kilo-bits per second 38 | func (b BitRate) Kbps() float32 { 39 | return float32(b) / float32(1000) 40 | } 41 | 42 | // Returns bitrate in Kilo-bytes per second 43 | func (b BitRate) KBps() float32 { 44 | return float32(b) / float32(8*1000) 45 | } 46 | 47 | func (b BitRate) String() string { 48 | return fmt.Sprintf("%d", b) 49 | } 50 | 51 | // Stats is type of measurement that TCPClient reports on its stats channel. 52 | type Stats struct { 53 | Stat string 54 | Type string 55 | Rate BitRate 56 | } 57 | 58 | type JSONStats struct { 59 | Stat string 60 | Type string 61 | Rate float32 62 | } 63 | 64 | // CCmdHandler is the receiver type for handling TCPClient control request 65 | type CCmdHandler struct { 66 | CmdCh chan Command 67 | } 68 | 69 | // CStatHandler is the reciever type for handling TCPClient stats requests 70 | type CStatHandler struct { 71 | StatCh chan Stats 72 | } 73 | 74 | // This handler parses the form from the user and initiates a TCPClient 75 | // measurement. 76 | func (c *CCmdHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 77 | var ( 78 | raddr string 79 | rport int 80 | pktt string 81 | tstt string 82 | txsize int 83 | txmult string 84 | txcont string 85 | ) 86 | params := map[string]interface{}{ 87 | "raddr": &raddr, 88 | "rport": &rport, 89 | "pktt": &pktt, 90 | "tstt": &tstt, 91 | "txsize": &txsize, 92 | "txmult": &txmult, 93 | "txcont": &txcont, 94 | } 95 | Mult := map[string]uint64{ 96 | "KB": 1024, 97 | "MB": 1024 * 1024, 98 | "GB": 1024 * 1024 * 1024, 99 | } 100 | 101 | getformparams(r, params) 102 | trace.Printf("|CMD|%s|%s|\n", tstt, raddr) 103 | log.Println("CMD: ", raddr, rport, pktt, tstt, txsize, txmult, txcont != "") 104 | 105 | cmd := Command{ 106 | Name: tstt, 107 | Cfg: SrvConfig{ 108 | Host: raddr, 109 | RPCPort: fmt.Sprint(rport), 110 | Count: uint64(txsize) * Mult[txmult], 111 | Repeat: txcont != "", 112 | }, 113 | } 114 | c.CmdCh <- cmd 115 | w.Header().Set("Content-Type", "text/html") 116 | w.Write([]byte("")) 117 | } 118 | 119 | // parse and store form parameters in the map that's passed in 120 | func getformparams(r *http.Request, params map[string]interface{}) { 121 | for i, x := range params { 122 | if v := r.FormValue(i); v != "" { 123 | fmt.Sscan(v, x) 124 | } 125 | } 126 | } 127 | 128 | // This handler deals with GET requests for TCPClient measurement results. 129 | // It returns the measurements since last snapshot in json format. 130 | func (s *CStatHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 131 | var jst JSONStats 132 | w.Header().Set("Content-Type", "text/plain") 133 | st, ok := <-s.StatCh 134 | if !ok { 135 | jst = JSONStats{Stat: "Error"} 136 | } else { 137 | jst = JSONStats{st.Stat, st.Type, st.Rate.Mbps()} 138 | } 139 | je := json.NewEncoder(w) 140 | je.Encode(jst) 141 | } 142 | 143 | // WebUI is an http server that provides an html UI to the user, annoucing itself at address 144 | // that is passed in. It handles requests for starting and stopping of the load testing 145 | // client and reporting of data. 146 | func WebUI(addr string, cch chan Command, sch chan Stats) { 147 | cl := &CCmdHandler{cch} 148 | st := &CStatHandler{sch} 149 | http.Handle("/cmd", cl) 150 | http.Handle("/stats", st) 151 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 152 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 153 | io.WriteString(w, htmlfile) 154 | 155 | }) 156 | err := http.ListenAndServe(addr, nil) 157 | if err != nil { 158 | log.Fatal("ListenAndServe: " + err.Error()) 159 | } 160 | } 161 | 162 | // unfortunately there isn't an easy way to include 163 | // a file; so make the changes to index.html and then 164 | // replace the contents of the htmlfile const with it. 165 | const htmlfile = ` 166 | 167 | 168 | 169 | 170 | 171 | 174 | 299 | 300 | 301 | 302 | 303 |
304 |

tcpmeter - TCP Speedometer

305 |
306 |
307 |
308 |
309 |
310 | Server Information 311 |

312 |
313 | 314 |

315 |
316 |

317 |

318 | Packet Type 319 |

320 | UDP 321 | TCP 322 |

323 |
324 |

325 |

326 |

327 | Type of Measurement 328 |

329 | Upload 330 | Download 331 | Round Trip
332 | Continuous 333 |

334 |
335 |

336 |

337 |

338 | Size of Dataset 339 |

340 |
341 | KB 342 | MB 343 | GB 344 |

345 |
346 |

347 |
348 |

349 | 350 | 351 |

352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |

Stopped

363 |
364 |
365 |
366 |
367 | 368 | ` 369 | --------------------------------------------------------------------------------