├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_EN.md ├── _build_binary └── build_gurl.go ├── color └── color.go ├── core └── message.go ├── ghttp ├── ghttp.go ├── ghttp_test.go ├── report.go ├── req.go ├── req_test.go └── url │ └── url.go ├── gurl.go ├── gurl_vs_ab.md ├── gurl_vs_ab_en.md ├── input ├── main.go └── read_file.go ├── output └── output.go ├── pipe └── pipe.go ├── report └── report.go ├── task └── task.go ├── utils └── parsetime.go └── ws ├── url └── url.go └── ws.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | *~ 7 | *swp 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13.x 4 | script: 5 | - go test -v ./... 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gurl 2 | #### Documentation 3 | * [English](./README_EN.md) 4 | 5 | [![Build Status](https://travis-ci.org/guonaihong/gurl.png)](https://travis-ci.org/guonaihong/gurl) 6 | #### 简介 7 | gurl 是http, websocket bench工具和curl的继承者 8 | 9 | #### 功能 10 | * 多协议支持http, websocket 11 | * 支持curl常用命令行选项 12 | * 支持压测模式,可以根据并发数和线程数,也可以根据持续时间,也可以指定每秒并发数压测http, websocket, tcp服务 13 | * url 支持简写 14 | * 支持管道模式 15 | 16 | #### install 17 | ```bash 18 | env GOPATH=`pwd` go get -u github.com/guonaihong/gurl/ 19 | ``` 20 | 21 | #### gurl 主命令选项 22 | - [http 子命令](#http-子命令) 23 | - [websocket 子命令](#websocket-子命令) 24 | #### http 子命令 25 | - [http 性能压测](#http-性能压测) 26 | - [RESTful API](#restful-api) 27 | 28 | ```console 29 | guonaihong https://github.com/guonaihong/gurl 30 | 31 | Usage of gurl: 32 | -A, --user-agent string 33 | Send User-Agent STRING to server (default "gurl") 34 | -F, --form string[] 35 | Specify HTTP multipart POST data (H) (default []) 36 | -H, --header string[] 37 | Pass custom header LINE to server (H) (default []) 38 | -I, --input-model 39 | open input mode 40 | -J string[] 41 | Turn key:value into {"key": "value"}) 42 | -Jfa string[] 43 | Specify HTTP multipart POST json data (H) 44 | -Jfa-string string[] 45 | Specify HTTP multipart POST json data (H) 46 | -O, --output-mode 47 | open output mode 48 | -R, --input-read string 49 | open input file 50 | -W, --output-write string 51 | open output file 52 | -X, --request string 53 | Specify request command to use 54 | -ac int 55 | Number of multiple requests to make (default 1) 56 | -an int 57 | Number of requests to perform (default 1) 58 | -bench 59 | Run benchmarks test 60 | -c, --color 61 | Color highlighting 62 | -connect-timeout string 63 | Maximum time allowed for connection 64 | -conns int 65 | Max open idle connections per target host (default 10000) 66 | -cpus int 67 | Number of CPUs to use 68 | -d, --data string 69 | HTTP POST data 70 | -debug 71 | open debug mode 72 | -duration string 73 | Duration of the test 74 | -form-string string[] 75 | Specify HTTP multipart POST data (H) (default []) 76 | -input-fields string 77 | sets the field separator (default " ") 78 | -l string 79 | Listen mode, HTTP echo server 80 | -m, --merge 81 | Combine the output results into the output 82 | -o, --output string 83 | Write to FILE instead of stdout (default "stdout") 84 | -oflag string 85 | Control the way you write(append|line|trunc) 86 | -q, --query string[] 87 | query string 88 | -r, --read-stream 89 | Read data from the stream 90 | -rate int 91 | Requests per second 92 | -skey, --input-setkey string 93 | Set a new name for the default key 94 | -url string 95 | Specify a URL to fetch 96 | -v, --verbose 97 | Make the operation more talkative 98 | -w, --write-stream 99 | Write data from the stream 100 | -wkey, --write-key string 101 | Key that can be write 102 | 103 | ``` 104 | ##### `-F 或 --form` 105 | 设置form表单, 比如-F text=文本内容,或者-F text=@./从文件里面读取, -F 选项的语义和curl命令一样 106 | ##### `--form-string` 107 | 和-F 或--form类似,不解释@符号,原样传递到服务端 108 | 109 | ##### `-ac` 110 | 指定线程数, 开ac个线程, 发送an个请求 111 | ```bash 112 | gurl http -an 10 -ac 2 -F text=good :1234 113 | ``` 114 | 115 | ##### `-an` 116 | 指定次数 117 | 118 | ##### http 性能压测 119 | `-bench` 120 | 压测模式,可以对http服务端进行压测,可以和-ac, -an, -duration, -rate 选项配合使用 121 | ``` console 122 | gurl http -bench -ac 25 -an 1000000 :1234 123 | Benchmarking 127.0.0.1 (be patient) 124 | Completed 100000 requests [2018-08-11 21:58:56.143] 125 | Completed 200000 requests [2018-08-11 21:59:00.374] 126 | Completed 300000 requests [2018-08-11 21:59:03.703] 127 | Completed 400000 requests [2018-08-11 21:59:06.559] 128 | Completed 500000 requests [2018-08-11 21:59:09.201] 129 | Completed 600000 requests [2018-08-11 21:59:11.757] 130 | Completed 700000 requests [2018-08-11 21:59:14.218] 131 | Completed 800000 requests [2018-08-11 21:59:16.639] 132 | Completed 900000 requests [2018-08-11 21:59:19.061] 133 | Completed 1000000 requests [2018-08-11 21:59:21.451] 134 | Finished 1000000 requests 135 | 136 | 137 | Server Software: gurl-server 138 | Server Hostname: 139 | Server Port: 1234 140 | 141 | Document Path: 142 | Document Length: 0 bytes 143 | 144 | Status Codes: 200:1000000 [code:count] 145 | Concurrency Level: 10 146 | Time taken for tests: 28.807 seconds 147 | Complete requests: 1000000 148 | Failed requests: 0 149 | Total transferred: 137000000 bytes 150 | HTML transferred: 0 bytes 151 | Requests per second: 34713.37 [#/sec] (mean) 152 | Time per request: 0.288 [ms] (mean) 153 | Time per request: 0.029 [ms] (mean, across all concurrent requests) 154 | Transfer rate: 4755.73 [Kbytes/sec] received 155 | Percentage of the requests served within a certain time (ms) 156 | 50% 0.21ms 157 | 66% 0.31ms 158 | 75% 0.38ms 159 | 80% 0.42ms 160 | 90% 0.57ms 161 | 95% 0.66ms 162 | 98% 0.79ms 163 | 99% 0.89ms 164 | 100% 16.45ms 165 | 166 | ``` 167 | 168 | ##### `-duration` 169 | 和-bench选项一起使用,可以控制压测时间,支持单位符,ms(毫秒), s(秒), m(分), h(小时), d(天), w(周), M(月), y(年) 170 | 也可以混合使用 -duration 1m10s 171 | 172 | ##### `-connect-timeout` 173 | 设置http 连接超时时间。支持单位符,ms(毫秒), s(秒), m(分), h(小时), d(天), w(周), M(月), y(年) 174 | 175 | ##### `-rate` 176 | 指定每秒写多少条 177 | ``` console 178 | gurl http -bench -ac 25 -an 3000 -rate 3000 :1234 179 | Benchmarking 127.0.0.1 (be patient) 180 | Completed 300 requests [2018-08-11 22:02:01.625] 181 | Completed 600 requests [2018-08-11 22:02:01.725] 182 | Completed 900 requests [2018-08-11 22:02:01.825] 183 | Completed 1200 requests [2018-08-11 22:02:01.925] 184 | Completed 1500 requests [2018-08-11 22:02:02.025] 185 | Completed 1800 requests [2018-08-11 22:02:02.125] 186 | Completed 2100 requests [2018-08-11 22:02:02.225] 187 | Completed 2400 requests [2018-08-11 22:02:02.325] 188 | Completed 2700 requests [2018-08-11 22:02:02.425] 189 | Completed 3000 requests [2018-08-11 22:02:02.525] 190 | Finished 3000 requests 191 | 192 | 193 | Server Software: gurl-server 194 | Server Hostname: 195 | Server Port: 1234 196 | 197 | Document Path: 198 | Document Length: 0 bytes 199 | 200 | Status Codes: 200:3000 [code:count] 201 | Concurrency Level: 10 202 | Time taken for tests: 1.000 seconds 203 | Complete requests: 3000 204 | Failed requests: 0 205 | Total transferred: 411000 bytes 206 | HTML transferred: 0 bytes 207 | Requests per second: 3000.08 [#/sec] (mean) 208 | Time per request: 3.333 [ms] (mean) 209 | Time per request: 0.333 [ms] (mean, across all concurrent requests) 210 | Transfer rate: 411.01 [Kbytes/sec] received 211 | Percentage of the requests served within a certain time (ms) 212 | 50% 0.17ms 213 | 66% 0.18ms 214 | 75% 0.18ms 215 | 80% 0.19ms 216 | 90% 0.21ms 217 | 95% 0.23ms 218 | 98% 0.26ms 219 | 99% 0.31ms 220 | 100% 1.34ms 221 | ``` 222 | ##### `-d 或 --data` 223 | 发送http body数据到服务端, 支持@符号打开一个文件, 如果不接@直接把-d后面字符串发送到服务端 224 | ```bash 225 | gurl http -d "good" :12345 226 | gurl http -d "@./file" :12345 227 | ``` 228 | 229 | ##### RESTful API 230 | `-J` 拼装json字段到body里面 231 | -J 后面的key和value 会被组装成json字符串发送到服务端. key:value,其中value会被解释成字符串, key:=value,value会被解决成bool或者数字或者小数 232 | * 普通用法 233 | ```bash 234 | ./gurl http -J username:admin passwd:123456 bool_val:=true int_val:=3 float_val:=0.3 -url http://127.0.0.1:12345 235 | { 236 | "bool_val": true, 237 | "float_val": 0.3, 238 | "int_val": 3, 239 | "passwd": "123456", 240 | "username": "admin" 241 | } 242 | ``` 243 | * 嵌套用法 244 | ```bash 245 | ./gurl http -J a.b.c.d:=true -J a.b.c.e:=111 http://127.0.0.1:12345 246 | { 247 | "a": { 248 | "b": { 249 | "c": { 250 | "d:": true, 251 | "e:": 111 252 | } 253 | } 254 | } 255 | } 256 | ``` 257 | ##### 查询字符串 258 | `-q` 后跟查询字符串, key=val形式 259 | 260 | ```bash 261 | gurl http -X POST -J hello:word startTime:123 endTime:456 -url :8080/test -vc -q appkey=hello world=hello 262 | > POST /test?appkey=hello&world=hello HTTP/1.1 263 | > Accept: */* 264 | > Host: 127.0.0.1:8080 265 | > User-Agent: gurl 266 | > 267 | 268 | < HTTP/1.1 200 OK 269 | < Date: Tue, 21 May 2019 12:39:13 GMT 270 | < Content-Length: 50 271 | < Content-Type: text/plain; charset=utf-8 272 | 273 | 274 | { 275 | "endTime": "456", 276 | "hello": "word", 277 | "startTime": "123" 278 | } 279 | 280 | ``` 281 | ##### `-Jfa` 282 | 向multipart字段中插入json数据 283 | ```bash 284 | ./gurl http -Jfa text=DisplayText:good text=Language:cn text2=look:me -F text=good :12345 285 | 286 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890 287 | Content-Disposition: form-data; name="text" 288 | 289 | {"DisplayText":"good","Language":"cn"} 290 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890 291 | Content-Disposition: form-data; name="text2" 292 | 293 | {"look":"me"} 294 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890 295 | Content-Disposition: form-data; name="text" 296 | 297 | good 298 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890-- 299 | ``` 300 | ##### `-Jfa-string` 301 | 和-Jfa语法类似,不解析@符号 302 | 303 | ##### `-H 或者 --header` 304 | 设置http 头,可以指定多个 305 | ```bash 306 | ./gurl http -H "header1:value1" -H "header2:value2" http://xxx.xxx.xxx.xxx:port 307 | ``` 308 | 309 | ##### `-url` 310 | 设置http url的地址, 可以使用简写 311 | * -url http://127.0.0.1:1234 --> 127.0.0.1:1234 312 | * -url http://127.0.0.1:1234 --> :1234 313 | * -url http://127.0.0.1/path --> /path 314 | 315 | ##### `-oflag` 316 | -oflag 一般和-o选项配合使用(控制写文件的行为) 317 | * -oflag append 默认-o的行为是新建文件然后写入,如果开启-ac -an选项,可以使用append肥所有的结果保存到一个文件中 318 | * -oflag line 如果服务端返回的结果,想使用换行符分隔 319 | 小提示: -oflag 后面的命令可以组合使用 "append|line"的意思是:把服务端的输出追加到某个文本中,并用'\n'分隔符 320 | 321 | 322 | #### 高级主题(stream功能) 323 | ##### `-I` 324 | 打开input模式 325 | 326 | ##### `-R` 327 | 打开列表文件, 可以使用-input-fields 指定分割符,默认是空格 328 | 329 | ##### `-skey` 330 | 给默认的名字取个别名,相当于取个好听的变量名,方便后面引用 331 | 332 | ##### `-r` 333 | 从流里面读取数据 334 | 335 | ##### `-w` 336 | 结果输出到流 337 | 338 | ##### `-merge` 339 | 把输入流里面的结果和识别结果组成大的结果,写到输出流 340 | 341 | ##### `-O` 342 | 打开output模式 343 | 344 | ##### `-wkey` 345 | 控制写出的json key 346 | 347 | ##### `|` 348 | 管道符主要拼接多个gurl功能块 349 | 350 | ##### 批量访问多个url 351 | 开5个线程访问url.list里面的url列表 352 | ``` 353 | cat url.list 354 | 355 | github.com 356 | www.baidu.com 357 | www.qq.com 358 | www.taobao.com 359 | 360 | gurl http -I -R url.list -skey "url=rf.col.0" "|" -ac 5 -r -w -merge "{url}" -o "/dev/null" "|" -O -wkey "status_code" 361 | ``` 362 | 363 | #### TODO 364 | * 集群模式 365 | * GUI 366 | 367 | #### websocket 子命令 368 | 369 | #### 命令行选项 370 | ```console 371 | guonaihong https://github.com/guonaihong/wsurl 372 | 373 | Usage of gurl: 374 | -A, --user-agent string 375 | Send User-Agent STRING to server (default "gurl") 376 | -H, --header string[] 377 | Pass custom header LINE to server (H) (default []) 378 | -I, --input-model 379 | open input mode 380 | -O, --output-mode 381 | open output mode 382 | -R, --input-read string 383 | open input file 384 | -W, --output-write string 385 | open output file 386 | -ac int 387 | Number of multiple requests to make (default 1) 388 | -an int 389 | Number of requests to perform (default 1) 390 | -bench 391 | Run benchmarks test 392 | -binary 393 | Send binary messages instead of utf-8 394 | -close 395 | Send close message 396 | -duration string 397 | Duration of the test 398 | -fsa, --first-send-after string 399 | Wait for the first time before sending 400 | -input-fields string 401 | sets the field separator (default " ") 402 | -l string 403 | Listen mode, websocket echo server 404 | -ld, --last-packet string 405 | The last packet is written to the connection 406 | -m, --merge 407 | Combine the output results into the output 408 | -o, --output string 409 | Write to FILE instead of stdout (default "stdout") 410 | -p, --packet string[] 411 | Data packet to be send per connection 412 | -r, --read-stream 413 | Read data from the stream 414 | -rate int 415 | Requests per second 416 | -send-rate string 417 | How many bytes of data in seconds 418 | -skey, --input-setkey string 419 | Set a new name for the default key 420 | -url string 421 | Specify a URL to fetch 422 | -w, --write-stream 423 | Write data from the stream 424 | -wkey, --write-key string 425 | Key that can be write 426 | 427 | ``` 428 | 429 | ##### `-H 或header` 430 | 设置websocket 的header和http header类似 431 | 432 | ##### `-p 或 --packet` 433 | 发送websocket body数据到服务端,支持@符号打开一个文件, 如果不接@直接把-d后面字符串发送到服务端 434 | ```bash 435 | wsurl -p "good" :12345 436 | wsurl -p "@./file" :12345 437 | ``` 438 | ##### `-send-rate` 439 | ``` bash 440 | # 指定每多少ms发多少字节 441 | wsurl -send-rate "8000B/250ms" -url ws://127.0.0.1:24986 442 | ``` 443 | 444 | ##### `-binary` 445 | 默认是以text格式作为websocket消息类型, 加上-binary就以text作为消息类型 446 | 447 | ##### `-ld` 448 | 发送最后一个websocket包的内容 449 | ```bash 450 | wsurl -ld "good" :12345 451 | wsurl -ld "@./file" :12345 452 | ``` 453 | 454 | ##### `-url` 455 | 设置websocket的url 456 | * -url http://127.0.0.1:1234 --> 127.0.0.1:1234 457 | * -url http://127.0.0.1:1234 --> :1234 458 | * -url http://127.0.0.1/path --> /path 459 | 460 | ##### `-ac` 461 | 指定线程数, 开ac个线程, 发送an个请求 462 | ```bash 463 | wsurl -an 10 -ac 2 -F text=good :1234 464 | ``` 465 | 466 | ##### `-an` 467 | 指定次数 468 | 469 | ##### `-duration` 470 | 和-bench选项一起使用,可以控制压测时间,支持单位符,s(秒), m(分), h(小时), d(天), w(周), M(月), y(年), ms(毫秒) 471 | 也可以混合使用 -duration 1m10s 472 | 473 | ##### `-rate` 474 | 指定每秒写多少条,目前只有打开-bench选项才起作用 475 | 476 | ##### `-close` 477 | 客户端主动发起close消息给服务端 478 | 479 | ##### `-bench` 480 | 压测模式 481 | wsurl -bench -ac 20 -an 10000 -url :33333 -close 482 | ``` console 483 | Connecting to to ws://127.0.0.1:33333 484 | Opened 1000 connections: [2018-08-23 20:50:55.987] 485 | Opened 2000 connections: [2018-08-23 20:50:56.129] 486 | Opened 3000 connections: [2018-08-23 20:50:56.266] 487 | Opened 4000 connections: [2018-08-23 20:50:56.409] 488 | Opened 5000 connections: [2018-08-23 20:50:56.552] 489 | Opened 6000 connections: [2018-08-23 20:50:56.684] 490 | Opened 7000 connections: [2018-08-23 20:50:56.835] 491 | Opened 8000 connections: [2018-08-23 20:50:56.098] 492 | Opened 9000 connections: [2018-08-23 20:50:57.125] 493 | Opened 10000 connections: [2018-08-23 20:50:57.268] 494 | 495 | Finished 10000 connections 496 | 497 | Concurrency Level: 20 498 | Time taken for tests: 1.432677765s 499 | Connected: 10000 500 | Disconnected: 0 501 | Failed: 0 502 | Total transferred: 0 503 | Total received 0 504 | Requests per second: 6979 [#/sec] (mean) 505 | Time per request: 716338.883 [ms] (mean) 506 | Time per request: 71.634 [ms] (mean, across all concurrent requests) 507 | Transfer rate: 0.000 [Kbytes/sec] received 508 | 509 | Percentage of the requests served within a certain time (ms) 510 | 50% 2.00ms 511 | 66% 2.00ms 512 | 75% 3.00ms 513 | 80% 3.00ms 514 | 90% 4.00ms 515 | 95% 6.00ms 516 | 98% 7.00ms 517 | 99% 9.00ms 518 | 100% 21.00ms 519 | ``` 520 | 521 | #### 高级主题(stream功能) 522 | ##### `-I` 523 | 打开input模式 524 | 525 | ##### `-R` 526 | 打开列表文件, 可以使用-input-fields 指定分割符,默认是空格 527 | 528 | ##### `-skey` 529 | 给默认的名字取个别名,相当于取个好听的变量名,方便后面引用 530 | 531 | ##### `-r` 532 | 从流里面读取数据 533 | 534 | ##### `-w` 535 | 结果输出到流 536 | 537 | ##### `-merge` 538 | 把输入流里面的结果和识别结果组成大的结果,写到输出流 539 | 540 | ##### `-O` 541 | 打开output模式 542 | 543 | ##### `-wkey` 544 | 控制写出的json key 545 | 546 | ##### `|` 547 | 管道符主要拼接多个gurl功能块 548 | 549 | #### tcp, udp 子命令用法 550 | * [tcp udp](./conn/README.md) 551 | 552 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # gurl 2 | 3 | #### Documentation 4 | * [chinese](./README.md) 5 | 6 | #### Introduction 7 | Gurl is the successor to http, websocket bench tools and curl 8 | 9 | #### Features 10 | * Multi-protocol support http, websocket, tcp, udp 11 | * Supports some of curl's features 12 | * Supports some of the functions of ab, and the performance is higher than the ab command 13 | * Support regular running gurl (support cron expression function) 14 | * Support lua as configuration file (support for if, else, for, func) 15 | * Url support abbreviations 16 | * Support pipeline mode 17 | 18 | #### install 19 | ```bash 20 | env GOPATH=`pwd` go get -u github.com/guonaihong/gurl/gurl 21 | ``` 22 | #### gurl main command usage 23 | ```console 24 | Usage of gurl: 25 | http Use the http subcommand 26 | tcp, udp Use the tcp or udp subcommand 27 | ws, websocket Use the websocket subcommand 28 | ``` 29 | #### websocket subcommand usage 30 | * [websocket](./wsurl/README.md) 31 | 32 | #### tcp, udp subcommand usage 33 | * [tcp udp](./conn/README.md) 34 | 35 | #### http subcommand usage 36 | ```console 37 | guonaihong https://github.com/guonaihong/gurl 38 | 39 | Usage of gurl: 40 | -A, --user-agent string 41 | Send User-Agent STRING to server (default "gurl") 42 | -F, --form string[] 43 | Specify HTTP multipart POST data (H) 44 | -H, --header string[] 45 | Pass custom header LINE to server (H) 46 | -J string[] 47 | Turn key:value into {"key": "value"}) 48 | -Jfa string[] 49 | Specify HTTP multipart POST json data (H) 50 | -Jfa-string string[] 51 | Specify HTTP multipart POST json data (H) 52 | -K, --config string 53 | lua script 54 | -X, --request string 55 | Specify request command to use 56 | -ac int 57 | Number of multiple requests to make (default 1) 58 | -an int 59 | Number of requests to perform (default 1) 60 | -bench 61 | Run benchmarks test 62 | -connect-timeout string 63 | Maximum time allowed for connection 64 | -conns int 65 | Max open idle connections per target host (default 10000) 66 | -cpus int 67 | Number of CPUs to use 68 | -cron string 69 | Cron expression 70 | -d, --data string 71 | HTTP POST data 72 | -duration string 73 | Duration of the test 74 | -form-string string[] 75 | Specify HTTP multipart POST data (H) 76 | -gen 77 | Generate the default lua script 78 | -kargs string 79 | Command line parameters passed to the configuration file 80 | -l string 81 | Listen mode, HTTP echo server 82 | -o, --output string 83 | Write to FILE instead of stdout (default "stdout") 84 | -oflag string 85 | Control the way you write(append|line|trunc) 86 | -rate int 87 | Requests per second 88 | -url string 89 | Specify a URL to fetch 90 | ``` 91 | 92 | ##### `-F 或 --form` 93 | Set the form form, such as -F text=text content, or -F text=@./read from the file, the semantics of the -F option are the same as the curl command 94 | ##### `--form-string` 95 | Similar to -F or --form, without interpreting the @symbol, passed to the server as it is 96 | 97 | ##### `-ac` 98 | Specify the number of threads, open ac threads, send an request 99 | ```bash 100 | ./gurl http -an 10 -ac 2 -F text=good :1234 101 | ``` 102 | 103 | ##### `-an` 104 | Specified number 105 | 106 | ##### `-bench` 107 | In the pressure test mode, the http server can be pressed and used with the -ac, -an, -duration, -rate option. 108 | ``` console 109 | gurl http -bench -ac 25 -an 1000000 :1234 110 | Benchmarking 127.0.0.1 (be patient) 111 | Completed 100000 requests [2018-08-11 21:58:56.143] 112 | Completed 200000 requests [2018-08-11 21:59:00.374] 113 | Completed 300000 requests [2018-08-11 21:59:03.703] 114 | Completed 400000 requests [2018-08-11 21:59:06.559] 115 | Completed 500000 requests [2018-08-11 21:59:09.201] 116 | Completed 600000 requests [2018-08-11 21:59:11.757] 117 | Completed 700000 requests [2018-08-11 21:59:14.218] 118 | Completed 800000 requests [2018-08-11 21:59:16.639] 119 | Completed 900000 requests [2018-08-11 21:59:19.061] 120 | Completed 1000000 requests [2018-08-11 21:59:21.451] 121 | Finished 1000000 requests 122 | 123 | 124 | Server Software: gurl-server 125 | Server Hostname: 126 | Server Port: 1234 127 | 128 | Document Path: 129 | Document Length: 0 bytes 130 | 131 | Status Codes: 200:1000000 [code:count] 132 | Concurrency Level: 10 133 | Time taken for tests: 28.807 seconds 134 | Complete requests: 1000000 135 | Failed requests: 0 136 | Total transferred: 137000000 bytes 137 | HTML transferred: 0 bytes 138 | Requests per second: 34713.37 [#/sec] (mean) 139 | Time per request: 0.288 [ms] (mean) 140 | Time per request: 0.029 [ms] (mean, across all concurrent requests) 141 | Transfer rate: 4755.73 [Kbytes/sec] received 142 | Percentage of the requests served within a certain time (ms) 143 | 50% 0.21ms 144 | 66% 0.31ms 145 | 75% 0.38ms 146 | 80% 0.42ms 147 | 90% 0.57ms 148 | 95% 0.66ms 149 | 98% 0.79ms 150 | 99% 0.89ms 151 | 100% 16.45ms 152 | 153 | ``` 154 | 155 | ##### `-duration` 156 | Used with the -bench option to control the press time, support unit, s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years) ) 157 | Can also be mixed -duration 1m10s 158 | 159 | ##### `-connect-timeout` 160 | Set the http connection timeout period. Support unit, ms (milliseconds), s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years) 161 | 162 | ##### `-rate` 163 | Specify how many times to write per second 164 | ``` console 165 | gurl http -bench -ac 25 -an 3000 -rate 3000 :1234 166 | Benchmarking 127.0.0.1 (be patient) 167 | Completed 300 requests [2018-08-11 22:02:01.625] 168 | Completed 600 requests [2018-08-11 22:02:01.725] 169 | Completed 900 requests [2018-08-11 22:02:01.825] 170 | Completed 1200 requests [2018-08-11 22:02:01.925] 171 | Completed 1500 requests [2018-08-11 22:02:02.025] 172 | Completed 1800 requests [2018-08-11 22:02:02.125] 173 | Completed 2100 requests [2018-08-11 22:02:02.225] 174 | Completed 2400 requests [2018-08-11 22:02:02.325] 175 | Completed 2700 requests [2018-08-11 22:02:02.425] 176 | Completed 3000 requests [2018-08-11 22:02:02.525] 177 | Finished 3000 requests 178 | 179 | 180 | Server Software: gurl-server 181 | Server Hostname: 182 | Server Port: 1234 183 | 184 | Document Path: 185 | Document Length: 0 bytes 186 | 187 | Status Codes: 200:3000 [code:count] 188 | Concurrency Level: 10 189 | Time taken for tests: 1.000 seconds 190 | Complete requests: 3000 191 | Failed requests: 0 192 | Total transferred: 411000 bytes 193 | HTML transferred: 0 bytes 194 | Requests per second: 3000.08 [#/sec] (mean) 195 | Time per request: 3.333 [ms] (mean) 196 | Time per request: 0.333 [ms] (mean, across all concurrent requests) 197 | Transfer rate: 411.01 [Kbytes/sec] received 198 | Percentage of the requests served within a certain time (ms) 199 | 50% 0.17ms 200 | 66% 0.18ms 201 | 75% 0.18ms 202 | 80% 0.19ms 203 | 90% 0.21ms 204 | 95% 0.23ms 205 | 98% 0.26ms 206 | 99% 0.31ms 207 | 100% 1.34ms 208 | ``` 209 | ##### `|` 210 | Pipeline mode, mainly designed to concatenate multiple lua scripts, the output of the first script becomes the input of the second script 211 | ```bash 212 | ./gurl http -an 1 -K ./producer.lua -kargs "-l all.txt" "|" -an 0 -ac 12 -K ./http_slice.lua -kargs "-appkey xx -url http://192.168.6.128:24990/asr/pcm " "|" -an 0 -K ./write_file.lua -kargs "-f asr.result" 213 | ``` 214 | 215 | ##### `-d 或 --data` 216 | Send http body data to the server, support @symbol to open a file, if not @ directly send the string after -d to the server 217 | ```bash 218 | gurl http -d "good" :12345 219 | gurl http -d "@./file" :12345 220 | ``` 221 | 222 | ##### `-J` 223 | The key and value after -J will be assembled into a json string and sent to the server. key:value, where value will be interpreted as a string, key:=value, value will be resolved to bool or number or decimal 224 | * Common usage 225 | ```bash 226 | ./gurl http -J username:admin -J passwd:123456 -J bool_val:=true -J int_val:=3 -J float_val:=0.3 http://127.0.0.1:12345 227 | { 228 | "bool_val": true, 229 | "float_val": 0.3, 230 | "int_val": 3, 231 | "passwd": "123456", 232 | "username": "admin" 233 | } 234 | ``` 235 | * Nested usage 236 | ```bash 237 | ./gurl http -J a.b.c.d:=true -J a.b.c.e:=111 http://127.0.0.1:12345 238 | { 239 | "a": { 240 | "b": { 241 | "c": { 242 | "d:": true, 243 | "e:": 111 244 | } 245 | } 246 | } 247 | } 248 | ``` 249 | 250 | ##### `-Jfa` 251 | Insert json data into the multipart field 252 | ```bash 253 | ./gurl http -Jfa text=DisplayText:good -Jfa text=Language:cn -Jfa text2=look:me -F text=good :12345 254 | 255 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890 256 | Content-Disposition: form-data; name="text" 257 | 258 | {"DisplayText":"good","Language":"cn"} 259 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890 260 | Content-Disposition: form-data; name="text2" 261 | 262 | {"look":"me"} 263 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890 264 | Content-Disposition: form-data; name="text" 265 | 266 | good 267 | --4361c4e6ae1b083e9e0508a7b40eb215bccd265c4bed00137cc7d112e890-- 268 | ``` 269 | ##### `-Jfa-string` 270 | Similar to the -Jfa syntax, does not parse the @symbol 271 | 272 | ##### `-H 或者 --header` 273 | Set http headers, you can specify multiple 274 | ```bash 275 | ./gurl http -H "header1:value1" -H "header2:value2" http://xxx.xxx.xxx.xxx:port 276 | ``` 277 | 278 | ##### `-cron` 279 | Send regularly (take results from the service every second) 280 | ```bash 281 | ./gurl http -cron "@every 1s" -H "session-id:f0c371f1-f418-477c-92d4-129c16c8e4d5" http://127.0.0.1:12345/asr/result 282 | ``` 283 | 284 | ##### `-url` 285 | * -url http://127.0.0.1:1234 --> 127.0.0.1:1234 286 | * -url http://127.0.0.1:1234 --> :1234 287 | * -url http://127.0.0.1/path --> /path 288 | 289 | 290 | ##### `-oflag` 291 | -oflag Generally used in conjunction with the -o option (controls the behavior of writing files) 292 | * -oflag append The default -o behavior is to create a new file and then write it. If you enable the -ac -an option, you can use append to save all the results to a file. 293 | * -oflag line If the server returns the result, I want to use a newline to separate 294 | Tip: The commands after -oflag can be combined with "append|line" to mean appending the output of the server to a text with the '\n' separator 295 | 296 | ##### `-gen` 297 | * Generate a configuration file from the command line's data (option -gen) 298 | ```lua 299 | gurl http -X POST -F mode=A -F text=good -F voice=@./good.opus -url http://127.0.0.1:24909/eval/opus -gen &>demo.lua 300 | 301 | #todo 302 | 303 | ``` 304 | * Convert the configuration file to the command line format (option -gen -K configuration file) 305 | ```bash 306 | gurl http -K demo.lua -gen 307 | gurl http -X POST -F mode=A -F text=good -F voice=@./good.opus -url http://127.0.0.1:24909/eval/opus 308 | ``` 309 | 310 | #### `-K` 311 | The -K option can execute lua script. For the usage of lua, you can search for it. 312 | 313 | #### `-kargs` 314 | This command option mainly passes parameters from the command line to lua script. 315 | 316 | The following example shows how to use gurl built-in lua function. The following code can be executed with the -K option, -kargs "here is the command line argument from the script" 317 | * Parse the command line configuration in the configuration file 318 | ```lua 319 | local flag = require("flag").new() 320 | local opt = flag 321 | :opt_str("f, file", "", "open audio file") 322 | :opt_str("a, addr", "", "Remote service address") 323 | :parse("-f ./tst.pcm -a 127.0.0.1:8080") 324 | 325 | 326 | if #opt["f"] == 0 or 327 | #opt["file"] == 0 or 328 | #opt["a"] == 0 or 329 | #opt["addr"] == 0 then 330 | 331 | opt.Usage() 332 | 333 | return 334 | end 335 | 336 | for k, v in pairs(opt) do 337 | print("cmd opt ("..k..") parse value ("..v..")") 338 | end 339 | ``` 340 | * Send http request 341 | ```lua 342 | local http = require("http") 343 | local rsp = http.send({ 344 | H = { 345 | "appkey:"..config.appkey, 346 | "X-Number:"..xnumber, 347 | "session-id:"..session_id, 348 | }, 349 | MF = { 350 | "voice=" .. bytes, 351 | }, 352 | url = config.url 353 | }) 354 | 355 | --print("bytes ("..bytes..")") 356 | if #rsp["err"] ~= 0 then 357 | print("rsp error is ".. rsp["err"]) 358 | return 359 | end 360 | 361 | if rsp["status_code"] == 200 then 362 | body = rsp["body"] 363 | if #rsp["body"] == 0 then 364 | body = "{}" 365 | end 366 | print(json.format(body)) 367 | else 368 | print("error http code".. rsp["status_code"]) 369 | end 370 | 371 | ``` 372 | 373 | * sleep 374 | ```lua 375 | local time = require("time") 376 | time.sleep("250ms") 377 | time.sleep("1s") 378 | time.sleep("1m") 379 | time.sleep("1h") 380 | time.sleep("1s250ms") 381 | ``` 382 | #### TODO 383 | * bugfix 384 | * Some add with very handy features 385 | -------------------------------------------------------------------------------- /_build_binary/build_gurl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | ) 7 | 8 | func main() { 9 | os := []string{"openbsd", "windows", "linux", "freebsd", "netbsd", "aix", "darwin", "solaris"} 10 | arch := []string{"arm", "arm64", "386", "amd64", "ppc64", "ppc64le", "mips", "mipsle", "mips64", "mips64le"} 11 | 12 | for _, o := range os { 13 | for _, a := range arch { 14 | testCmd := fmt.Sprintf("env GOPATH=`pwd` CGO_ENABLED=0 GOOS=%s GOARCH=%s go build -o gurl github.com/guonaihong/gurl/", o, a) 15 | cmd := exec.Command("bash", "-c", testCmd) 16 | _, err := cmd.Output() 17 | if err != nil { 18 | fmt.Printf("err :%s:%s\n", err, testCmd) 19 | return 20 | } 21 | 22 | cmd2 := exec.Command("bash", "-c", fmt.Sprintf("tar zcvf %s_%s.tar.gz gurl", o, a)) 23 | if err = cmd2.Run(); err != nil { 24 | fmt.Printf("err: %s\n", err) 25 | return 26 | } 27 | 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | const ( 4 | Gray = "\x1b[30;1m" 5 | Blue = "\x1b[34;1m" 6 | End = "\x1b[0m" 7 | ) 8 | 9 | func NewKeyVal(open bool) (keyStart, keyEnd, valStart, valEnd string) { 10 | if !open { 11 | return 12 | } 13 | 14 | return Gray, End, Blue, End 15 | } 16 | -------------------------------------------------------------------------------- /core/message.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import () 4 | 5 | type Message struct { 6 | In chan string 7 | Out chan string 8 | InDone chan string 9 | OutDone chan string 10 | K int 11 | } 12 | -------------------------------------------------------------------------------- /ghttp/ghttp.go: -------------------------------------------------------------------------------- 1 | package ghttp 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/guonaihong/flag" 8 | "github.com/guonaihong/gurl/core" 9 | url2 "github.com/guonaihong/gurl/ghttp/url" 10 | "github.com/guonaihong/gurl/input" 11 | "github.com/guonaihong/gurl/output" 12 | "github.com/guonaihong/gurl/task" 13 | "github.com/guonaihong/gurl/utils" 14 | "io" 15 | _ "io/ioutil" 16 | "net/http" 17 | "os" 18 | "runtime" 19 | "strings" 20 | "syscall" 21 | "time" 22 | ) 23 | 24 | const ( 25 | DefaultConnections = 10000 26 | ) 27 | 28 | type GurlCmd struct { 29 | task.Task 30 | bench bool 31 | writeStream bool 32 | merge bool 33 | report *Report 34 | *Gurl 35 | debug bool 36 | } 37 | 38 | func parse(val map[string]string, g *Gurl, inJson string) { 39 | 40 | err := json.Unmarshal([]byte(inJson), &val) 41 | if err != nil { 42 | fmt.Printf("%s\n", err) 43 | return 44 | } 45 | 46 | i := 0 47 | rs := make([]string, len(val)*2) 48 | for k, v := range val { 49 | 50 | rs[i] = "{" + k + "}" 51 | i++ 52 | rs[i] = v 53 | i++ 54 | } 55 | 56 | r := strings.NewReplacer(rs...) 57 | for k, v := range g.Json { 58 | g.Json[k] = r.Replace(v) 59 | } 60 | 61 | for k, v := range g.FormData { 62 | g.FormData[k] = r.Replace(v) 63 | } 64 | 65 | for k, v := range g.Header { 66 | g.Header[k] = r.Replace(v) 67 | } 68 | 69 | for k, v := range g.Jfa { 70 | g.Jfa[k] = r.Replace(v) 71 | } 72 | 73 | if len(g.UserAgent) > 0 { 74 | g.UserAgent = r.Replace(g.UserAgent) 75 | } 76 | g.Url = r.Replace(g.Url) 77 | g.Output = r.Replace(g.Output) 78 | g.Body = []byte(r.Replace(string(g.Body))) 79 | } 80 | 81 | func (cmd *GurlCmd) Init() { 82 | if !cmd.ReadStream { 83 | cmd.Gurl.ParseInit() 84 | } 85 | if cmd.bench { 86 | cmd.report = NewReport(cmd.C, cmd.N, cmd.Gurl.Url) // todo 87 | if len(cmd.Duration) > 0 { 88 | if t := utils.ParseTime(cmd.Duration); int(t) > 0 { 89 | cmd.report.SetDuration(t) // todo 90 | } 91 | } 92 | cmd.report.StartReport() 93 | } 94 | } 95 | 96 | func (cmd *GurlCmd) WaitAll() { 97 | if cmd.report != nil { 98 | cmd.report.Wait() 99 | } 100 | 101 | close(cmd.Message.Out) 102 | } 103 | 104 | //todo 105 | func (cmd *GurlCmd) streamWriteJson(rsp *Response, err error, inJson map[string]string) { 106 | m := map[string]interface{}{} 107 | m["err"] = "" 108 | m["status_code"] = fmt.Sprintf("%d", rsp.StatusCode) 109 | m["body"] = string(rsp.Body) 110 | m["header"] = rsp.Header 111 | 112 | if err != nil { 113 | m["err"] = err.Error() 114 | } 115 | 116 | output.WriteStream(m, inJson, cmd.merge, cmd.Message) 117 | //todo 118 | } 119 | 120 | func (cmd *GurlCmd) SubProcess(work chan string) { 121 | g := *cmd.Gurl //这里是copy不是操作指针 122 | g0 := Gurl{Client: g.Client} 123 | g0.GurlCore = *CopyAndNew(&g.GurlCore) 124 | var inJson map[string]string 125 | 126 | for v := range work { 127 | if len(v) > 0 && v[0] == '{' { 128 | inJson = map[string]string{} 129 | g.GurlCore = *CopyAndNew(&g.GurlCore) 130 | 131 | g.FormCache = nil 132 | g.NotParseAt = nil 133 | 134 | parse(inJson, &g, v) 135 | g.ParseInit() 136 | //fmt.Printf("read work:%s\n", v) 137 | } 138 | 139 | if cmd.debug { 140 | fmt.Println("input data is:", v) 141 | fmt.Printf("g.FormCache.len(%d)\n", len(g.FormCache)) 142 | } 143 | 144 | taskNow := time.Now() 145 | rsp, err := g.Send() 146 | if cmd.writeStream { 147 | cmd.streamWriteJson(rsp, err, inJson) 148 | } 149 | 150 | if err != nil { 151 | if cmd.report != nil { 152 | cmd.report.AddErrNum() 153 | } else { 154 | CmdErr(err) 155 | } 156 | continue 157 | } 158 | 159 | if cmd.report != nil { 160 | cmd.report.Cal(taskNow, rsp) 161 | } 162 | 163 | if len(v) > 0 && v[0] == '{' { 164 | g = g0 165 | } 166 | } 167 | } 168 | 169 | func CmdErr(err error) { 170 | if err == nil { 171 | return 172 | } 173 | 174 | if errors.Is(err, syscall.ECONNREFUSED) { 175 | fmt.Printf("gurl: (7) couldn't connect to host\n") 176 | return 177 | } 178 | 179 | fmt.Printf("%s\n", err) 180 | } 181 | 182 | func httpEcho(addr string) { 183 | 184 | http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 185 | io.Copy(os.Stdout, req.Body) 186 | w.Header().Add("Server", "gurl-server") 187 | req.Body.Close() 188 | return 189 | }) 190 | 191 | fmt.Println(http.ListenAndServe(addr, nil)) 192 | } 193 | 194 | func toFlag(output, str string) (flag int) { 195 | 196 | if output != "stdout" && output != "stderr" { 197 | flag |= os.O_CREATE | os.O_RDWR 198 | } 199 | 200 | flags := strings.Split(str, "|") 201 | for _, v := range flags { 202 | switch v { 203 | case "create": 204 | flag |= os.O_CREATE 205 | case "append": 206 | flag |= os.O_APPEND 207 | case "line": 208 | flag |= ADD_LINE 209 | case "trunc": 210 | flag |= os.O_TRUNC 211 | } 212 | } 213 | 214 | return flag 215 | } 216 | 217 | func Main(message core.Message, argv0 string, argv []string) { 218 | command := flag.NewFlagSet(argv0, flag.ExitOnError) 219 | 220 | headers := command.StringSlice("H, header", []string{}, "Pass custom header LINE to server (H)") 221 | forms := command.StringSlice("F, form", []string{}, "Specify HTTP multipart POST data (H)") 222 | formStrings := command.StringSlice("form-string", []string{}, "Specify HTTP multipart POST data (H)") 223 | 224 | jfa := command.Opt("Jfa", "Specify HTTP multipart POST json data (H)"). 225 | Flags(flag.GreedyMode). 226 | NewStringSlice([]string{}) 227 | 228 | jfaStrings := command.Opt("Jfa-string", "Specify HTTP multipart POST json data (H)"). 229 | Flags(flag.GreedyMode). 230 | NewStringSlice([]string{}) 231 | 232 | query := command.Opt("q, query", "query string"). 233 | Flags(flag.GreedyMode). 234 | NewStringSlice([]string{}) 235 | 236 | outputFileName := command.String("o, output", "stdout", "Write to FILE instead of stdout") 237 | oflag := command.String("oflag", "", "Control the way you write(append|line|trunc)") 238 | method := command.String("X, request", "", "Specify request command to use") 239 | 240 | color := command.Opt("c, color", "Color highlighting").Flags(flag.PosixShort).NewBool(false) 241 | toJson := command.Opt("J", `Turn key:value into {"key": "value"})`). 242 | Flags(flag.GreedyMode). 243 | NewStringSlice([]string{}) 244 | 245 | URL := command.String("url", "", "Specify a URL to fetch") 246 | an := command.Int("an", 1, "Number of requests to perform") 247 | ac := command.Int("ac", 1, "Number of multiple requests to make") 248 | rate := command.Int("rate", 0, "Requests per second") 249 | bench := command.Bool("bench", false, "Run benchmarks test") 250 | conns := command.Int("conns", DefaultConnections, "Max open idle connections per target host") 251 | cpus := command.Int("cpus", 0, "Number of CPUs to use") 252 | listen := command.String("l", "", "Listen mode, HTTP echo server") 253 | data := command.String("d, data", "", "HTTP POST data") 254 | verbose := command.Opt("v, verbose", "Make the operation more talkative"). 255 | Flags(flag.PosixShort). 256 | NewBool(false) 257 | userAgent := command.String("A, user-agent", "gurl", "Send User-Agent STRING to server") 258 | duration := command.String("duration", "", "Duration of the test") 259 | connectTimeout := command.String("connect-timeout", "", "Maximum time allowed for connection") 260 | 261 | readStream := command.Bool("r, read-stream", false, "Read data from the stream") 262 | writeStream := command.Bool("w, write-stream", false, "Write data from the stream") 263 | merge := command.Bool("m, merge", false, "Combine the output results into the output") 264 | 265 | inputMode := command.Bool("I, input-model", false, "open input mode") 266 | inputRead := command.String("R, input-read", "", "open input file") 267 | inputFields := command.String("input-fields", " ", "sets the field separator") 268 | inputSetKey := command.String("skey, input-setkey", "", "Set a new name for the default key") 269 | 270 | outputMode := command.Bool("O, output-mode", false, "open output mode") 271 | outputKey := command.String("wkey, write-key", "", "Key that can be write") 272 | outputWrite := command.String("W, output-write", "", "open output file") 273 | 274 | debug := command.Bool("debug", false, "open debug mode") 275 | command.Author("guonaihong https://github.com/guonaihong/gurl") 276 | command.Parse(argv) 277 | 278 | if !*inputMode { 279 | if len(*inputRead) > 0 { 280 | *inputMode = true 281 | } 282 | } 283 | 284 | if *inputMode { 285 | input.Main(*inputRead, *inputFields, *inputSetKey, message) 286 | return 287 | } 288 | 289 | if *outputMode { 290 | output.WriteFile(*outputWrite, *outputKey, message) 291 | return 292 | } 293 | 294 | if *listen != "" { 295 | httpEcho(*listen) 296 | return 297 | } 298 | 299 | as := command.Args() 300 | Url := *URL 301 | if *URL == "" && len(as) == 0 && !*bench { 302 | command.Usage() 303 | return 304 | } 305 | 306 | if len(as) > 0 { 307 | Url = as[0] 308 | } 309 | 310 | Url = url2.ModifyUrl(Url) 311 | 312 | if *cpus > 0 { 313 | runtime.GOMAXPROCS(*cpus) 314 | } 315 | 316 | /* 317 | dialer := &net.Dialer{ 318 | Timeout: gurlib.ParseTime(*connectTimeout), 319 | } 320 | */ 321 | 322 | client := http.Client{ 323 | Transport: &http.Transport{ 324 | MaxIdleConnsPerHost: *conns, 325 | //Dial: dialer.Dial, 326 | }, 327 | Timeout: utils.ParseTime(*connectTimeout), 328 | } 329 | 330 | g := Gurl{ 331 | Client: &client, 332 | GurlCore: GurlCore{ 333 | Method: *method, 334 | FormData: *forms, 335 | Header: *headers, 336 | Output: *outputFileName, 337 | Json: *toJson, 338 | Jfa: *jfa, 339 | Url: Url, 340 | Flag: toFlag(*outputFileName, *oflag), 341 | Body: []byte(*data), 342 | Verbose: *verbose, 343 | UserAgent: *userAgent, 344 | Color: *color, 345 | Query: *query, 346 | }, 347 | } 348 | 349 | g.AddFormStr(*formStrings) 350 | g.AddJsonFormStr(*jfaStrings) 351 | 352 | cmd := GurlCmd{ 353 | Task: task.Task{ 354 | Duration: *duration, 355 | N: *an, 356 | Work: make(chan string, 1000), 357 | ReadStream: *readStream, 358 | Message: message, 359 | Rate: *rate, 360 | C: *ac, 361 | }, 362 | 363 | writeStream: *writeStream, 364 | merge: *merge, 365 | Gurl: &g, 366 | bench: *bench, 367 | debug: *debug, 368 | } 369 | 370 | cmd.Producer() 371 | 372 | if *bench { 373 | g.Output = "" 374 | } 375 | 376 | cmd.Task.Processer = &cmd 377 | cmd.Task.RunMain() 378 | } 379 | -------------------------------------------------------------------------------- /ghttp/ghttp_test.go: -------------------------------------------------------------------------------- 1 | package ghttp 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "syscall" 8 | "testing" 9 | ) 10 | 11 | func Test_Ghttp_CmdErr(t *testing.T) { 12 | _, err := http.Get("http://127.0.0.1:3333") 13 | assert.True(t, errors.Is(err, syscall.ECONNREFUSED)) 14 | } 15 | -------------------------------------------------------------------------------- /ghttp/report.go: -------------------------------------------------------------------------------- 1 | package ghttp 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | type result struct { 12 | time float64 13 | statusCode int 14 | } 15 | 16 | type Report struct { 17 | allResult chan result 18 | waitQuit chan struct{} 19 | quit chan struct{} 20 | allTimes []float64 21 | statusCodes map[int]int 22 | startNow time.Time 23 | addr string 24 | laddr string 25 | serverName string 26 | port string 27 | path string 28 | c int 29 | n int 30 | recvN int 31 | step int 32 | length int 33 | duration time.Duration 34 | doneNum int32 35 | weNum int32 36 | totalRead int32 37 | totalBody int32 38 | } 39 | 40 | func NewReport(c, n int, url string) *Report { 41 | step := 0 42 | if n > 150 { 43 | if step = n / 10; step < 100 { 44 | step = 100 45 | } 46 | } 47 | 48 | r := &Report{ 49 | allResult: make(chan result, 1000), 50 | quit: make(chan struct{}, 1), 51 | waitQuit: make(chan struct{}, 1), 52 | laddr: url, 53 | startNow: time.Now(), 54 | c: c, 55 | n: n, 56 | step: step, 57 | statusCodes: make(map[int]int, 2), 58 | } 59 | 60 | r.parseUrl() 61 | 62 | return r 63 | } 64 | 65 | func (r *Report) SetDuration(t time.Duration) { 66 | r.duration = t 67 | } 68 | 69 | func (r *Report) AddErrNum() { 70 | atomic.AddInt32(&r.weNum, 1) 71 | } 72 | 73 | func (r *Report) Cal(now time.Time, resp *Response) { 74 | if r.serverName == "" { 75 | r.serverName = resp.Header.Get("Server") 76 | } 77 | 78 | atomic.AddInt32(&r.doneNum, 1) 79 | r.calBody(resp) 80 | 81 | r.allResult <- result{ 82 | time: float64(time.Now().Sub(now)) / float64(time.Millisecond), 83 | statusCode: resp.StatusCode, 84 | } 85 | } 86 | 87 | func (r *Report) calBody(resp *Response) { 88 | 89 | bodyN := len(resp.Body) 90 | 91 | r.length = bodyN 92 | 93 | hN := len(resp.Status) 94 | hN += len(resp.Proto) 95 | hN += 1 //space 96 | hN += 2 //\r\n 97 | for k, v := range resp.Header { 98 | hN += len(k) 99 | 100 | for _, hv := range v { 101 | hN += len(hv) 102 | } 103 | hN += 2 //:space 104 | hN += 2 //\r\n 105 | } 106 | 107 | hN += 2 108 | 109 | atomic.AddInt32(&r.totalBody, int32(bodyN)) 110 | atomic.AddInt32(&r.totalRead, int32(hN)) 111 | atomic.AddInt32(&r.totalRead, int32(bodyN)) 112 | } 113 | 114 | func (r *Report) report() { 115 | 116 | timeTake := time.Now().Sub(r.startNow) 117 | allTimes := r.allTimes 118 | 119 | fmt.Printf("\n\n") 120 | fmt.Printf("Server Software: %s\n", r.serverName) 121 | fmt.Printf("Server Hostname: %s\n", r.addr) 122 | fmt.Printf("Server Port: %s\n", r.port) 123 | fmt.Printf("\n") 124 | 125 | fmt.Printf("Document Path: %s\n", r.path) 126 | fmt.Printf("Document Length: %d bytes\n", r.length) 127 | fmt.Printf("\n") 128 | 129 | fmt.Printf("Status Codes: ") 130 | for k, v := range r.statusCodes { 131 | fmt.Printf(" %d:%d ", k, v) 132 | } 133 | fmt.Printf("[code:count]\n") 134 | 135 | fmt.Printf("Concurrency Level: %d\n", r.c) 136 | fmt.Printf("Time taken for tests: %.3f seconds\n", timeTake.Seconds()) 137 | fmt.Printf("Complete requests: %v\n", r.recvN) 138 | fmt.Printf("Failed requests: %v\n", r.doneNum-int32(r.recvN)) 139 | if r.weNum > 0 { 140 | fmt.Printf("Write errors: %v\n", r.weNum) 141 | } 142 | 143 | fmt.Printf("Total transferred: %d bytes\n", r.totalRead) 144 | fmt.Printf("HTML transferred: %v bytes\n", r.totalBody) 145 | fmt.Printf("Requests per second: %.2f [#/sec] (mean)\n", 146 | float64(r.doneNum)/timeTake.Seconds()) 147 | fmt.Printf("Time per request: %.3f [ms] (mean)\n", 148 | float64(r.c)*float64(timeTake)/float64(time.Millisecond)/float64(r.doneNum)) 149 | fmt.Printf("Time per request: %.3f [ms] (mean, across all concurrent requests)\n", 150 | float64(timeTake)/float64(time.Millisecond)/float64(r.doneNum)) 151 | fmt.Printf("Transfer rate: %.2f [Kbytes/sec] received\n", 152 | float64(r.totalRead)/float64(1000)/timeTake.Seconds()) 153 | 154 | sort.Slice(allTimes, func(i, j int) bool { 155 | return allTimes[i] < allTimes[j] 156 | }) 157 | 158 | if len(allTimes) > 1 { 159 | fmt.Printf("Percentage of the requests served within a certain time (ms)\n") 160 | fmt.Printf(" 50%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.5)]) 161 | fmt.Printf(" 66%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.66)]) 162 | fmt.Printf(" 75%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.75)]) 163 | fmt.Printf(" 80%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.80)]) 164 | fmt.Printf(" 90%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.90)]) 165 | fmt.Printf(" 95%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.95)]) 166 | fmt.Printf(" 98%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.98)]) 167 | fmt.Printf(" 99%% %0.2fms\n", allTimes[int(float64(len(allTimes))*0.99)]) 168 | fmt.Printf(" 100%% %0.2fms\n", allTimes[len(allTimes)-1]) 169 | } 170 | } 171 | 172 | func (r *Report) parseUrl() { 173 | 174 | addr := r.laddr 175 | if pos := strings.Index(addr, "http://"); pos != -1 { 176 | addr = addr[pos+7:] 177 | } 178 | 179 | if pos := strings.Index(addr, "/"); pos != -1 { 180 | r.path = addr[pos:] 181 | addr = addr[:pos] 182 | } 183 | 184 | if pos := strings.Index(addr, ":"); pos != -1 { 185 | r.port = addr[pos+1:] 186 | addr = addr[:pos] 187 | } 188 | 189 | fmt.Printf("Benchmarking %s (be patient)\n", addr) 190 | } 191 | 192 | func genTimeStr(now time.Time) string { 193 | year, month, day := now.Date() 194 | hour, min, sec := now.Clock() 195 | 196 | return fmt.Sprintf("%4d-%02d-%02d %02d:%02d:%02d.%06d", 197 | year, 198 | month, 199 | day, 200 | hour, 201 | min, 202 | sec, 203 | now.Nanosecond()/1e3, 204 | ) 205 | } 206 | 207 | func (r *Report) StartReport() { 208 | go func() { 209 | defer func() { 210 | fmt.Printf(" Finished %15d requests\n", r.recvN) 211 | r.waitQuit <- struct{}{} 212 | }() 213 | 214 | if r.step > 0 { 215 | for { 216 | select { 217 | case _, ok := <-r.quit: 218 | if !ok { 219 | return 220 | } 221 | case v := <-r.allResult: 222 | r.recvN++ 223 | if r.step > 0 && r.recvN%r.step == 0 { 224 | now := time.Now() 225 | 226 | fmt.Printf(" Opened %15d connections: [%s]\n", 227 | r.recvN, genTimeStr(now)) 228 | } 229 | 230 | r.allTimes = append(r.allTimes, v.time) 231 | r.statusCodes[v.statusCode]++ 232 | } 233 | } 234 | } else { 235 | begin := time.Now() 236 | interval := r.duration / 10 237 | 238 | if interval == 0 { 239 | interval = time.Second 240 | } 241 | nTick := time.NewTicker(interval) 242 | count := 1 243 | for { 244 | select { 245 | case <-nTick.C: 246 | now := time.Now() 247 | 248 | fmt.Printf(" Completed %15d requests [%s]\n", 249 | r.recvN, genTimeStr(now)) 250 | 251 | count++ 252 | next := begin.Add(time.Duration(count * int(interval))) 253 | if newInterval := next.Sub(time.Now()); newInterval > 0 { 254 | nTick = time.NewTicker(newInterval) 255 | } else { 256 | nTick = time.NewTicker(time.Millisecond * 100) 257 | } 258 | case v, ok := <-r.allResult: 259 | if !ok { 260 | return 261 | } 262 | 263 | r.recvN++ 264 | r.allTimes = append(r.allTimes, v.time) 265 | r.statusCodes[v.statusCode]++ 266 | case _, ok := <-r.quit: 267 | if !ok { 268 | return 269 | } 270 | } 271 | } 272 | } 273 | 274 | }() 275 | 276 | } 277 | 278 | func (r *Report) Wait() { 279 | close(r.quit) 280 | <-r.waitQuit 281 | r.report() 282 | } 283 | -------------------------------------------------------------------------------- /ghttp/req.go: -------------------------------------------------------------------------------- 1 | package ghttp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/TylerBrock/colorjson" 8 | "github.com/fatih/color" 9 | color2 "github.com/guonaihong/gurl/color" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "mime/multipart" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "path/filepath" 18 | "strconv" 19 | "strings" 20 | ) 21 | 22 | type FormVal struct { 23 | Tag string 24 | Fname string 25 | Body []byte 26 | } 27 | 28 | const ADD_LINE = 1 << 30 29 | 30 | type GurlCore struct { 31 | Method string `json:"method,omitempty"` 32 | 33 | Json []string `json:"Json,omitempty"` 34 | FormData []string `json:"FormData,omitempty"` 35 | Header []string `json:"Header,omitempty"` // http header 36 | Url string `json:"url,omitempty"` 37 | Output string `json:"o,omitempty"` 38 | 39 | Jfa []string `json:"Jfa,omitempty"` 40 | 41 | FormCache []FormVal `json:"-"` 42 | 43 | Body []byte `json:"body,omitempty"` 44 | Flag int 45 | Verbose bool `json:"-"` 46 | UserAgent string 47 | Color bool 48 | Query []string 49 | NotParseAt map[string]struct{} 50 | } 51 | 52 | func CopyAndNew(g *GurlCore) *GurlCore { 53 | return &GurlCore{ 54 | Method: g.Method, 55 | Json: append([]string{}, g.Json...), 56 | FormData: append([]string{}, g.FormData...), 57 | Header: append([]string{}, g.Header...), 58 | Url: g.Url, 59 | Output: g.Output, 60 | Jfa: append([]string{}, g.Jfa...), 61 | FormCache: append([]FormVal{}, g.FormCache...), 62 | Body: append([]byte{}, g.Body...), 63 | Flag: g.Flag, 64 | Verbose: g.Verbose, 65 | UserAgent: g.UserAgent, 66 | } 67 | } 68 | 69 | func (g *GurlCore) AddFormStr(FormData []string) { 70 | if len(FormData) == 0 { 71 | return 72 | } 73 | 74 | if g.NotParseAt == nil { 75 | g.NotParseAt = make(map[string]struct{}, 10) 76 | } 77 | 78 | oldLen := len(g.FormData) 79 | for i := 0; i < len(FormData); i++ { 80 | g.NotParseAt["FormData"+fmt.Sprintf("%d", oldLen+i)] = struct{}{} 81 | } 82 | 83 | g.FormData = append(g.FormData, FormData...) 84 | } 85 | 86 | func (g *GurlCore) AddJsonFormStr(Jfa []string) { 87 | if len(Jfa) == 0 { 88 | return 89 | } 90 | 91 | if g.NotParseAt == nil { 92 | g.NotParseAt = make(map[string]struct{}, 10) 93 | } 94 | oldLen := len(g.Jfa) 95 | for i := 0; i < len(Jfa); i++ { 96 | g.NotParseAt["Jfa"+fmt.Sprintf("%d", oldLen+i)] = struct{}{} 97 | } 98 | 99 | g.Jfa = append(g.Jfa, Jfa...) 100 | } 101 | 102 | func (g *GurlCore) formNotParseAt(idx int) bool { 103 | _, ok := g.NotParseAt[fmt.Sprintf("FormData%d", idx)] 104 | return ok 105 | } 106 | 107 | func (g *GurlCore) jsonFormNotParseAt(idx int) bool { 108 | _, ok := g.NotParseAt[fmt.Sprintf("Jfa%d", idx)] 109 | return ok 110 | } 111 | 112 | func parseVal(bodyJson map[string]interface{}, key, val string, notParseAt bool) { 113 | if val == "{}" { 114 | bodyJson[key] = map[string]interface{}{} 115 | return 116 | } 117 | 118 | f, err := strconv.ParseFloat(val, 0) 119 | if err == nil { 120 | bodyJson[key] = f 121 | return 122 | } 123 | 124 | i, err := strconv.ParseInt(val, 0, 0) 125 | if err == nil { 126 | bodyJson[key] = i 127 | return 128 | } 129 | 130 | b, err := strconv.ParseBool(val) 131 | if err == nil { 132 | bodyJson[key] = b 133 | return 134 | } 135 | 136 | bodyJson[key] = parseAt(val, notParseAt) 137 | } 138 | 139 | func parseVal2(bodyJson map[string]interface{}, key, val string, notParseAt bool) { 140 | bodyJson[key] = parseAt(val, notParseAt) 141 | } 142 | 143 | func toJson(Json []string, notParseAts []bool, bodyJson map[string]interface{}) { 144 | for j, v := range Json { 145 | pos := strings.Index(v, ":") 146 | if pos == -1 { 147 | bodyJson[v] = "" 148 | continue 149 | } 150 | 151 | key := v[:pos] 152 | val := v[pos+1:] 153 | 154 | notParseAt := false 155 | if len(notParseAts) > 0 { 156 | notParseAt = notParseAts[j] 157 | } 158 | 159 | if pos := strings.Index(key, "."); pos != -1 { 160 | keys := strings.Split(key, ".") 161 | 162 | parseValfn := parseVal2 163 | if strings.HasPrefix(val, "=") { 164 | val = val[1:] 165 | parseValfn = parseVal 166 | } 167 | 168 | type jsonObj map[string]interface{} 169 | 170 | curMap := bodyJson 171 | 172 | for i, v := range keys { 173 | if len(keys)-1 == i { 174 | parseValfn(curMap, v, val, notParseAt) 175 | break 176 | } 177 | 178 | vv, ok := curMap[v] 179 | if !ok { 180 | vv = jsonObj{} 181 | curMap[v] = vv 182 | } 183 | 184 | curMap = vv.(jsonObj) 185 | 186 | } 187 | continue 188 | } 189 | 190 | if len(val) == 0 { 191 | parseVal2(bodyJson, key, "", notParseAt) 192 | continue 193 | } 194 | 195 | if val[0] != '=' { 196 | parseVal2(bodyJson, key, val, notParseAt) 197 | continue 198 | } 199 | 200 | if len(key) == 1 { 201 | continue 202 | } 203 | 204 | val = val[1:] 205 | parseVal(bodyJson, key, val, notParseAt) 206 | 207 | } 208 | } 209 | 210 | func (g *GurlCore) form(FormData []string, fm *[]FormVal) { 211 | 212 | fileds := [2]string{} 213 | formVals := []FormVal{} 214 | 215 | for k, v := range FormData { 216 | 217 | fileds[0], fileds[1] = "", "" 218 | 219 | pos := strings.Index(v, "=") 220 | if pos == -1 { 221 | continue 222 | } 223 | 224 | fileds[0], fileds[1] = v[:pos], v[pos+1:] 225 | 226 | if !g.formNotParseAt(k) && strings.HasPrefix(fileds[1], "@") { 227 | fname := fileds[1][1:] 228 | 229 | fd, err := os.Open(fname) 230 | if err != nil { 231 | log.Fatalf("open file fail:%v\n", err) 232 | } 233 | 234 | body, err2 := ioutil.ReadAll(fd) 235 | if err != nil { 236 | log.Fatalf("read body fail:%v\n", err2) 237 | } 238 | 239 | formVals = append(formVals, FormVal{Tag: fileds[0], Fname: fname, Body: body}) 240 | 241 | fd.Close() 242 | } else { 243 | formVals = append(formVals, FormVal{Tag: fileds[0], Body: []byte(fileds[1])}) 244 | } 245 | 246 | //FormData[i] = fileds[0] 247 | } 248 | 249 | *fm = append(*fm, formVals...) 250 | } 251 | 252 | func (g *GurlCore) jsonFromAppend(JF []string, fm *[]FormVal) { 253 | 254 | JFMap := map[string][]string{} 255 | notParseAt := map[string][]bool{} 256 | fileds := [2]string{} 257 | formVals := []FormVal{} 258 | 259 | for i, v := range JF { 260 | 261 | fileds[0], fileds[1] = "", "" 262 | 263 | pos := strings.Index(v, "=") 264 | if pos == -1 { 265 | continue 266 | } 267 | 268 | fileds[0], fileds[1] = v[:pos], v[pos+1:] 269 | 270 | v, _ := JFMap[fileds[0]] 271 | JFMap[fileds[0]] = append(v, fileds[1]) 272 | notParseAt[fileds[0]] = append(notParseAt[fileds[0]], g.jsonFormNotParseAt(i)) 273 | 274 | } 275 | 276 | for k, v := range JFMap { 277 | 278 | bodyJson := map[string]interface{}{} 279 | 280 | toJson(v, notParseAt[k], bodyJson) 281 | 282 | body, err := json.Marshal(&bodyJson) 283 | 284 | if err != nil { 285 | log.Fatalf("marsahl fail:%s\n", err) 286 | return 287 | } 288 | 289 | formVals = append(formVals, FormVal{Tag: k, Body: body}) 290 | } 291 | 292 | *fm = append(*fm, formVals...) 293 | } 294 | 295 | func parseAt(data string, notParseAt bool) interface{} { 296 | if !notParseAt && strings.HasPrefix(data, "@") { 297 | body, err := ioutil.ReadFile(data[1:]) 298 | if err != nil { 299 | log.Fatalf("%v\n", err) 300 | return "" 301 | } 302 | return string(body) 303 | } 304 | 305 | if len(data) >= 2 { 306 | b := []byte(data) 307 | if json.Valid(b) { 308 | 309 | return json.RawMessage(b) 310 | } 311 | } 312 | 313 | return data 314 | } 315 | 316 | func ParseBody(Body *[]byte) { 317 | if bytes.HasPrefix(*Body, []byte("@")) { 318 | body, err := ioutil.ReadFile(string((*Body)[1:])) 319 | if err != nil { 320 | log.Fatalf("%v\n", err) 321 | return 322 | } 323 | 324 | *Body = body 325 | } 326 | } 327 | 328 | func (g *GurlCore) ParseInit() { 329 | 330 | if len(g.Body) > 0 { 331 | ParseBody(&g.Body) 332 | } 333 | 334 | if len(g.Json) > 0 { 335 | bodyJson := map[string]interface{}{} 336 | 337 | toJson(g.Json, nil, bodyJson) 338 | 339 | body, err := json.Marshal(&bodyJson) 340 | if err != nil { 341 | log.Fatalf("marsahl fail:%s\n", err) 342 | return 343 | } 344 | 345 | g.Body = body 346 | } 347 | 348 | //g.FormCache = []FormVal{} 349 | 350 | if len(g.Jfa) > 0 { 351 | g.jsonFromAppend(g.Jfa, &g.FormCache) 352 | } 353 | 354 | if len(g.FormData) > 0 { 355 | g.form(g.FormData, &g.FormCache) 356 | } 357 | } 358 | 359 | func (g *GurlCore) MultipartNew() (*http.Request, chan error, error) { 360 | 361 | var err error 362 | 363 | pipeReader, pipeWriter := io.Pipe() 364 | errChan := make(chan error, 10) 365 | writer := multipart.NewWriter(pipeWriter) 366 | 367 | go func() { 368 | 369 | defer pipeWriter.Close() 370 | 371 | var part io.Writer 372 | 373 | for _, fv := range g.FormCache { 374 | 375 | k := fv.Tag 376 | 377 | fname := fv.Fname 378 | 379 | if len(fname) == 0 { 380 | part, err = writer.CreateFormField(k) 381 | if err != nil { 382 | fmt.Printf("%s\n", err) 383 | continue 384 | } 385 | part.Write([]byte(fv.Body)) 386 | continue 387 | } 388 | 389 | body := bytes.NewBuffer(fv.Body) 390 | 391 | part, err = writer.CreateFormFile(k, filepath.Base(fname)) 392 | if err != nil { 393 | errChan <- err 394 | return 395 | } 396 | 397 | if _, err = io.Copy(part, body); err != nil { 398 | errChan <- err 399 | return 400 | } 401 | } 402 | 403 | errChan <- writer.Close() 404 | 405 | }() 406 | 407 | var req *http.Request 408 | req, err = http.NewRequest(g.Method, g.Url+g.addQueryString(), pipeReader) 409 | if err != nil { 410 | fmt.Printf("http neq request:%s\n", err) 411 | return nil, errChan, err 412 | } 413 | 414 | req.Header.Add("Content-Type", writer.FormDataContentType()) 415 | 416 | return req, errChan, nil 417 | } 418 | 419 | func (g *GurlCore) addQueryString() string { 420 | if len(g.Query) == 0 { 421 | return "" 422 | } 423 | 424 | u := url.Values{} 425 | for _, g := range g.Query { 426 | qs := strings.Split(g, "=") 427 | if len(qs) != 2 { 428 | continue 429 | } 430 | 431 | u.Add(qs[0], qs[1]) 432 | } 433 | 434 | s := u.Encode() 435 | if len(u) > 0 { 436 | return "?" + s 437 | } 438 | 439 | return "" 440 | } 441 | 442 | func (g *GurlCore) HeadersAdd(req *http.Request) { 443 | 444 | for _, v := range g.Header { 445 | 446 | headers := strings.Split(v, ":") 447 | 448 | if len(headers) != 2 { 449 | continue 450 | } 451 | 452 | headers[0] = strings.TrimSpace(headers[0]) 453 | headers[1] = strings.TrimSpace(headers[1]) 454 | 455 | req.Header.Add(headers[0], headers[1]) 456 | } 457 | 458 | if len(g.UserAgent) > 0 { 459 | req.Header.Set("User-Agent", g.UserAgent) 460 | } 461 | req.Header.Set("Accept", "*/*") 462 | req.Header.Set("Host", req.URL.Host) 463 | 464 | } 465 | 466 | func (g *GurlCore) writeHead(rsp *Response, w io.Writer) { 467 | 468 | if !g.Verbose { 469 | return 470 | } 471 | 472 | keyStart, keyEnd, valStart, valEnd := color2.NewKeyVal(g.Color) 473 | 474 | if rsp.Req != nil { 475 | req := rsp.Req 476 | path := "/" 477 | if len(req.URL.Path) > 0 { 478 | path = req.URL.RequestURI() 479 | } 480 | 481 | fmt.Fprintf(w, "> %s %s %s\r\n", req.Method, path, req.Proto) 482 | for k, v := range req.Header { 483 | fmt.Fprintf(w, "%s> %s%s: %s%s%s\r\n", keyStart, k, keyEnd, 484 | valStart, strings.Join(v, ","), valEnd) 485 | } 486 | 487 | fmt.Fprint(w, ">\r\n") 488 | fmt.Fprint(w, "\n") 489 | } 490 | 491 | if g.Color { 492 | all, colorOk := colorBody(w.(*os.File), g.Body) 493 | w.Write(all) 494 | if colorOk { 495 | fmt.Fprintf(w, "\r\n") 496 | } 497 | } 498 | 499 | fmt.Fprintf(w, "< %s %s\r\n", rsp.Proto, rsp.Status) 500 | 501 | for k, v := range rsp.Header { 502 | fmt.Fprintf(w, "%s< %s%s: %s%s%s\r\n", keyStart, k, keyEnd, 503 | valStart, strings.Join(v, ","), valEnd) 504 | } 505 | } 506 | 507 | func colorBody(fd *os.File, all []byte) ([]byte, bool) { 508 | var obj map[string]interface{} 509 | if len(all) > 0 && all[0] == '{' { 510 | err := json.Unmarshal(all, &obj) 511 | if err != nil { 512 | return all, false 513 | } 514 | 515 | f := colorjson.NewFormatter() 516 | f.KeyColor = color.New(color.FgHiBlue) 517 | f.Indent = 2 518 | all, _ = f.Marshal(obj) 519 | all = append(all, '\n') 520 | } 521 | 522 | return all, true 523 | } 524 | 525 | func (g *GurlCore) writeBytes(rsp *Response) (err error) { 526 | all := rsp.Body 527 | var fd *os.File 528 | 529 | switch g.Output { 530 | case "stdout": 531 | fd = os.Stdout 532 | case "stderr": 533 | fd = os.Stderr 534 | default: 535 | fd, err = os.OpenFile(g.Output, g.Flag, 0644) 536 | if err != nil { 537 | return 538 | } 539 | } 540 | 541 | var colorOk bool 542 | if g.Color { 543 | all, colorOk = colorBody(fd, all) 544 | } 545 | 546 | if fd != os.Stdout || fd != os.Stderr { 547 | defer fd.Close() 548 | } 549 | 550 | // write http head 551 | g.writeHead(rsp, fd) 552 | 553 | if g.Flag&ADD_LINE > 0 { 554 | out := &bytes.Buffer{} 555 | out.Write(all) 556 | out.Write([]byte("\n")) 557 | fd.Write(out.Bytes()) 558 | 559 | return 560 | } 561 | 562 | if colorOk { 563 | fd.Write([]byte("\n\n")) 564 | } 565 | fd.Write(all) 566 | return nil 567 | } 568 | 569 | type Gurl struct { 570 | *http.Client `json:"-"` 571 | 572 | GurlCore 573 | } 574 | 575 | type Response struct { 576 | StatusCode int `json:"status_code"` 577 | Err string `json:"err"` 578 | Body []byte `json:"body"` 579 | Status string `json:"status"` 580 | Proto string `json:"proto"` 581 | Header http.Header `json:"header"` 582 | Req *http.Request 583 | } 584 | 585 | func (g *Gurl) Send() (*Response, error) { 586 | return g.send(g.Client) 587 | } 588 | 589 | func (g *GurlCore) send(client *http.Client) (*Response, error) { 590 | rsp, err := g.sendExec(client) 591 | if rsp.Err == "" && len(g.Output) > 0 { 592 | g.writeBytes(rsp) 593 | } 594 | return rsp, err 595 | } 596 | 597 | func rspCopy(dst *Response, src *http.Response) { 598 | dst.StatusCode = src.StatusCode 599 | dst.Status = src.Status 600 | dst.Proto = src.Proto 601 | dst.Header = src.Header 602 | dst.Req = src.Request 603 | } 604 | 605 | func (g *GurlCore) GetOrBodyExec(client *http.Client) (*Response, error) { 606 | var rsp *http.Response 607 | var req *http.Request 608 | var err error 609 | 610 | body := bytes.NewBuffer(g.Body) 611 | req, err = http.NewRequest(g.Method, g.Url+g.addQueryString(), body) 612 | gurlRsp := &Response{} 613 | if err != nil { 614 | return &Response{Err: err.Error()}, err 615 | } 616 | 617 | g.HeadersAdd(req) 618 | 619 | rsp, err = client.Do(req) 620 | if err != nil { 621 | return &Response{Err: err.Error()}, err 622 | } 623 | 624 | defer rsp.Body.Close() 625 | gurlRsp.Body, err = ioutil.ReadAll(rsp.Body) 626 | if err != nil { 627 | return &Response{Err: err.Error()}, err 628 | } 629 | 630 | rspCopy(gurlRsp, rsp) 631 | return gurlRsp, nil 632 | } 633 | 634 | func (g *GurlCore) MultipartExec(client *http.Client) (*Response, error) { 635 | 636 | var rsp *http.Response 637 | var req *http.Request 638 | 639 | req, errChan, err := g.MultipartNew() 640 | if err != nil { 641 | fmt.Printf("multipart new fail:%s\n", err) 642 | return &Response{Err: err.Error()}, err 643 | } 644 | 645 | gurlRsp := &Response{} 646 | g.HeadersAdd(req) 647 | 648 | rsp, err = client.Do(req) 649 | if err != nil { 650 | fmt.Printf("client do fail:%s:URL(%s)\n", err, req.URL) 651 | return &Response{Err: err.Error()}, err 652 | } 653 | 654 | defer rsp.Body.Close() 655 | 656 | if err := <-errChan; err != nil { 657 | fmt.Printf("error:%s\n", err) 658 | return &Response{Err: err.Error()}, err 659 | } 660 | 661 | gurlRsp.Body, err = ioutil.ReadAll(rsp.Body) 662 | if err != nil { 663 | fmt.Printf("ioutil.Read:%s\n", err) 664 | return &Response{Err: err.Error()}, err 665 | } 666 | 667 | rspCopy(gurlRsp, rsp) 668 | return gurlRsp, nil 669 | } 670 | 671 | func (g *GurlCore) SendExec(client *http.Client) (*Response, error) { 672 | return g.sendExec(client) 673 | } 674 | 675 | func (g *GurlCore) sendExec(client *http.Client) (*Response, error) { 676 | if len(g.Method) == 0 { 677 | g.Method = "GET" 678 | if len(g.FormCache) > 0 || len(g.Body) > 0 { 679 | g.Method = "POST" 680 | } 681 | } 682 | 683 | if len(g.FormCache) > 0 { 684 | return g.MultipartExec(client) 685 | } 686 | 687 | // 创建http.NewRequest地方有两个,todo归一化 688 | return g.GetOrBodyExec(client) 689 | } 690 | -------------------------------------------------------------------------------- /ghttp/req_test.go: -------------------------------------------------------------------------------- 1 | package ghttp 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | // TODO 更多功能测试代码 12 | type testHeader struct { 13 | H1 string `header:"h1"` 14 | H2 string `header:"h2"` 15 | } 16 | 17 | func Test_Req_Header(t *testing.T) { 18 | router := func() *gin.Engine { 19 | router := gin.Default() 20 | 21 | need := testHeader{H1: "v1", H2: "v2"} 22 | got := testHeader{} 23 | 24 | router.GET("/test.header", func(c *gin.Context) { 25 | err := c.ShouldBindHeader(&got) 26 | assert.NoError(t, err) 27 | assert.Equal(t, need, got) 28 | }) 29 | 30 | return router 31 | }() 32 | 33 | ts := httptest.NewServer(http.HandlerFunc(router.ServeHTTP)) 34 | 35 | type testHeader struct { 36 | Sid string `header:"sid"` 37 | Code int 38 | } 39 | 40 | g := Gurl{Client: http.DefaultClient} 41 | 42 | g.Color = true 43 | g.GurlCore.Method = "GET" 44 | g.GurlCore.Header = []string{"h1:v1", "h2:v2"} 45 | g.GurlCore.Url = ts.URL + "/test.header" 46 | 47 | rsp, err := g.Send() 48 | 49 | assert.NoError(t, err) 50 | assert.Equal(t, rsp.StatusCode, 200) 51 | } 52 | -------------------------------------------------------------------------------- /ghttp/url/url.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func ModifyUrl(u string) string { 8 | if len(u) == 0 { 9 | return u 10 | } 11 | 12 | if len(u) > 0 && u[0] == ':' { 13 | return "http://127.0.0.1" + u 14 | } 15 | 16 | if len(u) > 0 && u[0] == '/' { 17 | return "http://127.0.0.1:80" + u 18 | } 19 | 20 | if !strings.HasPrefix(u, "http") { 21 | return "http://" + u 22 | } 23 | 24 | return u 25 | } 26 | -------------------------------------------------------------------------------- /gurl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/guonaihong/flag" 5 | "github.com/guonaihong/gurl/ghttp" 6 | "github.com/guonaihong/gurl/pipe" 7 | "github.com/guonaihong/gurl/ws" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | parent := flag.NewParentCommand(os.Args[0]) 13 | 14 | parent.SubCommand("http", "Use the http subcommand", func() { 15 | pipe.Main(os.Args[0], parent.Args(), ghttp.Main) 16 | }) 17 | 18 | parent.SubCommand("ws, websocket", "Use the websocket subcommand", func() { 19 | pipe.Main(os.Args[0], parent.Args(), ws.Main) 20 | }) 21 | 22 | /* 23 | parent.SubCommand("tcp, udp", "Use the tcp or udp subcommand", func() { 24 | pipe.Main(os.Args[0], parent.Args(), conn.Main) 25 | }) 26 | */ 27 | 28 | parent.Parse(os.Args[1:]) 29 | } 30 | -------------------------------------------------------------------------------- /gurl_vs_ab.md: -------------------------------------------------------------------------------- 1 | #### 简介 2 | gurl -bench 模式与ab命令横向对比评测 3 | 4 | #### Documentation 5 | * [Chinese](./gurl_vs_ab_en.md) 6 | 7 | * 准备 8 | ``` bash 9 | # 起动gurl自带http echo服务 10 | gurl -echo :12345 11 | ``` 12 | * gurl 13 | ``` 14 | gurl -ac 21 -an 1000000 -bench :12345 15 | 16 | Benchmarking 127.0.0.1 (be patient) 17 | Completed 100000 requests 18 | Completed 200000 requests 19 | Completed 300000 requests 20 | Completed 400000 requests 21 | Completed 500000 requests 22 | Completed 600000 requests 23 | Completed 700000 requests 24 | Completed 800000 requests 25 | Completed 900000 requests 26 | Completed 1000000 requests 27 | Finished 1000000 requests 28 | 29 | 30 | Server Software: gurl-server 31 | Server Hostname: 32 | Server Port: 12345 33 | 34 | Document Path: 35 | Document Length: 0 bytes 36 | 37 | Concurrency Level: 21 38 | Time taken for tests: 7.708 seconds 39 | Complete requests: 1000000 40 | Failed requests: 0 41 | Total transferred: 137000000 bytes 42 | HTML transferred: 0 bytes 43 | Requests per second: 129741.42 [#/sec] (mean) 44 | Time per request: 0.162 [ms] (mean) 45 | Time per request: 0.008 [ms] (mean, across all concurrent requests) 46 | Transfer rate: 17774.57 [Kbytes/sec] received 47 | Percentage of the requests served within a certain time (ms) 48 | 50% 0 49 | 66% 0 50 | 75% 0 51 | 80% 0 52 | 90% 0 53 | 95% 0 54 | 98% 0 55 | 99% 0 56 | 100% 40 57 | 58 | ``` 59 | 60 | * ab 61 | ``` 62 | This is ApacheBench, Version 2.3 <$Revision: 1706008 $> 63 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 64 | Licensed to The Apache Software Foundation, http://www.apache.org/ 65 | 66 | Benchmarking 127.0.01 (be patient) 67 | Completed 100000 requests 68 | Completed 200000 requests 69 | Completed 300000 requests 70 | Completed 400000 requests 71 | Completed 500000 requests 72 | Completed 600000 requests 73 | Completed 700000 requests 74 | Completed 800000 requests 75 | Completed 900000 requests 76 | Completed 1000000 requests 77 | Finished 1000000 requests 78 | 79 | 80 | Server Software: gurl-server 81 | Server Hostname: 127.0.01 82 | Server Port: 12345 83 | 84 | Document Path: / 85 | Document Length: 0 bytes 86 | 87 | Concurrency Level: 21 88 | Time taken for tests: 33.300 seconds 89 | Complete requests: 1000000 90 | Failed requests: 0 91 | Total transferred: 137000000 bytes 92 | HTML transferred: 0 bytes 93 | Requests per second: 30029.76 [#/sec] (mean) 94 | Time per request: 0.699 [ms] (mean) 95 | Time per request: 0.033 [ms] (mean, across all concurrent requests) 96 | Transfer rate: 4017.65 [Kbytes/sec] received 97 | 98 | Connection Times (ms) 99 | min mean[+/-sd] median max 100 | Connect: 0 0 0.1 0 3 101 | Processing: 0 0 0.4 0 212 102 | Waiting: 0 0 0.4 0 212 103 | Total: 0 1 0.4 1 212 104 | 105 | Percentage of the requests served within a certain time (ms) 106 | 50% 1 107 | 66% 1 108 | 75% 1 109 | 80% 1 110 | 90% 1 111 | 95% 1 112 | 98% 1 113 | 99% 1 114 | 100% 212 (longest request) 115 | ``` 116 | 117 | * 结论 118 | gurl 每秒可以发的消息数(12w/s)比ab(3w/s)命令多太数。核心数越多,gurl比ab的性能就越高。 119 | gurl比ab快的秘密,ab只用了单个线程压测,特别依赖cpu主频,主频快的cpu跑得才快。 120 | -------------------------------------------------------------------------------- /gurl_vs_ab_en.md: -------------------------------------------------------------------------------- 1 | #### Introduction 2 | "Gurl-bench" mode and "ab" command horizontal comparison evaluation. 3 | 4 | #### Documentation 5 | * [Chinese](./gurl_vs_ab.md) 6 | 7 | * Ready 8 | ``` bash 9 | # Start the "http echo" echo service provided by gurl 10 | 11 | gurl -echo :12345 12 | ``` 13 | * gurl 14 | ``` 15 | gurl -ac 21 -an 1000000 -bench :12345 16 | 17 | Benchmarking 127.0.0.1 (be patient) 18 | Completed 100000 requests 19 | Completed 200000 requests 20 | Completed 300000 requests 21 | Completed 400000 requests 22 | Completed 500000 requests 23 | Completed 600000 requests 24 | Completed 700000 requests 25 | Completed 800000 requests 26 | Completed 900000 requests 27 | Completed 1000000 requests 28 | Finished 1000000 requests 29 | 30 | 31 | Server Software: gurl-server 32 | Server Hostname: 33 | Server Port: 12345 34 | 35 | Document Path: 36 | Document Length: 0 bytes 37 | 38 | Concurrency Level: 21 39 | Time taken for tests: 7.708 seconds 40 | Complete requests: 1000000 41 | Failed requests: 0 42 | Total transferred: 137000000 bytes 43 | HTML transferred: 0 bytes 44 | Requests per second: 129741.42 [#/sec] (mean) 45 | Time per request: 0.162 [ms] (mean) 46 | Time per request: 0.008 [ms] (mean, across all concurrent requests) 47 | Transfer rate: 17774.57 [Kbytes/sec] received 48 | Percentage of the requests served within a certain time (ms) 49 | 50% 0 50 | 66% 0 51 | 75% 0 52 | 80% 0 53 | 90% 0 54 | 95% 0 55 | 98% 0 56 | 99% 0 57 | 100% 40 58 | 59 | ``` 60 | 61 | * ab 62 | ``` 63 | This is ApacheBench, Version 2.3 <$Revision: 1706008 $> 64 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 65 | Licensed to The Apache Software Foundation, http://www.apache.org/ 66 | 67 | Benchmarking 127.0.01 (be patient) 68 | Completed 100000 requests 69 | Completed 200000 requests 70 | Completed 300000 requests 71 | Completed 400000 requests 72 | Completed 500000 requests 73 | Completed 600000 requests 74 | Completed 700000 requests 75 | Completed 800000 requests 76 | Completed 900000 requests 77 | Completed 1000000 requests 78 | Finished 1000000 requests 79 | 80 | 81 | Server Software: gurl-server 82 | Server Hostname: 127.0.01 83 | Server Port: 12345 84 | 85 | Document Path: / 86 | Document Length: 0 bytes 87 | 88 | Concurrency Level: 21 89 | Time taken for tests: 33.300 seconds 90 | Complete requests: 1000000 91 | Failed requests: 0 92 | Total transferred: 137000000 bytes 93 | HTML transferred: 0 bytes 94 | Requests per second: 30029.76 [#/sec] (mean) 95 | Time per request: 0.699 [ms] (mean) 96 | Time per request: 0.033 [ms] (mean, across all concurrent requests) 97 | Transfer rate: 4017.65 [Kbytes/sec] received 98 | 99 | Connection Times (ms) 100 | min mean[+/-sd] median max 101 | Connect: 0 0 0.1 0 3 102 | Processing: 0 0 0.4 0 212 103 | Waiting: 0 0 0.4 0 212 104 | Total: 0 1 0.4 1 212 105 | 106 | Percentage of the requests served within a certain time (ms) 107 | 50% 1 108 | 66% 1 109 | 75% 1 110 | 80% 1 111 | 90% 1 112 | 95% 1 113 | 98% 1 114 | 99% 1 115 | 100% 212 (longest request) 116 | ``` 117 | 118 | * In conclusion 119 | 120 | The gurl command can send more messages per second (12w/s) than the ab(3w/s) command. The more cores, the higher the performance of "gurl" than "ab".The reason "gurl" is faster than "ab" is that ab only uses a single thread for pressure testing, and is particularly dependent on the cpu clock speed. The fast clock speed runs faster. 121 | -------------------------------------------------------------------------------- /input/main.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "fmt" 5 | "github.com/guonaihong/gurl/core" 6 | "os" 7 | ) 8 | 9 | func Main(fileName string, fields string, renameKey string, message core.Message) { 10 | out, err := ReadFile(fileName, fields, renameKey) 11 | if err != nil { 12 | fmt.Printf("%s\n", err) 13 | os.Exit(1) 14 | } 15 | 16 | defer close(message.Out) 17 | 18 | for { 19 | select { 20 | case v, ok := <-out.JsonOut: 21 | if !ok { 22 | return 23 | } 24 | message.Out <- v 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /input/read_file.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type StreamFile struct { 12 | JsonOut chan string 13 | file *os.File 14 | } 15 | 16 | func ReadFile(fileName string, fieldSeparator string, renameKey string) (*StreamFile, error) { 17 | file, err := os.Open(fileName) 18 | if err != nil { 19 | return nil, err 20 | } 21 | sf := StreamFile{ 22 | JsonOut: make(chan string, 10), 23 | file: file, 24 | } 25 | 26 | scanner := bufio.NewScanner(file) 27 | 28 | renameMap := map[string]string{} 29 | 30 | if len(renameKey) > 0 { 31 | defaultKeys := strings.FieldsFunc(renameKey, func(r rune) bool { return r == ',' }) 32 | for _, v := range defaultKeys { 33 | if pos := strings.Index(v, "="); pos != -1 { 34 | renameMap[v[pos+1:]] = v[:pos] 35 | } 36 | } 37 | } 38 | 39 | go func() { 40 | 41 | defer func() { 42 | sf.file.Close() 43 | close(sf.JsonOut) 44 | }() 45 | 46 | for scanner.Scan() { 47 | 48 | ls := strings.Split(scanner.Text(), fieldSeparator) 49 | m := make(map[string]string) 50 | for k, v := range ls { 51 | colName := fmt.Sprintf("rf.col.%d", k) 52 | newKey, ok := renameMap[colName] 53 | if ok { 54 | m[newKey] = v 55 | continue 56 | } 57 | 58 | m[colName] = v 59 | } 60 | 61 | all, err := json.Marshal(m) 62 | if err != nil { 63 | fmt.Printf("%s\n", err) 64 | os.Exit(1) 65 | } 66 | 67 | sf.JsonOut <- string(all) 68 | } 69 | 70 | }() 71 | 72 | return &sf, nil 73 | } 74 | -------------------------------------------------------------------------------- /output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/guonaihong/gurl/core" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func WriteStream(rsp map[string]interface{}, inJson map[string]string, merge bool, message core.Message) { 12 | if merge { 13 | for k, v := range inJson { 14 | rsp[k] = v 15 | } 16 | } 17 | 18 | all, err := json.Marshal(rsp) 19 | if err != nil { 20 | fmt.Printf("%s\n", err) 21 | return 22 | } 23 | 24 | message.Out <- string(all) 25 | } 26 | 27 | func WriteFile(fileName string, onlyWriteKey string, message core.Message) error { 28 | var fd *os.File 29 | var err error 30 | 31 | fd = os.Stdout //default 32 | switch fileName { 33 | case "stdout", "": //stdout 34 | case "stderr": 35 | fd = os.Stderr 36 | default: 37 | fd, err = os.Create(fileName) 38 | if err != nil { 39 | return err 40 | } 41 | defer fd.Close() 42 | } 43 | 44 | onlyWriteMap := map[string]struct{}{} 45 | if len(onlyWriteKey) > 0 { 46 | onlyWriteList := strings.FieldsFunc(onlyWriteKey, func(r rune) bool { return r == ',' }) 47 | for _, v := range onlyWriteList { 48 | onlyWriteMap[v] = struct{}{} 49 | } 50 | } 51 | 52 | for v := range message.In { 53 | if len(onlyWriteMap) > 0 { 54 | only := map[string]interface{}{} 55 | err := json.Unmarshal([]byte(v), &only) 56 | if err != nil { 57 | fmt.Printf("writefile:%s, v(%s)\n", err, v) 58 | continue 59 | } 60 | 61 | only2 := map[string]interface{}{} 62 | for k, _ := range onlyWriteMap { 63 | only2[k] = only[k] 64 | } 65 | 66 | all, err := json.Marshal(only2) 67 | if err != nil { 68 | fmt.Printf("%s\n", err) 69 | continue 70 | } 71 | fd.Write(all) 72 | continue 73 | } 74 | 75 | fd.WriteString(v) 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pipe/pipe.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "github.com/guonaihong/gurl/core" 5 | "log" 6 | "sync" 7 | ) 8 | 9 | type Chan struct { 10 | ch chan string 11 | done chan string 12 | } 13 | 14 | func Main(name string, args []string, subMain func(core.Message, string, []string)) { 15 | 16 | var wg sync.WaitGroup 17 | var cmds [][]string 18 | 19 | prevPos := 0 20 | for k, v := range args { 21 | if v == "|" { 22 | cmds = append(cmds, args[prevPos:k]) 23 | prevPos = k + 1 24 | } 25 | } 26 | 27 | log.SetFlags(log.LstdFlags | log.Lmicroseconds) 28 | 29 | if len(cmds) == 0 { 30 | cmds = [][]string{args} 31 | } 32 | 33 | if prevPos != 0 && prevPos < len(args) { 34 | cmds = append(cmds, args[prevPos:]) 35 | } 36 | 37 | var channel []*Chan 38 | wg.Add(len(cmds)) 39 | defer wg.Wait() 40 | 41 | for k, v := range cmds { 42 | channel = append(channel, &Chan{ 43 | done: make(chan string), 44 | ch: make(chan string, 1000), 45 | }) 46 | 47 | go func(ch []*Chan, k int, v []string) { 48 | defer func() { 49 | wg.Done() 50 | }() 51 | 52 | m := core.Message{ 53 | Out: ch[k].ch, 54 | OutDone: ch[k].done, 55 | K: k, 56 | } 57 | 58 | if k > 0 { 59 | m.In = ch[k-1].ch 60 | m.InDone = ch[k-1].done 61 | } 62 | 63 | subMain(m, name, v) 64 | }(channel, k, v) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | type result struct { 12 | time float64 13 | } 14 | 15 | type Report struct { 16 | c int 17 | n int 18 | url string 19 | step int 20 | connNum int 21 | errN int32 22 | readBytes int64 23 | writeBytes int64 24 | allResult chan result 25 | allTimes []float64 26 | sync.Mutex 27 | errMap map[string]int 28 | waitQuit chan struct{} 29 | quit chan struct{} 30 | startNow time.Time 31 | duration time.Duration 32 | } 33 | 34 | func NewReport(c, n int, url string) *Report { 35 | r := Report{} 36 | r.c, r.n = c, n 37 | r.url = url 38 | 39 | if n > 150 { 40 | if r.step = n / 10; r.step < 100 { 41 | r.step = 100 42 | } 43 | } 44 | 45 | r.quit = make(chan struct{}) 46 | r.waitQuit = make(chan struct{}) 47 | r.allResult = make(chan result, 1000) 48 | r.errMap = make(map[string]int, 3) 49 | r.startNow = time.Now() 50 | return &r 51 | } 52 | 53 | func (r *Report) report() { 54 | timeTake := time.Now().Sub(r.startNow) 55 | 56 | fmt.Printf("Concurrency Level: %d\n", r.c) 57 | fmt.Printf("Time taken for tests: %v\n", timeTake) 58 | fmt.Printf("Connected: %d\n", r.connNum) 59 | fmt.Printf("Disconnected: %d\n", r.errN) 60 | fmt.Printf("Failed: %d\n", r.errN) 61 | if len(r.errMap) > 0 { 62 | fmt.Printf("Failed message: ") 63 | for message, count := range r.errMap { 64 | fmt.Printf("%s:count(%d)\n", message, count) 65 | } 66 | fmt.Printf("\n") 67 | } 68 | //todo:Calculate the websocket protocol header 69 | fmt.Printf("Total transferred: %d\n", r.writeBytes) 70 | fmt.Printf("Total received %d\n", r.readBytes) 71 | fmt.Printf("Requests per second: %d [#/sec] (mean)\n", int(float64(r.connNum)/timeTake.Seconds())) 72 | fmt.Printf("Time per request: %.3f [ms] (mean)\n", 73 | float64(r.c)*float64(timeTake)/float64(time.Millisecond)/float64(r.connNum)) 74 | fmt.Printf("Time per request: %.3f [ms] (mean, across all concurrent requests)\n", 75 | float64(timeTake)/float64(time.Millisecond)/float64(r.connNum)) 76 | fmt.Printf("Transfer rate: %.3f [Kbytes/sec] received\n", float64(r.readBytes)/float64(r.connNum)) 77 | 78 | sort.Slice(r.allTimes, func(i, j int) bool { 79 | return r.allTimes[i] < r.allTimes[j] 80 | }) 81 | 82 | fmt.Printf("\n") 83 | if len(r.allTimes) > 1 { 84 | fmt.Printf("Percentage of the requests served within a certain time (ms)\n") 85 | fmt.Printf(" 50%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.5)]) 86 | fmt.Printf(" 66%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.65)]) 87 | fmt.Printf(" 75%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.75)]) 88 | fmt.Printf(" 80%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.80)]) 89 | fmt.Printf(" 90%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.90)]) 90 | fmt.Printf(" 95%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.95)]) 91 | fmt.Printf(" 98%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.98)]) 92 | fmt.Printf(" 99%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes))*0.99)]) 93 | fmt.Printf(" 100%% %0.2fms\n", r.allTimes[int(float64(len(r.allTimes)-1))]) 94 | } 95 | } 96 | 97 | func (r *Report) Add(openTime time.Time, rb int, wb int) { 98 | atomic.AddInt64(&r.readBytes, int64(rb)) 99 | atomic.AddInt64(&r.writeBytes, int64(wb)) 100 | r.allResult <- result{ 101 | time: float64(time.Now().Sub(openTime) / time.Millisecond), 102 | } 103 | } 104 | 105 | func (r *Report) SetDuration(t time.Duration) { 106 | r.duration = t 107 | } 108 | 109 | func (r *Report) AddErr(err error) { 110 | atomic.AddInt32(&r.errN, int32(1)) 111 | r.Lock() 112 | r.errMap[err.Error()]++ 113 | r.Unlock() 114 | } 115 | 116 | func genTimeStr(now time.Time) string { 117 | year, month, day := now.Date() 118 | hour, min, sec := now.Clock() 119 | 120 | return fmt.Sprintf("%4d-%02d-%02d %02d:%02d:%02d.%06d", 121 | year, 122 | month, 123 | day, 124 | hour, 125 | min, 126 | sec, 127 | now.Nanosecond()/1e3, 128 | ) 129 | } 130 | 131 | func (r *Report) Start() { 132 | fmt.Printf("Connecting to to %s\n", r.url) 133 | go func() { 134 | 135 | defer func() { 136 | fmt.Printf("\n Finished %d connections\n\n", r.connNum) 137 | r.waitQuit <- struct{}{} 138 | }() 139 | 140 | if r.step > 0 { 141 | for { 142 | select { 143 | case _, ok := <-r.quit: 144 | if !ok { 145 | return 146 | } 147 | case v := <-r.allResult: 148 | r.connNum++ 149 | if r.step > 0 && r.connNum%r.step == 0 { 150 | now := time.Now() 151 | 152 | fmt.Printf(" Opened %15d connections: [%s]\n", 153 | r.connNum, genTimeStr(now)) 154 | } 155 | 156 | r.allTimes = append(r.allTimes, v.time) 157 | } 158 | } 159 | 160 | } else { 161 | begin := time.Now() 162 | interval := r.duration / 10 163 | 164 | if interval == 0 { 165 | interval = time.Second 166 | } 167 | nTick := time.NewTicker(interval) 168 | count := 1 169 | for { 170 | select { 171 | case <-nTick.C: 172 | now := time.Now() 173 | 174 | fmt.Printf(" Completed %15d requests [%s]\n", 175 | r.connNum, genTimeStr(now)) 176 | 177 | count++ 178 | next := begin.Add(time.Duration(count * int(interval))) 179 | if newInterval := next.Sub(time.Now()); newInterval > 0 { 180 | nTick = time.NewTicker(newInterval) 181 | } else { 182 | nTick = time.NewTicker(time.Millisecond * 100) 183 | } 184 | case v, ok := <-r.allResult: 185 | if !ok { 186 | return 187 | } 188 | 189 | r.connNum++ 190 | r.allTimes = append(r.allTimes, v.time) 191 | case _, ok := <-r.quit: 192 | if !ok { 193 | return 194 | } 195 | } 196 | } 197 | } 198 | }() 199 | } 200 | 201 | func (r *Report) Wait() { 202 | close(r.quit) 203 | <-r.waitQuit 204 | r.report() 205 | } 206 | -------------------------------------------------------------------------------- /task/task.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "github.com/guonaihong/gurl/core" 5 | "github.com/guonaihong/gurl/utils" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type Task struct { 13 | Duration string 14 | Work chan string 15 | ReadStream bool 16 | N int 17 | C int 18 | Rate int 19 | core.Message 20 | Wg sync.WaitGroup 21 | Processer 22 | } 23 | 24 | type Processer interface { 25 | Init() 26 | SubProcess(chan string) 27 | WaitAll() 28 | } 29 | 30 | func (T *Task) Producer() { 31 | work, n := T.Work, T.N 32 | 33 | if T.ReadStream { 34 | go func() { 35 | for v := range T.In { 36 | work <- v 37 | } 38 | close(work) 39 | }() 40 | return 41 | } 42 | 43 | if len(T.Duration) > 0 { 44 | 45 | if t := utils.ParseTime(T.Duration); int(t) > 0 { 46 | T.N = -1 47 | 48 | ticker := time.NewTicker(t) 49 | go func() { 50 | 51 | defer func() { 52 | close(work) 53 | for range work { 54 | } 55 | }() 56 | 57 | for { 58 | select { 59 | case <-ticker.C: 60 | return 61 | case work <- "": 62 | } 63 | 64 | } 65 | 66 | }() 67 | return 68 | } 69 | } 70 | 71 | go func() { 72 | 73 | defer close(work) 74 | if T.N >= 0 { 75 | 76 | for i := 0; i < n; i++ { 77 | work <- "" 78 | } 79 | 80 | return 81 | } 82 | 83 | for { 84 | work <- "" 85 | } 86 | 87 | return 88 | 89 | }() 90 | } 91 | 92 | func (T *Task) RunMain() { 93 | 94 | work, wg := T.Work, &T.Wg 95 | 96 | sig := make(chan os.Signal, 1) 97 | done := make(chan struct{}, 1) 98 | signal.Notify(sig, os.Interrupt) 99 | 100 | T.Init() 101 | 102 | begin := time.Now() 103 | 104 | interval := 0 105 | if T.Rate > 0 { 106 | interval = int(time.Second) / T.Rate 107 | } 108 | 109 | if interval > 0 { 110 | count := 0 111 | oldwork := work 112 | work = make(chan string, 1000) 113 | wg.Add(1) 114 | go func() { 115 | defer func() { 116 | close(work) 117 | wg.Done() 118 | }() 119 | 120 | n := T.N 121 | for { 122 | next := begin.Add(time.Duration(count * interval)) 123 | time.Sleep(next.Sub(time.Now())) 124 | 125 | select { 126 | case _, ok := <-oldwork: 127 | if !ok { 128 | return 129 | } 130 | default: 131 | } 132 | 133 | work <- "" 134 | if count++; count == n { 135 | return 136 | } 137 | } 138 | }() 139 | } 140 | 141 | for i, c := 0, T.C; i < c; i++ { 142 | 143 | wg.Add(1) 144 | 145 | go func() { 146 | defer wg.Done() 147 | T.SubProcess(work) 148 | }() 149 | } 150 | 151 | go func() { 152 | wg.Wait() 153 | done <- struct{}{} 154 | }() 155 | 156 | end: 157 | for { 158 | select { 159 | case <-sig: 160 | T.WaitAll() 161 | break end 162 | case <-done: 163 | T.WaitAll() 164 | break end 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /utils/parsetime.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func ParseTime(t string) (rv time.Duration) { 8 | 9 | t0 := 0 10 | for k, _ := range t { 11 | v := int(t[k]) 12 | switch { 13 | case v >= '0' && v <= '9': 14 | t0 = t0*10 + (v - '0') 15 | case v == 's': 16 | rv += time.Duration(t0) * time.Second 17 | t0 = 0 18 | case v == 'm': 19 | if k+1 < len(t) && t[k+1] == 's' { 20 | rv += time.Duration(t0) * time.Millisecond 21 | t0 = 0 22 | k++ 23 | continue 24 | } 25 | rv += time.Duration(t0*60) * time.Second 26 | t0 = 0 27 | case v == 'h': 28 | rv += time.Duration(t0*60*60) * time.Second 29 | t0 = 0 30 | case v == 'd': 31 | rv += time.Duration(t0*60*60*24) * time.Second 32 | t0 = 0 33 | case v == 'w': 34 | rv += time.Duration(t0*60*60*24*7) * time.Second 35 | t0 = 0 36 | case v == 'M': 37 | rv += time.Duration(t0*60*60*24*7*31) * time.Second 38 | t0 = 0 39 | case v == 'y': 40 | rv += time.Duration(t0*60*60*24*7*31*365) * time.Second 41 | t0 = 0 42 | } 43 | } 44 | 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /ws/url/url.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func ModifyUrl(u string) string { 8 | if len(u) == 0 { 9 | return u 10 | } 11 | 12 | if len(u) > 0 && u[0] == ':' { 13 | return "ws://127.0.0.1" + u 14 | } 15 | 16 | if len(u) > 0 && u[0] == '/' { 17 | return "ws://127.0.0.1:80" + u 18 | } 19 | 20 | if !strings.HasPrefix(u, "ws") { 21 | return "ws://" + u 22 | } 23 | 24 | return u 25 | } 26 | -------------------------------------------------------------------------------- /ws/ws.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gorilla/websocket" 7 | "github.com/guonaihong/flag" 8 | "github.com/guonaihong/gurl/core" 9 | "github.com/guonaihong/gurl/input" 10 | "github.com/guonaihong/gurl/output" 11 | "github.com/guonaihong/gurl/report" 12 | "github.com/guonaihong/gurl/task" 13 | "github.com/guonaihong/gurl/utils" 14 | url2 "github.com/guonaihong/gurl/ws/url" 15 | _ "io/ioutil" 16 | "log" 17 | "net" 18 | "net/http" 19 | "net/url" 20 | "os" 21 | "strings" 22 | "syscall" 23 | "time" 24 | ) 25 | 26 | var upgrader = websocket.Upgrader{} 27 | 28 | type wsClient struct { 29 | *websocket.Conn 30 | url string 31 | } 32 | 33 | // 命令wsmd由几部分的数据组成 34 | // task.Task, 负责并发控制 35 | // wsCmdData(业务数据), 存放具体的数据,为并发模块提供燃料 36 | // bool类型 37 | // 报表, bench模式下,输出报表供人观看 38 | 39 | type wsCmdData struct { 40 | packet []string 41 | firstSendAfter string 42 | userAgent string 43 | header []string 44 | sendRate string 45 | url string 46 | output string 47 | reqHeader http.Header 48 | } 49 | 50 | type wsCmd struct { 51 | *task.Task 52 | 53 | wsCmdData 54 | 55 | mt int 56 | closeMessage bool 57 | bench bool 58 | 59 | writeStream bool 60 | merge bool 61 | 62 | outFd *os.File 63 | report *report.Report 64 | } 65 | 66 | func (w *wsCmd) headersAdd() { 67 | 68 | for _, v := range w.header { 69 | 70 | headers := strings.Split(v, ":") 71 | 72 | if len(headers) != 2 { 73 | continue 74 | } 75 | 76 | headers[0] = strings.TrimSpace(headers[0]) 77 | headers[1] = strings.TrimSpace(headers[1]) 78 | 79 | w.reqHeader.Add(headers[0], headers[1]) 80 | } 81 | 82 | if len(w.userAgent) > 0 { 83 | w.reqHeader.Set("User-Agent", w.userAgent) 84 | } 85 | 86 | w.reqHeader.Set("Accept", "*/*") 87 | //req.Header.Set("Host", req.URL.Host) 88 | 89 | } 90 | 91 | func newWsClient(u string, header http.Header) (*wsClient, error) { 92 | u1, err := url.Parse(u) 93 | if err != nil { 94 | return nil, err 95 | } 96 | c, _, err := websocket.DefaultDialer.Dial(u1.String(), header) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | wsc := &wsClient{ 102 | url: u, 103 | Conn: c, 104 | } 105 | 106 | return wsc, nil 107 | } 108 | 109 | func (w *wsClient) Close() { 110 | w.Conn.Close() 111 | } 112 | 113 | func (ws *wsCmd) write(c *wsClient, mt int, data string) (rv int) { 114 | if !strings.HasPrefix(data, "@") { 115 | c.WriteMessage(mt, []byte(data)) 116 | rv += len(data) 117 | } else { 118 | fd, err := os.Open(data[1:]) 119 | if err != nil { 120 | fmt.Println(err.Error()) 121 | os.Exit(1) 122 | } 123 | 124 | defer fd.Close() 125 | 126 | rate := &rate{} 127 | genRate(ws.sendRate, &rate) 128 | bufsize := 4096 * 2 129 | if rate != nil && rate.B > 0 { 130 | bufsize = rate.B 131 | } 132 | 133 | buf := make([]byte, bufsize) 134 | for { 135 | n, err := fd.Read(buf) 136 | if err != nil { 137 | break 138 | } 139 | 140 | rv += n 141 | err = c.WriteMessage(mt, buf[:n]) 142 | if err != nil { 143 | return 144 | } 145 | 146 | if rate != nil && rate.T > 0 { 147 | time.Sleep(time.Duration(rate.T)) 148 | } 149 | } 150 | } 151 | return 152 | } 153 | 154 | func (ws *wsCmd) webSocketEcho(addr string) { 155 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 156 | 157 | c, err := upgrader.Upgrade(w, r, nil) 158 | if err != nil { 159 | log.Print("upgrade:", err) 160 | return 161 | } 162 | defer c.Close() 163 | 164 | for { 165 | mt, message, err := c.ReadMessage() 166 | if err != nil { 167 | log.Println("read:", err) 168 | break 169 | } 170 | 171 | log.Printf("recv: %s", message) 172 | err = c.WriteMessage(mt, message) 173 | if err != nil { 174 | log.Println("write:", err) 175 | break 176 | } 177 | } 178 | 179 | }) 180 | 181 | fmt.Println(http.ListenAndServe(addr, nil)) 182 | } 183 | 184 | type rate struct { 185 | B int 186 | T int64 187 | } 188 | 189 | func parseTime(s string) time.Duration { 190 | s = strings.ToLower(s) 191 | 192 | rv := int64(0) 193 | 194 | fmt.Sscanf(s, "%d", &rv) 195 | switch { 196 | case strings.HasSuffix(s, "ms"): 197 | rv = rv * int64(time.Millisecond) 198 | case strings.HasSuffix(s, "s"): 199 | rv = rv * int64(time.Second) 200 | } 201 | return time.Duration(rv) 202 | } 203 | 204 | func genRate(rateStr string, rv **rate) { 205 | rates := strings.Split(rateStr, "/") 206 | 207 | if len(rates) != 2 { 208 | return 209 | } 210 | 211 | rates[0] = strings.ToLower(rates[0]) 212 | rates[1] = strings.ToLower(rates[1]) 213 | 214 | r := rate{} 215 | fmt.Sscanf(rates[0], "%d", &r.B) 216 | fmt.Sscanf(rates[1], "%d", &r.T) 217 | switch { 218 | case strings.HasSuffix(rates[0], "b"): 219 | case strings.HasSuffix(rates[0], "kb"): 220 | r.B *= 1024 221 | case strings.HasSuffix(rates[0], "mb"): 222 | r.B *= 1024 * 1024 223 | } 224 | 225 | switch { 226 | case strings.HasSuffix(rates[1], "ms"): 227 | r.T = r.T * int64(time.Millisecond) 228 | case strings.HasSuffix(rates[1], "s"): 229 | r.T = r.T * int64(time.Second) 230 | } 231 | 232 | if r.B <= 0 { 233 | return 234 | } 235 | 236 | if r.T <= 0 { 237 | return 238 | } 239 | 240 | *rv = &r 241 | } 242 | 243 | type wsResult struct { 244 | wb int 245 | rb int 246 | lastBody []byte 247 | } 248 | 249 | func (ws *wsCmd) outputFileNew() { 250 | 251 | var err error 252 | 253 | if ws.output != "" { 254 | switch ws.output { 255 | case "stdout": 256 | ws.outFd = os.Stdout 257 | case "stderr": 258 | ws.outFd = os.Stderr 259 | default: 260 | ws.outFd, err = os.OpenFile(ws.output, os.O_CREATE|os.O_RDWR, 0644) 261 | if err != nil { 262 | fmt.Printf("%s\n", err) 263 | } 264 | } 265 | } 266 | } 267 | 268 | func (ws *wsCmd) outputFileWrite(m []byte) { 269 | if ws.outFd != nil { 270 | ws.outFd.Write(m) 271 | } 272 | } 273 | 274 | func (ws *wsCmd) outputClose() { 275 | if ws.outFd != nil && ws.outFd != os.Stdout { 276 | ws.outFd.Close() 277 | } 278 | 279 | } 280 | 281 | func (ws *wsCmd) one() (rv wsResult, err error) { 282 | 283 | var c *wsClient 284 | c, err = newWsClient(ws.url, ws.reqHeader) 285 | if err != nil { 286 | return 287 | } 288 | defer c.Close() 289 | 290 | mt := ws.mt 291 | 292 | if len(ws.firstSendAfter) > 0 { 293 | if t := utils.ParseTime(ws.firstSendAfter); int(t) > 0 { 294 | time.Sleep(t) 295 | } 296 | } 297 | 298 | ws.outputFileNew() 299 | 300 | done := make(chan struct{}) 301 | go func() { 302 | defer close(done) 303 | 304 | for { 305 | _, m, err := c.ReadMessage() 306 | if err != nil { 307 | if !websocket.IsCloseError(err, 1000) { 308 | fmt.Println("read fail:", err) 309 | } 310 | return 311 | } 312 | 313 | rv.lastBody = m 314 | rv.rb += len(m) 315 | if !ws.bench { 316 | ws.outputFileWrite(m) 317 | } 318 | } 319 | }() 320 | 321 | for _, v := range ws.packet { 322 | wb := ws.write(c, mt, v) 323 | rv.wb += wb 324 | } 325 | 326 | if ws.closeMessage { 327 | err = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 328 | if err != nil { 329 | fmt.Printf("write close message \n") 330 | } 331 | } 332 | 333 | <-done 334 | ws.outputClose() 335 | return 336 | } 337 | 338 | func CmdErr(err error) { 339 | if err == nil { 340 | return 341 | } 342 | 343 | if noerr, ok := err.(*net.OpError); ok { 344 | if scerr, ok := noerr.Err.(*os.SyscallError); ok { 345 | if scerr.Err == syscall.ECONNREFUSED { 346 | fmt.Printf("ws: (7) couldn't connect to host\n") 347 | os.Exit(7) 348 | } 349 | } 350 | } 351 | 352 | fmt.Printf("%s\n", err) 353 | } 354 | 355 | func (ws *wsCmd) Init() { 356 | if ws.bench { 357 | ws.report = report.NewReport(ws.C, ws.N, ws.url) 358 | if len(ws.Duration) > 0 { 359 | if t := utils.ParseTime(ws.Duration); int(t) > 0 { 360 | ws.report.SetDuration(t) 361 | } 362 | } 363 | ws.report.Start() 364 | } 365 | } 366 | 367 | func (ws *wsCmd) WaitAll() { 368 | if ws.report != nil { 369 | ws.report.Wait() 370 | } 371 | close(ws.Out) 372 | } 373 | 374 | func (ws *wsCmd) parse(val map[string]string, inJson string) { 375 | err := json.Unmarshal([]byte(inJson), &val) 376 | if err != nil { 377 | fmt.Printf("%s\n", err) 378 | return 379 | } 380 | 381 | i := 0 382 | rs := make([]string, len(val)*2) 383 | for k, v := range val { 384 | 385 | rs[i] = "{" + k + "}" 386 | i++ 387 | rs[i] = v 388 | i++ 389 | } 390 | 391 | r := strings.NewReplacer(rs...) 392 | for k, v := range ws.header { 393 | ws.header[k] = r.Replace(v) 394 | } 395 | 396 | ws.userAgent = r.Replace(ws.userAgent) 397 | ws.url = r.Replace(ws.url) 398 | for k, v := range ws.packet { 399 | ws.packet[k] = r.Replace(v) 400 | } 401 | 402 | ws.firstSendAfter = r.Replace(ws.firstSendAfter) 403 | } 404 | 405 | func (ws *wsCmd) copyAndNew() *wsCmdData { 406 | return &wsCmdData{ 407 | firstSendAfter: ws.firstSendAfter, 408 | userAgent: ws.userAgent, 409 | header: append([]string{}, ws.header...), 410 | sendRate: ws.sendRate, 411 | url: ws.url, 412 | packet: append([]string{}, ws.packet...), 413 | output: ws.output, 414 | reqHeader: make(map[string][]string, 3), 415 | } 416 | } 417 | 418 | //todo 419 | func (cmd *wsCmd) streamWriteJson(rsp wsResult, err error, inJson map[string]string) { 420 | m := map[string]interface{}{} 421 | m["err"] = "" 422 | m["last_body"] = string(rsp.lastBody) 423 | 424 | if err != nil { 425 | m["err"] = err.Error() 426 | } 427 | 428 | output.WriteStream(m, inJson, cmd.merge, cmd.Message) 429 | } 430 | 431 | func (cmd *wsCmd) SubProcess(work chan string) { 432 | 433 | var inJson map[string]string 434 | 435 | ws := *cmd 436 | ws0 := *cmd 437 | ws0.wsCmdData = *ws.copyAndNew() 438 | 439 | for v := range work { 440 | 441 | if len(v) > 0 && v[0] == '{' { 442 | inJson = map[string]string{} 443 | ws.wsCmdData = *ws.copyAndNew() 444 | ws.parse(inJson, v) 445 | ws.headersAdd() 446 | } 447 | 448 | taskNow := time.Now() 449 | rv, err := ws.one() 450 | if cmd.writeStream { 451 | cmd.streamWriteJson(rv, err, inJson) 452 | } 453 | //todo Give this judgment a name 454 | 455 | if err != nil { 456 | if ws.report != nil { 457 | ws.report.AddErr(err) 458 | } else { 459 | CmdErr(err) 460 | } 461 | continue 462 | } 463 | 464 | if ws.report != nil { 465 | ws.report.Add(taskNow, rv.rb, rv.wb) 466 | } 467 | 468 | if len(v) > 0 && v[0] == '{' { 469 | ws = ws0 470 | } 471 | } 472 | } 473 | 474 | func Main(message core.Message, argv0 string, argv []string) { 475 | command := flag.NewFlagSet(argv0, flag.ExitOnError) 476 | an := command.Int("an", 1, "Number of requests to perform") 477 | ac := command.Int("ac", 1, "Number of multiple requests to make") 478 | sendRate := command.String("send-rate", "", "How many bytes of data in seconds") 479 | rate := command.Int("rate", 0, "Requests per second") 480 | duration := command.String("duration", "", "Duration of the test") 481 | connectTimeout := command.String("connect-timeout", "", "Maximum time allowed for connection") 482 | bench := command.Bool("bench", false, "Run benchmarks test") 483 | outputFileName := command.String("o, output", "stdout", "Write to FILE instead of stdout") 484 | firstSendAfter := command.String("fsa, first-send-after", "", "Wait for the first time before sending") 485 | URL := command.String("url", "", "Specify a URL to fetch") 486 | headers := command.StringSlice("H, header", []string{}, "Pass custom header LINE to server (H)") 487 | binary := command.Bool("binary", false, "Send binary messages instead of utf-8") 488 | listen := command.String("l", "", "Listen mode, websocket echo server") 489 | userAgent := command.String("A, user-agent", "gurl", "Send User-Agent STRING to server") 490 | closeMessage := command.Bool("close", false, "Send close message") 491 | 492 | readStream := command.Bool("r, read-stream", false, "Read data from the stream") 493 | writeStream := command.Bool("w, write-stream", false, "Write data from the stream") 494 | merge := command.Bool("m, merge", false, "Combine the output results into the output") 495 | 496 | inputMode := command.Bool("I, input-model", false, "open input mode") 497 | inputRead := command.String("R, input-read", "", "open input file") 498 | inputFields := command.String("input-fields", " ", "sets the field separator") 499 | inputSetKey := command.String("skey, input-setkey", "", "Set a new name for the default key") 500 | 501 | outputMode := command.Bool("O, output-mode", false, "open output mode") 502 | outputKey := command.String("wkey, write-key", "", "Key that can be write") 503 | outputWrite := command.String("W, output-write", "", "open output file") 504 | 505 | packet := command.Opt("p, packet", "Data packet to be send per connection"). 506 | Flags(flag.GreedyMode). 507 | NewStringSlice([]string{}) 508 | 509 | command.Parse(argv) 510 | 511 | if !*inputMode { 512 | if len(*inputRead) > 0 { 513 | *inputMode = true 514 | } 515 | } 516 | 517 | if *inputMode { 518 | input.Main(*inputRead, *inputFields, *inputSetKey, message) 519 | return 520 | } 521 | 522 | if *outputMode { 523 | output.WriteFile(*outputWrite, *outputKey, message) 524 | return 525 | } 526 | 527 | if len(*connectTimeout) > 0 { 528 | websocket.DefaultDialer.HandshakeTimeout = utils.ParseTime(*connectTimeout) 529 | } 530 | 531 | wscmd := &wsCmd{ 532 | Task: &task.Task{ 533 | Duration: *duration, 534 | N: *an, 535 | Work: make(chan string, 1000), 536 | ReadStream: *readStream, 537 | Message: message, 538 | Rate: *rate, 539 | C: *ac, 540 | }, 541 | wsCmdData: wsCmdData{ 542 | packet: *packet, 543 | firstSendAfter: *firstSendAfter, 544 | header: *headers, 545 | sendRate: *sendRate, 546 | userAgent: *userAgent, 547 | reqHeader: make(map[string][]string, 3), 548 | output: *outputFileName, 549 | }, 550 | mt: websocket.TextMessage, 551 | closeMessage: *closeMessage, 552 | bench: *bench, 553 | writeStream: *writeStream, 554 | merge: *merge, 555 | } 556 | 557 | if *binary { 558 | wscmd.mt = websocket.BinaryMessage 559 | } 560 | 561 | wscmd.headersAdd() 562 | 563 | if len(*listen) > 0 { 564 | wscmd.webSocketEcho(*listen) 565 | return 566 | } 567 | 568 | wscmd.Producer() 569 | 570 | if *URL == "" { 571 | command.Usage() 572 | return 573 | } 574 | 575 | wscmd.url = url2.ModifyUrl(*URL) 576 | 577 | wscmd.Task.Processer = wscmd 578 | wscmd.Task.RunMain() 579 | } 580 | --------------------------------------------------------------------------------