[源码赏析] 经典压力测试工具之WebBench
前言
身在运维部,跑压测是必不可少的日常工作。所以通过读WebBench项目的源码,简单了解一下压力测试工具的基本原理,在此做个记录。
整理过后的源码点击这里下载,包含了正确的CMake文件,并做了适当修改,可以使用CLion在Windows上编译,Linux平台应该也可以。
在Windows平台上的实际运行效果如下:
源码赏析
socket.c
/* $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
/* * (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
版权声明:
作者:xiaoniba
链接:https://blog.xiaoniba.com/2016/08/27/%e6%ba%90%e7%a0%81%e8%b5%8f%e6%9e%90-%e7%bb%8f%e5%85%b8%e5%8e%8b%e5%8a%9b%e6%b5%8b%e8%af%95%e5%b7%a5%e5%85%b7%e4%b9%8bwebbench/
来源:小泥吧的博客
文章版权归作者所有,未经允许请勿转载。
THE END
二维码
共有 0 条评论