简介
webbench是一款用C编写的开源工具,主要用来在Linux下进行网站压力测试。最多可以模拟3万个连接去测试网站的负载能力,并可以设置运行的客户端数、测试时间、使用的http协议版本、请求方法、是否需要等待服务器响应等选项,最后统计每分钟相应请求次数(paga/min)和每秒钟传输数据量(byte/sec),以及请求成功数和失败数,表现测试网站的压力承载能力。
源码及其相关函数分析
webbench源码
getopt_long
sigaction
fdopen
工作流程
首先解析终端输入的命令,然后根据输入的url构建相应的HTTP请求,开始压力测试后,
创建pipe管道,再创建client个子进程,在子进程中向服务端发送HTTP请求进行压力测试,
然后通过管道将测试信息传送给父进程。父进程将通过管道读取每个子进程的测试结果,
进行汇总统计。
测试
代码与测试相结合,有助于理解
./webbench -c 3000 -t 5 http://baidu.com/
//结果
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
Benchmarking: GET http://baidu.com/
3000 clients, running 5 sec.
Speed=28164 pages/min, 22326 bytes/sec.
Requests: 1648 susceed, 699 failed.
源码解析
webbench.c
main()
int main(int argc, char *argv[])
{
int opt=0;
int options_index=0;
char *tmp=NULL;
// 如果在终端只输入了./webbench,后面没有跟参数,打印用法,直接退出程序,返回码为2,表示格式错误
if(argc==1)
{
usage();
return 2;
}
// ./webbench -c 3000 -t 5 url
// 循环解析终端输入选项,每次解析一个选项及其后面可能跟的参数
//该函数主要作用就是解析终端输入的参数,是否与(短选项)“912Vfrt:p:c:?h”或者与(长选项)“long_options”中任一符合
while((opt=getopt_long(argc,argv,"912Vfrt:p:c:?h",long_options,&options_index))!=EOF )
{
switch(opt)
{
case 0 : break;
// 如果选项为'f',那么设置不等待服务器响应,发送请求后直接关闭连接
case 'f': force=1;break;
// 如果选项为'r',那么设置强制代理服务器重新发送请求
case 'r': force_reload=1;break;
// 如果选项为'9',那么设置在条件允许范围内使用HTTP/0.9协议
case '9': http10=0;break;
// 如果选项为'1',那么设置在条件允许范围内使用HTTP/1.0协议
case '1': http10=1;break;
// 如果选项为'2',那么设置在条件允许范围内使用HTTP/1.1协议
case '2': http10=2;break;
// 如果选项为'V',那么打印程序版本,然后退出程序
case 'V': printf(PROGRAM_VERSION"\n");exit(0);
// 如果选项为't',那么记录其后所跟参数数值到运行基准时间benchtime中
case 't': benchtime=atoi(optarg);break;
// 如果选项为'p',那么表示使用代理服务器
case 'p':
/* proxy server parsing server:port */
// 记录参数中最后出现字符':'的位置及其之后的内容到tmp中
tmp=strrchr(optarg,':');
// 记录参数到代理主机proxyhost中
proxyhost=optarg;
// 如果参数中没有字符':',说明没有端口号,直接退出switch
if(tmp==NULL)
{
break;
}
// 如果参数中只有一个字符':',说明端口号在最前,打印缺失主机名,然后直接返回,返回码为2,表示格式错误
if(tmp==optarg)
{
fprintf(stderr,"Error in option --proxy %s: Missing hostname.\n",optarg);
return 2;
}
// 如果参数中最后一个':'之后没有内容,打印缺失端口号,然后直接返回,返回码为2,表示格式错误
if(tmp==optarg+strlen(optarg)-1)
{
fprintf(stderr,"Error in option --proxy %s Port number is missing.\n",optarg);
return 2;
}
// 将proxyhost中的内容从最后一个':'处进行截断,只记录':'之前的内容
*tmp='\0';
// 将最后一个':'之后内容转化为数字并记录在代理服务器端口号proxyport中
proxyport=atoi(tmp+1);break;
// 如果选项为':'、'h'、'?',那么打印用法,并直接退出程序,返回码为2,表示格式错误
case ':':
case 'h':
case '?': usage();return 2;break;
// 如果选项为'c',那么记录其后所跟参数数值到客户端数量clients中
case 'c': clients=atoi(optarg);break;
}
}
// 如果参数后没有其它内容,打印缺失测试URL,打印用法后直接退出程序,返回码为2,表示格式错误
if(optind==argc) {
fprintf(stderr,"webbench: Missing URL!\n");
usage();
return 2;
}
// 如果输入客户端数量为0,则设置为默认值1
if(clients==0) clients=1;
// 如果输入运行测试的秒数为0,则设置为默认值60
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"
);
// 将测试URL作为参数传入build_request方法中,构建http请求
build_request(argv[optind]);
/* print bench info */
// 打印Benchmarking、请求方法、测试URL
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]);
// 判断使用的http协议类型,如果使用的是默认的HTTP/1.0则不打印
switch(http10)
{
// 如果http10的值为0,打印使用HTTP/0.9
case 0: printf(" (using HTTP/0.9)");break;
// 如果http10的值为2,打印使用HTTP/1.1
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();
}
bench
static int bench(void)
{
int i,j,k;
pid_t pid=0;
FILE *f;
/* check avaibility of target server */
// 建立一个TCP连接,检查连接可用性,如果设置了代理服务器,那么连接代理服务器,否则直接连接目标服务器
i=Socket(proxyhost==NULL?host:proxyhost,proxyport);
// 连接失败那么打印错误信息,并直接退出,返回码为1,表示基准测试失败(服务器未联机)
if(i<0) {
fprintf(stderr,"\nConnect to server failed. Aborting benchmark.\n");
return 1;
}
// 关闭连接,这次连接不计入测试
close(i);
/* create pipe */
// 创建管道,如果失败,直接退出程序,返回码为3,表示内部错误
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 */
// 创建clients个子进程,由子进程进行真正的测试
for(i=0;i<clients;i++)
{
pid=fork();
// 如果是子进程或者创建失败,休眠1s后退出循环,让父进程先执行,完成初始化,并且保证子进程中不会再fork出新的子进程
if(pid <= (pid_t) 0)
{
/* child process or error*/
sleep(1); /* make childs faster */
break;
}
}
// 如果创建子进程失败,那么打印fork失败,直接退出程序,返回码为3,表示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)
{
/* I am a child */
// 如果不使用代理服务器,那么子进程直接对目标服务器发出http请求,否则向代理服务器发出http请求
if(proxyhost==NULL)
benchcore(host,proxyport,request);
else
benchcore(proxyhost,proxyport,request);
/* write results to pipe */
// 获取管道写端的文件指针
f=fdopen(mypipe[1],"w");
// 获取失败,直接退出,返回码为3,表示内部错误
if(f==NULL)
{
perror("open pipe for writing failed.");
return 3;
}
/* fprintf(stderr,"Child - %d %d\n",speed,failed); */
// 将子进程的传输速率,测试失败数,总传输字节数写入管道,关闭写端
fprintf(f,"%d %d %d\n",speed,failed,bytes);
fclose(f);
// 返回0,表示运行成功
return 0;
} else // 父进程中
{
// 获取管道读端的指针
f=fdopen(mypipe[0],"r");
// 获取失败,直接退出,返回码为3,表示内部错误
if(f==NULL)
{
perror("open pipe for reading failed.");
return 3;
}
// 设置不使用缓冲。每个I/O操作都被即时写入管道
setvbuf(f,NULL,_IONBF,0);
// 初始化传输速率,测试失败次数,传输总字节数都为0
speed=0;
failed=0;
bytes=0;
// 父进程循环读取数据
while(1)
{
// 循环从管道中每3个一组读取子进程的输出数据,并且获取成功读取的参数个数
pid=fscanf(f,"%d %d %d",&i,&j,&k);
// 如果成功读取的个数小于2,说明有子进程中途挂掉,直接退出读取循环
//把每个子进程输出的三个数据读取,在父进程中统计
if(pid<2)
{
fprintf(stderr,"Some of our childrens died.\n");
break;
}
// 否则更新传输速率(测试成功个数),测试失败次数,传输总字节数
//父进程中speed,filed,bytes三个变量对clients中的数据进行统计综合
//父子进程遵循读时共享,写时复制,分析时请注意下
speed+=i;
failed+=j;
bytes+=k;
/* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */
// 客户端数减一后如果等于0,说明没有多的客户端数据读取,直接退出循环
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;
}
benchcore
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 */
// 设置SIGALRM的信号处理函数
sa.sa_handler=alarm_handler;
sa.sa_flags=0;
if(sigaction(SIGALRM,&sa,NULL))
exit(3);
// 设置计时器时间为运行测试的时间,到期后发送SIGALRM信号
alarm(benchtime);
// 获取请求报文大小
rlen=strlen(req);
// 进入循环,每次客户端建立一个连接,计时器时间到期后再退出
nexttry:while(1)
{
// 如果timerexpired等于1,说明收到了SIGALRM信号,表示计时器到期了,直接返回
if(timerexpired)
{
// 如果失败的测试数大于0,那么失败的测试数减一
if(failed>0)
{
/* fprintf(stderr,"Correcting failed by signal\n"); */
failed--;
}
return;
}
// 建立与目标服务器的TCP连接
s=Socket(host,port);
// 如果连接失败,测试的失败数加一,继续循环
if(s<0) { failed++;continue;}
// 如果请求报文写入套接字失败,测试的失败数加一,继续循环
if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;}
// 如果使用HTTP/0.9协议,因为会在服务器回复后自动断开连接,所以可以先关闭写端
if(http10==0)
// 如果写端关闭失败,那么说明是不正常的连接状态,测试的失败数加一,关闭连接,继续循环
if(shutdown(s,1)) { failed++;close(s);continue;}
// 如果设置需要等待服务器响应,那么还要处理响应数据,否则直接关闭连接
if(force==0)
{
/* read all available data from socket */
// 从套接字中读取数据
while(1)
{
// 如果计时器到期,结束读取
if(timerexpired) break;
// 将数据读取进buf中
i=read(s,buf,1500);
/* fprintf(stderr,"%d\n",i); */
// 如果读取失败,测试的失败数加一,关闭连接,继续循环,客户端重新建立连接,直到计时器到期后再退出
if(i<0)
{
failed++;
close(s);
goto nexttry;
}
else
// 如果已经读取到文件末尾,结束读取数据
if(i==0) break;
// 如果读取到了数据,将总共传送的字节数加上读取到的数据的字节数
else
bytes+=i;
}
}
// 关闭连接,如果失败,测试失败数加一,继续循环
if(close(s)) {failed++;continue;}
// 传输速率(这里用测试成功数表示,后面除以时间得到真正的传输速率)加一
speed++;
}
}
socket.c
/*
* 建立与目标的TCP连接,返回客户端连接使用的套接字
* host: 目标域名或主机名
* clientPort: 目标端口
*/
int Socket(const char *host, int clientPort)
{
int sock; // 客户端套接字标识符
unsigned long inaddr; // 主机名的IP地址的数字形式
struct sockaddr_in ad; // 套接字地址结构
struct hostent *hp; // 域名IP地址
// 初始化目标套接字的地址,指定使用的IP协议为IPv4
memset(&ad, 0, sizeof(ad));
ad.sin_family = AF_INET;
// 将目标主机名的IP地址转换为数字
inaddr = inet_addr(host);
// 如果返回值不为INADDR_NONE,说明不是无效的IP地址,设置目标套接字的IP地址
if (inaddr != INADDR_NONE)
memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));
// 否则说明是无效的IP地址,host不是主机名而是域名
else
{
// 通过域名获取IP地址
hp = gethostbyname(host);
// 如果获取失败。返回-1
if (hp == NULL)
return -1;
// 设置目标套接字的IP地址
memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);
}
// 设置目标套接字的端口号
ad.sin_port = htons(clientPort);
// 创建一个使用TCP协议的socket
sock = socket(AF_INET, SOCK_STREAM, 0);
// 如果创建失败,直接返回
if (sock < 0)
return sock;
// 进行连接,如果连接不成功,返回-1
if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0)
return -1;
// 如果连接成功,返回sock
return sock;
}