简介
webbench是一款用C编写的开源工具,主要用来在Linux下进行网站压力测试。最多可以模拟3万个连接去测试网站的负载能力,并可以设置运行的客户端数、测试时间、使用的http协议版本、请求方法、是否需要等待服务器响应等选项,最后统计每分钟相应请求次数(paga/min)和每秒钟传输数据量(byte/sec),以及请求成功数和失败数,表现测试网站的压力承载能力。
源码及其相关函数分析
webbench源码
getopt_long
sigaction
fdopen
工作流程
首先解析终端输入的命令,然后根据输入的url构建相应的HTTP请求,开始压力测试后,
创建pipe管道,再创建client个子进程,在子进程中向服务端发送HTTP请求进行压力测试,
然后通过管道将测试信息传送给父进程。父进程将通过管道读取每个子进程的测试结果,
进行汇总统计。
测试
代码与测试相结合,有助于理解
./webbench -c 3000 -t 5 http://baidu.com///结果
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.Benchmarking: GET http://baidu.com/
3000 clients, running 5 sec.Speed=28164 pages/min, 22326 bytes/sec.
Requests: 1648 susceed, 699 failed.
源码解析
webbench.c
main()
int main(int argc, char *argv[])
{int opt=0;int options_index=0;char *tmp=NULL;// 如果在终端只输入了./webbench,后面没有跟参数,打印用法,直接退出程序,返回码为2,表示格式错误if(argc==1){usage();return 2;} // ./webbench -c 3000 -t 5 url// 循环解析终端输入选项,每次解析一个选项及其后面可能跟的参数//该函数主要作用就是解析终端输入的参数,是否与(短选项)“912Vfrt:p:c:?h”或者与(长选项)“long_options”中任一符合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;// 如果参数中没有字符':',说明没有端口号,直接退出switchif(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,则设置为默认值1if(clients==0) clients=1;// 如果输入运行测试的秒数为0,则设置为默认值60if(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、请求方法、测试URLprintf("\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.9case 0: printf(" (using HTTP/0.9)");break;// 如果http10的值为2,打印使用HTTP/1.1case 2: printf(" (using HTTP/1.1)");break;}printf("\n");// 打印客户端数、运行秒数if(clients==1) printf("1 client");elseprintf("%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();
}
bench
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;i<clients;i++){pid=fork();// 如果是子进程或者创建失败,休眠1s后退出循环,让父进程先执行,完成初始化,并且保证子进程中不会再fork出新的子进程if(pid <= (pid_t) 0){/* child process or error*/sleep(1); /* make childs faster */break;}}// 如果创建子进程失败,那么打印fork失败,直接退出程序,返回码为3,表示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 */// 如果不使用代理服务器,那么子进程直接对目标服务器发出http请求,否则向代理服务器发出http请求if(proxyhost==NULL)benchcore(host,proxyport,request);elsebenchcore(proxyhost,proxyport,request);/* write results to pipe */// 获取管道写端的文件指针f=fdopen(mypipe[1],"w");// 获取失败,直接退出,返回码为3,表示内部错误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);// 返回0,表示运行成功return 0;} else // 父进程中{// 获取管道读端的指针f=fdopen(mypipe[0],"r");// 获取失败,直接退出,返回码为3,表示内部错误if(f==NULL) {perror("open pipe for reading failed.");return 3;}// 设置不使用缓冲。每个I/O操作都被即时写入管道setvbuf(f,NULL,_IONBF,0);// 初始化传输速率,测试失败次数,传输总字节数都为0speed=0;failed=0;bytes=0;// 父进程循环读取数据 while(1){// 循环从管道中每3个一组读取子进程的输出数据,并且获取成功读取的参数个数pid=fscanf(f,"%d %d %d",&i,&j,&k);// 如果成功读取的个数小于2,说明有子进程中途挂掉,直接退出读取循环//把每个子进程输出的三个数据读取,在父进程中统计if(pid<2){fprintf(stderr,"Some of our childrens died.\n");break;}// 否则更新传输速率(测试成功个数),测试失败次数,传输总字节数//父进程中speed,filed,bytes三个变量对clients中的数据进行统计综合//父子进程遵循读时共享,写时复制,分析时请注意下speed+=i;failed+=j;bytes+=k;/* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */// 客户端数减一后如果等于0,说明没有多的客户端数据读取,直接退出循环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
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的信号处理函数sa.sa_handler=alarm_handler;sa.sa_flags=0;if(sigaction(SIGALRM,&sa,NULL))exit(3);// 设置计时器时间为运行测试的时间,到期后发送SIGALRM信号alarm(benchtime);// 获取请求报文大小rlen=strlen(req);// 进入循环,每次客户端建立一个连接,计时器时间到期后再退出nexttry:while(1){// 如果timerexpired等于1,说明收到了SIGALRM信号,表示计时器到期了,直接返回if(timerexpired){// 如果失败的测试数大于0,那么失败的测试数减一if(failed>0){/* 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;// 如果读取到了数据,将总共传送的字节数加上读取到的数据的字节数elsebytes+=i;}}// 关闭连接,如果失败,测试失败数加一,继续循环if(close(s)) {failed++;continue;}// 传输速率(这里用测试成功数表示,后面除以时间得到真正的传输速率)加一speed++;}
}
socket.c
/* * 建立与目标的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协议为IPv4memset(&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);// 如果获取失败。返回-1if (hp == NULL)return -1;// 设置目标套接字的IP地址memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);}// 设置目标套接字的端口号ad.sin_port = htons(clientPort);// 创建一个使用TCP协议的socketsock = socket(AF_INET, SOCK_STREAM, 0);// 如果创建失败,直接返回if (sock < 0)return sock;// 进行连接,如果连接不成功,返回-1if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0)return -1;// 如果连接成功,返回sockreturn sock;
}