├── .github └── workflows │ ├── go.yml │ └── release.yml ├── LICENSE ├── README.md ├── benchmark.go ├── benchmark_test.go ├── conn.go ├── connection.go ├── go.mod ├── go.sum ├── main.go ├── proxy.go └── quic.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version-file: 'go.mod' 23 | id: go 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: go build -v . 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: release 2 | name: Build 3 | jobs: 4 | release-linux-amd64: 5 | name: release linux/amd64 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | - name: Set up Go 11 | uses: actions/setup-go@v4 12 | with: 13 | go-version-file: 'go.mod' 14 | id: go 15 | - name: Build 16 | run: | 17 | GOOS=linux GOARCH=amd64 go build 18 | tar -zcvf benchmark_linux_amd64.tar.gz benchmark 19 | - name: Release 20 | uses: softprops/action-gh-release@v1 21 | with: 22 | files: benchmark_linux_amd64.tar.gz 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | release-windows-amd64: 26 | name: release windows/amd64 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v2 31 | - name: Set up Go 32 | uses: actions/setup-go@v4 33 | with: 34 | go-version-file: 'go.mod' 35 | id: go 36 | - name: Build 37 | run: | 38 | GOOS=windows GOARCH=amd64 go build 39 | tar -zcvf benchmark_windows_amd64.tar.gz benchmark.exe 40 | - name: Release 41 | uses: softprops/action-gh-release@v1 42 | with: 43 | files: benchmark_windows_amd64.tar.gz 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | release-darwin-amd64: 47 | name: release darwin/amd64 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v2 52 | - name: Set up Go 53 | uses: actions/setup-go@v4 54 | with: 55 | go-version-file: 'go.mod' 56 | id: go 57 | - name: Build 58 | run: | 59 | GOOS=darwin GOARCH=amd64 go build 60 | tar -zcvf benchmark_darwin_amd64.tar.gz benchmark 61 | - name: Release 62 | uses: softprops/action-gh-release@v1 63 | with: 64 | files: benchmark_darwin_amd64.tar.gz 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 benchmark 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | A simple benchmark testing tool implemented in golang, the basic functions refer to wrk and ab, added some small features based on personal needs. 3 | 4 | ![Build](https://github.com/cnlh/benchmark/workflows/Build/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/cnlh/benchmark)](https://goreportcard.com/report/github.com/cnlh/benchmark) 5 | ## Why use Benchmark? 6 | - http and socks5 proxy support 7 | - good performance as wrk(some implements in golang not work well) 8 | - simple code, easy to change 9 | ## Building 10 | 11 | ```shell script 12 | go get github.com/cnlh/benchmark 13 | ``` 14 | ## Usage 15 | 16 | basic usage is quite simple: 17 | ```shell script 18 | benchmark [flags] url 19 | ``` 20 | 21 | with the flags being 22 | ```shell script 23 | -b string 24 | the body of request 25 | -c int 26 | the number of connection (default 1000) 27 | -cpu int 28 | the number of cpu used 29 | -h string 30 | request header, split by \r\n 31 | -host string 32 | the host of request 33 | -m string 34 | request method (default "GET") 35 | -n int 36 | the number of request (default 100000) 37 | -t int 38 | request/socket timeout in ms (default 3000) 39 | -proxy string 40 | proxy of request 41 | -proxy-transport string 42 | proxy transport of request, "tcp" or "quic" (default "tcp") 43 | -quic-protocol string 44 | tls application protocol of quic transport (default "h3") 45 | ``` 46 | for example 47 | ```shell script 48 | benchmark -c 1100 -n 1000000 http://127.0.0.1/ 49 | benchmark -c 1100 -n 1000000 -proxy http://111:222@127.0.0.1:1235 http://127.0.0.1/ 50 | benchmark -c 1100 -n 1000000 -proxy socks5://111:222@127.0.0.1:1235 http://127.0.0.1/ 51 | benchmark -c 1100 -n 1000000 -h "Connection: close\r\nCache-Control: no-cache" http://127.0.0.1/ 52 | ``` 53 | 54 | ## Example Output 55 | ```shell script 56 | Running 1000000 test @ 127.0.0.1:80 by 1100 connections 57 | Requset as following format: 58 | 59 | GET / HTTP/1.1 60 | Host: 127.0.0.1:80 61 | 62 | 1000000 requests in 5.73s, 4.01GB read, 33.42MB write 63 | Requests/sec: 174420.54 64 | Transfer/sec: 721.21MB 65 | Error : 0 66 | Percentage of the requests served within a certain time (ms) 67 | 50% 5 68 | 65% 6 69 | 75% 7 70 | 80% 7 71 | 90% 9 72 | 95% 13 73 | 98% 19 74 | 99% 23 75 | 100% 107 76 | ``` 77 | 78 | ## Known Issues 79 | - Consumes more cpu when testing short connections -------------------------------------------------------------------------------- /benchmark.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The benchmark. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | "sort" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | // benchmark is used to manager connection and deal with the result 17 | type benchmark struct { 18 | connectionNum int 19 | reqNum int64 20 | requestBytes []byte 21 | target string 22 | schema string 23 | proxy string 24 | proxyTransport string 25 | quicProtocol string 26 | timeout int 27 | startTime time.Time 28 | endTime time.Time 29 | wg sync.WaitGroup 30 | finishNum int64 31 | reqConnList []*ReqConn 32 | } 33 | 34 | // Start benchmark with the param has setting 35 | func (pf *benchmark) Run() { 36 | fmt.Printf("Running %d test @ %s by %d connections\n", pf.reqNum, pf.target, pf.connectionNum) 37 | if pf.proxyTransport == "quic" { 38 | fmt.Printf("Using transport \"%s\" with application protocol \"%s\"\n", pf.proxyTransport, pf.quicProtocol) 39 | } 40 | fmt.Printf("Request as following format:\n\n%s\n", string(pf.requestBytes)) 41 | dialer, err := NewProxyConn(pf.proxy, clientProtocol{ 42 | transport: pf.proxyTransport, 43 | quicProtocol: pf.quicProtocol, 44 | }) 45 | if err != nil { 46 | fmt.Println(err) 47 | os.Exit(0) 48 | } 49 | 50 | pf.startTime = time.Now() 51 | pf.wg.Add(pf.connectionNum) 52 | successCount := int32(0) 53 | for i := 0; i < pf.connectionNum; i++ { 54 | rc := &ReqConn{ 55 | Count: pf.reqNum, 56 | NowNum: &pf.finishNum, 57 | timeout: pf.timeout, 58 | reqTimes: make([]int, 0), 59 | remoteAddr: pf.target, 60 | schema: pf.schema, 61 | dialer: dialer, 62 | readWriter: NewHttpReadWriter(pf.requestBytes), 63 | } 64 | go func(idx int, rc *ReqConn) { 65 | if err := rc.Start(); err != nil { 66 | fmt.Println("Failed to start connection", idx, ":", err.Error()) 67 | if !*ignoreErr { 68 | fmt.Printf("Try increasing the timeout using flag `-t`, or use `-ignore-err` to bypass.\n\n") 69 | os.Exit(0) 70 | } 71 | } else { 72 | atomic.AddInt32(&successCount, 1) 73 | } 74 | pf.wg.Done() 75 | }(i, rc) 76 | pf.reqConnList = append(pf.reqConnList, rc) 77 | } 78 | pf.wg.Wait() 79 | pf.endTime = time.Now() 80 | 81 | if successCount < int32(pf.connectionNum) { 82 | fmt.Printf("\nOnly %d successful connections, with %d failure. Try increasing the timeout using flag `-t`.\n\n", successCount, int32(pf.connectionNum)-successCount) 83 | } 84 | return 85 | } 86 | 87 | // Print the result of benchmark on console 88 | func (pf *benchmark) Print() { 89 | readAll := 0 90 | writeAll := 0 91 | allTimes := make([]int, 0) 92 | allError := 0 93 | for _, v := range pf.reqConnList { 94 | readAll += v.readLen 95 | writeAll += v.writeLen 96 | allTimes = append(allTimes, v.reqTimes...) 97 | allError += v.ErrorTimes 98 | } 99 | runSecond := pf.endTime.Sub(pf.startTime).Seconds() 100 | fmt.Printf("%d requests in %.2fs, %s read, %s write\n", pf.reqNum, runSecond, formatFlow(float64(readAll)), formatFlow(float64(writeAll))) 101 | fmt.Printf("Requests/sec: %.2f\n", float64(pf.reqNum)/runSecond) 102 | fmt.Printf("Transfer/sec: %s\n", formatFlow(float64(readAll+writeAll)/runSecond)) 103 | fmt.Printf("Error(s) : %d\n", allError) 104 | sort.Ints(allTimes) 105 | rates := []int{50, 65, 75, 80, 90, 95, 98, 99, 100} 106 | fmt.Println("Percentage of the requests served within a certain time (ms)") 107 | for _, v := range rates { 108 | // ceil 109 | fmt.Printf(" %3d%%\t\t\t\t%d\n", v, allTimes[(len(allTimes)*v+100-1)/100-1]) 110 | } 111 | } 112 | 113 | // Format the flow data 114 | func formatFlow(size float64) string { 115 | var rt float64 116 | var suffix string 117 | const ( 118 | Byte = 1 119 | KByte = Byte * 1024 120 | MByte = KByte * 1024 121 | GByte = MByte * 1024 122 | ) 123 | if size > GByte { 124 | rt = size / GByte 125 | suffix = "GB" 126 | } else if size > MByte { 127 | rt = size / MByte 128 | suffix = "MB" 129 | } else if size > KByte { 130 | rt = size / KByte 131 | suffix = "KB" 132 | } else { 133 | rt = size 134 | suffix = "bytes" 135 | } 136 | return fmt.Sprintf("%.2f%v", rt, suffix) 137 | } 138 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httputil" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestMain(m *testing.M) { 12 | // create a http server 13 | http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 14 | io.WriteString(writer, "work well!") 15 | }) 16 | go http.ListenAndServe(":15342", nil) 17 | time.Sleep(time.Second) 18 | m.Run() 19 | } 20 | 21 | func TestBenchmark_Run(t *testing.T) { 22 | // create a request 23 | r, err := http.NewRequest("GET", "http://127.0.0.0.1:15342", nil) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | writeBytes, err := httputil.DumpRequest(r, true) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | p := &benchmark{ 32 | connectionNum: 100, 33 | reqNum: 20000, 34 | requestBytes: writeBytes, 35 | target: "127.0.0.1:15342", 36 | schema: r.URL.Scheme, 37 | timeout: 30000, 38 | reqConnList: make([]*ReqConn, 0), 39 | } 40 | p.Run() 41 | p.Print() 42 | } 43 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The benchmark. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "io" 11 | "strconv" 12 | ) 13 | 14 | var ( 15 | bodyHeaderSepBytes = []byte{13, 10, 13, 10} 16 | bodyHeaderSepBytesLen = 4 17 | headerSepBytes = []byte{13, 10} 18 | contentLengthBytes = []byte{67, 111, 110, 116, 101, 110, 116, 45, 76, 101, 110, 103, 116, 104, 58, 32} 19 | contentLengthBytesLen = 16 20 | ) 21 | 22 | // ConnReadWriter is defines the read and request behaviour of a connection 23 | type ConnReadWriter interface { 24 | Read(conn io.Reader) (int, error) 25 | Write(conn io.Writer) (int, error) 26 | } 27 | 28 | // HttpReadWriter is a simple and efficient implementation of ConnReadWriter 29 | type HttpReadWriter struct { 30 | buf []byte 31 | writeBytes []byte 32 | } 33 | 34 | // Create a new HttpReadWriter 35 | func NewHttpReadWriter(writeBytes []byte) ConnReadWriter { 36 | return &HttpReadWriter{ 37 | buf: make([]byte, 65535), 38 | writeBytes: writeBytes, 39 | } 40 | } 41 | 42 | // Implement the Read func of ConnReadWriter 43 | func (h *HttpReadWriter) Read(r io.Reader) (readLen int, err error) { 44 | var contentLen string 45 | var bodyHasRead int 46 | var headerHasRead int 47 | var n int 48 | readHeader: 49 | n, err = r.Read(h.buf[headerHasRead:]) 50 | if err != nil { 51 | return 52 | } 53 | readLen += n 54 | headerHasRead += n 55 | var bbArr [2][]byte 56 | bodyPos := bytes.Index(h.buf[:headerHasRead], bodyHeaderSepBytes) 57 | if bodyPos > -1 { 58 | bbArr[0] = h.buf[:bodyPos] 59 | bbArr[1] = h.buf[bodyPos+bodyHeaderSepBytesLen : headerHasRead] 60 | } else { 61 | goto readHeader 62 | } 63 | contentStartPos := bytes.Index(bbArr[0], contentLengthBytes) 64 | if contentStartPos == -1 { 65 | err = errors.New("can not found content length") 66 | return 67 | } 68 | start := contentStartPos + contentLengthBytesLen 69 | end := bytes.Index(bbArr[0][start:], headerSepBytes) 70 | if end == -1 { 71 | contentLen = Bytes2str(bbArr[0][start:]) 72 | } else { 73 | contentLen = Bytes2str(bbArr[0][start : start+end]) 74 | } 75 | contentLenI, _ := strconv.Atoi(contentLen) 76 | bodyHasRead += len(bbArr[1]) 77 | for { 78 | if bodyHasRead >= contentLenI { 79 | break 80 | } 81 | n, err = r.Read(h.buf) 82 | if err != nil { 83 | return 84 | } 85 | readLen += n 86 | bodyHasRead += n 87 | } 88 | return 89 | } 90 | 91 | // Implement the Write func of ConnReadWriter 92 | func (h *HttpReadWriter) Write(r io.Writer) (readLen int, err error) { 93 | return r.Write(h.writeBytes) 94 | } 95 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The benchmark. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "crypto/tls" 9 | "fmt" 10 | "io" 11 | "net" 12 | "strings" 13 | "sync/atomic" 14 | "time" 15 | "unsafe" 16 | ) 17 | 18 | // ReqConn is used to create a connection and record data 19 | type ReqConn struct { 20 | ErrorTimes int 21 | Count int64 22 | NowNum *int64 23 | FailedNum atomic.Int64 24 | timeout int 25 | writeLen int 26 | readLen int 27 | reqTimes []int 28 | conn net.Conn 29 | readWriter ConnReadWriter 30 | remoteAddr string 31 | schema string 32 | dialer ProxyConn 33 | } 34 | 35 | // Connect to the server, http and socks5 proxy support 36 | // If the target is https, convert connection to tls client 37 | func (rc *ReqConn) dial() error { 38 | if rc.conn != nil { 39 | rc.conn.Close() 40 | } 41 | conn, err := rc.dialer.Dial("tcp", rc.remoteAddr, time.Millisecond*time.Duration(rc.timeout)) 42 | if err != nil { 43 | return err 44 | } 45 | rc.conn = conn 46 | if rc.schema == "https" { 47 | var h string 48 | h, _, err = net.SplitHostPort(rc.remoteAddr) 49 | if err != nil { 50 | return err 51 | } 52 | conf := &tls.Config{ 53 | InsecureSkipVerify: true, 54 | ServerName: h, 55 | } 56 | rc.conn = tls.Client(rc.conn, conf) 57 | } 58 | return nil 59 | } 60 | 61 | // Start a connection, send request to server and read response from server 62 | func (rc *ReqConn) Start() (err error) { 63 | var n int 64 | var reqTime time.Time 65 | re: 66 | if err != nil && err != io.EOF && !strings.Contains(err.Error(), "connection reset by peer") { 67 | rc.ErrorTimes += 1 68 | } 69 | if rc.FailedNum.Load() >= rc.Count { 70 | fmt.Println("Test aborted due to too many errors, last error:", err) 71 | return 72 | } 73 | if err = rc.dial(); err != nil { 74 | return 75 | } 76 | for { 77 | reqTime = time.Now() 78 | rc.conn.SetDeadline(time.Now().Add(time.Millisecond * time.Duration(rc.timeout))) 79 | n, err = rc.readWriter.Write(rc.conn) 80 | if err != nil { 81 | rc.FailedNum.Add(1) 82 | goto re 83 | } 84 | rc.writeLen += n 85 | n, err = rc.readWriter.Read(rc.conn) 86 | if err != nil { 87 | rc.FailedNum.Add(1) 88 | goto re 89 | } 90 | rc.readLen += n 91 | rc.reqTimes = append(rc.reqTimes, int(time.Now().Sub(reqTime).Milliseconds())) 92 | if atomic.AddInt64(rc.NowNum, 1) >= rc.Count { 93 | return 94 | } 95 | } 96 | } 97 | 98 | // Convert bytes to strings 99 | func Bytes2str(b []byte) string { 100 | return *(*string)(unsafe.Pointer(&b)) 101 | } 102 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cnlh/benchmark 2 | 3 | go 1.21 4 | 5 | require golang.org/x/net v0.15.0 6 | 7 | require ( 8 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 9 | github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect 10 | github.com/onsi/ginkgo/v2 v2.12.1 // indirect 11 | github.com/quic-go/qtls-go1-20 v0.3.4 // indirect 12 | github.com/quic-go/quic-go v0.39.0 // indirect 13 | go.uber.org/mock v0.3.0 // indirect 14 | golang.org/x/crypto v0.13.0 // indirect 15 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 16 | golang.org/x/mod v0.12.0 // indirect 17 | golang.org/x/sys v0.12.0 // indirect 18 | golang.org/x/tools v0.13.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 4 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 5 | github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= 6 | github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= 7 | github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= 8 | github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= 11 | github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= 12 | github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So= 13 | github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= 17 | go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 18 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 19 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 20 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 21 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 22 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 23 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 24 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 25 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 26 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 27 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 29 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The benchmark. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "net/http" 11 | "net/http/httputil" 12 | "net/url" 13 | "runtime" 14 | "strings" 15 | ) 16 | 17 | var ( 18 | header = flag.String("h", "", "request header, split by \\r\\n") 19 | method = flag.String("m", "GET", "request method") 20 | timeout = flag.Int("t", 10000, "request/socket timeout in ms") 21 | connectionNum = flag.Int("c", 1000, "number of connection") 22 | requestNum = flag.Int("n", 100000, "number of request") 23 | body = flag.String("b", "", "body of request") 24 | cpu = flag.Int("cpu", 0, "number of cpu used") 25 | host = flag.String("host", "", "host of request") 26 | proxyUrl = flag.String("proxy", "", "proxy of request") 27 | proxyTransport = flag.String("proxy-transport", "tcp", "proxy transport of request, \"tcp\" or \"quic\"") 28 | quicProtocol = flag.String("quic-protocol", "h3", "tls application protocol of quic transport") 29 | ignoreErr = flag.Bool("ignore-err", false, "`true` to ignore error when creating connection (default false)") 30 | ) 31 | 32 | func main() { 33 | flag.Parse() 34 | if u, err := url.Parse(flag.Arg(0)); err != nil || u.Host == "" { 35 | fmt.Printf("the request url %s is not correct \n", flag.Arg(0)) 36 | return 37 | } 38 | payload := strings.NewReader(*body) 39 | req, err := http.NewRequest(*method, flag.Arg(0), payload) 40 | if err != nil { 41 | fmt.Println(err) 42 | return 43 | } 44 | var target = req.Host 45 | if *host != "" { 46 | req.Host = *host 47 | } 48 | if *cpu > 0 { 49 | runtime.GOMAXPROCS(*cpu) 50 | } 51 | if *header != "" { 52 | for _, v := range strings.Split(*header, "\\r\\n") { 53 | a := strings.Split(v, ":") 54 | if len(a) == 2 { 55 | req.Header.Set(strings.Trim(a[0], " "), strings.Trim(a[1], " ")) 56 | } 57 | } 58 | } 59 | writeBytes, err := httputil.DumpRequest(req, true) 60 | if err != nil { 61 | fmt.Println(err) 62 | return 63 | } 64 | if !strings.Contains(target, ":") { 65 | if req.URL.Scheme == "http" { 66 | target = target + ":80" 67 | } else { 68 | target = target + ":443" 69 | } 70 | } 71 | 72 | p := &benchmark{ 73 | connectionNum: *connectionNum, 74 | reqNum: int64(*requestNum), 75 | requestBytes: writeBytes, 76 | target: target, 77 | schema: req.URL.Scheme, 78 | timeout: *timeout, 79 | reqConnList: make([]*ReqConn, 0), 80 | proxy: *proxyUrl, 81 | proxyTransport: *proxyTransport, 82 | quicProtocol: *quicProtocol, 83 | } 84 | p.Run() 85 | p.Print() 86 | } 87 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The benchmark. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "encoding/base64" 10 | "errors" 11 | "golang.org/x/net/proxy" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "time" 16 | ) 17 | 18 | // Return based on proxy url 19 | func NewProxyConn(proxyUrl string, protocol clientProtocol) (ProxyConn, error) { 20 | u, err := url.Parse(proxyUrl) 21 | if err != nil { 22 | return nil, err 23 | } 24 | switch u.Scheme { 25 | case "socks5": 26 | return NewSocks5Client(u, protocol), nil 27 | case "http": 28 | return NewHttpClient(u, protocol), nil 29 | default: 30 | return &DefaultClient{}, nil 31 | } 32 | } 33 | 34 | // ProxyConn is used to define the proxy 35 | type ProxyConn interface { 36 | Dial(network string, address string, timeout time.Duration) (net.Conn, error) 37 | } 38 | 39 | // DefaultClient is used to implement a proxy in default 40 | type DefaultClient struct { 41 | rAddr *net.TCPAddr 42 | } 43 | 44 | // Socks5 implementation of ProxyConn 45 | // Set KeepAlive=-1 to reduce the call of syscall 46 | func (dc *DefaultClient) Dial(network string, address string, timeout time.Duration) (conn net.Conn, err error) { 47 | if dc.rAddr == nil { 48 | dc.rAddr, err = net.ResolveTCPAddr("tcp", address) 49 | if err != nil { 50 | return nil, err 51 | } 52 | } 53 | return net.DialTCP(network, nil, dc.rAddr) 54 | } 55 | 56 | type clientProtocol struct { 57 | transport string 58 | quicProtocol string 59 | } 60 | 61 | // Socks5Client is used to implement a proxy in socks5 62 | type Socks5Client struct { 63 | proxyUrl *url.URL 64 | clientProtocol 65 | forward proxy.Dialer 66 | } 67 | 68 | func NewSocks5Client(proxyUrl *url.URL, protocol clientProtocol) *Socks5Client { 69 | c := &Socks5Client{proxyUrl, protocol, nil} 70 | if c.transport == "quic" { 71 | c.forward = NewQuicDialer([]string{c.quicProtocol}) 72 | } 73 | return c 74 | } 75 | 76 | // Socks5 implementation of ProxyConn 77 | func (s5 *Socks5Client) Dial(network string, address string, timeout time.Duration) (net.Conn, error) { 78 | d, err := proxy.FromURL(s5.proxyUrl, s5.forward) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return d.Dial(network, address) 83 | } 84 | 85 | // Socks5Client is used to implement a proxy in http 86 | type HttpClient struct { 87 | proxyUrl *url.URL 88 | clientProtocol 89 | qd *QuicDialer 90 | } 91 | 92 | func NewHttpClient(proxyUrl *url.URL, protocol clientProtocol) *HttpClient { 93 | c := &HttpClient{proxyUrl, protocol, nil} 94 | if c.transport == "quic" { 95 | c.qd = NewQuicDialer([]string{c.quicProtocol}) 96 | } 97 | return c 98 | } 99 | 100 | func SetHTTPProxyBasicAuth(req *http.Request, username, password string) { 101 | auth := username + ":" + password 102 | authEncoded := base64.StdEncoding.EncodeToString([]byte(auth)) 103 | req.Header.Set("Proxy-Authorization", "Basic "+authEncoded) 104 | } 105 | 106 | // Http implementation of ProxyConn 107 | func (hc *HttpClient) Dial(network string, address string, timeout time.Duration) (net.Conn, error) { 108 | req, err := http.NewRequest("CONNECT", "http://"+address, nil) 109 | if err != nil { 110 | return nil, err 111 | } 112 | password, _ := hc.proxyUrl.User.Password() 113 | SetHTTPProxyBasicAuth(req, hc.proxyUrl.User.Username(), password) 114 | var proxyConn net.Conn 115 | if hc.transport == "quic" { 116 | proxyConn, err = hc.qd.Dial(network, hc.proxyUrl.Host) 117 | } else { 118 | proxyConn, err = net.DialTimeout("tcp", hc.proxyUrl.Host, timeout) 119 | } 120 | if err != nil { 121 | return nil, err 122 | } 123 | if err = req.Write(proxyConn); err != nil { 124 | return nil, err 125 | } 126 | res, err := http.ReadResponse(bufio.NewReader(proxyConn), req) 127 | if err != nil { 128 | return nil, err 129 | } 130 | _ = res.Body.Close() 131 | if res.StatusCode != 200 { 132 | return nil, errors.New("Proxy error " + res.Status) 133 | } 134 | return proxyConn, nil 135 | } 136 | -------------------------------------------------------------------------------- /quic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "github.com/quic-go/quic-go" 6 | "golang.org/x/net/context" 7 | "net" 8 | "runtime" 9 | "sync/atomic" 10 | ) 11 | 12 | type QuicDialer struct { 13 | NextProtos []string 14 | streams atomic.Uint32 15 | c quic.Connection 16 | } 17 | 18 | func NewQuicDialer(nextProtos []string) *QuicDialer { 19 | return &QuicDialer{ 20 | NextProtos: nextProtos, 21 | } 22 | } 23 | 24 | const maxStreams = 32 25 | 26 | func (d *QuicDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 27 | now := d.streams.Add(1) 28 | if now > maxStreams { 29 | // wait for dialing 30 | for { 31 | if d.streams.Load() < maxStreams { 32 | break 33 | } 34 | runtime.Gosched() 35 | } 36 | return d.DialContext(ctx, network, address) 37 | } 38 | if now == maxStreams || now == 1 { 39 | c, err := quic.DialAddr(ctx, address, &tls.Config{ 40 | InsecureSkipVerify: true, 41 | NextProtos: d.NextProtos, 42 | }, nil) 43 | if err != nil { 44 | d.streams.Store(0) 45 | return nil, err 46 | } 47 | d.c = c 48 | d.streams.Store(1) 49 | } 50 | if d.c == nil { 51 | // still in initial dialing 52 | return d.DialContext(ctx, network, address) 53 | } 54 | s, err := d.c.OpenStreamSync(ctx) 55 | if err != nil { 56 | // dial a new connection in next time 57 | d.streams.Store(0) 58 | return nil, err 59 | } 60 | return NewStream(s, d.c.LocalAddr(), d.c.RemoteAddr()), nil 61 | } 62 | 63 | func (d *QuicDialer) Dial(network, address string) (net.Conn, error) { 64 | return d.DialContext(context.Background(), network, address) 65 | } 66 | 67 | type Stream struct { 68 | quic.Stream 69 | lAddr net.Addr 70 | rAddr net.Addr 71 | } 72 | 73 | func NewStream(s quic.Stream, lAddr, rAddr net.Addr) net.Conn { 74 | return &Stream{ 75 | Stream: s, 76 | lAddr: lAddr, 77 | rAddr: rAddr, 78 | } 79 | } 80 | 81 | func (s *Stream) LocalAddr() net.Addr { 82 | return s.lAddr 83 | } 84 | 85 | func (s *Stream) RemoteAddr() net.Addr { 86 | return s.rAddr 87 | } 88 | 89 | func (s *Stream) Close() error { 90 | s.CancelRead(0) 91 | return s.Stream.Close() 92 | } 93 | --------------------------------------------------------------------------------