├── README.md ├── conf ├── kbang-bad-item.conf ├── kbang-bad-section.conf ├── kbang.conf ├── parse.go └── parse_test.go ├── kbang.go └── robot ├── print.go └── robot.go /README.md: -------------------------------------------------------------------------------- 1 | kbang 2 | =================================== 3 | http 压力测试工具,支持不同类型请求并发,支持请求权重 4 | 5 | ###工具安装 6 | 7 | ```$ go get github.com/kaimixu/kbang``` 8 | 9 | ### 使用方法 10 | #####方法1(单请求并发) 11 | 12 | ``` 13 | Usage: kbang [options...] 14 | 15 | options: 16 | -n Number of requests to run (default: 10) 17 | -c Number of requests to run concurrency (default: 1) 18 | -t Request connection timeout in second (default: 1s) 19 | -H Http header, eg. -H "Host: www.example.com" 20 | -k[=true|false] Http keep-alive (default: false) 21 | -d Http request body to POST 22 | -T Content-type header to POST, eg. 'application/x-www-form-urlencoded' 23 | (Default:text/plain) 24 | ``` 25 | 26 | #####方法2(多请求并发) 27 | ``` 28 | Usage: kbang [options...] -f kbang.conf 29 | ``` 30 | 多请求并发需编写配置文件 31 | 32 | ### 配置文件描述 33 | ``` 34 | # 多请求配置文件,[request]用于区分不同请求, 35 | # weight表示请求权重,如下两请求权重比例为1:2,假如总请求数为300(-n 参数指定), 36 | # 请求1执行100次,请求2执行200次。 37 | [request] 38 | weight = 1 39 | # only support GET、POST 40 | method = GET 41 | url = http://www.example.com/ 42 | 43 | [request] 44 | weight = 2 45 | method = POST 46 | content_type = text/plain 47 | url = http://www.example.com/ 48 | post_data = a=1&b=2 49 | ``` 50 | -------------------------------------------------------------------------------- /conf/kbang-bad-item.conf: -------------------------------------------------------------------------------- 1 | # bad item 2 | bad_item1 3 | 4 | # good item 5 | item = good_item -------------------------------------------------------------------------------- /conf/kbang-bad-section.conf: -------------------------------------------------------------------------------- 1 | # bad config item 2 | item = bad_section 3 | 4 | # section 5 | [server 6 | ip = 127.0.0.1 7 | port = 80 8 | 9 | # good section 10 | [server] 11 | ip = 127.0.0.1 12 | port = 80 -------------------------------------------------------------------------------- /conf/kbang.conf: -------------------------------------------------------------------------------- 1 | # 2 | # 多请求配置文件,[request]用于区分不同请求. 3 | # weight表示请求权重,如下两请求权重比例为1:2,假如总请求数为300(-n参数指定),请求1执行100次,请求2执行200次 4 | # 5 | [request] 6 | weight = 1 7 | # only support GET、POST 8 | method = GET 9 | url = http://www.example.com/ 10 | 11 | [request] 12 | weight = 2 13 | method = POST 14 | content_type = text/plain 15 | url = http://www.example.com/ 16 | post_data = a=1&b=2 17 | -------------------------------------------------------------------------------- /conf/parse.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "strings" 7 | "strconv" 8 | "reflect" 9 | ) 10 | 11 | type Conf struct { 12 | curPtr *map[string]string 13 | basic map[string]string 14 | section map[string][]map[string]string 15 | } 16 | 17 | func NewConf() *Conf { 18 | return &Conf{ 19 | basic: make(map[string]string), 20 | section: make(map[string][]map[string]string), 21 | } 22 | } 23 | 24 | type sectionConf struct { 25 | closed bool 26 | name string 27 | data map[string]string 28 | } 29 | 30 | func initSection() *sectionConf { 31 | return §ionConf{ 32 | closed:true, 33 | name:"", 34 | data:make(map[string]string), 35 | } 36 | } 37 | 38 | func (this *Conf) LoadFile(configFile string) error{ 39 | data, err := ioutil.ReadFile(configFile) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | section := initSection() 45 | dataSlice := strings.Split(string(data), "\n") 46 | for ln, line := range dataSlice { 47 | line = strings.TrimSpace(line) 48 | if line == "" || line[0] == '#' || len(line) <= 3 { 49 | continue 50 | } 51 | 52 | if strings.HasPrefix(line, "[") { 53 | if !strings.HasSuffix(line, "]") { 54 | return errors.New("line " + strconv.Itoa(ln) + ": invalid config syntax") 55 | } 56 | // end section 57 | if !section.closed { 58 | this.section[section.name] = append(this.section[section.name], section.data) 59 | } 60 | 61 | // start section 62 | section = initSection() 63 | section.closed = false 64 | section.name = strings.Trim(line, "[]") 65 | continue 66 | } 67 | 68 | lineSlice := strings.SplitN(line, "=", 2) 69 | if len(lineSlice) != 2 { 70 | return errors.New("line " + strconv.Itoa(ln) + ": invalid config syntax") 71 | } 72 | itemK := strings.TrimSpace(lineSlice[0]) 73 | itemV := strings.TrimSpace(lineSlice[1]) 74 | if !section.closed { 75 | section.data[itemK] = itemV 76 | }else { 77 | this.basic[itemK] = itemV 78 | } 79 | } 80 | 81 | // end the last section 82 | if !section.closed { 83 | this.section[section.name] = append(this.section[section.name], section.data) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (this *Conf) Parse(obj interface{}) error { 90 | objT := reflect.TypeOf(obj) 91 | eT := objT.Elem() 92 | if objT.Kind() != reflect.Ptr || eT.Kind() != reflect.Struct { 93 | return errors.New("obj must be poiner to struct") 94 | } 95 | objV := reflect.ValueOf(obj) 96 | eV := objV.Elem() 97 | 98 | this.curPtr = &this.basic 99 | this.parseField(eT, eV) 100 | return nil 101 | } 102 | 103 | func (this *Conf) parseField(eT reflect.Type, eV reflect.Value) { 104 | for i := 0; i < eT.NumField(); i++ { 105 | f := eT.Field(i) 106 | k := string(f.Tag) 107 | if k == "" { 108 | k = f.Name 109 | } 110 | 111 | fV := eV.Field(i) 112 | if !fV.CanSet() { 113 | continue 114 | } 115 | 116 | switch (f.Type.Kind()) { 117 | case reflect.Bool: 118 | if v, e := this.getItemBool(k); e { 119 | fV.SetBool(v) 120 | } 121 | case reflect.Int: 122 | if v, e := this.getItemInt(k); e { 123 | fV.SetInt(v) 124 | } 125 | case reflect.String: 126 | if v, e := this.getItemString(k); e { 127 | fV.SetString(v) 128 | } 129 | /*case reflect.Slice: 130 | if f.Type.String() == "[]string" { 131 | }*/ 132 | case reflect.Array: 133 | eT2 := eT 134 | eV2 := eV 135 | for idx := 0; idx < fV.Len() && idx < len(this.section[k]); idx++ { 136 | this.curPtr = &this.section[k][idx] 137 | eT = f.Type.Elem() 138 | eV = fV.Index(idx) 139 | 140 | this.parseField(eT, eV) 141 | } 142 | eT = eT2 143 | eV = eV2 144 | default: 145 | } 146 | } 147 | } 148 | 149 | func (this *Conf) getItemBool(name string) (bool, bool) { 150 | val := (*this.curPtr)[name] 151 | if val == "" { 152 | return false, false 153 | } 154 | 155 | v, _ := strconv.ParseBool(strings.ToLower(val)) 156 | return v, true 157 | } 158 | 159 | func (this *Conf) getItemInt(name string) (int64, bool) { 160 | val, exist := (*this.curPtr)[name] 161 | if !exist { 162 | return 0, false 163 | } 164 | 165 | v, err := strconv.Atoi(val) 166 | if err != nil { 167 | return 0, false 168 | } 169 | return int64(v), true 170 | } 171 | 172 | func (this *Conf) getItemString(name string) (string, bool) { 173 | val, exist := (*this.curPtr)[name] 174 | return val, exist 175 | } -------------------------------------------------------------------------------- /conf/parse_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBadSyntax(t *testing.T) { 8 | conf := NewConf() 9 | err := conf.LoadFile("kbang-bad-item.conf") 10 | if err == nil { 11 | t.Error("TestBadSyntax: no detection bad item") 12 | } 13 | 14 | err = conf.LoadFile("kbang-bad-section.conf") 15 | if err == nil { 16 | t.Error("TestBadSection: no detection bad section") 17 | } 18 | } 19 | 20 | func TestGoodConf(t *testing.T) { 21 | type Section struct { 22 | Ip string `ip` 23 | Weight int `weight` 24 | } 25 | type Config struct { 26 | Item string `item` 27 | Request [2]Section `request` 28 | } 29 | var cfg Config 30 | conf := NewConf() 31 | err := conf.LoadFile("kbang.conf") 32 | if err != nil { 33 | t.Error("TestGoodConf: load file failed") 34 | } 35 | 36 | err = conf.Parse(&cfg) 37 | if err != nil { 38 | t.Error("TestGoodConf: parse failed") 39 | } 40 | 41 | if cfg.Item != "" { 42 | t.Error("TestGoodConf: unkonw item") 43 | } 44 | if cfg.Request[0].Ip != "" || cfg.Request[0].Weight == 0 { 45 | t.Error("TestGoodConf: unkonw item") 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /kbang.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "flag" 7 | "os" 8 | "github.com/kaimixu/kbang/conf" 9 | "github.com/kaimixu/kbang/robot" 10 | //"./conf" 11 | //"./robot" 12 | ) 13 | 14 | var ( 15 | n,c,t int 16 | keepalive bool 17 | url string 18 | requestBody string 19 | contentType string 20 | header string 21 | 22 | cfgFile string 23 | ) 24 | 25 | var usage = 26 | `Usage: kbang [options...] (1st form) 27 | or: kbang [options...] -f configfile (2st form) 28 | 29 | options: 30 | -n Number of requests to run (default: 10) 31 | -c Number of requests to run concurrency (default: 1) 32 | -t Request connection timeout in second (default: 5s) 33 | -H Http header, eg. -H "Host: www.example.com" 34 | -k[=true|false] Http keep-alive (default: false) 35 | -d Http request body to POST 36 | -T Content-type header to POST, eg. 'application/x-www-form-urlencoded' 37 | (Default:text/plain) 38 | 39 | ` 40 | 41 | func main() { 42 | runtime.GOMAXPROCS(runtime.NumCPU()) 43 | 44 | flag.Usage = func() { 45 | fmt.Fprint(os.Stderr, usage) 46 | } 47 | flag.IntVar(&n, "n", 10, "") 48 | flag.IntVar(&c, "c", 1, "") 49 | flag.IntVar(&t, "t", 5, "") 50 | flag.BoolVar(&keepalive, "k", false, "") 51 | flag.StringVar(&cfgFile, "f", "", "") 52 | flag.StringVar(&requestBody, "d", "", "") 53 | flag.StringVar(&contentType, "T", "text/plain", "") 54 | flag.StringVar(&header, "H", "", "") 55 | flag.Parse() 56 | 57 | if flag.NArg() < 1 && cfgFile == "" { 58 | abort("") 59 | } 60 | 61 | method := "GET" 62 | if requestBody != "" { 63 | method = "POST" 64 | } 65 | 66 | var httpConf = robot.HttpConf{ 67 | KeepAlive: keepalive, 68 | Header: header, 69 | Timeout: t, 70 | } 71 | if flag.NArg() > 0 { 72 | url = flag.Args()[0] 73 | httpConf.Request[0] = robot.RequestConf{ 74 | Weight: 1, 75 | Method: method, 76 | Url: url, 77 | ContentType: contentType, 78 | PostData: requestBody, 79 | } 80 | }else { 81 | cfg := conf.NewConf() 82 | err := cfg.LoadFile(cfgFile) 83 | if err != nil { 84 | abort(err.Error()) 85 | } 86 | err = cfg.Parse(&httpConf) 87 | if err != nil { 88 | abort(err.Error()) 89 | } 90 | } 91 | 92 | robot := robot.NewRoboter(n, c, &httpConf) 93 | err := robot.CreateRequest() 94 | if err != nil { 95 | abort(err.Error()) 96 | } 97 | 98 | robot.Run() 99 | } 100 | 101 | func abort(errmsg string) { 102 | if errmsg != "" { 103 | fmt.Fprintf(os.Stderr, "%s\n\n", errmsg) 104 | } 105 | 106 | flag.Usage() 107 | os.Exit(1) 108 | } 109 | -------------------------------------------------------------------------------- /robot/print.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "os" 7 | ) 8 | 9 | type result struct { 10 | statusCode int 11 | duration time.Duration 12 | } 13 | 14 | type output struct { 15 | average float64 16 | rps float64 17 | 18 | n int 19 | c int 20 | reqNumTotal int64 21 | reqNumFail int64 22 | reqNumSucc int64 23 | costTimeTotal float64 24 | results chan *result 25 | } 26 | 27 | func newOutput(n int, c int) *output { 28 | return &output{ 29 | n: n, 30 | c: c, 31 | results: make(chan *result, n), 32 | } 33 | } 34 | 35 | func (this *output) addResult(res *result) { 36 | this.results <- res 37 | } 38 | 39 | func (this *output) finalize(costTime float64) { 40 | this.costTimeTotal = costTime 41 | 42 | for { 43 | select { 44 | case res := <-this.results: 45 | this.reqNumTotal++ 46 | if res.statusCode != 200 { 47 | this.reqNumFail++ 48 | }else { 49 | this.reqNumSucc++ 50 | } 51 | default: 52 | this.rps = float64(this.reqNumTotal) / this.costTimeTotal 53 | this.average = this.costTimeTotal / float64(this.reqNumTotal) 54 | this.print() 55 | return 56 | } 57 | } 58 | } 59 | 60 | func (this *output) print() { 61 | if this.reqNumTotal > 0 { 62 | fmt.Printf("Summary:\n") 63 | fmt.Printf(" Concurrency Level:\t%d\n", this.c) 64 | fmt.Printf(" Time taken for tests:\t%0.4f secs\n", this.costTimeTotal) 65 | fmt.Printf(" Complete requests:\t%d\n", this.reqNumTotal) 66 | fmt.Printf(" Failed requests:\t%d\n", this.reqNumFail) 67 | fmt.Printf(" Success requests:\t%d\n", this.reqNumSucc) 68 | fmt.Printf(" Requests per second:\t%0.4f\n", this.rps) 69 | fmt.Printf(" Average time per request:\t%0.4f\n", this.average) 70 | } 71 | 72 | os.Exit(0) 73 | } -------------------------------------------------------------------------------- /robot/robot.go: -------------------------------------------------------------------------------- 1 | package robot 2 | 3 | import ( 4 | "time" 5 | "os" 6 | "os/signal" 7 | "net/http" 8 | "strings" 9 | "errors" 10 | "io/ioutil" 11 | "sync" 12 | "fmt" 13 | "net" 14 | ) 15 | 16 | type RequestConf struct { 17 | Weight int `weight` 18 | Method string `method` 19 | Url string `url` 20 | ContentType string `content_type` 21 | PostData string `post_data` 22 | } 23 | 24 | type HttpConf struct { 25 | KeepAlive bool `keepalive` 26 | Header string `header` 27 | Timeout int `timeout` 28 | Request [10]RequestConf `request` 29 | } 30 | 31 | type Roboter struct { 32 | n int 33 | c int 34 | weight int 35 | httpConf *HttpConf 36 | 37 | hc *http.Client 38 | requests []*http.Request 39 | output *output 40 | } 41 | 42 | func NewRoboter(n, c int, httpConf *HttpConf) *Roboter{ 43 | return &Roboter{ 44 | n: n, 45 | c: c, 46 | httpConf: httpConf, 47 | requests: make([]*http.Request, 0), 48 | output: newOutput(n, c), 49 | } 50 | } 51 | 52 | func (this *Roboter) CreateRequest() error { 53 | var header []string 54 | if this.httpConf.Header != "" { 55 | header = strings.SplitN(this.httpConf.Header, ":", 2) 56 | if len(header) != 2 { 57 | return errors.New("invalid http header") 58 | } 59 | } 60 | 61 | type request struct { 62 | preq *http.Request 63 | conf *RequestConf 64 | } 65 | reqs := make([]*request, 0) 66 | for _, reqC := range this.httpConf.Request { 67 | method := strings.ToUpper(reqC.Method) 68 | if method == "" { 69 | continue 70 | } 71 | if method != "GET" && method != "POST" { 72 | return errors.New("invalid http method") 73 | } 74 | 75 | r, err := http.NewRequest(method, reqC.Url, nil) 76 | if err != nil { 77 | return err 78 | } 79 | if len(reqC.ContentType) != 0 { 80 | r.Header.Set("Content-Type", reqC.ContentType) 81 | } 82 | if len(header) == 2 { 83 | r.Header.Add(header[0], header[1]) 84 | } 85 | 86 | for i := 0; i < reqC.Weight; i++ { 87 | reqs = append(reqs, &request{preq:r, conf:&reqC}) 88 | } 89 | 90 | this.weight += reqC.Weight 91 | } 92 | 93 | for i := 0; i < this.n; i++ { 94 | req := reqs[i%this.weight] 95 | this.requests = append(this.requests, cloneRequest(req.preq, req.conf.PostData)) 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (this *Roboter) Run() { 102 | s := make(chan os.Signal, 1) 103 | signal.Notify(s, os.Interrupt) 104 | 105 | tr := &http.Transport{ 106 | Dial: (&net.Dialer{ 107 | Timeout: time.Duration(this.httpConf.Timeout) * time.Second, 108 | KeepAlive: 30 * time.Second, 109 | }).Dial, 110 | DisableKeepAlives: !this.httpConf.KeepAlive, 111 | } 112 | this.hc = &http.Client{Transport: tr} 113 | 114 | fmt.Println("start...") 115 | st := time.Now() 116 | go func() { 117 | <-s 118 | fmt.Println("receive sigint") 119 | this.output.finalize(time.Now().Sub(st).Seconds()) 120 | os.Exit(1) 121 | }() 122 | 123 | this.startWorkers() 124 | this.output.finalize(time.Now().Sub(st).Seconds()) 125 | } 126 | 127 | func (this *Roboter) startWorkers() { 128 | var wg sync.WaitGroup 129 | wg.Add(this.c) 130 | 131 | for i := 0; i < this.c; i++ { 132 | go func(rid int) { 133 | this.startWorker(rid, this.n / this.c) 134 | wg.Done() 135 | }(i) 136 | } 137 | wg.Wait() 138 | } 139 | 140 | func (this *Roboter) startWorker(rid, num int) { 141 | for i := 0; i < num; i++ { 142 | req := this.requests[rid*num+i] 143 | this.sendRequest(req) 144 | } 145 | } 146 | 147 | func (this *Roboter) sendRequest(req *http.Request) { 148 | s := time.Now() 149 | var code int 150 | 151 | resp, err := this.hc.Do(req) 152 | if err == nil { 153 | code = resp.StatusCode 154 | } 155 | 156 | this.output.addResult(&result{ 157 | statusCode: code, 158 | duration: time.Now().Sub(s), 159 | }) 160 | } 161 | 162 | func cloneRequest(r *http.Request, body string) *http.Request { 163 | r2 := new(http.Request) 164 | *r2 = *r 165 | r2.Header = make(http.Header, len(r.Header)) 166 | for k, s := range r.Header { 167 | r2.Header[k] = append([]string(nil), s...) 168 | } 169 | r2.Body = ioutil.NopCloser(strings.NewReader(body)) 170 | 171 | return r2 172 | } 173 | --------------------------------------------------------------------------------