开源项目Web Bench 详解,内附视频

一、项目结构

Web Bench 项目主要包括三个文件:webbench.c、socket.c、Makefile。

  • webbench.c : 测压主文件
  • socket.c : 将 socket 的连接封装成了函数
  • Makefile : 程序的编译文件

二、Web Bench 流程图

三、构建环境

Linux version 5.15.0-46-generic (buildd@lcy02-amd64-007) (gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #49~20.04.1-Ubuntu SMP Thu Aug 4 19:15:44 UTC 2022

四、构建方法

make
./webbench 参数信息

五、源码分析

5.1 源码

/*
 * (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 
#include 
#include 
#include 
#include 
#include 
#include 

/* values */
volatile int timerexpired = 0; // 标记测试时间是否用完
int speed = 0;
int failed = 0; // 记录失败次数
int bytes = 0;  // 记录读取的字节数
/* globals */
/* 0 - http/0.9
   1 - http/1.0
   2 - http/1.1
*/
int http10 = 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;
int clients = 1; // 进程个数,默认 1 个
int force = 0;   // 是否接受服务区的返回信息
int force_reload = 0;
int proxyport = 80;
char *proxyhost = NULL;
int benchtime = 30;
/* internal */
int mypipe[2];
char host[MAXHOSTNAMELEN];
#define REQUEST_SIZE 2048
char request[REQUEST_SIZE];

// 预先定义的长参数
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}
};

// 函数声明
static void benchcore(const char* host,const int port, const char *request);
static int bench(void);
static void build_request(const char *url);

// 当 SIGALRM 信号触发时,调用此函数
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           Run benchmark for  seconds. Default 30.\n" // 运行测试多长时间
	"  -p|--proxy  Use proxy server for request.\n"  // 使用代理服务器
	"  -c|--clients          Run  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"  // 使用 GET 请求方法
	"  --head                   Use HEAD request method.\n" // 使用 HEAD 请求方法
	"  --options                Use OPTIONS request method.\n" // 使用 OPTIONS 请求方法
	"  --trace                  Use TRACE request method.\n"  // 使用 TRACE 请求方法
	"  -?|-h|--help             This information.\n"        // 显示帮助信息
	"  -V|--version             Display program version.\n" // 显示版本信息
	);
};

//主函数
int main(int argc, char *argv[])
{
   // 1. 参数解析
   int opt = 0;  // 接收 getopt_long 返回结果
   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);
         case 't': benchtime=atoi(optarg);break; // 转化为测压时间	     
         case 'p': // 设置了代理
	         /* 解析代理服务器 server:port */
	         tmp = strrchr(optarg,':'); // tmp 为 port
            proxyhost = optarg;
            if(tmp == NULL) {
               break;
            }
            if(tmp==optarg) { // 缺少 hostname
               fprintf(stderr,"Error in option --proxy %s: Missing hostname.\n",optarg);
               return 2;
            }
            if(tmp==optarg+strlen(optarg)-1) { //缺少 port
               fprintf(stderr,"Error in option --proxy %s Port number is missing.\n",optarg);
               return 2;
            }
            *tmp='\0';
            proxyport=atoi(tmp+1);break; //将 port 转化为数字,此时,proxyhost 和 proxyport 都解析出来了
         case ':':
         case 'h':
         case '?': usage();return 2;break; // :/h/? 都输出参数说明
         case 'c': clients=atoi(optarg);break; // 转化为客户端个数
      }
   }
 
   if(optind == argc) { // 没有 URL
      fprintf(stderr,"webbench: Missing URL!\n");
		usage();
		return 2;
   }

   if(clients == 0) clients = 1; // 默认 clients 为 1
   if(benchtime == 0) benchtime = 60; // 设置 benchtime 为 60s
   /* Copyright */
   fprintf(stderr,"Webbench - Simple Web Benchmark "PROGRAM_VERSION"\n"
	   "Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.\n");

   // 2. 构建请求
   build_request(argv[optind]);

   // 3. 输出测压信息
   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]); // 输出 URL
   switch(http10) // 输出使用的 HTTP 协议版本
   {
	   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");

   //4. 开始测压
   return bench();
}

// 构建请求
void build_request(const char *url)
{
   char tmp[10];
   int i;

   // 初始化
   bzero(host,MAXHOSTNAMELEN);
   bzero(request,REQUEST_SIZE);

   // 1. 检查通信协议,添加请求方法到 request
   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;
   }
  
   // 2. 添加空格
   strcat(request," ");

   // 3. 检查 URL
   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);
   }
   
   // 如果代理为空,且 url 不是以 http:// 开头,则输出错误
   if(proxyhost == NULL)
	   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;

   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 */
      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);
	      if(proxyport==0) proxyport=80;
      } else {
         strncpy(host,url+i,strcspn(url+i,"/"));
      }
      // 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);
   }
  
   // 处理通信协议
   if(http10 == 1)
	   strcat(request," HTTP/1.0"); // 将 " HTTP/1.0" 追加到 request 后
   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"); 
  
   // 打印 request
   printf("Req=%s\n",request);
}

/* vraci system rc error kod */
static int bench(void)
{
  printf("function bench\n");
  int i, j, k;	
  pid_t pid = 0;
  FILE *f;

   // 检查目标服务器是否可用
   printf("host = %s\n", host);// www.baidu.com
   i = Socket(proxyhost == NULL ? host:proxyhost,proxyport);
   if(i < 0) { 
      fprintf(stderr,"\nConnect to server failed. Aborting benchmark.\n");
      return 1;
   }
   close(i);

   // 创建管道,用于父子进程之间通信
   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 产生 clients 个子进程
   for(i = 0; i < clients; i++) {
	   pid = fork();
	   if(pid <= (pid_t) 0) {
		   /* child process or error*/
	      sleep(1); /* make childs faster */
		   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) {
      // 子进程执行测压任务
      if(proxyhost == NULL) // 分为有代理和无代理
         benchcore(host,proxyport,request);
      else
         benchcore(proxyhost,proxyport,request);

      // 以写的方式打开管道
	   f = fdopen(mypipe[1],"w");
	   if(f == NULL) {
		   perror("open pipe for writing failed.");
		   return 3;
	   }

      // 将自身数据写入到管道中,供父进程读取
	   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(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;
		  // 读取完所有子进程则跳出循环
		  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;
}

/**
 * @brief 与服务器进行连接
 * 
 * @param host 
 * @param port 
 * @param req 
 */
void benchcore(const char *host,const int port,const char *req)
{
   int rlen;
   char buf[1500];  // 用于读取返回值
   int s, i;
   struct sigaction sa;

   sa.sa_handler = alarm_handler;
   sa.sa_flags = 0;
   // 当 SIGALRM 信号触发时,调用 alarm_handler 函数
   // 在 alarm_handler 函数中设置 timerexpired = 1,表示超时
   if(sigaction(SIGALRM, &sa, NULL))
      exit(3);
   
   // alarm 函数是一个定时器,当 benchtime 时间后触发 SIGALRM 信号
   alarm(benchtime);

   // 计算 req 长度
   rlen = strlen(req);

   // 使用了 goto 语句,执行 goto nexttry 语句时,回到此处
   nexttry:while(1) {
      if(timerexpired) {      // 检测是否超时
         if(failed > 0) {
            /* fprintf(stderr,"Correcting failed by signal\n"); */
            failed--;
         }
         return;
      }

      // 创建一个 socket,Socket 为 socket.c 文件中封装的函数
      s = Socket(host, port);                          
      if(s < 0) { 
         failed++;
         continue;
      }

      // 发送数据
      if(rlen != write(s,req,rlen)) {
         failed++;
         close(s);
         continue;
      }

      if(http10 == 0) {
         if(shutdown(s,1)) { // 关闭 socket 写
            failed++;
            close(s); // 关闭失败后,使用 close 关闭
            continue;
         }
      } 
	   
      // force 等于 0 需要等待服务器返回结果
      if(force == 0) {
         //读取所有数据
	      while(1) {
            if(timerexpired) break; // 超时直接跳出,不再接收
	         i = read(s, buf, 1500);
	         if(i < 0) { 
               failed++;
               close(s);
               goto nexttry;
            } else {
               if(i == 0) break;
		         else
			         bytes += i; //累计读取的字节数
            }
		         
	      }
      }

      if(close(s)) {
         failed++;
         continue;
      }
      speed++;
   }
}

Github webbench 项目

5.2 参数解析

  1. 通过 main 函数接收参数 argc 和 argv;
  2. C语言库函数 getopt_long 解析参数,其中 getopt_long 如下所是:

头文件:

#include 

函数原型:

int getopt_long(int argc, char * const argv[], const char *optstring,
                const struct option *longopts, int *longindex);

各个参数含义如下:

  • argc :参数个数
  • argv : 参数数组
  • optstring : 短选项字符串
  • longopts : 长选项字符串
  • longindex : longopts 的下标值,如果非空,表示当前参数符合 longopts 中的参数的下标
  • 更详细内容可以参考:https://blog.csdn.net/qq_33850438/article/details/80172275

5.3 构建请求

  1. 判断请求方法,将请求方法存储到 request 字符串数组中;
    该请求是一个 http 的报文,例如:
GET / HTTP/1.0
User-Agent: WebBench 1.5
Host: www.baidu.com

5.4 打印测压配置信息

输出 方法、URL、HTTP协议版本、客户端数量、将要运行多长时间等

5.5 测压和输出

  1. 通过 Socket 测试 host 是否通;
  2. pipe 创建父子进程通信的管道;
  3. fork 函数产生 clients 个客户端;
  4. 子进程执行测压,父进程接收子进程测压结果,并进行统计,最后输出统计信息

六、实例演示

  1. 输入参数
    输入数据:测试 ./webbench -c 10 -t 15 http://www.baidu.com/
    其中,各个参数表示如下:
    -c 10 : 模拟 10 个客户端进行测压;
    -t 15 : 测压时间为 15s;
    http://www.baidu.com/ : 测压网站,需要写完整

  2. 解析参数
    参数 -c 解析为 clients = 10;
    参数 -t 解析为 benchtime = 15;

  3. 构建请求 && 输出测压信息

Req=GET / HTTP/1.0
User-Agent: WebBench 1.5
Host: www.baidu.com

Benchmarking: GET http://www.baidu.com/
10 clients, running 15 sec.

构建了一个 http 报文,通过 10 个客户端,每个 15s 进行测压

  1. 输出测压信息
Speed=1388 pages/min, 8510421 bytes/sec.
Requests: 347 susceed, 0 failed.

七、视频详解

如果有还不理解的地方,可以下面录制的这个视频,更详细!记得一键三连哦!

开源项目详解-webbench网站测压

八、参考文献

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

你可能感兴趣的:(面试,开源项目,C/C++,多进程)