一、我與webbench二三事
Webbench是一個在linux下使用的非常簡單的網站壓測工具。它使用fork()模擬多個客戶端同時訪問我們設定的URL,測試網站在壓力下工作的性能。Webbench使用C語言編寫,下面是其下載鏈接:
http://home.tiscali.cz/~cz210552/webbench.html
說到這里,我趕腳非常有必要給這個網站局部一個截圖,如下圖:
第一次看到這張圖片,着實吃了一精!居然是2004年最后一次更新,我和我的小伙伴們都驚呆了。不過既然現在大家還都使用,其中一定有些很通用的思想,所以我不妨學習一下,也能為以后的工具開發做鋪墊。當然,另外一個讓我沖動地想研究一下的原因是,webbench的代碼實在太簡潔了,源碼加起來不到600行……
把webbench-1.5.tar.gz這個文件下載下來之后解壓縮,進入webbench-1.5文件夾,然后執行make,就可以看到文件夾下多了一個可執行程序webbench。嘗試運行一下,就可以得到如圖所示的結果。
可以看到,我們模擬了10個client同時訪問URL所示的某個圖片,測試執行了5秒。最終得到的結果是,我們發送http GET請求的速度為188892pages/min,服務器響應速度為5518794bytes/sec,請求中有15741個成功,0個失敗。
大概知道了怎么用以后,我們就可以深入了解其源代碼了。
二、與webbench的初步相識
我們首先來看一下webbench的工作流程,如下圖:
webbench主要的工作原理就是以下幾點:
1. 主函數進行必要的准備工作,進入bench開始壓測
2. bench函數使用fork模擬出多個客戶端,調用socket並發請求,每個子進程記錄自己的訪問數據,並寫入管道
3. 父進程從管道讀取子進程的輸出信息
4. 使用alarm函數進行時間控制,到時間后會產生SIGALRM信號,調用信號處理函數使子進程停止
5. 最后只留下父進程將所有子進程的輸出數據匯總計算,輸出到屏幕上
三、走進webbench的內心世界
接下來我們詳細截圖webbench的源代碼。查看webbench的源代碼,發現代碼文件只有兩個,Socket.c和webbench.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)函數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" ); };
從名字來看就很明顯,這是教你如何使用webbench的函數,在linux命令行調用webbench方法不對的時候運行,作為提示。有一些比較常用的,比如-c來指定並發進程的多少;-t指定壓測的時間,以秒為單位;支持HTTP0.9,HTTP1.0,HTTP1.1三個版本;支持GET,HEAD,OPTIONS,TRACE四種請求方式。不要忘了調用時,命令行最后還應該附上要測的服務端URL。
(4)函數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。對這些基礎函數不太熟悉的同學可以借這個函數復習一下。build_request的具體內容在此不做過多闡述。
(5)函數6:int main(int argc, char *argv[]);
之所以把函數6放在了函數4和函數5之前,是因為函數4和5是整個工具的最核心代碼,我們把他放在最后分析。先來看一下整個程序的起始點:主函數(即函數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)函數4: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出來。每個子進程都調用函數5進行測試,並將結果輸出到管道,供父進程讀取。父進程負責收集所有子進程的測試數據,並匯總輸出。
(7)函數5: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表示我們是否在發出請求后需要等待服務器的響應結果。
四、昨天,今天,明天
了解了webbench的具體代碼以后,下面一步就要考慮一下如何進行改進了。代碼中有一些過時的函數可以更新一下,加入一些新的功能,例如支持POST方法,支持異步壓測等,這些就留到以后去探索了。第一次寫源碼分析,望多多指教。希望本文能幫助大家在以后與webbench愉快地玩耍。且用且珍惜!