在http://blog.csdn.net/scmuzi18/article/details/77049710这篇文章中介绍了webbench的安装及使用,于是今天我们将对webbench的源码简单的剖析。 webbench中一个开源的测压工具,我们能看到他有两个文件socket.c和webbench.c
webbench的工作原理: 1、主函数进行必要的准备工作,进入bench开始压测 2、bench函数使用fork模拟出多个客户端,调用socket并发请求,每个子进程记录自己的访问数据,并写入管道 3、父进程从管道读取子进程的输出信息 4、使用alarm函数进行时间控制,到时间后会差生SIGALRM信号,调用信号处理函数使子进程停止 5、最后父进程将所有子进程的输出数据汇总计算,输出到屏幕上。
webbench工作流程图如下所示:
首先,简单的看一下socket.c中的代码。
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> /*功能:通过host和clientport建立socket连接(通过地址和端口建立网络连接) host:网络地址,支持域名 clientport:端口 return:成功返回socket描述符,失败返回-1*/ int Socket(const char *host, int clientPort) { int sock; unsigned long inaddr; struct sockaddr_in ad; struct hostent *hp; memset(&ad, 0, sizeof(ad)); /*指代协议族,在socket编程中只能是AF_INET*/ ad.sin_family = AF_INET; /*存储IP地址,将点分十进制的IP转为无符号长整型*/ inaddr = inet_addr(host); /*判断该地址是否合法 INADDR_NONE是32位均为1的值,即255.255.255.255,它是Internet的有限广播地址*/ if (inaddr != INADDR_NONE) memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr)); else /*host是域名*/ { /*通过域名获取IP地址 返回对应于给定主机的包含主机名的包含主机名字和地址信息的hostent指针*/ hp = gethostbyname(host); /*获取IP地址失败*/ if (hp == NULL) return -1; memcpy(&ad.sin_addr, hp->h_addr, hp->h_length); } /*将一个无符号短整型的主机数值转换为网络字节序*/ ad.sin_port = htons(clientPort); /*创建socket套接字,第二个参数设置为SOCK_STREAM,表明提供面向连接的稳定数据传输,即TCP协议*/ 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; }main.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 <rpc/types.h> #include <getopt.h> #include <strings.h> #include <time.h> #include <signal.h> /* values */ volatile int timerexpired=0;//检测时长是否达到指定时长 int speed=0;//子进程成功得到服务器响应的总数 int failed=0;//子进程请求失败总数 int bytes=0;//读取到的字节总数 /* globals */ int http10=1; /* 0 - http/0.9, 1 - http/1.0, 2 - http/1.1 */ /* 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;//http请求方法,默认为GET int clients=1;//并发数,即子进程个数,默认为1,可以由命令行参数-c指定 int force=0;//是否等待从服务器获取数据,0表示等待 int force_reload=0;//是否使用cache,0为使用 int proxyport=80;//代理服务器端口,默认80 char *proxyhost=NULL;//代理服务器IP,默认为NULL /* 执行时间,默认为30秒,可由命令行参数-t指定, 当子进程执行时间超过这个秒数之后, 发送SIGALRM信号,将timerexpired设置为1, 让所有子进程退出 */ int benchtime=30; /* internal */ int mypipe[2];//创建管道(半双工),用于父子进程间通信 char host[MAXHOSTNAMELEN];//服务器IP #define REQUEST_SIZE 2048 char request[REQUEST_SIZE];//http请求信息 /*命令行选项配置表,参见文档:man getopt_long*/ 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 [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函数读取命令行参数,来设置所涉及到的全局变量*/ 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); /*-t后跟压力测试时间,optarg返回,使用atoi转换成整数*/ case 't': benchtime=atoi(optarg);break; case 'p': /* proxy server parsing server:port */ /*strrchr(),找一个字符c在另一个字符串str中最后一次出现的位置,并返回从字符串的这个位置开始, 一直到字符串结束的所有字符。如果未能找到指定字符,那么返回NULL*/ tmp=strrchr(optarg,':');//查找端口号 proxyhost=optarg;//设定地址 if(tmp==NULL)//没有端口号 { break; } if(tmp==optarg)//没有输入IP地址 { 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': clients=atoi(optarg);break;//并发数目 } } /* 扫描参数选项时,optind标识下一个选项的索引,扫描结束后,标识第一个非选项参数索引; 如果optind=argc,说明非选项参数即服务器URL缺失,此变量是系统定义的。 optind返回第一个不包含选项的命令行参数,此处为URL值 */ if(optind==argc) { fprintf(stderr,"webbench: Missing URL!\n"); usage(); return 2; } //此处做判断可预防bug,因为上文并发用户数目可能为0 if(clients==0) clients=1; //压力测试时间默认为30s,如果用户写成0,则默认60s 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" ); /* 调用build_request函数构造完整的http请求头,http request存储全局变量char request[REQUEST_SIZE] build_request(argv[optind]);参数为URL值 */ build_request(argv[optind]); /* print bench info */ /* 在屏幕上打印测试的信息,如http协议,请求方式,并发个数,请求时间等 */ 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"); /* 调用bench函数,开始压力测试,bench()为压力测试核心代码 */ return bench(); } /* 此函数主要目的是要把类似于http GET请求的信息全部存储到全局变量request[REQUEST_SIZE]中, 其中换行操作使用“\r\n”,其中应用了大量的字符串操作函数。 创建url请求连接,http头,创建好的请求放在全局变量request中 */ void build_request(const char *url) { char tmp[10]; int i; /* void bzero(void *s,int n),将指定字符串s的前n个字节清零 请求地址和请求连接初始化清零 */ bzero(host,MAXHOSTNAMELEN); bzero(request,REQUEST_SIZE); /*协议适配,判断应该使用的http协议*/ 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," "); //strstr(sr1,str2)用于判断str2是否是str1的子串 if(NULL==strstr(url,"://")) { // 打印错误信息 fprintf(stderr, "\n%s: is not a valid URL.\n",url); exit(2); } // 判断URL长度是否太长 if(strlen(url)>1500) { fprintf(stderr,"URL is too long.\n"); exit(2); } if(proxyhost==NULL) //判断是否有代理服务器, //webbench只支持http协议 //比较前7个字符串是否为http:// if (0!=strncasecmp("http://",url,7)) { fprintf(stderr,"\nOnly HTTP protocol is directly supported, set --proxy for others.\n"); exit(2); } /* protocol/host delimiter */ //找到IP域名开始的位置,即http://后第一个位置,即主机名 i=strstr(url,"://")-url+3; /* printf("%d\n",i); */ // url+i指向http://后第一个位置 if(strchr(url+i,'/')==NULL) { fprintf(stderr,"\nInvalid URL syntax - hostname don't ends with '/'.\n"); exit(2); } if(proxyhost==NULL) { /* get port from hostname */ //判断URL中是否制定了端口号 //index函数返回字符c第一次出现的位置 if(index(url+i,':')!=NULL && index(url+i,':')<index(url+i,'/')) { //取出主机地址 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); */ //端口号转换为int proxyport=atoi(tmp); if(proxyport==0) proxyport=80; } else { //没有用主机+端口的访问方式,比如:http://www.baidu.com strncpy(host,url+i,strcspn(url+i,"/")); } // printf("Host=%s\n",host); //strcspn函数返回str1和str2中不同元素的个数 strcat(request+strlen(request),url+i+strcspn(url+i,"/")); } else { // printf("ProxyHost=%s\nProxyPort=%d\n",proxyhost,proxyport); //url地址 strcat(request,url); } // 开始封装http请求 if(http10==1) //版本号 strcat(request," HTTP/1.0"); else if (http10==2) strcat(request," HTTP/1.1"); //回车换行 strcat(request,"\r\n"); if(http10>0) strcat(request,"User-Agent: WebBench "PROGRAM_VERSION"\r\n"); 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"); } 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 */ //创建管道和子进程,对http请求进行测试 /* 1、首先测试TCP连接是否合法 2、调用pipe建立管道 3、调用fork创建子进程(父子进程共享文件),对每个子进程调用benchcore函数 4、将每个子进程的测试数据写入到管道中 5、父进程读取管道中的内容并做汇总 */ static int bench(void) { int i,j,k; pid_t pid=0; FILE *f; /* check avaibility of target server */ //调用socket函数创建socket连接,测试地址是否可以正常访问 i=Socket(proxyhost==NULL?host:proxyhost,proxyport); if(i<0) { //连接建立失败 fprintf(stderr,"\nConnect to server failed. Aborting benchmark.\n"); return 1; } //close(fd)函数,当使用完文件后,若不需要则可用close()关闭该文件 //close()会让数据写回磁盘,并释放该文件所占用的资源,参数为文件描述符 close(i); /* create pipe */ /* int pipe(int filedes[2]),建立管道,并将文件描述符有参数数组filedes数组返回 filedes[0]为管道里的读取端, filedes[1]为管道里的写入端 成功返回0,失败返回-1 */ //创建管道用于父子进程间通信 if(pipe(mypipe)) { //perror()将上一个函数发生错误的原因输出到标准stderr 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 */ //使用fork系统调用创建子进程,父进程和子进程是共享文件的,因此他们共享一个管道 for(i=0;i<clients;i++) { /* fork(),通过系统调用创建一个与原来进城几乎完全相同的进程,也就是两个进程可以做完全相同的事, 返回值>0表明是父进程,返回值=0表明是子进程,返回值<0表明出错 */ //根据并发数据,创建相应数目的子进程 pid=fork(); if(pid <= (pid_t) 0) { /* child process or error*/ sleep(1); /* make childs faster */ //创建完毕就跳出循环,否则创建出来的子进程也会继续fork子进程 break; } } //fork失败,打印错误信息 if( pid< (pid_t) 0) { fprintf(stderr,"problems forking worker no. %d\n",i); perror("fork failed."); return 3; } //子进程 if(pid== (pid_t) 0) { /* I am a child */ //子进程执行请求,知道超时返回,并判断是否使用代理服务器 if(proxyhost==NULL) // 测试http请求 benchcore(host,proxyport,request); else benchcore(proxyhost,proxyport,request); /* write results to pipe */ //子进程打开管道写入端 f=fdopen(mypipe[1],"w"); 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(),设置文件缓冲区函数,使得打开文件后,用户可建立自己的缓冲区, 而不使用fopen()函数打开文件设定的默认缓冲区,由malloc函数来分配缓冲区 */ 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; } /* benchcore函数是子进程进行压力测试的函数,被每个子进程调用 host:地址 port:端口 req:http格式方法 */ 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 */ //当程序执行到指定的秒数后,发送SIGALRM函数,即设置alarm_handler函数为信号处理函数 sa.sa_handler=alarm_handler; sa.sa_flags=0; //sigaction成功返回0,失败返回-1,超时会产生 信号SIGALRM,用sa指定函数处理 if(sigaction(SIGALRM,&sa,NULL)) exit(3); //开始计时 alarm(benchtime); rlen=strlen(req); //无限执行请求,直到收到SIGALRM信号将timerexpired设置为1时 nexttry:while(1) { //一旦超时,则返回 if(timerexpired) { if(failed>0) { /* fprintf(stderr,"Correcting failed by signal\n"); */ failed--; } return; } //连接远程服务器,通过调用socket函数建立TCP连接 s=Socket(host,port); //连接失败,failed数+1 if(s<0) { failed++;continue;} //发出请求,header大小与发送的不相等,则失败 if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;} //针对http0.9做的特殊处理,则关闭socket的写操作,成功返回0,错误返回-1 if(http10==0) if(shutdown(s,1)) { failed++;close(s);continue;} /* 全局变量force表示是否要等待服务器当的数据 如果等待数据返回,则读取响应数据,计算传输的字节数 发出请求后需要等待服务器的响应结果,force=0表示等待从 server返回的数据 */ if(force==0) { /* read all available data from socket */ while(1) { //timerexpired默认为0,在规定时间内读取当为1时表示定时结束 if(timerexpired) break; //从socket读取返回数据 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++; } }