# webbench_comments **Repository Path**: stugeek/webbench_comments ## Basic Information - **Project Name**: webbench_comments - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-04-10 - **Last Updated**: 2022-04-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # webbench源码解析 ## webbench简介 webbench是一款用C编写的开源工具,主要用来在Linux下进行网站压力测试。最多可以模拟3万个连接去测试网站的负载能力,并可以设置运行的客户端数、测试时间、使用的http协议版本、请求方法、是否需要等待服务器响应等选项,最后统计每分钟相应请求次数(paga/min)和每秒钟传输数据量(byte/sec),以及请求成功数和失败数,表现测试网站的压力承载能力。 ## 版本 最新版webbench 1.5 ## 运行方式 在目录下,直接输入`make`进行编译,然后输入 ``` ./webbench -c 3000 -t 5 url ``` `-c 3000`表示模拟的客户端连接数为3000,`-t 5`表示运行测试时间为5s,url是测试网站,即可进行测试。 ## 工作流程 1. 首先在主函数中处理终端输入的命令参数,然后利用build_request方法解析终端输入的测试url,构造HTTP请求,打印相关信息后,使用bench方法开始压力测试。 2. 在bench方法中,首先尝试对目标建立TCP连接,检查连接可用性,如果失败,说明目标可能未联机,直接退出程序,成功则继续进行。 3. 创建管道,然后根据终端输入客户端个数,创建指定个数的子进程,在每个子进程中,使用benchcore方法在测试时间内不断向服务器建立连接并发送http请求。 4. 在benchcore方法中,每个子进程会先根据终端输入测试时间,设置计时器的时间,然后进入循环,每次循环客户端都会向目标建立连接并发送HTTP请求,然后更新请求成功数和失败数、传输数据量等信息,当计时器到期后,退出循环,子进程结束。 5. 每个子进程将最后总共的传输速率,测试失败数,总传输字节数等相应统计信息写入管道,父进程则负责从管道中循环读取子进程的相应统计信息,进行统计,并将最终每分钟相应请求次数(paga/min)和每秒钟传输数据量(byte/sec),以及请求成功数和失败数等压力测试数据打印在屏幕上。 ## 源码解析 webbench的源码由`socket.c`和`webbench.c`两个文件组成,`socket.c`中只有一个方法`int Socket`,用来建立与目标的TCP连接,并返回客户端连接使用的套接字,对命令行参数的处理,构建请求,创建子进程进行压力测试等在`webbench.c`文件中完成。 **`socket.c`:** ```C #include #include #include #include #include #include #include #include #include #include #include #include /* * 建立与目标的TCP连接,返回客户端连接使用的套接字 * host: 目标域名或主机名 * clientPort: 目标端口 */ int Socket(const char *host, int clientPort) { int sock; // 客户端套接字标识符 unsigned long inaddr; // 主机名的IP地址的数字形式 struct sockaddr_in ad; // 套接字地址结构 struct hostent *hp; // 域名IP地址 // 初始化目标套接字的地址,指定使用的IP协议为IPv4 memset(&ad, 0, sizeof(ad)); ad.sin_family = AF_INET; // 将目标主机名的IP地址转换为数字 inaddr = inet_addr(host); // 如果返回值不为INADDR_NONE,说明不是无效的IP地址,设置目标套接字的IP地址 if (inaddr != INADDR_NONE) memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr)); // 否则说明是无效的IP地址,host不是主机名而是域名 else { // 通过域名获取IP地址 hp = gethostbyname(host); // 如果获取失败。返回-1 if (hp == NULL) return -1; // 设置目标套接字的IP地址 memcpy(&ad.sin_addr, hp->h_addr, hp->h_length); } // 设置目标套接字的端口号 ad.sin_port = htons(clientPort); // 创建一个使用TCP协议的socket sock = socket(AF_INET, SOCK_STREAM, 0); // 如果创建失败,直接返回 if (sock < 0) return sock; // 进行连接,如果连接不成功,返回-1 if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0) return -1; // 如果连接成功,返回sock return sock; } ``` **`webbench.c`:** ```C #include "socket.c" #include #include #include #include #include #include #include /* values */ volatile int timerexpired=0; // 表示计时器是否到期,到期为1,否则为0 int speed=0; // 传送速率(先获得测试成功次数,后面除以时间得到真正的传输速率) int failed=0; // 测试的失败数 int bytes=0; // 总共传送的字节数 /* globals */ // 使用的http协议版本,http/0.9为0,http/1.0为1,http/1.1为2 int http10=1; /* 0 - http/0.9, 1 - http/1.0, 2 - http/1.1 */ /* Allow: GET, HEAD, OPTIONS, TRACE */ #define METHOD_GET 0 // 方法GET的宏定义为0 #define METHOD_HEAD 1 // 方法HEAD的宏定义为1 #define METHOD_OPTIONS 2 // 方法OPTIONS的宏定义为2 #define METHOD_TRACE 3 // 方法TRACE的宏定义为3 #define PROGRAM_VERSION "1.5" // 程序的版本为1.5,即webbench1.5 int method=METHOD_GET; // 请求方式默认为GEI int clients=1; // 客户端数量默认为2 int force=0; // 是否不等待服务器响应,发送请求后直接关闭连接,默认需要等待 int force_reload=0; // 是否强制代理服务器重新发送请求,默认不发送 int proxyport=80; // 访问端口默认为80 char *proxyhost=NULL; // 代理服务器,默认无 int benchtime=30; // 测试运行时间默认为30s /* internal */ int mypipe[2]; // 读写管道,0为读取端,1为写入端 char host[MAXHOSTNAMELEN]; // 目标服务器的网络地址 #define REQUEST_SIZE 2048 // 请求的最大长度 char request[REQUEST_SIZE]; // 请求内容 // 构造长选项与短选项的对应关系,no_argument表示选项没有参数,required_argument表示选项需要参数 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); // 信号处理函数 static void alarm_handler(int signal) { // 设置计时器到期 timerexpired=1; } // 用法,描述了参数设置,可打印出来 static void usage(void) { fprintf(stderr, // 用法是webbench后面加相应的选项,最后加上要测试的URL "webbench [option]... URL\n" // 不用等待服务器的响应,发送请求后直接关闭连接 " -f|--force Don't wait for reply from server.\n" // 发送重新加载请求(无缓存) " -r|--reload Send reload request - Pragma: no-cache.\n" // 运行基准测试的秒数,默认为30s " -t|--time Run benchmark for seconds. Default 30.\n" // 使用代理服务器发送请求 " -p|--proxy Use proxy server for request.\n" // 一次运行的http客户端个数,默认为1 " -c|--clients Run HTTP clients at once. Default one.\n" // 使用HTTP/0.9协议 " -9|--http09 Use HTTP/0.9 style requests.\n" // 使用HTTP/1.0协议 " -1|--http10 Use HTTP/1.0 protocol.\n" // 使用HTTP/1.1协议 " -2|--http11 Use HTTP/1.1 protocol.\n" // 请求方法使用GET " --get Use GET request method.\n" // 请求方法使用HEAD " --head Use HEAD request method.\n" // 请求方法使用OPTIONS " --options Use OPTIONS request method.\n" // 请求方法使用TRACE " --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; // 如果在终端只输入了./webbench,后面没有跟参数,打印用法,直接退出程序,返回码为2,表示格式错误 if(argc==1) { usage(); return 2; } // 循环解析终端输入选项,每次解析一个选项及其后面可能跟的参数 while((opt=getopt_long(argc,argv,"912Vfrt:p:c:?h",long_options,&options_index))!=EOF ) { switch(opt) { case 0 : break; // 如果选项为'f',那么设置不等待服务器响应,发送请求后直接关闭连接 case 'f': force=1;break; // 如果选项为'r',那么设置强制代理服务器重新发送请求 case 'r': force_reload=1;break; // 如果选项为'9',那么设置在条件允许范围内使用HTTP/0.9协议 case '9': http10=0;break; // 如果选项为'1',那么设置在条件允许范围内使用HTTP/1.0协议 case '1': http10=1;break; // 如果选项为'2',那么设置在条件允许范围内使用HTTP/1.1协议 case '2': http10=2;break; // 如果选项为'V',那么打印程序版本,然后退出程序 case 'V': printf(PROGRAM_VERSION"\n");exit(0); // 如果选项为't',那么记录其后所跟参数数值到运行基准时间benchtime中 case 't': benchtime=atoi(optarg);break; // 如果选项为'p',那么表示使用代理服务器 case 'p': /* proxy server parsing server:port */ // 记录参数中最后出现字符':'的位置及其之后的内容到tmp中 tmp=strrchr(optarg,':'); // 记录参数到代理主机proxyhost中 proxyhost=optarg; // 如果参数中没有字符':',说明没有端口号,直接退出switch if(tmp==NULL) { break; } // 如果参数中只有一个字符':',说明端口号在最前,打印缺失主机名,然后直接返回,返回码为2,表示格式错误 if(tmp==optarg) { fprintf(stderr,"Error in option --proxy %s: Missing hostname.\n",optarg); return 2; } // 如果参数中最后一个':'之后没有内容,打印缺失端口号,然后直接返回,返回码为2,表示格式错误 if(tmp==optarg+strlen(optarg)-1) { fprintf(stderr,"Error in option --proxy %s Port number is missing.\n",optarg); return 2; } // 将proxyhost中的内容从最后一个':'处进行截断,只记录':'之前的内容 *tmp='\0'; // 将最后一个':'之后内容转化为数字并记录在代理服务器端口号proxyport中 proxyport=atoi(tmp+1);break; // 如果选项为':'、'h'、'?',那么打印用法,并直接退出程序,返回码为2,表示格式错误 case ':': case 'h': case '?': usage();return 2;break; // 如果选项为'c',那么记录其后所跟参数数值到客户端数量clients中 case 'c': clients=atoi(optarg);break; } } // 如果参数后没有其它内容,打印缺失测试URL,打印用法后直接退出程序,返回码为2,表示格式错误 if(optind==argc) { fprintf(stderr,"webbench: Missing URL!\n"); usage(); return 2; } // 如果输入客户端数量为0,则设置为默认值1 if(clients==0) clients=1; // 如果输入运行测试的秒数为0,则设置为默认值60 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" ); // 将测试URL作为参数传入build_request方法中,构建http请求 build_request(argv[optind]); /* print bench info */ // 打印Benchmarking、请求方法、测试URL 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]); // 判断使用的http协议类型,如果使用的是默认的HTTP/1.0则不打印 switch(http10) { // 如果http10的值为0,打印使用HTTP/0.9 case 0: printf(" (using HTTP/0.9)");break; // 如果http10的值为2,打印使用HTTP/1.1 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"); // 开始压力测试 return bench(); } // 利用传入的测试url参数,构建对测试url的http请求 void build_request(const char *url) { char tmp[10]; int i; bzero(host,MAXHOSTNAMELEN); bzero(request,REQUEST_SIZE); // 缓存和代理需要HTTP/1.0及以上才能使用,自动调整使用http协议版本 if(force_reload && proxyhost!=NULL && http10<1) http10=1; // 请求方法HEAD需要HTTP/1.0及以上才能使用,自动调整使用http协议版本 if(method==METHOD_HEAD && http10<1) http10=1; // 请求方法OPTIONS和TRACE需要HTTP/1.1及以上才能使用,自动调整使用http协议版本 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的格式合法性 */ // 如果url中没有"://",打印是无效的URL,直接退出程序,返回码为2,表示格式错误 if(NULL==strstr(url,"://")) { fprintf(stderr, "\n%s: is not a valid URL.\n",url); exit(2); } // 如果url的长度大于1500,打印URL过长,直接退出程序,返回码为2,表示格式错误 if(strlen(url)>1500) { fprintf(stderr,"URL is too long.\n"); exit(2); } // 如果不使用代理服务器 if(proxyhost==NULL) // 如果url的前7位不是任意大小写的"http://",打印仅直接支持HTTP协议,可能需要选择使用代理服务器的选项,直接退出程序,返回码为2,表示格式错误 if (0!=strncasecmp("http://",url,7)) { fprintf(stderr,"\nOnly HTTP protocol is directly supported, set --proxy for others.\n"); exit(2); } /* protocol/host delimiter */ // 定位目标主机名的开始位置 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); } /* 判断完url的合法性后填写url到请求行 */ // 没有设置代理服务器时 if(proxyhost==NULL) { /* get port from hostname */ // 从主机名之后开始寻找':'所在的位置,如果':'存在且位置在'/'之前 if(index(url+i,':')!=NULL && index(url+i,':')0) strcat(request,"User-Agent: WebBench "PROGRAM_VERSION"\r\n"); // 如果没有使用代理服务器且使用的是HTTP/1.0及其之后的版本,拼接目标服务器地址到请求头中 if(proxyhost==NULL && http10>0) { strcat(request,"Host: "); strcat(request,host); strcat(request,"\r\n"); } // 如果设置了强制代理服务器重新发送请求且代理服务器不为空,拼接不使用缓存到请求头中 if(force_reload && proxyhost!=NULL) { strcat(request,"Pragma: no-cache\r\n"); } // 如果使用的是HTTP/1.1协议,因为不用传输任何内容,那么拼接不使用长连接到请求头中,这样可以降低维护连接的消耗 if(http10>1) strcat(request,"Connection: close\r\n"); /* add empty line at end */ // 最后填入空行完成构建请求头 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 */ // 建立一个TCP连接,检查连接可用性,如果设置了代理服务器,那么连接代理服务器,否则直接连接目标服务器 i=Socket(proxyhost==NULL?host:proxyhost,proxyport); // 连接失败那么打印错误信息,并直接退出,返回码为1,表示基准测试失败(服务器未联机) if(i<0) { fprintf(stderr,"\nConnect to server failed. Aborting benchmark.\n"); return 1; } // 关闭连接,这次连接不计入测试 close(i); /* create pipe */ // 创建管道,如果失败,直接退出程序,返回码为3,表示内部错误 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;i0) { /* fprintf(stderr,"Correcting failed by signal\n"); */ failed--; } return; } // 建立与目标服务器的TCP连接 s=Socket(host,port); // 如果连接失败,测试的失败数加一,继续循环 if(s<0) { failed++;continue;} // 如果请求报文写入套接字失败,测试的失败数加一,继续循环 if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;} // 如果使用HTTP/0.9协议,因为会在服务器回复后自动断开连接,所以可以先关闭写端 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; // 将数据读取进buf中 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++; } } ```