├── testdata └── postfile.txt ├── .travis.yml ├── .gitignore ├── report_test.go ├── common_test.go ├── context_test.go ├── config_test.go ├── benchmark.go ├── context.go ├── common.go ├── main.go ├── LICENSE ├── benchmark_test.go ├── monitor.go ├── monitor_test.go ├── README.md ├── report.go ├── http_test.go ├── config.go └── http.go /testdata/postfile.txt: -------------------------------------------------------------------------------- 1 | email=test&password=testing -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.1 4 | - 1.2 5 | - 1.3 6 | - 1.4 7 | - tip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | temp/ 2 | raw/ 3 | *.sh 4 | *.csv 5 | *.pprof 6 | profilingtool.go 7 | gb 8 | -------------------------------------------------------------------------------- /report_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestStdDev(t *testing.T) { 9 | testData := map[float64][]time.Duration{ 10 | 2.0: []time.Duration{2, 4, 4, 4, 5, 5, 7, 9}, 11 | 1.5811: []time.Duration{5, 6, 8, 9}, 12 | } 13 | 14 | for expectedData, testingData := range testData { 15 | if result := stdDev(testingData); int(result*1000) != int(expectedData*1000) { 16 | t.Errorf("expected %f, got %f", expectedData, result) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestStopWatch(t *testing.T) { 9 | testData := []int{100, 200} //Millisecond 10 | for _, value := range testData { 11 | sw := &StopWatch{} 12 | 13 | sw.Start() 14 | time.Sleep(time.Duration(value) * time.Millisecond) 15 | sw.Stop() 16 | 17 | if int64(sw.Elapsed)/1000000 != int64(time.Duration(value)) { 18 | t.Errorf("expected %d, got %d", time.Duration(value)*time.Millisecond, sw.Elapsed) 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSetAndGetString(t *testing.T) { 8 | key := "key" 9 | value := "value" 10 | 11 | context := NewContext(&Config{}) 12 | context.SetString(key, value) 13 | 14 | got := context.GetString(key) 15 | if value != got { 16 | t.Fatalf("expected %s, got %s", value, got) 17 | } 18 | } 19 | 20 | func TestSetAndGetInt(t *testing.T) { 21 | key := "key" 22 | value := 123 23 | 24 | context := NewContext(&Config{}) 25 | context.SetInt(key, value) 26 | 27 | got := context.GetInt(key) 28 | if value != got { 29 | t.Fatalf("expected %d, got %d", value, got) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | func TestExtractHostAndPort(t *testing.T) { 9 | 10 | type Pair struct { 11 | host string 12 | port int 13 | } 14 | 15 | testData := map[string]Pair{ 16 | "http://localhost:8080/": Pair{"localhost", 8080}, 17 | "https://www.google.com/": Pair{"www.google.com", 443}, 18 | "http://localhost/": Pair{"localhost", 80}, 19 | } 20 | 21 | for testingData, expectedData := range testData { 22 | URL, _ := url.Parse(testingData) 23 | host, port := extractHostAndPort(URL) 24 | 25 | if host != expectedData.host && port != expectedData.port { 26 | t.Errorf("expected host:%s and port:%d, got", host, port) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /benchmark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | type Benchmark struct { 9 | c *Context 10 | collector chan *Record 11 | } 12 | 13 | type Record struct { 14 | responseTime time.Duration 15 | contentSize int64 16 | Error error 17 | } 18 | 19 | func NewBenchmark(context *Context) *Benchmark { 20 | collector := make(chan *Record, context.config.requests) 21 | return &Benchmark{context, collector} 22 | } 23 | 24 | func (b *Benchmark) Run() { 25 | 26 | jobs := make(chan *http.Request, b.c.config.concurrency*GoMaxProcs) 27 | 28 | for i := 0; i < b.c.config.concurrency; i++ { 29 | go NewHTTPWorker(b.c, jobs, b.collector).Run() 30 | } 31 | 32 | base, _ := NewHTTPRequest(b.c.config) 33 | for i := 0; i < b.c.config.requests; i++ { 34 | jobs <- CopyHTTPRequest(b.c.config, base) 35 | } 36 | close(jobs) 37 | 38 | <-b.c.stop 39 | } 40 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Context struct { 8 | config *Config 9 | start *sync.WaitGroup 10 | stop chan struct{} 11 | rwm *sync.RWMutex 12 | store map[string]interface{} 13 | } 14 | 15 | func NewContext(config *Config) *Context { 16 | start := &sync.WaitGroup{} 17 | start.Add(config.concurrency) 18 | return &Context{config, start, make(chan struct{}), &sync.RWMutex{}, make(map[string]interface{})} 19 | } 20 | 21 | func (c *Context) SetString(key string, value string) { 22 | c.rwm.Lock() 23 | defer c.rwm.Unlock() 24 | c.store[key] = value 25 | } 26 | 27 | func (c *Context) GetString(key string) string { 28 | c.rwm.RLock() 29 | defer c.rwm.RUnlock() 30 | return c.store[key].(string) 31 | } 32 | 33 | func (c *Context) SetInt(key string, value int) { 34 | c.rwm.Lock() 35 | defer c.rwm.Unlock() 36 | c.store[key] = value 37 | } 38 | 39 | func (c *Context) GetInt(key string) int { 40 | c.rwm.RLock() 41 | defer c.rwm.RUnlock() 42 | return c.store[key].(int) 43 | } 44 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "time" 9 | ) 10 | 11 | type StopWatch struct { 12 | start time.Time 13 | Elapsed time.Duration 14 | } 15 | 16 | func (s *StopWatch) Start() { 17 | s.start = time.Now() 18 | } 19 | 20 | func (s *StopWatch) Stop() { 21 | s.Elapsed = time.Now().Sub(s.start) 22 | } 23 | 24 | func TraceException(msg interface{}) { 25 | switch { 26 | case Verbosity > 1: 27 | // print recovered error and stacktrace 28 | var buffer bytes.Buffer 29 | buffer.WriteString(fmt.Sprintf("recover: %v\n", msg)) 30 | for skip := 1; ; skip++ { 31 | pc, file, line, ok := runtime.Caller(skip) 32 | if !ok { 33 | break 34 | } 35 | f := runtime.FuncForPC(pc) 36 | buffer.WriteString(fmt.Sprintf("\t%s:%d %s()\n", file, line, f.Name())) 37 | } 38 | buffer.WriteString("\n") 39 | fmt.Fprint(os.Stderr, buffer.String()) 40 | case Verbosity > 0: 41 | // print recovered error only 42 | fmt.Fprintf(os.Stderr, "recover: %v\n", msg) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "runtime" 9 | "time" 10 | ) 11 | 12 | const ( 13 | GBVersion = "0.1.9" 14 | MaxExecutionTimeout = time.Duration(30) * time.Second 15 | MaxRequests = 50000 // for timelimit 16 | ) 17 | 18 | var ( 19 | Verbosity = 0 20 | GoMaxProcs int 21 | ContinueOnError bool 22 | ) 23 | 24 | func main() { 25 | if config, err := LoadConfig(); err != nil { 26 | fmt.Println(err) 27 | flag.Usage() 28 | os.Exit(-1) 29 | } else { 30 | context := NewContext(config) 31 | if err := DetectHost(context); err != nil { 32 | log.Fatal(err) 33 | } else { 34 | runtime.GOMAXPROCS(GoMaxProcs) 35 | startBenchmark(context) 36 | } 37 | } 38 | } 39 | 40 | func startBenchmark(context *Context) { 41 | PrintHeader() 42 | 43 | benchmark := NewBenchmark(context) 44 | monitor := NewMonitor(context, benchmark.collector) 45 | go monitor.Run() 46 | go benchmark.Run() 47 | 48 | PrintReport(context, <-monitor.output) 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Brandon Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "sync/atomic" 7 | "testing" 8 | ) 9 | 10 | func TestBenchmark(t *testing.T) { 11 | 12 | requests := 100 13 | var received int64 14 | 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | w.Write([]byte("hello")) 17 | atomic.AddInt64(&received, 1) 18 | })) 19 | defer ts.Close() 20 | 21 | config := &Config{ 22 | concurrency: 10, 23 | requests: requests, 24 | method: "GET", 25 | executionTimeout: MaxExecutionTimeout, 26 | url: ts.URL, 27 | } 28 | 29 | context := NewContext(config) 30 | context.SetInt(FieldContentSize, 5) 31 | benchmark := NewBenchmark(context) 32 | 33 | go benchmark.Run() 34 | 35 | go func() { 36 | counter := 0 37 | for record := range benchmark.collector { 38 | counter++ 39 | if counter == requests || record.Error != nil { 40 | break 41 | } 42 | } 43 | close(context.stop) 44 | }() 45 | 46 | context.start.Wait() 47 | <-context.stop 48 | 49 | if actualReceived := atomic.LoadInt64(&received); int64(requests) != actualReceived { 50 | t.Fatalf("expected to send %d requests and receive %d responses, but got %d responses", requests, requests, actualReceived) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /monitor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "time" 8 | ) 9 | 10 | type Monitor struct { 11 | c *Context 12 | collector chan *Record 13 | output chan *Stats 14 | } 15 | 16 | type Stats struct { 17 | responseTimeData []time.Duration 18 | 19 | totalRequests int 20 | totalExecutionTime time.Duration 21 | totalResponseTime time.Duration 22 | totalReceived int64 23 | totalFailedReqeusts int 24 | 25 | errLength int 26 | errConnect int 27 | errReceive int 28 | errException int 29 | errResponse int 30 | } 31 | 32 | func NewMonitor(context *Context, collector chan *Record) *Monitor { 33 | return &Monitor{context, collector, make(chan *Stats)} 34 | } 35 | 36 | func (m *Monitor) Run() { 37 | 38 | // catch interrupt signal 39 | userInterrupt := make(chan os.Signal, 1) 40 | signal.Notify(userInterrupt, os.Interrupt) 41 | 42 | stats := &Stats{} 43 | stats.responseTimeData = make([]time.Duration, 0, m.c.config.requests) 44 | 45 | var timelimiter <-chan time.Time 46 | if m.c.config.timelimit > 0 { 47 | t := time.NewTimer(time.Duration(m.c.config.timelimit) * time.Second) 48 | defer t.Stop() 49 | timelimiter = t.C 50 | } 51 | 52 | // waiting for all of http workers to start 53 | m.c.start.Wait() 54 | 55 | fmt.Printf("Benchmarking %s (be patient)\n", m.c.config.host) 56 | sw := &StopWatch{} 57 | sw.Start() 58 | 59 | loop: 60 | for { 61 | select { 62 | case record := <-m.collector: 63 | 64 | updateStats(stats, record) 65 | 66 | if record.Error != nil && !ContinueOnError { 67 | break loop 68 | } 69 | 70 | if stats.totalRequests >= 10 && stats.totalRequests%(m.c.config.requests/10) == 0 { 71 | fmt.Printf("Completed %d requests\n", stats.totalRequests) 72 | } 73 | 74 | if stats.totalRequests == m.c.config.requests { 75 | fmt.Printf("Finished %d requests\n", stats.totalRequests) 76 | break loop 77 | } 78 | 79 | case <-timelimiter: 80 | break loop 81 | case <-userInterrupt: 82 | break loop 83 | } 84 | } 85 | 86 | sw.Stop() 87 | stats.totalExecutionTime = sw.Elapsed 88 | 89 | // shutdown benchmark and all of httpworkers to stop 90 | close(m.c.stop) 91 | signal.Stop(userInterrupt) 92 | m.output <- stats 93 | } 94 | 95 | func updateStats(stats *Stats, record *Record) { 96 | stats.totalRequests++ 97 | 98 | if record.Error != nil { 99 | stats.totalFailedReqeusts++ 100 | 101 | switch record.Error.(type) { 102 | case *ConnectError: 103 | stats.errConnect++ 104 | case *ExceptionError: 105 | stats.errException++ 106 | case *LengthError: 107 | stats.errLength++ 108 | case *ReceiveError: 109 | stats.errReceive++ 110 | case *ResponseError: 111 | stats.errResponse++ 112 | default: 113 | stats.errException++ 114 | } 115 | 116 | } else { 117 | stats.totalResponseTime += record.responseTime 118 | stats.totalReceived += record.contentSize 119 | stats.responseTimeData = append(stats.responseTimeData, record.responseTime) 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /monitor_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestMonitorWithSuccessedResponse(t *testing.T) { 11 | 12 | config := &Config{ 13 | requests: 2, 14 | } 15 | 16 | collector := make(chan *Record, config.requests) 17 | 18 | context := NewContext(config) 19 | monitor := NewMonitor(context, collector) 20 | 21 | request1 := &Record{10, 10, nil} 22 | request2 := &Record{20, 20, nil} 23 | 24 | collector <- request1 25 | collector <- request2 26 | 27 | devnull, _ := os.Open(os.DevNull) 28 | defer devnull.Close() 29 | 30 | stdout := os.Stdout 31 | os.Stdout = devnull 32 | 33 | go monitor.Run() 34 | stats := <-monitor.output 35 | 36 | os.Stdout = stdout 37 | if stats.totalRequests != config.requests { 38 | t.Fatalf("expected %d requests, actual %d requests", config.requests, stats.totalRequests) 39 | } 40 | 41 | if stats.responseTimeData[0] != request1.responseTime || stats.responseTimeData[1] != request2.responseTime { 42 | t.Fatalf("expected %s responseTimeData, actual %s responseTimeData", []time.Duration{request1.responseTime, request2.responseTime}, stats.responseTimeData) 43 | } 44 | 45 | if stats.totalReceived != request1.contentSize+request2.contentSize { 46 | t.Fatalf("expected %d content received, actual %d content received", request1.contentSize+request2.contentSize, stats.totalReceived) 47 | } 48 | } 49 | 50 | func TestMonitorWithFailedResponse(t *testing.T) { 51 | 52 | ContinueOnError = true 53 | 54 | config := &Config{ 55 | requests: 6, 56 | } 57 | 58 | collector := make(chan *Record, config.requests) 59 | 60 | context := NewContext(config) 61 | monitor := NewMonitor(context, collector) 62 | 63 | dummy := errors.New("dummy error") 64 | 65 | records := []*Record{ 66 | &Record{Error: &LengthError{dummy}}, 67 | &Record{Error: &ConnectError{dummy}}, 68 | &Record{Error: &ReceiveError{dummy}}, 69 | &Record{Error: &ExceptionError{dummy}}, 70 | &Record{Error: &ResponseError{dummy}}, 71 | &Record{Error: &ResponseTimeoutError{dummy}}, 72 | } 73 | 74 | expectedStat := &Stats{ 75 | totalRequests: config.requests, 76 | totalFailedReqeusts: 6, 77 | errLength: 1, 78 | errConnect: 1, 79 | errReceive: 1, 80 | errException: 2, 81 | errResponse: 1, 82 | } 83 | 84 | for _, record := range records { 85 | collector <- record 86 | } 87 | 88 | devnull, _ := os.Open(os.DevNull) 89 | defer devnull.Close() 90 | 91 | stdout := os.Stdout 92 | os.Stdout = devnull 93 | 94 | go monitor.Run() 95 | actualStats := <-monitor.output 96 | os.Stdout = stdout 97 | 98 | if actualStats.totalRequests != expectedStat.totalRequests || 99 | actualStats.totalFailedReqeusts != expectedStat.totalFailedReqeusts || 100 | actualStats.errLength != expectedStat.errLength || 101 | actualStats.errConnect != expectedStat.errConnect || 102 | actualStats.errReceive != expectedStat.errReceive || 103 | actualStats.errException != expectedStat.errException || 104 | actualStats.errResponse != expectedStat.errResponse { 105 | t.Fatalf("expected %#+v , actual %#+v", expectedStat, actualStats) 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go-HttpBench 2 | 3 | ==== 4 | 5 | *an ab-like benchmark tool run on multi-core cpu* 6 | 7 | Installation 8 | -------------- 9 | 1. install [Go](http://golang.org/doc/install) into your environment 10 | 2. download and build Go-HttpBench 11 | 12 | ``` 13 | go get github.com/parkghost/gohttpbench 14 | go build -o gb github.com/parkghost/gohttpbench 15 | ``` 16 | 17 | Usage 18 | ----------- 19 | 20 | ``` 21 | Usage: gb [options] http[s]://hostname[:port]/path 22 | Options are: 23 | -A="": Add Basic WWW Authentication, the attributes are a colon separated username and password. 24 | -C=[]: Add cookie, eg. 'Apache=1234. (repeatable) 25 | -G=2: Number of CPU 26 | -H=[]: Add Arbitrary header line, eg. 'Accept-Encoding: gzip' Inserted after all normal header lines. (repeatable) 27 | -T="text/plain": Content-type header for POSTing, eg. 'application/x-www-form-urlencoded' Default is 'text/plain' 28 | -c=1: Number of multiple requests to make 29 | -h=false: Display usage information (this message) 30 | -i=false: Use HEAD instead of GET 31 | -k=false: Use HTTP KeepAlive feature 32 | -n=1: Number of requests to perform 33 | -p="": File containing data to POST. Remember also to set -T 34 | -r=false: Don't exit when errors 35 | -t=0: Seconds to max. wait for responses 36 | -u="": File containing data to PUT. Remember also to set -T 37 | -v=0: How much troubleshooting info to print 38 | -z=false: Use HTTP Gzip feature 39 | ``` 40 | 41 | ### Example: 42 | $ gb -c 100 -n 100000 -k http://localhost/10k.dat 43 | 44 | This is GoHttpBench, Version 0.1.9, https://github.com/parkghost/gohttpbench 45 | Author: Brandon Chen, Email: parkghost@gmail.com 46 | Licensed under the MIT license 47 | 48 | Benchmarking localhost (be patient) 49 | Completed 10000 requests 50 | Completed 20000 requests 51 | Completed 30000 requests 52 | Completed 40000 requests 53 | Completed 50000 requests 54 | Completed 60000 requests 55 | Completed 70000 requests 56 | Completed 80000 requests 57 | Completed 90000 requests 58 | Completed 100000 requests 59 | Finished 100000 requests 60 | 61 | 62 | Server Software: nginx/1.2.6 (Ubuntu) 63 | Server Hostname: localhost 64 | Server Port: 80 65 | 66 | Document Path: /10k.dat 67 | Document Length: 10240 bytes 68 | 69 | Concurrency Level: 100 70 | Time taken for tests: 5.36 seconds 71 | Complete requests: 100000 72 | Failed requests: 0 73 | HTML transferred: 1024000000 bytes 74 | Requests per second: 18652.41 [#/sec] (mean) 75 | Time per request: 5.361 [ms] (mean) 76 | Time per request: 0.054 [ms] (mean, across all concurrent requests) 77 | HTML Transfer rate: 186524.05 [Kbytes/sec] received 78 | 79 | Connection Times (ms) 80 | min mean[+/-sd] median max 81 | Total: 0 0 3.03 4 32 82 | 83 | Percentage of the requests served within a certain time (ms) 84 | 50% 4 85 | 66% 5 86 | 75% 6 87 | 80% 7 88 | 90% 8 89 | 95% 10 90 | 98% 12 91 | 99% 14 92 | 100% 32 (longest request) 93 | 94 | 95 | Author 96 | ------- 97 | 98 | **Brandon Chen** 99 | 100 | + http://github.com/parkghost 101 | 102 | 103 | License 104 | --------------------- 105 | 106 | This project is licensed under the MIT license 107 | -------------------------------------------------------------------------------- /report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "net/url" 8 | "sort" 9 | "time" 10 | ) 11 | 12 | func PrintHeader() { 13 | fmt.Println(` 14 | This is GoHttpBench, Version ` + GBVersion + `, https://github.com/parkghost/gohttpbench 15 | Author: Brandon Chen, Email: parkghost@gmail.com 16 | Licensed under the MIT license 17 | `) 18 | } 19 | 20 | func PrintReport(context *Context, stats *Stats) { 21 | 22 | var buffer bytes.Buffer 23 | 24 | config := context.config 25 | responseTimeData := stats.responseTimeData 26 | totalFailedReqeusts := stats.totalFailedReqeusts 27 | totalRequests := stats.totalRequests 28 | totalExecutionTime := stats.totalExecutionTime 29 | totalReceived := stats.totalReceived 30 | 31 | URL, _ := url.Parse(config.url) 32 | 33 | fmt.Fprint(&buffer, "\n\n") 34 | fmt.Fprintf(&buffer, "Server Software: %s\n", context.GetString(FieldServerName)) 35 | fmt.Fprintf(&buffer, "Server Hostname: %s\n", config.host) 36 | fmt.Fprintf(&buffer, "Server Port: %d\n\n", config.port) 37 | 38 | fmt.Fprintf(&buffer, "Document Path: %s\n", URL.RequestURI()) 39 | fmt.Fprintf(&buffer, "Document Length: %d bytes\n\n", context.GetInt(FieldContentSize)) 40 | 41 | fmt.Fprintf(&buffer, "Concurrency Level: %d\n", config.concurrency) 42 | fmt.Fprintf(&buffer, "Time taken for tests: %.2f seconds\n", totalExecutionTime.Seconds()) 43 | fmt.Fprintf(&buffer, "Complete requests: %d\n", totalRequests) 44 | if totalFailedReqeusts == 0 { 45 | fmt.Fprintln(&buffer, "Failed requests: 0") 46 | } else { 47 | fmt.Fprintf(&buffer, "Failed requests: %d\n", totalFailedReqeusts) 48 | fmt.Fprintf(&buffer, " (Connect: %d, Receive: %d, Length: %d, Exceptions: %d)\n", stats.errConnect, stats.errReceive, stats.errLength, stats.errException) 49 | } 50 | if stats.errResponse > 0 { 51 | fmt.Fprintf(&buffer, "Non-2xx responses: %d\n", stats.errResponse) 52 | } 53 | fmt.Fprintf(&buffer, "HTML transferred: %d bytes\n", totalReceived) 54 | 55 | if len(responseTimeData) > 0 && totalExecutionTime > 0 { 56 | stdDevOfResponseTime := stdDev(responseTimeData) / 1000000 57 | sort.Sort(durationSlice(responseTimeData)) 58 | 59 | meanOfResponseTime := int64(totalExecutionTime) / int64(totalRequests) / 1000000 60 | medianOfResponseTime := responseTimeData[len(responseTimeData)/2] / 1000000 61 | minResponseTime := responseTimeData[0] / 1000000 62 | maxResponseTime := responseTimeData[len(responseTimeData)-1] / 1000000 63 | 64 | fmt.Fprintf(&buffer, "Requests per second: %.2f [#/sec] (mean)\n", float64(totalRequests)/totalExecutionTime.Seconds()) 65 | fmt.Fprintf(&buffer, "Time per request: %.3f [ms] (mean)\n", float64(config.concurrency)*float64(totalExecutionTime.Nanoseconds())/1000000/float64(totalRequests)) 66 | fmt.Fprintf(&buffer, "Time per request: %.3f [ms] (mean, across all concurrent requests)\n", float64(totalExecutionTime.Nanoseconds())/1000000/float64(totalRequests)) 67 | fmt.Fprintf(&buffer, "HTML Transfer rate: %.2f [Kbytes/sec] received\n\n", float64(totalReceived/1024)/totalExecutionTime.Seconds()) 68 | 69 | fmt.Fprint(&buffer, "Connection Times (ms)\n") 70 | fmt.Fprint(&buffer, " min\tmean[+/-sd]\tmedian\tmax\n") 71 | fmt.Fprintf(&buffer, "Total: %d \t%d %.2f \t%d \t%d\n\n", 72 | minResponseTime, 73 | meanOfResponseTime, 74 | stdDevOfResponseTime, 75 | medianOfResponseTime, 76 | maxResponseTime) 77 | 78 | fmt.Fprintln(&buffer, "Percentage of the requests served within a certain time (ms)") 79 | 80 | percentages := []int{50, 66, 75, 80, 90, 95, 98, 99} 81 | 82 | for _, percentage := range percentages { 83 | fmt.Fprintf(&buffer, " %d%%\t %d\n", percentage, responseTimeData[percentage*len(responseTimeData)/100]/1000000) 84 | } 85 | fmt.Fprintf(&buffer, " %d%%\t %d (longest request)\n", 100, maxResponseTime) 86 | } 87 | fmt.Println(buffer.String()) 88 | } 89 | 90 | type durationSlice []time.Duration 91 | 92 | func (s durationSlice) Len() int { return len(s) } 93 | func (s durationSlice) Less(i, j int) bool { return s[i] < s[j] } 94 | func (s durationSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 95 | 96 | // StdDev calculate standard deviation 97 | func stdDev(data []time.Duration) float64 { 98 | var sum int64 99 | for _, d := range data { 100 | sum += int64(d) 101 | } 102 | avg := float64(sum / int64(len(data))) 103 | 104 | sumOfSquares := 0.0 105 | for _, d := range data { 106 | 107 | sumOfSquares += math.Pow(float64(d)-avg, 2) 108 | } 109 | return math.Sqrt(sumOfSquares / float64(len(data))) 110 | 111 | } 112 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var getRequestConfig = &Config{ 12 | url: "http://localhost/", 13 | method: "GET", 14 | executionTimeout: time.Duration(100) * time.Millisecond, 15 | } 16 | var postRequestConfig = &Config{ 17 | url: "http://localhost/", 18 | method: "POST", 19 | contentType: "application/x-www-form-urlencoded", 20 | executionTimeout: time.Duration(100) * time.Millisecond, 21 | } 22 | 23 | func init() { 24 | loadFile(postRequestConfig, "testdata/postfile.txt") 25 | } 26 | 27 | func TestHTTPWithGet(t *testing.T) { 28 | 29 | //fake http server 30 | responseStr := "hello" 31 | 32 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | w.Write([]byte(responseStr)) 34 | })) 35 | defer ts.Close() 36 | 37 | // http worker 38 | 39 | config := &Config{ 40 | concurrency: 1, 41 | requests: 1, 42 | method: "GET", 43 | executionTimeout: MaxExecutionTimeout, 44 | url: ts.URL, 45 | } 46 | 47 | context := NewContext(config) 48 | context.SetInt(FieldContentSize, len(responseStr)) 49 | jobs := make(chan *http.Request) 50 | collector := make(chan *Record) 51 | 52 | worker := NewHTTPWorker(context, jobs, collector) 53 | 54 | go worker.Run() 55 | 56 | request, err := NewHTTPRequest(config) 57 | if err != nil { 58 | t.Fatalf("new http request failed: %s", err) 59 | } 60 | 61 | jobs <- request 62 | record := <-collector 63 | close(jobs) 64 | close(context.stop) 65 | 66 | if record.Error != nil { 67 | t.Fatalf("sent a http reqeust but was error: %s", record.Error) 68 | } 69 | 70 | if record.contentSize != int64(len(responseStr)) { 71 | t.Fatalf("send a http reqeust but content size dismatch") 72 | } 73 | } 74 | 75 | func TestHTTPWithPost(t *testing.T) { 76 | 77 | //fake http server 78 | responseStr := "hello" 79 | 80 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 81 | // from values from testdata/postfile.txt 82 | if r.FormValue("email") == "test" && r.FormValue("password") == "testing" { 83 | w.Write([]byte(responseStr)) 84 | } 85 | })) 86 | defer ts.Close() 87 | 88 | // http worker 89 | 90 | config := &Config{ 91 | concurrency: 1, 92 | requests: 1, 93 | method: "POST", 94 | contentType: "application/x-www-form-urlencoded", 95 | executionTimeout: MaxExecutionTimeout, 96 | url: ts.URL, 97 | } 98 | loadFile(config, "testdata/postfile.txt") 99 | 100 | context := NewContext(config) 101 | context.SetInt(FieldContentSize, len(responseStr)) 102 | jobs := make(chan *http.Request) 103 | collector := make(chan *Record) 104 | 105 | worker := NewHTTPWorker(context, jobs, collector) 106 | 107 | go worker.Run() 108 | 109 | request, err := NewHTTPRequest(config) 110 | 111 | if err != nil { 112 | t.Fatalf("new http request failed: %s", err) 113 | } 114 | 115 | jobs <- request 116 | record := <-collector 117 | close(jobs) 118 | close(context.stop) 119 | 120 | if record.Error != nil { 121 | t.Fatalf("sent a http reqeust but was error: %s", record.Error) 122 | } 123 | 124 | if record.contentSize != int64(len(responseStr)) { 125 | t.Fatalf("send a http reqeust but content size dismatch") 126 | } 127 | } 128 | 129 | func TestHTTPWorkerWithTimeout(t *testing.T) { 130 | 131 | //fake http server 132 | responseStr := "hello" 133 | 134 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 | time.Sleep(time.Duration(200) * time.Millisecond) 136 | w.Write([]byte(responseStr)) 137 | })) 138 | defer ts.Close() 139 | 140 | // http worker 141 | 142 | config := &Config{ 143 | concurrency: 1, 144 | requests: 1, 145 | method: "GET", 146 | executionTimeout: time.Duration(100) * time.Millisecond, 147 | url: ts.URL, 148 | } 149 | 150 | context := NewContext(config) 151 | context.SetInt(FieldContentSize, len(responseStr)) 152 | jobs := make(chan *http.Request) 153 | collector := make(chan *Record) 154 | 155 | worker := NewHTTPWorker(context, jobs, collector) 156 | 157 | go worker.Run() 158 | 159 | request, err := NewHTTPRequest(config) 160 | if err != nil { 161 | t.Fatalf("new http request failed: %s", err) 162 | } 163 | 164 | jobs <- request 165 | record := <-collector 166 | close(jobs) 167 | close(context.stop) 168 | 169 | if record.Error == nil { 170 | 171 | fmt.Println(record) 172 | t.Fatal("expected timeout error") 173 | } 174 | } 175 | 176 | func BenchmarkNewHTTPRequestWithGet(b *testing.B) { 177 | b.ReportAllocs() 178 | for i := 0; i < b.N; i++ { 179 | NewHTTPRequest(getRequestConfig) 180 | } 181 | } 182 | 183 | func BenchmarkNewHTTPRequestWithPost(b *testing.B) { 184 | b.ReportAllocs() 185 | for i := 0; i < b.N; i++ { 186 | NewHTTPRequest(postRequestConfig) 187 | } 188 | } 189 | 190 | func BenchmarkCopyHTTPRequestWithGet(b *testing.B) { 191 | b.ReportAllocs() 192 | base, _ := NewHTTPRequest(getRequestConfig) 193 | for i := 0; i < b.N; i++ { 194 | CopyHTTPRequest(getRequestConfig, base) 195 | } 196 | } 197 | 198 | func BenchmarkCopyHTTPRequestWithPost(b *testing.B) { 199 | b.ReportAllocs() 200 | base, _ := NewHTTPRequest(postRequestConfig) 201 | for i := 0; i < b.N; i++ { 202 | CopyHTTPRequest(postRequestConfig, base) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "net/url" 9 | "os" 10 | "regexp" 11 | "runtime" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | type Config struct { 18 | requests int 19 | concurrency int 20 | timelimit int 21 | executionTimeout time.Duration 22 | 23 | method string 24 | bodyContent []byte 25 | contentType string 26 | headers []string 27 | cookies []string 28 | gzip bool 29 | keepAlive bool 30 | basicAuthentication string 31 | userAgent string 32 | 33 | url string 34 | host string 35 | port int 36 | } 37 | 38 | func LoadConfig() (config *Config, err error) { 39 | // setup command-line flags 40 | flag.IntVar(&Verbosity, "v", 0, "How much troubleshooting info to print") 41 | flag.IntVar(&GoMaxProcs, "G", runtime.NumCPU(), "Number of CPU") 42 | flag.BoolVar(&ContinueOnError, "r", false, "Don't exit when errors") 43 | 44 | request := flag.Int("n", 1, "Number of requests to perform") 45 | concurrency := flag.Int("c", 1, "Number of multiple requests to make") 46 | timelimit := flag.Int("t", 0, "Seconds to max. wait for responses") 47 | 48 | postFile := flag.String("p", "", "File containing data to POST. Remember also to set -T") 49 | putFile := flag.String("u", "", "File containing data to PUT. Remember also to set -T") 50 | headMethod := flag.Bool("i", false, "Use HEAD instead of GET") 51 | contentType := flag.String("T", "text/plain", "Content-type header for POSTing, eg. 'application/x-www-form-urlencoded' Default is 'text/plain'") 52 | 53 | var headers, cookies stringSet 54 | flag.Var(&headers, "H", "Add Arbitrary header line, eg. 'Accept-Encoding: gzip' Inserted after all normal header lines. (repeatable)") 55 | flag.Var(&cookies, "C", "Add cookie, eg. 'Apache=1234. (repeatable)") 56 | 57 | basicAuthentication := flag.String("A", "", "Add Basic WWW Authentication, the attributes are a colon separated username and password.") 58 | keepAlive := flag.Bool("k", false, "Use HTTP KeepAlive feature") 59 | gzip := flag.Bool("z", false, "Use HTTP Gzip feature") 60 | 61 | showHelp := flag.Bool("h", false, "Display usage information (this message)") 62 | 63 | flag.Usage = func() { 64 | fmt.Print("Usage: gb [options] http[s]://hostname[:port]/path\nOptions are:\n") 65 | flag.PrintDefaults() 66 | } 67 | 68 | flag.Parse() 69 | 70 | if *showHelp { 71 | flag.Usage() 72 | os.Exit(0) 73 | } 74 | 75 | if flag.NArg() != 1 { 76 | flag.Usage() 77 | os.Exit(-1) 78 | } 79 | 80 | urlStr := strings.Trim(strings.Join(flag.Args(), ""), " ") 81 | isURL, _ := regexp.MatchString(`http.*?://.*`, urlStr) 82 | 83 | if !isURL { 84 | flag.Usage() 85 | os.Exit(-1) 86 | } 87 | 88 | // build configuration 89 | config = &Config{} 90 | config.requests = *request 91 | config.concurrency = *concurrency 92 | 93 | switch { 94 | case *postFile != "": 95 | config.method = "POST" 96 | if err = loadFile(config, *postFile); err != nil { 97 | return 98 | } 99 | case *putFile != "": 100 | config.method = "PUT" 101 | if err = loadFile(config, *putFile); err != nil { 102 | return 103 | } 104 | case *headMethod: 105 | config.method = "HEAD" 106 | default: 107 | config.method = "GET" 108 | } 109 | 110 | if *timelimit > 0 { 111 | config.timelimit = *timelimit 112 | if config.requests == 1 { 113 | config.requests = MaxRequests 114 | } 115 | } 116 | config.executionTimeout = MaxExecutionTimeout 117 | 118 | config.contentType = *contentType 119 | config.keepAlive = *keepAlive 120 | config.gzip = *gzip 121 | config.basicAuthentication = *basicAuthentication 122 | config.headers = []string(headers) 123 | config.cookies = []string(cookies) 124 | config.userAgent = "GoHttpBench/" + GBVersion 125 | 126 | URL, err := url.Parse(urlStr) 127 | if err != nil { 128 | return 129 | } 130 | config.host, config.port = extractHostAndPort(URL) 131 | config.url = urlStr 132 | 133 | if Verbosity > 1 { 134 | fmt.Printf("dump config: %#+v\n", config) 135 | } 136 | 137 | // validate configuration 138 | if config.requests < 1 || config.concurrency < 1 || config.timelimit < 0 || GoMaxProcs < 1 || Verbosity < 0 { 139 | err = errors.New("wrong number of arguments") 140 | return 141 | } 142 | 143 | if config.concurrency > config.requests { 144 | err = errors.New("Cannot use concurrency level greater than total number of requests") 145 | return 146 | } 147 | 148 | return 149 | 150 | } 151 | 152 | func loadFile(config *Config, filename string) error { 153 | bytes, err := ioutil.ReadFile(filename) 154 | if err != nil { 155 | return err 156 | } 157 | config.bodyContent = bytes 158 | return nil 159 | } 160 | 161 | type stringSet []string 162 | 163 | func (f *stringSet) String() string { 164 | return fmt.Sprint([]string(*f)) 165 | } 166 | 167 | func (f *stringSet) Set(value string) error { 168 | *f = append(*f, value) 169 | return nil 170 | } 171 | 172 | func extractHostAndPort(url *url.URL) (host string, port int) { 173 | 174 | hostname := url.Host 175 | pos := strings.LastIndex(hostname, ":") 176 | if pos > 0 { 177 | portInt64, _ := strconv.Atoi(hostname[pos+1:]) 178 | host = hostname[0:pos] 179 | port = int(portInt64) 180 | } else { 181 | host = hostname 182 | if url.Scheme == "http" { 183 | port = 80 184 | } else if url.Scheme == "https" { 185 | port = 443 186 | } else { 187 | panic("unsupported protocol schema:" + url.Scheme) 188 | } 189 | } 190 | 191 | return 192 | } 193 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | FieldServerName = "ServerName" 18 | FieldContentSize = "ContentSize" 19 | MaxBufferSize = 8192 20 | ) 21 | 22 | var ( 23 | ErrInvalidContnetSize = errors.New("invalid content size") 24 | ) 25 | 26 | type HTTPWorker struct { 27 | c *Context 28 | client *http.Client 29 | jobs chan *http.Request 30 | collector chan *Record 31 | discard io.ReaderFrom 32 | } 33 | 34 | func NewHTTPWorker(context *Context, jobs chan *http.Request, collector chan *Record) *HTTPWorker { 35 | 36 | var buf []byte 37 | contentSize := context.GetInt(FieldContentSize) 38 | if contentSize < MaxBufferSize { 39 | buf = make([]byte, contentSize) 40 | } else { 41 | buf = make([]byte, MaxBufferSize) 42 | } 43 | 44 | return &HTTPWorker{ 45 | context, 46 | NewClient(context.config), 47 | jobs, 48 | collector, 49 | &Discard{buf}, 50 | } 51 | } 52 | 53 | func (h *HTTPWorker) Run() { 54 | h.c.start.Done() 55 | h.c.start.Wait() 56 | 57 | timer := time.NewTimer(h.c.config.executionTimeout) 58 | 59 | for job := range h.jobs { 60 | 61 | timer.Reset(h.c.config.executionTimeout) 62 | asyncResult := h.send(job) 63 | 64 | select { 65 | case record := <-asyncResult: 66 | h.collector <- record 67 | 68 | case <-timer.C: 69 | h.collector <- &Record{Error: &ResponseTimeoutError{errors.New("execution timeout")}} 70 | h.client.Transport.(*http.Transport).CancelRequest(job) 71 | 72 | case <-h.c.stop: 73 | h.client.Transport.(*http.Transport).CancelRequest(job) 74 | timer.Stop() 75 | return 76 | } 77 | } 78 | timer.Stop() 79 | } 80 | 81 | func (h *HTTPWorker) send(request *http.Request) (asyncResult chan *Record) { 82 | 83 | asyncResult = make(chan *Record, 1) 84 | go func() { 85 | record := &Record{} 86 | sw := &StopWatch{} 87 | sw.Start() 88 | 89 | var contentSize int64 90 | 91 | defer func() { 92 | if r := recover(); r != nil { 93 | if Err, ok := r.(error); ok { 94 | record.Error = Err 95 | } else { 96 | record.Error = &ExceptionError{errors.New(fmt.Sprint(r))} 97 | } 98 | 99 | } else { 100 | record.contentSize = contentSize 101 | record.responseTime = sw.Elapsed 102 | } 103 | 104 | if record.Error != nil { 105 | TraceException(record.Error) 106 | } 107 | 108 | asyncResult <- record 109 | }() 110 | 111 | resp, err := h.client.Do(request) 112 | if err != nil { 113 | record.Error = &ConnectError{err} 114 | return 115 | } 116 | 117 | defer resp.Body.Close() 118 | 119 | if resp.StatusCode < 200 || resp.StatusCode > 300 { 120 | record.Error = &ResponseError{err} 121 | return 122 | } 123 | 124 | contentSize, err = h.discard.ReadFrom(resp.Body) 125 | if err != nil { 126 | if err == io.ErrUnexpectedEOF { 127 | record.Error = &LengthError{ErrInvalidContnetSize} 128 | return 129 | } 130 | 131 | record.Error = &ReceiveError{err} 132 | return 133 | } 134 | 135 | sw.Stop() 136 | }() 137 | return asyncResult 138 | } 139 | 140 | type Discard struct { 141 | blackHole []byte 142 | } 143 | 144 | func (d *Discard) ReadFrom(r io.Reader) (n int64, err error) { 145 | readSize := 0 146 | for { 147 | readSize, err = r.Read(d.blackHole) 148 | n += int64(readSize) 149 | if err != nil { 150 | if err == io.EOF { 151 | return n, nil 152 | } 153 | return 154 | } 155 | } 156 | } 157 | 158 | func DetectHost(context *Context) (err error) { 159 | defer func() { 160 | if r := recover(); r != nil { 161 | TraceException(r) 162 | } 163 | }() 164 | 165 | client := NewClient(context.config) 166 | reqeust, err := NewHTTPRequest(context.config) 167 | if err != nil { 168 | return 169 | } 170 | 171 | resp, err := client.Do(reqeust) 172 | 173 | if err != nil { 174 | return 175 | } 176 | 177 | defer resp.Body.Close() 178 | body, _ := ioutil.ReadAll(resp.Body) 179 | 180 | context.SetString(FieldServerName, resp.Header.Get("Server")) 181 | headerContentSize := resp.Header.Get("Content-Length") 182 | 183 | if headerContentSize != "" { 184 | contentSize, _ := strconv.Atoi(headerContentSize) 185 | context.SetInt(FieldContentSize, contentSize) 186 | } else { 187 | context.SetInt(FieldContentSize, len(body)) 188 | } 189 | 190 | return 191 | } 192 | 193 | func NewClient(config *Config) *http.Client { 194 | 195 | // skip certification check for self-signed certificates 196 | tlsconfig := &tls.Config{ 197 | InsecureSkipVerify: true, 198 | } 199 | 200 | // TODO: tcp options 201 | // TODO: monitor tcp metrics 202 | transport := &http.Transport{ 203 | DisableCompression: !config.gzip, 204 | DisableKeepAlives: !config.keepAlive, 205 | TLSClientConfig: tlsconfig, 206 | } 207 | 208 | return &http.Client{Transport: transport} 209 | } 210 | 211 | func NewHTTPRequest(config *Config) (request *http.Request, err error) { 212 | 213 | var body io.Reader 214 | 215 | if config.method == "POST" || config.method == "PUT" { 216 | body = bytes.NewReader(config.bodyContent) 217 | } 218 | 219 | request, err = http.NewRequest(config.method, config.url, body) 220 | 221 | if err != nil { 222 | return 223 | } 224 | 225 | request.Header.Set("Content-Type", config.contentType) 226 | request.Header.Set("User-Agent", config.userAgent) 227 | 228 | if config.keepAlive { 229 | request.Header.Set("Connection", "keep-alive") 230 | } 231 | 232 | for _, header := range config.headers { 233 | pair := strings.Split(header, ":") 234 | request.Header.Add(pair[0], pair[1]) 235 | } 236 | 237 | for _, cookie := range config.cookies { 238 | pair := strings.Split(cookie, "=") 239 | c := &http.Cookie{Name: pair[0], Value: pair[1]} 240 | request.AddCookie(c) 241 | } 242 | 243 | if config.basicAuthentication != "" { 244 | pair := strings.Split(config.basicAuthentication, ":") 245 | request.SetBasicAuth(pair[0], pair[1]) 246 | } 247 | 248 | return 249 | } 250 | 251 | func CopyHTTPRequest(config *Config, request *http.Request) *http.Request { 252 | newRequest := *request 253 | if request.Body != nil { 254 | newRequest.Body = ioutil.NopCloser(bytes.NewReader(config.bodyContent)) 255 | } 256 | return &newRequest 257 | } 258 | 259 | type LengthError struct { 260 | err error 261 | } 262 | 263 | func (e *LengthError) Error() string { 264 | return e.err.Error() 265 | } 266 | 267 | type ConnectError struct { 268 | err error 269 | } 270 | 271 | func (e *ConnectError) Error() string { 272 | return e.err.Error() 273 | } 274 | 275 | type ReceiveError struct { 276 | err error 277 | } 278 | 279 | func (e *ReceiveError) Error() string { 280 | return e.err.Error() 281 | } 282 | 283 | type ExceptionError struct { 284 | err error 285 | } 286 | 287 | func (e *ExceptionError) Error() string { 288 | return e.err.Error() 289 | } 290 | 291 | type ResponseError struct { 292 | err error 293 | } 294 | 295 | func (e *ResponseError) Error() string { 296 | return e.err.Error() 297 | } 298 | 299 | type ResponseTimeoutError struct { 300 | err error 301 | } 302 | 303 | func (e *ResponseTimeoutError) Error() string { 304 | return e.err.Error() 305 | } 306 | --------------------------------------------------------------------------------