網站(Web)壓測工具Webbench源碼分析

1、我與webbench二三事

Webbench是一個在linux下使用的很是簡單的網站壓測工具。它使用fork()模擬多個客戶端同時訪問咱們設定的URL,測試網站在壓力下工做的性能。Webbench使用C語言編寫,下面是其下載連接:html

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

說到這裏,我趕腳很是有必要給這個網站局部一個截圖,以下圖:web

第一次看到這張圖片,着實吃了一精!竟然是2004年最後一次更新,我和個人小夥伴們都驚呆了。不過既然如今你們還都使用,其中必定有些很通用的思想,因此我不妨學習一下,也能爲之後的工具開發作鋪墊。固然,另一個讓我衝動地想研究一下的緣由是,webbench的代碼實在太簡潔了,源碼加起來不到600行……緩存

webbench-1.5.tar.gz這個文件下載下來以後解壓縮,進入webbench-1.5文件夾,而後執行make,就能夠看到文件夾下多了一個可執行程序webbench。嘗試運行一下,就能夠獲得如圖所示的結果。服務器

能夠看到,咱們模擬了10client同時訪問URL所示的某個圖片,測試執行了5秒。最終獲得的結果是,咱們發送http GET請求的速度爲188892pages/min,服務器響應速度爲5518794bytes/sec,請求中有15741個成功,0個失敗。併發

大概知道了怎麼用之後,咱們就能夠深刻了解其源代碼了。異步

2、與webbench的初步相識

咱們首先來看一下webbench的工做流程,以下圖:socket

 

webbench主要的工做原理就是如下幾點:函數

1. 主函數進行必要的準備工做,進入bench開始壓測工具

2. bench函數使用fork模擬出多個客戶端,調用socket併發請求,每一個子進程記錄本身的訪問數據,並寫入管道

3. 父進程從管道讀取子進程的輸出信息

4. 使用alarm函數進行時間控制,到時間後會產生SIGALRM信號,調用信號處理函數使子進程中止

5. 最後只留下父進程將全部子進程的輸出數據彙總計算,輸出到屏幕上

3、走進webbench的心裏世界

接下來咱們詳細截圖webbench的源代碼。查看webbench的源代碼,發現代碼文件只有兩個,Socket.cwebbench.c。首先看一下Socket.c,它當中只有一個函數int Socket(const char *host, int clientPort),大體內容以下:

複製代碼
int Socket(const char *host, int clientPort)
{
    //以host爲服務器端ip,clientPort爲服務器端口號創建socket鏈接
    //鏈接類型爲TCP,使用IPv4網域
    //一旦出錯,返回-1
    //正常鏈接,則返回socket描述符
}
複製代碼

 這段代碼比較直觀,所以就不列舉其中的細節了。此函數供另一個文件webbench.c中的函數調用。

接着咱們來瞧一下webbench.c文件。這個文件中包含了如下幾個函數,咱們一一列舉出來:

複製代碼
static void alarm_handler(int signal); //爲方便下文引用,咱們稱之爲函數1。
static void usage(void); //函數2
void build_request(const char *url); //函數3
static int bench(void); //函數4
void benchcore(const char *host, const int port, const char *req); //函數5
int main(int argc, char *argv[]); //函數6
複製代碼

 下面咱們分別作講解。

1)全局變量列表

源文件中出如今全部函數前面的全局變量,主要有如下幾項,咱們以註釋的方式解釋其在程序中的用途

複製代碼
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請求
複製代碼

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

首先,來看一下最簡單的函數,即函數1,它的內容以下:

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

 webbench在運行時能夠設定壓測的持續時間,以秒爲單位。例如咱們但願測試30秒,也就意味着壓測30秒後程序應該退出了。webbench中使用信號(signal)來控制程序結束。函數1是在到達結束時間時運行的信號處理函數。它僅僅是將一個記錄是否超時的變量timerexpired標記爲true。後面會看到,在程序的while循環中會不斷檢測此值,只有timerexpired=1,程序纔會跳出while循環並返回。

3)函數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"
    );
};
複製代碼

 從名字來看就很明顯,這是教你如何使用webbench的函數,在linux命令行調用webbench方法不對的時候運行,做爲提示。有一些比較經常使用的,好比-c來指定併發進程的多少;-t指定壓測的時間,以秒爲單位;支持HTTP0.9HTTP1.0HTTP1.1三個版本;支持GETHEADOPTIONSTRACE四種請求方式。不要忘了調用時,命令行最後還應該附上要測的服務端URL

4)函數3void 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來肯定的。該函數使用了大量的字符串操做函數,例如strcpystrstrstrncasecmpstrlenstrchrindexstrncpystrcat。對這些基礎函數不太熟悉的同窗能夠借這個函數複習一下。build_request的具體內容在此不作過多闡述。

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

之因此把函數6放在了函數4和函數5以前,是由於函數45是整個工具的最核心代碼,咱們把他放在最後分析。先來看一下整個程序的起始點:主函數(即函數6)。

複製代碼
int main(int argc, char *argv[])
{
    /*函數最開始,使用getopt_long函數讀取命令行參數,
    來設置(1)中所說起的全局變量的值。
    關於getopt_long的具體使用方法,這裏有一個配有講解的小例子,能夠幫助學習:
    http://blog.csdn.net/lanyan822/article/details/7692013
    在此期間若是出現錯誤,會調用函數2告知用戶此工具使用方法,而後退出。
    */
    
    build_request(argv[optind]); //參數讀完後,argv[optind]即放在命令行最後的url
                              //調用函數3創建完整的HTTP request,
                            //HTTP request存儲在所有變量char request[REQUEST_SIZE]
    
    /*接下來的部分,main函數的全部代碼都是在網屏幕上打印這次測試的信息,
    例如即將測試多少秒,幾個併發進程,使用哪一個HTTP版本等。
    這些信息並不是程序核心代碼,所以咱們也略去。
    */
    
    return bench(); //簡簡單單一句話,原來,壓力測試在這最後一句才真正開始!
                 //全部的壓測都在bench函數(即函數4)實現
}
複製代碼

 這真是一件很浪費感情的事情,看了半天,一直到最後一句纔開始執行真正的測試過程,前面的都是一些準備工做。好了,那咱們如今開始進入到static int bench(void)中。

6)函數4static 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出來。每一個子進程都調用函數5進行測試,並將結果輸出到管道,供父進程讀取。父進程負責收集全部子進程的測試數據,並彙總輸出。

7)函數5void 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表示咱們是否在發出請求後須要等待服務器的響應結果。

4、昨天,今天,明天

瞭解了webbench的具體代碼之後,下面一步就要考慮一下如何進行改進了。代碼中有一些過期的函數能夠更新一下,加入一些新的功能,例如支持POST方法,支持異步壓測等,這些就留到之後去探索了。第一次寫源碼分析,望多多指教。但願本文能幫助你們在之後與webbench愉快地玩耍。且用且珍惜!

相關文章
相關標籤/搜索