[源码赏析] 经典压力测试工具之WebBench
前言
身在运维部,跑压测是必不可少的日常工作。所以通过读WebBench项目的源码,简单了解一下压力测试工具的基本原理,在此做个记录。
整理过后的源码点击这里下载,包含了正确的CMake文件,并做了适当修改,可以使用CLion在Windows上编译,Linux平台应该也可以。
在Windows平台上的实际运行效果如下:
源码赏析
socket.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
/* $Id: socket.c 1.1 1995/01/01 07:11:14 cthuang Exp $ * * This module has been modified by Radim Kolar for OS/2 emx */ /*********************************************************************** module: socket.c program: popclient SCCS ID: @(#)socket.c 1.5 4/1/94 programmer: Virginia Tech Computing Center compiler: DEC RISC C compiler (Ultrix 4.1) environment: DEC Ultrix 4.3 description: UNIX sockets code. ***********************************************************************/ #include <sys/types.h> #include <sys/socket.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <sys/time.h> #include <string.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <stdarg.h> // 这个函数简单来说就是根据目标地址和端口号进行连接,并返回一个socket int Socket(const char *host, int clientPort) { int sock; unsigned long inaddr; struct sockaddr_in ad; struct hostent *hp; memset(&ad, 0, sizeof(ad)); ad.sin_family = AF_INET; // 首先将一个点分十进制的IP地址转换成整数 inaddr = inet_addr(host); if (inaddr != INADDR_NONE) memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr)); else { // 如果转换失败,说明这是个域名地址,于是调用gethostbyname()进行DNS解析 hp = gethostbyname(host); if (hp == NULL) return -1; memcpy(&ad.sin_addr, hp->h_addr, hp->h_length); } ad.sin_port = htons(clientPort); // 建立socket并发起连接 sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return sock; if (connect(sock, (struct sockaddr *) &ad, sizeof(ad)) < 0) return -1; return sock; } |
webbench.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
/* * (C) Radim Kolar 1997-2004 * This is free software, see GNU Public License version 2 for * details. * * Simple forking WWW Server benchmark: * * Usage: * webbench --help * * Return codes: * 0 - sucess * 1 - benchmark failed (server is not on-line) * 2 - bad param * 3 - internal error, fork failed * */ #include "socket.c" #include <unistd.h> #include <sys/param.h> #include <getopt.h> #include <strings.h> #include <time.h> #include <signal.h> /* values */ // volatile关键字声明这个变量是易变的,为了一致性,要把它放到内存里去 volatile int timerexpired = 0; int speed = 0; int failed = 0; int bytes = 0; // HTTP协议版本 /* globals */ int http10 = 1; /* 0 - http/0.9, 1 - http/1.0, 2 - http/1.1 */ // 这里定义程序允许的HTTP方法 /* Allow: GET, HEAD, OPTIONS, TRACE */ #define METHOD_GET 0 #define METHOD_HEAD 1 #define METHOD_OPTIONS 2 #define METHOD_TRACE 3 #define PROGRAM_VERSION "1.5" int method = METHOD_GET; // 并发clients数量 int clients = 1; int force = 0; int force_reload = 0; // 代理服务器地址和端口 int proxyport = 80; char *proxyhost = NULL; int benchtime = 30; /* internal */ int mypipe[2]; char host[MAXHOSTNAMELEN]; #define REQUEST_SIZE 2048 // 这个字符串就是存储HTTP请求的内容的 char request[REQUEST_SIZE]; /* * 为了使用getopt_long()函数,我们需要先确定两个结构: 1. 一个字符串,包括所需要的短选项字符,如果选项后有参数,字符后加一个":"符号。 2. 一个包含长选项字符串的结构体数组,每一个结构体包含4个域, 第一个域为长选项字符串,第二个域是一个标识,只能为0,1,2,分别代表没有选项、有选项、选项可选。 第三个域永远为NULL。第四个选项域为对应的短选项字符串。结构体数组的最后一个元素全部位NULL和0,标识结束。 */ static const struct option long_options[] = { {"force", no_argument, &force, 1}, {"reload", no_argument, &force_reload, 1}, {"time", required_argument, NULL, 't'}, {"help", no_argument, NULL, '?'}, {"http09", no_argument, NULL, '9'}, {"http10", no_argument, NULL, '1'}, {"http11", no_argument, NULL, '2'}, {"get", no_argument, &method, METHOD_GET}, {"head", no_argument, &method, METHOD_HEAD}, {"options", no_argument, &method, METHOD_OPTIONS}, {"trace", no_argument, &method, METHOD_TRACE}, {"version", no_argument, NULL, 'V'}, {"proxy", required_argument, NULL, 'p'}, {"clients", required_argument, NULL, 'c'}, {NULL, 0, NULL, 0} }; /* prototypes */ static void benchcore(const char *host, const int port, const char *request); static int bench(void); static void build_request(const char *url); // 这是个信号处理函数 // 如果发生了alarm信号,就设置超时标志为1 // 发起请求的循环检测到这个标志后就停止继续发起新的请求 static void alarm_handler(int signal) { timerexpired = 1; } // 将帮助信息打印到标准输出 static void usage(void) { fprintf(stderr, "webbench [option]... URL\n" " -f|--force Don't wait for reply from server.\n" " -r|--reload Send reload request - Pragma: no-cache.\n" " -t|--time <sec> Run benchmark for <sec> seconds. Default 30.\n" " -p|--proxy <server:port> Use proxy server for request.\n" " -c|--clients <n> Run <n> HTTP clients at once. Default one.\n" " -9|--http09 Use HTTP/0.9 style requests.\n" " -1|--http10 Use HTTP/1.0 protocol.\n" " -2|--http11 Use HTTP/1.1 protocol.\n" " --get Use GET request method.\n" " --head Use HEAD request method.\n" " --options Use OPTIONS request method.\n" " --trace Use TRACE request method.\n" " -?|-h|--help This information.\n" " -V|--version Display program version.\n" ); }; int main(int argc, char *argv[]) { int opt = 0; int options_index = 0; char *tmp = NULL; // 如果调用时没有包含额外参数,就打印帮助信息并退出 if (argc == 1) { usage(); return 2; } // 使用getopt_long()获取命令行选项 // 每次会分析出一个选项,如果分析完毕了,返回EOF while ((opt = getopt_long(argc, argv, "912Vfrt:p:c:?h", long_options, &options_index)) != EOF) { switch (opt) { case 0 : break; case 'f': force = 1; break; case 'r': force_reload = 1; break; case '9': http10 = 0; break; case '1': http10 = 1; break; case '2': http10 = 2; break; case 'V': printf(PROGRAM_VERSION"\n"); exit(0); case 't': benchtime = atoi(optarg); break; case 'p': /* proxy server parsing server:port */ // optarg是字符串指针,指向parsing后得到的参数 tmp = strrchr(optarg, ':'); proxyhost = optarg; if (tmp == NULL) { break; } // 这里在检查代理服务器参数是否正确 if (tmp == optarg) { fprintf(stderr, "Error in option --proxy %s: Missing hostname.\n", optarg); return 2; } if (tmp == optarg + strlen(optarg) - 1) { fprintf(stderr, "Error in option --proxy %s Port number is missing.\n", optarg); return 2; } // 没问题的话此处就设置了代理服务器端口和地址 *tmp = '\0'; proxyport = atoi(tmp + 1); break; case ':': case 'h': case '?': usage(); return 2; break; case 'c': // 这是设置有多少个并发client clients = atoi(optarg); break; } } // If there are no more option characters, getopt() returns -1. // Then optind is the index in argv of the first argv-element that is not an option. // 意思是说optind这个变量指向argv中不属于option的第一个元素的位置,正常情况下应该是指向URL // 如果optind指向argv以外,说明没有给出URL,所以报缺少参数的错误 if (optind == argc) { fprintf(stderr, "webbench: Missing URL!\n"); usage(); return 2; } // 处理非法参数,重置为正确的值 if (clients == 0) clients = 1; if (benchtime == 0) benchtime = 60; /* Copyright */ fprintf(stderr, "Webbench - Simple Web Benchmark "PROGRAM_VERSION"\n" "Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.\n" ); // 正常情况向下optind应该指向URL字符串在argv中的位置,也就是最后一个元素 // 基于这个URL地址来构建请求内容 build_request(argv[optind]); /* print bench info */ // 下面就是打印和Benchmark相关的信息 // 比如是什么请求,URL地址,协议版本,client数量等等 printf("\nBenchmarking: "); switch (method) { case METHOD_GET: default: printf("GET"); break; case METHOD_OPTIONS: printf("OPTIONS"); break; case METHOD_HEAD: printf("HEAD"); break; case METHOD_TRACE: printf("TRACE"); break; } printf(" %s", argv[optind]); switch (http10) { case 0: printf(" (using HTTP/0.9)"); break; case 2: printf(" (using HTTP/1.1)"); break; } printf("\n"); if (clients == 1) printf("1 client"); else printf("%d clients", clients); // 还是根据各种参数打印信息 printf(", running %d sec", benchtime); if (force) printf(", early socket close"); if (proxyhost != NULL) printf(", via proxy server %s:%d", proxyhost, proxyport); if (force_reload) printf(", forcing reload"); printf(".\n"); // 最后发起测试,请求已经构建好并存放在request字符串里面了 return bench(); } // 这个函数用于构建request内容,并存储到全局变量request字符数组中 void build_request(const char *url) { char tmp[10]; int i; // 将内存(字符串)前n个字节清零 bzero(host, MAXHOSTNAMELEN); bzero(request, REQUEST_SIZE); // 这里检查参数并重置为合法的状态 if (force_reload && proxyhost != NULL && http10 < 1) http10 = 1; if (method == METHOD_HEAD && http10 < 1) http10 = 1; if (method == METHOD_OPTIONS && http10 < 2) http10 = 2; if (method == METHOD_TRACE && http10 < 2) http10 = 2; // 根据方法来构建请求内容,总之就是一堆字符串操作 switch (method) { default: case METHOD_GET: strcpy(request, "GET"); break; case METHOD_HEAD: strcpy(request, "HEAD"); break; case METHOD_OPTIONS: strcpy(request, "OPTIONS"); break; case METHOD_TRACE: strcpy(request, "TRACE"); break; } strcat(request, " "); // 检查URL合法性 // 必须包含协议名称 if (NULL == strstr(url, "://")) { fprintf(stderr, "\n%s: is not a valid URL.\n", url); exit(2); } // 不能太长 if (strlen(url) > 1500) { fprintf(stderr, "URL is too long.\n"); exit(2); } // 代理服务器必须是http协议 if (proxyhost == NULL) if (0 != strncasecmp("http://", url, 7)) { fprintf(stderr, "\nOnly HTTP protocol is directly supported, set --proxy for others.\n"); exit(2); } /* protocol/host delimiter */ // 找到去除*://协议头后URL地址的真正内容的起始位置 i = strstr(url, "://") - url + 3; /* printf("%d\n",i); */ // 检查URL是否以'/'结尾 if (strchr(url + i, '/') == NULL) { fprintf(stderr, "\nInvalid URL syntax - hostname don't ends with '/'.\n"); exit(2); } if (proxyhost == NULL) { // 如果不使用代理服务器,得拿出hostname,作为发送request的目标地址 // 首先我们要看URL地址里面是否包含了一个端口号 if (index(url + i, ':') != NULL && index(url + i, ':') < index(url + i, '/')) { // 如果包含端口号的话,得把hostname和端口号区分开,先拿出hostname strncpy(host, url + i, strchr(url + i, ':') - url - i); // 再手动解析出端口字符串,转换为整数,然后先存起来 bzero(tmp, 10); strncpy(tmp, index(url + i, ':') + 1, strchr(url + i, '/') - index(url + i, ':') - 1); /* printf("tmp=%s\n",tmp); */ proxyport = atoi(tmp); if (proxyport == 0) proxyport = 80; } else { // 否则的话,直接拿出hostname就好了 // strcspn()这个函数返回的是到某些字符以前的字符串长度 // 比如这里就是到'/'以前有几个字符 strncpy(host, url + i, strcspn(url + i, "/")); } // 最后再把请求参数附带上 // printf("Host=%s\n",host); strcat(request + strlen(request), url + i + strcspn(url + i, "/")); } else { // 如果使用代理服务器的话,就不用管hostname了,因为发送地址将会是代理服务器的地址 // 这样的话,直接把URL贴进request内容里面去就可以了,代理服务器会去按照这个地址发送请求 // printf("ProxyHost=%s\nProxyPort=%d\n",proxyhost,proxyport); strcat(request, url); } // 协议版本 if (http10 == 1) strcat(request, " HTTP/1.0"); else if (http10 == 2) strcat(request, " HTTP/1.1"); strcat(request, "\r\n"); // 带上UA if (http10 > 0) strcat(request, "User-Agent: WebBench "PROGRAM_VERSION"\r\n"); // 如果不用代理,还得带上Host参数 if (proxyhost == NULL && http10 > 0) { strcat(request, "Host: "); strcat(request, host); strcat(request, "\r\n"); } // 以及其他几个HTTP参数 // 从这些参数处理中我们可以看出,有些时候那些选项并不总按照你想要的方式来处理 // 比如这个程序就在代码中对选项进行了各种修正,以符合规则 if (force_reload && proxyhost != NULL) { strcat(request, "Pragma: no-cache\r\n"); } if (http10 > 1) strcat(request, "Connection: close\r\n"); /* 最后再加上一个空行来结束请求 */ if (http10 > 0) strcat(request, "\r\n"); // printf("Req=%s\n",request); } /* vraci system rc error kod */ // 这个函数负责发起请求前的一系列准备工作 static int bench(void) { int i, j, k; pid_t pid = 0; FILE *f; /* check avaibility of target server */ // 创建socket连接到目标服务器,检查是否能够正常连接 i = Socket(proxyhost == NULL ? host : proxyhost, proxyport); if (i < 0) { fprintf(stderr, "\nConnect to server failed. Aborting benchmark.\n"); return 1; } close(i); /* create pipe */ // 然后创建管道用于和子进程交互 if (pipe(mypipe)) { perror("pipe failed."); return 3; } /* not needed, since we have alarm() in childrens */ /* wait 4 next system clock tick */ /* cas=time(NULL); while(time(NULL)==cas) sched_yield(); */ /* fork childs */ // 按照预设的clients数量来创建子进程 for (i = 0; i < clients; i++) { pid = fork(); // 如果是子进程或者出错,就退出循环 if (pid <= (pid_t) 0) { /* child process or error*/ sleep(1); /* make childs faster */ break; } } // 如果发现fork()出错,得报错退出 if (pid < (pid_t) 0) { fprintf(stderr, "problems forking worker no. %d\n", i); perror("fork failed."); return 3; } // 对于子进程,让它们调用Benchmark函数来进行测试 if (pid == (pid_t) 0) { /* I am a child */ // 根据是否使用代理,来把request发送到不同的hostname if (proxyhost == NULL) benchcore(host, proxyport, request); else benchcore(proxyhost, proxyport, request); // fdopen()函数将文件描述符转换为FILE *,从而能够使用标准库函数 f = fdopen(mypipe[1], "w"); // 如果打开失败的话,还是报错退出 // 其实这里最好把错误码define一下,更有可读性 if (f == NULL) { perror("open pipe for writing failed."); return 3; } /* fprintf(stderr,"Child - %d %d\n",speed,failed); */ // 将测试结果写入到管道,供父进程读取 fprintf(f, "%d %d %d\n", speed, failed, bytes); fclose(f); return 0; } else { // 父进程要读取子进程所返回的结果 // 这里的模式相当于一系列子进程向同一个管道写入,然后被父进程所读取出来 f = fdopen(mypipe[0], "r"); if (f == NULL) { perror("open pipe for reading failed."); return 3; } // 关掉这个文件流的缓冲区机制 // 以免下面读取的时候出现等待 setvbuf(f, NULL, _IONBF, 0); speed = 0; failed = 0; bytes = 0; // 这里开始循环读取子进程的返回结果 while (1) { pid = fscanf(f, "%d %d %d", &i, &j, &k); if (pid < 2) { fprintf(stderr, "Some of our childrens died.\n"); break; } speed += i; failed += j; bytes += k; /* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */ // 如果读取次数与子进程数量相同,就可以退出循环了 if (--clients == 0) break; } fclose(f); // 最后打印汇总情况 printf("\nSpeed=%d pages/min, %d bytes/sec.\nRequests: %d susceed, %d failed.\n", (int) ((speed + failed) / (benchtime / 60.0f)), (int) (bytes / (float) benchtime), speed, failed); } return i; } // 这个函数是用来发起请求的核心函数 void benchcore(const char *host, const int port, const char *req) { int rlen; char buf[1500]; int s, i; struct sigaction sa; /* setup alarm signal handler */ // 这里增加一个信号处理,使得我们可以通过定时器信号来停止Benchmark sa.sa_handler = alarm_handler; sa.sa_flags = 0; if (sigaction(SIGALRM, &sa, NULL)) exit(3); // 然后我们设置一个定时器 // 等到时间以后,发送一个alarm信号 // 这样程序就会停止了 alarm(benchtime); rlen = strlen(req); // 这里进入主循环 nexttry: while (1) { // 如果检测到停止信号,就退出循环 if (timerexpired) { // 当然这个时候需要对统计数据进行一个修正 // 因为已经有一个连接被这个信号打断,但这个fail不能被算进来 if (failed > 0) { /* fprintf(stderr,"Correcting failed by signal\n"); */ failed--; } return; } // 发起连接 s = Socket(host, port); // 如果连接失败,加入统计 if (s < 0) { failed++; continue; } // 如果没能成功发起请求,同样加入错误统计 if (rlen != write(s, req, rlen)) { failed++; close(s); continue; } // 反正就是统计各种情况下的可能错误 if (http10 == 0) if (shutdown(s, 1)) { failed++; close(s); continue; } // 如果我们不强制断开连接 // 那么就也有必要读取所有的返回数据 if (force == 0) { /* read all available data from socket */ while (1) { // 当然这个读取过程应该能够被停止信号所打断 if (timerexpired) break; i = read(s, buf, 1500); /* fprintf(stderr,"%d\n",i); */ // 如果读着读着出问题了的话,也得加入统计; // 如果读完了,就退出 if (i < 0) { failed++; close(s); goto nexttry; } else if (i == 0) break; else // 如果读取正常,得统计一下字节数 bytes += i; } } // 连接也得能正常关闭 if (close(s)) { failed++; continue; } speed++; } } |
转至: https://blog.finaltheory.me/research/WebBench-Source-Code.html