webbench源码解析

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.cwebbench.c两个文件组成,socket.c中只有一个方法int Socket,用来建立与目标的TCP连接,并返回客户端连接使用的套接字,对命令行参数的处理,构建请求,创建子进程进行压力测试等在webbench.c文件中完成。

socket.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

#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++;
 }
}

你可能感兴趣的:(源码解析,c语言)