软件压力测试工具Webbench源码分析

网站压测工具Webbench源码分析

原文链接:点击打开链接

Webbench是一个在Linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL测试网站在压力下工作的性能。Webbench使用C语言编写,下面是其下载链接:

http://home.tiscali.cz/~cz210552/webbench.html

Webbench架构

    该测试工具原理较为简单,使用fork创建子进程,通过子进程来测试http连接,把测试结果写到管道,再有父进程读取管道信息来计算测试结果。流程图下:

软件压力测试工具Webbench源码分析


webbench源码

    webbench的源代码,代码文件只有两个,Socket.c和webbench.c。首先看一下Socket.c,它当中只有一个函数int Socket(const char *host, int clientPort),大致内容如下:

[cpp] view plain copy
  1. int Socket(const char *host, int clientPort)  
  2. {  
  3.     //以host为服务器端ip,clientPort为服务器端口号建立socket连接  
  4.     //连接类型为TCP,使用IPv4网域  
  5.     //一旦出错,返回-1  
  6.     //正常连接,则返回socket描述符  
  7. }  

接着我们来瞧一下webbench.c文件。这个文件中包含了以下几个函数,我们一一列举出来:

[html] view plain copy
  1. <strong>static void alarm_handler(int signal); //为方便下文引用,我们称之为函数1。  
  2. static void usage(void); //函数2  
  3. void build_request(const char *url); //函数3  
  4. static int bench(void); //函数4  
  5. void benchcore(const char *host, const int port, const char *req); //函数5  
  6. int main(int argc, char *argv[]); //函数6</strong>  

(1)全局变量列表

源文件中出现在所有函数前面的全局变量,主要有以下几项,我们以注释的方式解释其在程序中的用途

[cpp] view plain copy
  1. <strong>volatile int timerexpired=0;//判断压测时长是否已经到达设定的时间  
  2. int speed=0; //记录进程成功得到服务器响应的数量  
  3. int failed=0;//记录失败的数量(speed表示成功数,failed表示失败数)  
  4. int bytes=0;//记录进程成功读取的字节数  
  5. int http10=1;//http版本,0表示http0.9,1表示http1.0,2表示http1.1  
  6. int method=METHOD_GET; //默认请求方式为GET,也支持HEAD、OPTIONS、TRACE  
  7. int clients=1;//并发数目,默认只有1个进程发请求,通过-c参数设置  
  8. int force=0;//是否需要等待读取从server返回的数据,0表示要等待读取  
  9. int force_reload=0;//是否使用缓存,1表示不缓存,0表示可以缓存页面  
  10. int proxyport=80; //代理服务器的端口  
  11. char *proxyhost=NULL; //代理服务器的ip  
  12. int benchtime=30; //压测时间,默认30秒,通过-t参数设置  
  13. int mypipe[2]; //使用管道进行父进程和子进程的通信  
  14. char host[MAXHOSTNAMELEN]; //服务器端ip  
  15. char request[REQUEST_SIZE]; //所要发送的http请求</strong>  

(2)函数1: static void alarm_handler(int signal);

首先,来看一下最简单的函数,即函数1,它的内容如下:

[cpp] view plain copy
  1. <strong>static void alarm_handler(int signal)  
  2. {  
  3.    timerexpired=1;  
  4. }</strong>  
webbench在运行时可以设定压测的持续时间,以秒为单位。例如我们希望测试30秒,也就意味着压测30秒后程序应该退出了。webbench中使用信号(signal)来控制程序结束。函数1是在到达结束时间时运行的信号处理函数。它仅仅是将一个记录是否超时的变量timerexpired标记为true。后面会看到,在程序的while循环中会不断检测此值,只有timerexpired=1,程序才会跳出while循环并返回。


3)函数static void usage(void);

[cpp] view plain copy
  1. <strong>static void usage(void)  
  2. {  
  3.    fprintf(stderr,  
  4.     "webbench [option]... URL\n"  
  5.     "  -f|--force               Don't wait for reply from server.\n"  
  6.     "  -r|--reload              Send reload request - Pragma: no-cache.\n"  
  7.     "  -t|--time <sec>          Run benchmark for <sec> seconds. Default 30.\n"  
  8.     "  -p|--proxy <server:port> Use proxy server for request.\n"  
  9.     "  -c|--clients <n>         Run <n> HTTP clients at once. Default one.\n"  
  10.     "  -9|--http09              Use HTTP/0.9 style requests.\n"  
  11.     "  -1|--http10              Use HTTP/1.0 protocol.\n"  
  12.     "  -2|--http11              Use HTTP/1.1 protocol.\n"  
  13.     "  --get                    Use GET request method.\n"  
  14.     "  --head                   Use HEAD request method.\n"  
  15.     "  --options                Use OPTIONS request method.\n"  
  16.     "  --trace                  Use TRACE request method.\n"  
  17.     "  -?|-h|--help             This information.\n"  
  18.     "  -V|--version             Display program version.\n"  
  19.     );  
  20. };</strong>  

4)函数3void build_request(const char *url);

这个函数主要操作全局变量char request[REQUEST_SIZE],根据url填充其内容。一个典型的http GET请求如下:

[html] view plain copy
  1. <strong>GET /test.jpg HTTP/1.1  
  2. User-Agent: WebBench 1.5  
  3. Host:192.168.10.1  
  4. Pragma: no-cache  
  5. Connection: close</strong>  

build_request函数的目的就是要把类似于以上这一大坨信息全部存到全局变量request[REQUEST_SIZE]中,其中换行操作使用的是”\r\n”。而以上这一大坨信息的具体内容是要根据命令行输入的参数,以及url来确定的。该函数使用了大量的字符串操作函数,例如strcpystrstrstrncasecmpstrlenstrchrindexstrncpystrcat。对这些基础函数不太熟悉的同学可以借这个函数复习一下。build_request的具体内容在此不做过多阐述。


5)函数6int main(int argc, char *argv[]);

之所以把函数6放在了函数4和函数5之前,是因为函数45是整个工具的最核心代码,我们把他放在最后分析。先来看一下整个程序的起始点:主函数(即函数6)。

[cpp] view plain copy
  1. <strong>int main(int argc, char *argv[])  
  2. {  
  3.     /*函数最开始,使用getopt_long函数读取命令行参数, 
  4.     来设置(1)中所提及的全局变量的值。 
  5.     关于getopt_long的具体使用方法,这里有一个配有讲解的小例子,可以帮助学习: 
  6.     http://blog.csdn.net/lanyan822/article/details/7692013 
  7.     在此期间如果出现错误,会调用函数2告知用户此工具使用方法,然后退出。 
  8.     */  
  9.       
  10.     build_request(argv[optind]); //参数读完后,argv[optind]即放在命令行最后的url  
  11.                               //调用函数3建立完整的HTTP request,  
  12.                             //HTTP request存储在全部变量char request[REQUEST_SIZE]  
  13.       
  14.     /*接下来的部分,main函数的所有代码都是在网屏幕上打印此次测试的信息, 
  15.     例如即将测试多少秒,几个并发进程,使用哪个HTTP版本等。 
  16.     这些信息并非程序核心代码,因此我们也略去。 
  17.     */  
  18.       
  19.     return bench(); //简简单单一句话,原来,压力测试在这最后一句才真正开始!  
  20.                  //所有的压测都在bench函数(即函数4)实现  
  21. }</strong>  

6)函数4static int bench(void);
[cpp] view plain copy
  1. static int bench(void){  
  2.   int i,j,k;      
  3.   pid_t pid=0;  
  4.   FILE *f;  
  5.     
  6.   i=Socket(proxyhost==NULL?host:proxyhost,proxyport); //调用了Socket.c文件中的函数  
  7.   if(i<0){ /*错误处理*/ }  
  8.   close(i);  
  9.   
  10.   if(pipe(mypipe)){ /*错误处理*/ } //管道用于子进程向父进程回报数据  
  11.   for(i=0;i<clients;i++){//根据clients大小fork出来足够的子进程进行测试  
  12.        pid=fork();  
  13.        if(pid <= (pid_t) 0){  
  14.            sleep(1); /* make childs faster */  
  15.            break;  
  16.        }  
  17.   }  
  18.   if( pid< (pid_t) 0){ /*错误处理*/ }  
  19.   
  20.   if(pid== (pid_t) 0){//如果是子进程,调用benchcore进行测试  
  21.     if(proxyhost==NULL)  
  22.       benchcore(host,proxyport,request);  
  23.     else  
  24.       benchcore(proxyhost,proxyport,request);  
  25.   
  26.      f=fdopen(mypipe[1],"w");//子进程将测试结果输出到管道  
  27.      if(f==NULL){ /*错误处理*/ }  
  28.      fprintf(f,"%d %d %d\n",speed,failed,bytes);  
  29.      fclose(f);  
  30.      return 0;  
  31.   } else{//如果是父进程,则从管道读取子进程输出,并作汇总  
  32.      f=fdopen(mypipe[0],"r");  
  33.       if(f==NULL) { /*错误处理*/ }  
  34.       setvbuf(f,NULL,_IONBF,0);  
  35.       speed=0;  failed=0;  bytes=0;  
  36.   
  37.       while(1){ //从管道读取数据,fscanf为阻塞式函数  
  38.           pid=fscanf(f,"%d %d %d",&i,&j,&k);  
  39.           if(pid<2){ /*错误处理*/ }  
  40.           speed+=i;  failed+=j;  bytes+=k;  
  41.           if(--clients==0) break;//这句用于记录已经读了多少个子进程的数据,读完就退出  
  42.       }  
  43.       fclose(f);  
  44.     //最后将结果打印到屏幕上  
  45.      printf("\nSpeed=%d pages/min, %d bytes/sec.\nRequests: %d susceed, %d failed.\n",  
  46.           (int)((speed+failed)/(benchtime/60.0f)), (int)(bytes/(float)benchtime), speed, failed);  
  47.   }  
  48.   return i;  
  49. }  

 这段代码,一上来先进行了一次socket连接,确认能连通以后,才进行后续步骤。调用pipe函数初始化一个管道,用于子进行向父进程汇报测试数据。子进程根据clients数量fork出来。每个子进程都调用函数5进行测试,并将结果输出到管道,供父进程读取。父进程负责收集所有子进程的测试数据,并汇总输出。


(7)函数5:void benchcore(const char *host,const int port,const char *req);

[cpp] view plain copy
  1. void benchcore(const char *host,const int port,const char *req){  
  2.  int rlen;  
  3.  char buf[1500];//记录服务器响应请求所返回的数据  
  4.  int s,i;  
  5.  struct sigaction sa;  
  6.   
  7.  sa.sa_handler=alarm_handler; //设置函数1为信号处理函数  
  8.  sa.sa_flags=0;  
  9.  if(sigaction(SIGALRM,&sa,NULL)) //超时会产生信号SIGALRM,用sa中的指定函数处理  
  10.     exit(3);   
  11.    
  12.  alarm(benchtime);//开始计时  
  13.  rlen=strlen(req);  
  14.  nexttry:while(1){  
  15.     if(timerexpired){//一旦超时则返回  
  16.        if(failed>0){failed--;}  
  17.        return;  
  18.     }  
  19.     s=Socket(host,port);//调用Socket函数建立TCP连接  
  20.     if(s<0) { failed++;continue;}   
  21.     if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;} //发出请求  
  22.       if(http10==0) //针对http0.9做的特殊处理  
  23.         if(shutdown(s,1)) { failed++;close(s);continue;}  
  24.       
  25.     if(force==0){//全局变量force表示是否要等待服务器返回的数据  
  26.         while(1){  
  27.         if(timerexpired) break;  
  28.           i=read(s,buf,1500);//从socket读取返回数据  
  29.           if(i<0) {   
  30.           failed++;  
  31.           close(s);  
  32.           goto nexttry;  
  33.         }else{  
  34.           if(i==0) break;  
  35.             else  
  36.               bytes+=i;  
  37.         }  
  38.         }  
  39.     }  
  40.     if(close(s)) {failed++;continue;}  
  41.     speed++;  
  42.  }  
  43. }  

benchcore是子进程进行压力测试的函数,被每个子进程调用。这里使用了SIGALRM信号来控制时间,alarm函数设置了多少时间之后产生SIGALRM信号,一旦产生此信号,将运行函数1,使得timerexpired=1,这样可以通过判断timerexpired值来退出程序。另外,全局变量force表示我们是否在发出请求后需要等待服务器的响应结果。