Webench是一款輕量級的網站測壓工具,最多可以對網站模拟3w左右的并發請求,可以控制時間、是否使用緩存、是否等待伺服器回複等等,且對中小型網站有明顯的效果,基本上可以測出中小型網站的承受能力,對于大型的網站,如百度、淘寶這些巨型網站沒有意義,因為其承受能力非常大。同時測試結果也受自身網速、以及自身主機的性能與記憶體的限制,性能好、記憶體大的主機可以模拟的并發就明顯要多。
Webbench用C語言編寫,運作于linux平台,下載下傳源碼後直接編譯即可使用,非常迅速快捷,對于中小型網站的制作者,在上線前用webbench進行系列并發測試不失為一個好的測試方法。
本篇文章主要是對webbench架構進行梳理,以及主要函數實作進行分析。
其中主要的函數有:
函數名 | 函數作用 |
---|---|
usage | 列印輔助資訊包括參數選項,幫助資訊 |
build_request | 用于建構http的請求封包。 |
bench | 壓力測試函數,主要用于建立程序和統計測試資訊 |
benchcore | 真正的測試函數,與伺服器建立連接配接,接收資料,測試連接配接。 |
alarm_handler | 鬧鈴回調函數,用于判斷進行工作時間是否到期。 |
主函數的工作過程:
對于指令參數分析,用到的是getopt_long函數。
構造http請求封包:build_request函數
這個請求封包子產品中有大量的錯誤分析。分析使用者可能填錯誤的資訊。
壓力測試函數:壓力測試子產品,由函數bench進行
真正的測試函數benchcore函數:
其中函數benchcore每個程序在要求時間内循環發送請求封包,該函數會記錄請求的成功次數、失敗次數、以及伺服器回複的位元組數。
webbench有的是兩個檔案,socket.c和webbench.c或者是socket.h,socket.c,webbench.c。
socket.c中主要就是封裝了連接配接伺服器的方法。建立socket,bind,connect這些步驟,不在這裡贅述。
下面主要是webbench.c源碼的剖析:
/*
* (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 <unistd.h>
#include <sys/param.h>
#include <rpc/types.h>
#include <getopt.h>
#include <strings.h>
#include <time.h>
#include <signal.h>
/* values */
volatile int timerexpired=0;
int speed=0;
int failed=0;
int bytes=0;
/* globals */
//所用協定版本
int http10=1; /* 0 - http/0.9, 1 - http/1.0, 2 - http/1.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;
int force=0; //預設等待伺服器響應
int force_reload=0; //預設不重新發送請求
int proxyport=80; //預設通路端口為80
char *proxyhost=NULL; //預設無代理伺服器
int benchtime=30; //預設模拟測試時間為30秒
/* internal */
int mypipe[2]; //父子程序間通信的管道
char host[MAXHOSTNAMELEN]; //存放目标伺服器的網絡位址
#define REQUEST_SIZE 2048
char request[REQUEST_SIZE]; //存放請求封包的位元組流
//構造長選項與段選項的對應。主要用于getopt_long 函數中
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}
};
/* prototypes */
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 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 <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 main(int argc, char *argv[])
{
int opt=0;
int options_index=0;
char *tmp=NULL;
//沒有輸出參數時,會調用usage函數,列印幫助資訊
if(argc==1)
{
usage();
return 2;
}
/* 有選項時一個一個解析
* getopt_long函數中第三個參數表示的是 指令的選項 帶有':'的表示必須有參數。其中 t,p,c表示必須有參數。
*/
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; /*optarg指向的是目前選項的參數。當到選項t時,其參數是測試的時間。将其儲存到開始定義的全局變量中*/
case 'p':
/* proxy server parsing server:port
* 使用代理伺服器,則設定其網絡号和端口号,格式 -p server:port
*/
tmp=strrchr(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'; //正常情況下将proxyhost截斷,proxyhost裡面儲存主機名,proxyport儲存(tmp+1)的端口号
proxyport=atoi(tmp+1);break;
case ':':
case 'h':
case '?': usage();return 2;break;
case 'c': clients=atoi(optarg);break; //上同
}
}
// 将參數解析完畢後,剛好讀取到url。此時的argv[optind]指向url
/*表示說明沒有輸入url*/
if(optind==argc) {
fprintf(stderr,"webbench: Missing URL!\n");
usage();
return 2;
}
if(clients==0) clients=1;
if(benchtime==0) benchtime=60;
/* Copyright */
fprintf(stderr,"Webbench - Simple Web Benchmark "PROGRAM_VERSION"\n"
"Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.\n"
);
/*解析完指令參數後,開始建構http封包段。參數為url*/
build_request(argv[optind]);
/* print bench info */
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]);
switch(http10)
{
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");
return bench(); //調用測壓函數
}
void build_request(const char *url)
{
char tmp[10];
int i;
bzero(host,MAXHOSTNAMELEN);
bzero(request,REQUEST_SIZE);
/*
* 緩存和代理是在http1.0後才使用的
* 無緩存和代理需要在http1.0以上才能使用
* 友善對照:0 - http/0.9, 1 - http/1.0, 2 - http/1.1
* force 預設等待伺服器響應 force_reload 預設不重新發送請求
* 以下是對版本的處理
*/
if(force_reload && proxyhost!=NULL && http10<1) http10=1;
/*HEAD請求是http1.0後才有*/
if(method==METHOD_HEAD && http10<1) http10=1;
/*OPTIONS和TRACE都是http1.1才有*/
if(method==METHOD_OPTIONS && http10<2) http10=2;
if(method==METHOD_TRACE && http10<2) http10=2;
/*開始填寫http請求。請求行,請求方法*/
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的合法性*/
/* 在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);
}
/*如果不是代理模式,檢視是否是正确的 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);
}
}
/* strstr函數定位到':'的位置
* 繼續 - url 表示的是 中間的位置距離+3表示往後移位三個位元組。此時指向了'/' 後面。也就是主機名開始的位置
* http://localhost:8080/
*/
i=strstr(url,"://")-url+3;
/*從主機名開始尋找'/' 如果沒有則是非法的 */
if(strchr(url+i,'/')==NULL) {
fprintf(stderr,"\nInvalid URL syntax - hostname don't ends with '/'.\n");
exit(2);
}
/*判斷完url合法性,開始填充url*/
/* 無代理時*/
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); /*将主機名儲存到host中*/
bzero(tmp,10);
strncpy(tmp,index(url+i,':')+1,strchr(url+i,'/')-index(url+i,':')-1); /*将端口号儲存到tmp中*/
/* printf("tmp=%s\n",tmp); */
proxyport=atoi(tmp); /*設定端口号*/
/*避免寫了: 沒有寫端口号*/
if(proxyport==0) proxyport=80;
} else /*沒有端口号*/
{
strncpy(host,url+i,strcspn(url+i,"/")); /*尋找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);
}
/* 填寫http請求到request中 */
if(http10==1)
strcat(request," HTTP/1.0");
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");
/*沒有代理的時候需要将host或者ip填入*/
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");
}
/*
*測試的目的是構造請求給網站,不需要傳輸任何内容,不必用長連接配接
*否則太多的連接配接維護會造成太大的消耗,大大降低可構造的請求數與用戶端數
*http1.1後是預設keep-alive的
*/
if(http10>1)
strcat(request,"Connection: close\r\n");
/* add empty line at end */
/*http 請求封包最後為空行 */
if(http10>0) strcat(request,"\r\n");
// printf("Req=%s\n",request);
}
/* vraci system rc error kod */
static int bench(void)
{
int i,j,k;
pid_t pid=0;
FILE *f;
/* check avaibility of target server */
/*先進行連接配接測試,如果能功能則繼續執行,否則退出*/
i=Socket(proxyhost==NULL?host:proxyhost,proxyport);
if(i<0) {
fprintf(stderr,"\nConnect to server failed. Aborting benchmark.\n");
return 1;
}
close(i);
/* create pipe */
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 childs */
for(i=0;i<clients;i++) /*數目由指令參數時決定*/
{
pid=fork();
if(pid <= (pid_t) 0)
{
/* child process or error*/
sleep(1); /* make childs faster */
break; /* break的作用是防止parent的child程序繼續建立子程序,我們隻需要建立clients個程序。 */
}
}
if( pid< (pid_t) 0) /*建立失敗*/
{
fprintf(stderr,"problems forking worker no. %d\n",i);
perror("fork failed.");
return 3;
}
if(pid== (pid_t) 0) /*子程序*/
{
/* I am a child */
if(proxyhost==NULL) /*沒有代理時候,參數是主機名/ipd位址 端口号 請求*/
benchcore(host,proxyport,request); /*調用真正的測試函數*/
else
benchcore(proxyhost,proxyport,request); /*有代理隻需要将代理host,端口,請求傳入*/
/* write results to pipe */
/* fdopen函數可以了解為将檔案描述符修改為一個檔案指針,指向的是寫端口 */
f=fdopen(mypipe[1],"w");
if(f==NULL)
{
perror("open pipe for writing failed.");
return 3;
}
/* fprintf(stderr,"Child - %d %d\n",speed,failed); */
/*格式化的将資料通過f發送到父程序*/
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;
}
/* fopen是自帶有緩沖區的
* 由于每次通信發送的資料量比較小,是以将f設定為無緩沖區,意味着讀取消息時不在是從緩沖區讀取,是在管道中直接讀取。
*/
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) /*資料都是三個一組,如果小于2說明有子程序死亡*/
{
fprintf(stderr,"Some of our childrens died.\n");
break;
}
/* 統計資訊*/
speed+=i;
failed+=j;
bytes+=k;
/* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */
/*父程序讀取資料的次數應該和用戶端數量是相同的。因為正常情況下讀取clients次後可結束*/
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;
}
/*子程序真正的向伺服器送出請求封包并以其得到此期間的相關資料*/
void benchcore(const char *host,const int port,const char *req)
{
int rlen;
char buf[1500];
int s,i;
struct sigaction sa;
/* setup alarm signal handler */
/*三部曲:注冊信号,設定回調函數,開啟鬧鈴*/
sa.sa_handler=alarm_handler;
sa.sa_flags=0;
if(sigaction(SIGALRM,&sa,NULL))
exit(3);
/*開啟鬧鈴,回調函數的作用僅僅是将timerexpired改為1或是true*/
alarm(benchtime);
rlen=strlen(req);
nexttry:while(1)
{
/*判斷是否到期*/
if(timerexpired)
{
if(failed>0)
{
/* fprintf(stderr,"Correcting failed by signal\n"); */
failed--;
}
return;
}
/* 連接配接伺服器,封裝在socket.c 中*/
s=Socket(host,port);
if(s<0) /*連接配接失敗*/
{
failed++;continue;
}
if(rlen!=write(s,req,rlen)) /*發送請求封包失敗,要關閉掉s*/
{
failed++;
close(s);
continue;
}
/*對于http0.9版本,
*http0.9的特殊處理因為http0.9是在伺服器回複後自動斷開連接配接的,不keep-alive
*在此可以提前先徹底關閉套接字的寫的一半,如果失敗了那麼肯定是個不正常的狀态,
*如果關閉成功則繼續往後,因為可能還有需要接收伺服器的回複内容
*但是寫這一半是一定可以關閉了,作為用戶端程序上不需要再寫了
*是以我們主動破壞套接字的寫端,但是這不是關閉套接字,關閉還是得close
*事實上,關閉寫端後,伺服器沒寫完的資料也不會再寫了,這個就不考慮了
*/
if(http10==0)
if(shutdown(s,1)) { failed++;close(s);continue;}
/* -f沒有設定時預設等待伺服器的回複 */
if(force==0)
{
/* read all available data from socket */
while(1)
{
if(timerexpired) break;
i=read(s,buf,1500);
/* fprintf(stderr,"%d\n",i); */
if(i<0)
{
failed++;
close(s); /*失敗後要關閉s,不然浪費資源*/
goto nexttry;
}
else
if(i==0) break; /*伺服器端關閉連接配接*/
else
bytes+=i; /*正常情況統計收到的位元組*/
}
}
if(close(s)) {failed++;continue;}
speed++;
}
}