webbench源码剖析

Webbench是有名网站压力测试工具,它是由Lionbridge公司开发
http://www.lionbridge.com
Webbench能测试处在相同硬件上,不同服务的性能以及不同硬件上同一个服务的运行状况。webbench的标准测试可以向我们展示服务器的两项内容:每秒钟相应请求数和每秒钟传输数据量。webbench不但能具有便准静态页面的测试能力,还能对动态页面(ASP,PHP,JAVA,CGI)进 行测试的能力。还有就是他支持对含有SSL的安全网站例如电子商务网站进行静态或动态的性能测试。
Webbench最多可以模拟3万个并发连接去测试网站的负载能力。
webbench的源码很简单,只有socket.c和webbench.c两个文件,加起来也就500行左右的代码。
实例:
这里写图片描述
流程图如下:
这里写图片描述
整个代码主要有以下几个函数:
int Socket(const char *host, int clientPort):建立socket连接
void usage(void):使用说明
void build_request(const char *url):构造http请求
static int bench(void):,创建管道,对http请求进行测试
void benchcore(const char *host,const int port,const char *req):测试http请求

//socket.c 文件
#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连接,返回-1连接失败,成功返回socket描述符
    host:网络地址
    clientPort:端口

*/
int Socket(const char *host, int clientPort)
{
    int sock;
    unsigned long inaddr;
    struct sockaddr_in ad;//声明一个ipv4地质类型的结构体
    struct hostent *hp;

    memset(&ad, 0, sizeof(ad));
    ad.sin_family = AF_INET;//设置为流式(TCP协议数据传输方式)

    inaddr = inet_addr(host);
    /*
        点分十进制的IP地址转换成网络中传输的长整型数。如果传入的字符串不是一个合法的IP地址,
        将返回INADDR_NONE,INADDR_NONE是255.255.255.255 是一个无效地址
    */
    if (inaddr != INADDR_NONE)//判断是否为合法IP地址
        memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));//转换后的IP拷贝到ad结构体
    else
    {
        //如果不是IP地址而是域名
        hp = gethostbyname(host);//通过域名获取IP地址
       //gethostbyname(),返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针。        
        if (hp == NULL)
            return -1;
        memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);
    }
    ad.sin_port = htons(clientPort);//设置端口号
        //htons(),将一个无符号短整型的主机数值转换为网络字节
    sock = socket(AF_INET, SOCK_STREAM, 0);//申请一个套接字
    //socket(),获取文件描述符,成功返回一个套接字描述符,失败返回-1
    if (sock < 0)
        return sock;
    if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0)//和刚才的ad结构体建立连接,也就是和host建立连接
        return -1;
    /*
    connet(),用于建立与指定socket的连接
    参数1:标识一个未连接的socket
    参数2:指向要连接套接字的sockaddr结构体的指针
    参数3:sockadd人结构体的字节长度
    */
    return sock;
}

//webbench.c文件
#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"
//请求方法设为GET,此外还支持OPTIONS,HEAD,TRACE等方法
int method=METHOD_GET;
int clients=1;//并发数,由命令行参数-c指定,默认为1
int force=0;//是否等待服务器应答
int force_reload=0;//是否使用cache,默认为0,使用
int proxyport=80;//代理服务器端口号,默认80
char *proxyhost=NULL;//代理服务器地址
int benchtime=30;//测试时间,由命令行参数-t指定,默认为30秒
/* internal */
int mypipe[2];//创建管道,用于父子进程间通信
char host[MAXHOSTNAMELEN];//主机名
#define REQUEST_SIZE 2048
char request[REQUEST_SIZE];//HTTP请求信息
/*
struct option类型数组.该数据结构中的每个元素对应了一个长选项,并且每个元素是由四个域组成。通常情况下,可以按以下规则使用:
第一个元素,描述长选项的名称;第二个选项,代表该选项是否需要跟着参数,需要参数则为1,反之为0;第三个选项,可以赋为NULL
;第四个选项,是该长选项对应的短选项名称。另外,数据结构的最后一个元素,要求所有域的内容均为0,即{NULL,0,NULL,0}。
结构中的元素解释如下: 
1)const char *name:选项名,前面没有短横线。譬如"help"、"verbose"之类。 
2)int has_arg:描述长选项是否有选项参数,如果有,是哪种类型的参数,其值见下表:  
符号常量             数值        含义  
no_argument            0    选项没有参数 
required_argument      1    选项需要参数
optional_argument      2    选项参数是可选的
3)int *flag:如果该指针为NULL,那么getopt_long返回val字段的值;如果该指针不为NULL,那么会使得它所指向的结构填入val字段的值,
同时getopt_long返回0
4)int val:如果flag是NULL,那么val通常是个字符常量,如果短选项和长选项一致,那么该字符就应该与optstring中出现的这个选项的参数相同;
*/
//命令行的选项配置表,参见 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);
//信号处理函数,webbench采用时钟信号的方式控制对服务端的访问时长,
//函数中timerexpired设置为1,执行时检测到timerexpired=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;
 //使用错误,没有传参,调用使用说明usage()
 if(argc==1)
 {
      usage();
          return 2;
 } 

 /*
 getopt_long是对命令进行解析的库函数,参数挨个解析,解析完毕返回-1
 int getopt_long(int argc, char * const argv[],
                  const char *optstring,
                  const struct option *longopts, int *longindex);
 参数1、2:main函数的argc、argv参数
 参数3:由该命令要处理的各个选项组成的字符串。选项后面带有冒号时,表示该选项是一个带参数的选项
 参数4:结构体struct option数组
 参数5:函数getopt_long()返回时,干参数的值是struct option数组的索引
 optarg:处理带输入参数的选项时,选项参数保存至char *optarg中。 
 optind:下一个处理的选项在argv中的地址,所有选项处理完后,optind 指向未识别的项。 
 optopt:最后一个已知项
 */
 //检查参数,并设置对应选项
 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;//optarg:表示命令后的参数,例如:-c 100,optarg=100    
   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;//将端口号保存在proxyport中
   case ':':
   case 'h':
   case '?': usage();return 2;break;
   case 'c': clients=atoi(optarg);break;//将客户端数量保存在clients中
  }
 }
 //optind:命令行参数中未读取的下一个元素下标值
 if(optind==argc) {
                      fprintf(stderr,"webbench: Missing URL!\n");
              usage();
              return 2;
                    }
 //如果客户端数量为0,则重新置为1
 if(clients==0) clients=1;
 //如果指定访问时间为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(argv[optind]);//opt为url的位置,构造http请求
 /* print bench info */
 //输出提示信息
 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()函数,进行压力测试
 return bench();
}

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," ");
  //判断url是否合法
  if(NULL==strstr(url,"://"))//strstr()查找子串,判断url中是否包含“://”
  {
      //打印错误信息
      fprintf(stderr, "\n%s: is not a valid URL.\n",url);
      exit(2);
  }
  if(strlen(url)>1500)//判断url长度是否太长
  {
         fprintf(stderr,"URL is too long.\n");
     exit(2);
  }
  //判断是否是http协议,webbench只支持http
  if(proxyhost==NULL)//判断是否有代理服务器
       if (0!=strncasecmp("http://",url,7)) //判断前7个字符串是否为http://
       { 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); */
  //域名或者IP都必须以/作为结束标志
  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 */
      //主机加端口的访问方式:http://127.0.0.1:8080/
      //char* index(const char* s,int c)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); */
       proxyport=atoi(tmp);//端口号转换为Int
       if(proxyport==0) proxyport=80;
   } else
   {//没有用主机+端口的访问方式,比如: http://www.baidu.com/
     strncpy(host,url+i,strcspn(url+i,"/"));
   }
   //strcspn:返回str1和str2中不同元素的个数
   // printf("Host=%s\n",host);
   strcat(request+strlen(request),url+i+strcspn(url+i,"/"));
  } else
  {
   // printf("ProxyHost=%s\nProxyPort=%d\n",proxyhost,proxyport);
   strcat(request,url);//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请求进行测试
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 */
  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 */
           break;//创建完毕就跳出循环,否则创建出来的子进程也会继续fork子进程
       }
  }
  //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)//判断是否使用代理服务器
      benchcore(host,proxyport,request);//测试http请求
         else
      benchcore(proxyhost,proxyport,request);

         /* write results to pipe */
    /*
        FILE* fdopen(int filedes,const char* type),用于在一个已经打开的文件描述符上打开一个流,
        参数1:已经打开的文件描述符,参数2:打开的方式
        w:打开只写文件,若文件存在则文件长度清零,若不存在则创建该文件
        r:打开只读文件,该文件必须存在
        成功时返回指向该流的文件指针,失败返回NULL
    */
     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;
}
//测试http请求,req:请求信息
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 */
 sa.sa_handler=alarm_handler;//信号处理函数
 sa.sa_flags=0;
 /*
    sigaction(),检查或修改与指定信号相关联的处理动作
 */
 //超时会产生SIGALRM信号,调用alarm_handler函数处理信号
 if(sigaction(SIGALRM,&sa,NULL))
    exit(3);
 /*
    alarm(),设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程
    如果seconds=0,则之前设置的闹钟会被取消,并将剩下的时间返回
 */
 alarm(benchtime);//设置闹钟,开始计时

 rlen=strlen(req);//计算请求报文的大小
 nexttry:while(1)//包含goto语句的while循环
 {
    //计时结束,产生信号,信号处理函数将timerexpired置1,退出函数
    if(timerexpired)
    {
       if(failed>0)
       {
          /* fprintf(stderr,"Correcting failed by signal\n"); */
          failed--;
       }
       return;
    }
    s=Socket(host,port);   //调用socket.c中的Socket函数建立tcp连接,返回套接字                       
    if(s<0) { failed++;continue;} //连接失败,filed+1
    if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;}//发出请求
    if(http10==0) //针对HTTP0.9进行特殊处理
        if(shutdown(s,1)) { failed++;close(s);continue;}
    if(force==0) //是否等待服务器响应,-f选项为不等待
    {
            /* read all available data from socket */
        while(1)
        {
              if(timerexpired) break; //判断是否超时
          i=read(s,buf,1500);//读取服务器的响应数据,放在数组buf中
              /* fprintf(stderr,"%d\n",i); */
          if(i<0) //读取数据失败
              { 
                 failed++;//失败请求数计数
                 close(s);//关闭当前socket
                 goto nexttry;//跳转下一次循环
              }
           else     //读取数据成功
               if(i==0) break;
               else
                   bytes+=i;//读取字节数增加相应数目
        }
    }
    if(close(s)) {failed++;continue;}
    speed++;//HTTP测试成功,speed+1
 }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值