webbench:其為linux上一款web效能壓力測試工具,它最多可以類比3萬個並發串連數來測試伺服器壓力,其原理為fork多個子進程,每個子進程都迴圈做web訪問測試,子進程將訪問的結果通過管道告訴父進程,父進程做最終結果統計。
其主要原理如下圖:
其代碼實現中主要運用4個函數:getopt_long()系統命令列解析函數,build_request()函數,bench()函數,benchcore()函數。
流程如下:
webbench.c:
全域變數選項參數:都有預設值
1.命令列參數解析,參數構造(getopt_long()函數實現)(若使用者沒有傳
參設定則為其預設值);
2.根據1得到的一些參數,進行http請求構造,調用build_request()函數:
(1)構造第一行:
要求方法:method–>request[]
url:判斷url合法性,合法:
無Proxy 伺服器:
將其網路地址–>host[],連接埠號碼–>proxyport;
url資源路徑–>request[]
有Proxy 伺服器:
url直接填入–>request[];
http協議版本:http0.9 http1.0 http1.1
(2)請求前序:Name:Value–>request[]
(3)填入空行–>request[]
構造完成
3.進行壓力測試,調用bench()函數:
(1)進行一次串連合法性測試;
(2)建立管道通訊;
(3)按照用戶端數目派生子進程;
(4)每個子進程調用benchcore()函數進行串連請求:
benchcore函數:
建立自訂訊號捕捉函數,建立benchtime時間鬧鐘:
(1)調用Socket(),得到串連通訊端;
(2)向網路中寫請求;
(3)http0.9協議處理(關閉串連寫一半);
(4)讀取伺服器響應訊息;
(5)關閉串連;
在以上過程中統計各自進程的faild(失敗次數)、bytes(伺服器回應位元組 數)、speed(串連成功次數);
(5)benchcore函數調用完成,每個進程向管道中寫入faild、bytes、speed;
(6)父進程開啟管道讀取每個進程寫入的資訊,進行統計計算,輸出壓力測試結果。
Socket.c:
1.建立通訊端:調用socket();
2.建立串連:調用connect();
返回通訊端:sock.
源碼如下:
webbench.c
#include "socket.c" #include <unistd.h> #include <sys/param.h> #include <rpc/types.h> #include <getopt.h> #include <strings.h> #include <time.h> #include <signal.h> //統計的壓力測試最終結果表示volatile int timerexpired = 0; int speed = 0; int failed = 0; int bytes = 0;//http要求方法#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; //預設要求方法為GET方式 int clients = 1; //預設只類比一個用戶端 int force = 0; //預設需要等待伺服器響應int force_reload = 0; //失敗時重新請求 int proxyport = 80; //預設訪問伺服器連接埠為80 char *proxyhost = NULL; //預設無Proxy 伺服器 int benchtime = 30; //預設類比請求時間為30s // globals 版本號碼 int http10 = 1; //0:- http/0.9, 1:- http/1.0, 2:- http/1.1 int mypipe[2]; //管道用於父子進程通訊char host[MAXHOSTNAMELEN]; //儲存伺服器網路地址 #define REQUEST_SIZE 2048 char request[REQUEST_SIZE]; //存放http請求報文資訊數組//函式宣告 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 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" ); }; //結構體數組:每一個元素格式為:{長選項,選項後是否帶有參數,int*指標(為NULL),對應短選項或static const struct option long_options[]= //不為NULL,將第四個參數值給第三個參數} { {"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} }; int main(int argc, char *argv[]) { int opt = 0; int options_index = 0; char *tmp = NULL; //一、檢驗命令列參數 //1.不帶選項時直接輸出用法help資訊 if(argc == 1) { usage(); return 2; } //2.帶選項時則解析命令列參數並根據傳入選項進行相關設定 // getopt_long 為命令列解析的庫函數,根據argc來尋找(argv,"912Vfrt:p:c:?h")這兩個字串匹配的選項, //如果是短選項,則直接返回這個選項給opt, //如果是長選項,則到option long_options[]結構體數組中尋找匹配其長選項,返回其對應的短選項給opt, //若其第三個參數不為NULL,將第四個參數值給第三個參數,並且返回0給opt //此函數內建全域變數: //optarg:指向選項後的參數:-t 100,指向100; //optind: 當前訪問到的argv索引值 //opterr: 其值非0時,代表有無效選項,缺少參數,輸出錯誤資訊 //optopt: 發現無效選項時,函數返回“? / :”,將其值設為無效選項字元 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; //force=1代表不等待伺服器響應 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 'c': clients = atoi(optarg); //設定建立的用戶端數 break; case 'p': //使用Proxy 伺服器,設定其代理網路號和連接埠號碼:格式:-p server:port tmp = strrchr(optarg,':'); //尋找“:”在optarg中最後一次出現的位置 proxyhost = optarg; //設定網路號 if(tmp == NULL) //沒有:號,沒有連接埠號碼 { break; } if(tmp == optarg) //連接埠號碼在首位置,錯誤:缺失主機名稱 { fprintf(stderr,"Error in option --proxy %s: Missing hostname.\n",optarg); return 2; } if(tmp == optarg + strlen(optarg)-1) //:號在末位,缺少連接埠號碼 { fprintf(stderr,"Error in option --proxy %s Port number is missing.\n",optarg); return 2; } *tmp = '\0'; //將:號置為“\0” proxyport = atoi(tmp+1); //設定新的連接埠號碼 break; case ':': case 'h': case '?': usage();return 2;break; } } //getopt_long函數將選項解析完成後,讀到url不會在讀取,此時argv[optind]指向url //optind 被 getopt_long設定為命令列參數中未讀取的下一個元素下標值 if(optind == argc) //若相等即沒有輸入URL { fprintf(stderr,"webbench: Missing URL!\n"); usage(); return 2; } //若用戶端選項後參數設為0,則更改 if(clients == 0) clients = 1; if(benchtime == 0) benchtime = 60; //輸出webbench版本相關資訊 fprintf(stderr,"Webbench - Simple Web Benchmark "PROGRAM_VERSION"\n" "Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.\n" ); //二、構造HTTP請求到request數組 build_request(argv[optind]); //傳入URL //http請求構造成功後 //以下輸出提示資訊 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"); //輸出Proxy 伺服器的資訊 if(proxyhost != NULL) printf(", via proxy server %s:%d",proxyhost,proxyport); if(force_reload) printf(", forcing reload"); printf(".\n"); //開始壓力測試,返回 bench 函數執行結果 return bench(); } //二、構造HTTP請求到request數組void build_request(const char *url) { char tmp[10]; int i; //初始化 bzero(host,MAXHOSTNAMELEN); bzero(request,REQUEST_SIZE); //判斷應該使用的 HTTP 協議 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; //1.填寫http請求第一行 //填寫要求方法method 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 合法性判斷 //若沒有"://"則不合法 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); } if(proxyhost == NULL) //若無Proxy 伺服器 { if(0 != strncasecmp("http://",url,7)) //忽略大小寫比較 { //只支援 HTTP 地址 fprintf(stderr,"\nOnly HTTP protocol is directly supported, set --proxy for others.\n"); exit(2); } } //找到主機名稱開始的地方:如:http://baidu.com:80/ i = strstr(url,"://")-url+3; //i==7 // 必須以 / 結束 if(strchr(url+i,'/')==NULL) //在字串中尋找“/” ,找不到則非法URL { fprintf(stderr,"\nInvalid URL syntax - hostname don't ends with '/'.\n"); exit(2); } if(proxyhost == NULL) //若無Proxy 伺服器 { // 得到連接埠號碼從主機名稱 if(index(url+i,':') != NULL && index(url+i,':') < index(url+i,'/')) //若帶有連接埠號碼,index函數與strchr相似 { //設定網路號 strncpy(host,url+i,strchr(url+i,':')-url-i); //如將baidu.com拷貝到host數組裡即網路地址 //初始化 bzero(tmp,10); strncpy(tmp,index(url+i,':')+1,strchr(url+i,'/')-index(url+i,':')-1); //將連接埠號碼拷貝到tmp數組中 //設定連接埠 proxyport = atoi(tmp); if(proxyport==0) proxyport=80; } else //沒有連接埠號碼,直接拷貝網域名稱到host數組中 { strncpy(host,url+i,strcspn(url+i,"/")); //strcspn找url+i到“/”之間的字元個數 } //將資源路徑填入請求行裡 strcat(request+strlen(request),url+i+strcspn(url+i,"/")); } else //若有Proxy 伺服器 { strcat(request,url); //直接填入URL到請求行中 } //填入http版本號碼到請求行中 if(http10 == 1) strcat(request," HTTP/1.0"); else if (http10==2) strcat(request," HTTP/1.1"); strcat(request,"\r\n"); //2.填請求前序:NAME:VALUE 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"); //3.填入空行 if(http10>0) strcat(request,"\r\n"); //構造完成} static int bench(void) //父進程做的工作{ int i,j,k; pid_t pid = 0; FILE *f; //建立網路連接 :先測試一次,伺服器是否可以正常串連成功 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; } //派生子進程進行壓力測試 :傳入多少個用戶端則建立多少個子進程進行串連 for(i = 0;i < clients;i++) { pid = fork(); if(pid <= (pid_t)0) { sleep(1); 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);