Webbench源码剖析

Webbench源码剖析

目录

Web Bench是一个网站压力测试的工具。其最后更新时间是2004年,已经十年多了。其源代码总共才不到600行,全部使用C语言编写,最多可以模拟3万个并发连接。真可谓是简洁代码的代表之作。
用法:
可以在安装后直接输入 webbench 或 webbench -h 或 webbench –help. 可以看到:

webbench [option]... URL  
  -f|--force               Don't wait for reply from server.  
  -r|--reload              Send reload request - Pragma: no-cache.  
  -t|--time <sec>          Run benchmark for <sec> seconds. Default 30.  
  -p|--proxy <server:port> Use proxy server for request.  
  -c|--clients <n>         Run <n> HTTP clients at once. Default one.  
  -9|--http09              Use HTTP/0.9 style requests.  
  -1|--http10              Use HTTP/1.0 protocol.  
  -2|--http11              Use HTTP/1.1 protocol.  
  --get                    Use GET request method.  
  --head                   Use HEAD request method.  
  --options                Use OPTIONS request method.  
  --trace                  Use TRACE request method.  
  -?|-h|--help             This information.  
  -V|--version             Display program version.  

说一下主要的几个选项: 指定 -f 时不等待服务器数据返回, -t 为指定压力测试运行时间, -c 指定由多少个客户端发起测试请求。
-9 -1 -2 分别为指定 HTTP/0.9 HTTP/1.0 HTTP/1.1。

Webbench的代码实现原理也是相当简单,就是一个父进程fork出很多个子进程,子进程分别去执行http测试,最后把执行结果汇总写入管道,父进程读取管道数据然后进行最终计算。整个工具的实现流程如下:
这里写图片描述

其源代码包括两个文件,非常简单,一个是socket.c,一个是webbench.c。其中,socket.c是用来建立socket连接的,webbench.c负责实现主要的功能。

socket.c源代码及注释:

[cpp] 
/* $Id: socket.c 1.1 1995/01/01 07:11:14 cthuang Exp $ 
 * 
 * This module has been modified by Radim Kolar for OS/2 emx 
 */  

/*********************************************************************** 
  module:       socket.c 
  program:      popclient 
  SCCS ID:      @(#)socket.c    1.5  4/1/94 
  programmer:   Virginia Tech Computing Center 
  compiler:     DEC RISC C compiler (Ultrix 4.1) 
  environment:  DEC Ultrix 4.3  
  description:  UNIX sockets code. 
 ***********************************************************************/  

#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>  
/*********** 
功能:通过地址和端口建立网络连接 
@host:网络地址 
@clientPort:端口 
Return:建立的socket连接。 
        如果返回-1,表示建立连接失败 
************/  
int Socket(const char *host, int clientPort)  
{  
    int sock;  
    unsigned long inaddr;  
    struct sockaddr_in ad;  
    struct hostent *hp;  

    memset(&ad, 0, sizeof(ad));  
    ad.sin_family = AF_INET;  

    inaddr = inet_addr(host);//将点分的十进制的IP转为无符号长整形  
    if (inaddr != INADDR_NONE)  
        memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));  
    else//如果host是域名  
    {  
        hp = gethostbyname(host);//用域名获取IP  
        if (hp == NULL)  
            return -1;  
        memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);  
    }  
    ad.sin_port = htons(clientPort);  

    sock = socket(AF_INET, SOCK_STREAM, 0);  
    if (sock < 0)  
        return sock;  
        //连接  
    if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0)  
        return -1;  
    return sock;  
}  

socket函数的大致内容如下:

int Socket(const char *host, int clientPort)
{
    //以host为服务器端ip,clientPort为服务器端口号建立socket连接
    //连接类型为TCP,使用IPv4网域
    //一旦出错,返回-1
    //正常连接,则返回socket描述符
}

Webbench.c中包含以下几个主要的函数:
- static void usage(void):提示Webbench的用法及命令
- static void alarm_handler(int signal) :信号处理函数,时钟结束时进行调用
- void build_request(const char *url):创建http连接请求
- static int bench(void):创建管道和子进程,调用测试http函数,实现父子进程通信并计算结果
- void benchcore(const char *host,const int port,const char *req):对http请求进行测试(子进程的具体工作)

主要流程:

  1. 解析命令行参数,根据命令行指定参数设定变量,可以认为是初始化配置。
  2.根据指定的配置构造 HTTP 请求报文格式。
  3.开始执行 bench 函数,先进行一次 socket 连接建立与断开,测试是否可以正常访问。
  4.建立管道,派生根据指定进程数派生子进程。
  5.每个子进程调用 benchcore 函数,先通过 sigaction 安装信号,用 alarm 设置闹钟函数,到时间后会产生SIGALRM信号,调用信号处理函数使子进程停止。接着不断建立 socket 进行通信,与服务器交互数据,直到收到信号结束访问测试。子进程将访问测试结果写进管道。
  6.父进程读取管道数据,汇总子进程信息,收到所有子进程消息后,输出汇总信息,结束。

程序调用的流程示意图

全局变量:

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

每个函数的具体分析:
(1)alarm_handler();

static void alarm_handler(int signal)
{
   timerexpired=1;
}

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

(2)static void usage(void);

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"
    );
};

其中,-9 -1 -2 分别代表http0.9、http1.0和http1.1协议。webbench支持GET,HEAD,OPTIONS,TRACE四种请求方式。

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

GET /test.jpg HTTP/1.1
User-Agent: WebBench 1.5
Host:192.168.10.1
Pragma: no-cache
Connection: close

build_request函数的目的就是要把类似于以上这一大坨信息全部存到全局变量request[REQUEST_SIZE]中,其中换行操作使用的是”\r\n”。而以上这一大坨信息的具体内容是要根据命令行输入的参数,以及url来确定的。该函数使用了大量的字符串操作函数,例如strcpy,strstr,strncasecmp,strlen,strchr,index,strncpy,strcat。

(4)主函数main();

int main(int argc, char *argv[])
{
    /*函数最开始,使用getopt_long函数读取命令行参数,来设置全局变量的值。
    关于getopt_long的具体使用方法,这里有一个配有讲解的小例子,可以帮助学习:
    http://blog.csdn.net/lanyan822/article/details/7692013
    在此期间如果出现错误,会调用函数(1)告知用户此工具使用方法,然后退出。
    */

    build_request(argv[optind]); //参数读完后,argv[optind]即放在命令行最后的url
                              //调用函数(2)建立完整的HTTP request,
                            //HTTP request存储在全部变量char request[REQUEST_SIZE]

    /*接下来的部分,main函数的所有代码都是在网屏幕上打印此次测试的信息,
    例如即将测试多少秒,几个并发进程,使用哪个HTTP版本等。
    这些信息并非程序核心代码,因此我们也略去。
    */

    return bench(); //简简单单一句话,原来,压力测试在这最后一句才真正开始!
                 //所有的压测都在bench函数(即函数(5))实现
}

(5)static int bench(void);

static int bench(void){
  int i,j,k;    
  pid_t pid=0;
  FILE *f;

  i=Socket(proxyhost==NULL?host:proxyhost,proxyport); //调用了Socket.c文件中的函数
  if(i<0){ /*错误处理*/ }
  close(i);

  if(pipe(mypipe)){ /*错误处理*/ } //管道用于子进程向父进程回报数据
  for(i=0;i<clients;i++){//根据clients大小fork出来足够的子进程进行测试
       pid=fork();
       if(pid <= (pid_t) 0){
           sleep(1); /* make childs faster */
           break;
       }
  }
  if( pid< (pid_t) 0){ /*错误处理*/ }

  if(pid== (pid_t) 0){//如果是子进程,调用benchcore进行测试
    if(proxyhost==NULL)
      benchcore(host,proxyport,request);
    else
      benchcore(proxyhost,proxyport,request);

     f=fdopen(mypipe[1],"w");//子进程将测试结果输出到管道
     if(f==NULL){ /*错误处理*/ }
     fprintf(f,"%d %d %d\n",speed,failed,bytes);
     fclose(f);
     return 0;
  } else{//如果是父进程,则从管道读取子进程输出,并作汇总
     f=fdopen(mypipe[0],"r");
      if(f==NULL) { /*错误处理*/ }
      setvbuf(f,NULL,_IONBF,0);
      speed=0;  failed=0;  bytes=0;

      while(1){ //从管道读取数据,fscanf为阻塞式函数
          pid=fscanf(f,"%d %d %d",&i,&j,&k);
          if(pid<2){ /*错误处理*/ }
          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;
}

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

(6)void benchcore(const char *host,const int port,const char *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; //设置函数1为信号处理函数
 sa.sa_flags=0;
 if(sigaction(SIGALRM,&sa,NULL)) //超时会产生信号SIGALRM,用sa中的指定函数处理
    exit(3); 

 alarm(benchtime);//开始计时
 rlen=strlen(req);
 nexttry:while(1){
    if(timerexpired){//一旦超时则返回
       if(failed>0){failed--;}
       return;
    }
    s=Socket(host,port);//调用Socket函数建立TCP连接
    if(s<0) { failed++;continue;} 
    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){//全局变量force表示是否要等待服务器返回的数据
        while(1){
        if(timerexpired) break;
          i=read(s,buf,1500);//从socket读取返回数据
          if(i<0) { 
          failed++;
          close(s);
          goto nexttry;
        }else{
          if(i==0) break;
            else
              bytes+=i;
        }
        }
    }
    if(close(s)) {failed++;continue;}
    speed++;
 }
}

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

疑问及解答

  1. 线程占用的空间比进程要小,而且线程切换的时间开销也小,但为什么程序的实现上采用的是fork进程而不是使用多线程呢?
    答:因为默认情况下:
    主线程+辅助线程<253个自己的线程<=256
    含主线程和一个辅助线程,最多255个,即一个用户只能生成253个线程。
    而进程的最大数目则跟系统本身限制相关。
    2.webbench中在多个进程进行写管道的情况下,在代码中没有采取同步措施,程序是如何保持数据正确呢?
    答:管道写的数据不大于 PIPE_BUF 时,系统可以保证写的原子性。在2.6.29内核中,\include\linux\limits.h定义:
#define PIPE_BUF 4096

涉及到的知识点有:
命令行参数解析(getopt_long)、 信号处理(sigaction)、 socket 管道读写 。

源码中的一些函数用法及启示

getoptlong()

在写程序时常常需要对命令行参数进行处理,当命令行参数个数较多时,如果按照顺序一个一个定义参数含义很容易造成混乱,而且如果程序只按顺序处理参数的话,一些“可选参数”的功能将很难实现。
在Linux中,可以使用getopt、getopt_long、getopt_long_only来对处理这个问题。
具体用法可见http://blog.csdn.net/cashey1991/article/details/7942809

sleep(1)的功能:
让CPU能够空闲下来,不至于使CPU使用率一直高居不下;本线程放弃cpu时间片,其他线程处理之后,再开始本线程,多线程处理socket接收发送的时候经常这样处理,防止接收发送出现问题。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值