天天看點

webbench壓力測試工具源碼剖析(一)

        Webench是一款輕量級的網站測壓工具,最多可以對網站模拟3w左右的并發請求,可以控制時間、是否使用緩存、是否等待伺服器回複等等,且對中小型網站有明顯的效果,基本上可以測出中小型網站的承受能力,對于大型的網站,如百度、淘寶這些巨型網站沒有意義,因為其承受能力非常大。同時測試結果也受自身網速、以及自身主機的性能與記憶體的限制,性能好、記憶體大的主機可以模拟的并發就明顯要多。

        Webbench用C語言編寫,運作于linux平台,下載下傳源碼後直接編譯即可使用,非常迅速快捷,對于中小型網站的制作者,在上線前用webbench進行系列并發測試不失為一個好的測試方法。

本篇文章主要是對webbench架構進行梳理,以及主要函數實作進行分析。

其中主要的函數有:

函數名 函數作用
usage 列印輔助資訊包括參數選項,幫助資訊
build_request 用于建構http的請求封包。
bench 壓力測試函數,主要用于建立程序和統計測試資訊
benchcore 真正的測試函數,與伺服器建立連接配接,接收資料,測試連接配接。
alarm_handler 鬧鈴回調函數,用于判斷進行工作時間是否到期。

主函數的工作過程:

webbench壓力測試工具源碼剖析(一)

對于指令參數分析,用到的是getopt_long函數。

構造http請求封包:build_request函數

webbench壓力測試工具源碼剖析(一)

這個請求封包子產品中有大量的錯誤分析。分析使用者可能填錯誤的資訊。

壓力測試函數:壓力測試子產品,由函數bench進行

webbench壓力測試工具源碼剖析(一)

真正的測試函數benchcore函數:

webbench壓力測試工具源碼剖析(一)

其中函數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++;
		}
}
           

繼續閱讀