Web Bench是一个网站压力测试的工具。其最后更新时间是2004年,已经十年多了。其源代码总共才不到600行,全部使用C语言编写,最多可以模拟3万个并发连接。真可谓是简洁代码的代表之作。
用法:可以在安装后直接输入 webbench 或 webbench -h 或 webbench –help. 可以看到:
webbench [option]... URL
-f|--force Don't wait for reply from server.
-r|--reload Send reload request - Pragma: no-cache.
-t|--time <sec> Run benchmark for <sec> seconds. Default 30.
-p|--proxy <server:port> Use proxy server for request.
-c|--clients <n> Run <n> HTTP clients at once. Default one.
-9|--http09 Use HTTP/0.9 style requests.
-1|--http10 Use HTTP/1.0 protocol.
-2|--http11 Use HTTP/1.1 protocol.
--get Use GET request method.
--head Use HEAD request method.
--options Use OPTIONS request method.
--trace Use TRACE request method.
-?|-h|--help This information.
-V|--version Display program version.
说一下主要的几个选项: 指定 -f 时不等待服务器数据返回, -t 为指定压力测试运行时间, -c 指定由多少个客户端发起测试请求。
-9 -1 -2 分别为指定 HTTP/0.9 HTTP/1.0 HTTP/1.1。
Webbench的代码实现原理也是相当简单,就是一个父进程fork出很多个子进程,子进程分别去执行http测试,最后把执行结果汇总写入管道,父进程读取管道数据然后进行最终计算。整个工具的实现流程如下:
其源代码包括两个文件,非常简单,一个是socket.c,一个是webbench.c。其中,socket.c是用来建立socket连接的,webbench.c负责实现主要的功能。
socket.c源代码及注释:
[cpp]
/* $Id: socket.c 1.1 1995/01/01 07:11:14 cthuang Exp $
*
* This module has been modified by Radim Kolar for OS/2 emx
*/
/***********************************************************************
module: socket.c
program: popclient
SCCS ID: @(#)socket.c 1.5 4/1/94
programmer: Virginia Tech Computing Center
compiler: DEC RISC C compiler (Ultrix 4.1)
environment: DEC Ultrix 4.3
description: UNIX sockets code.
***********************************************************************/
#include <sys types.h="">
#include <sys socket.h="">
#include <fcntl.h>
#include <netinet in.h="">
#include
#include <netdb.h>
#include <sys time.h="">
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
/***********
功能:通过地址和端口建立网络连接
@host:网络地址
@clientPort:端口
Return:建立的socket连接。
如果返回-1,表示建立连接失败
************/
int Socket(const char *host, int clientPort)
{
int sock;
unsigned long inaddr;
struct sockaddr_in ad;
struct hostent *hp;
memset(&ad, 0, sizeof(ad));
ad.sin_family = AF_INET;
inaddr = inet_addr(host);//将点分的十进制的IP转为无符号长整形
if (inaddr != INADDR_NONE)
memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));
else//如果host是域名
{
hp = gethostbyname(host);//用域名获取IP
if (hp == NULL)
return -1;
memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);
}
ad.sin_port = htons(clientPort);
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
return sock;
//连接
if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0)
return -1;
return sock;
}
socket函数的大致内容如下:
int Socket(const char *host, int clientPort)
{
//以host为服务器端ip,clientPort为服务器端口号建立socket连接
//连接类型为TCP,使用IPv4网域
//一旦出错,返回-1
//正常连接,则返回socket描述符
}
Webbench.c中包含以下几个主要的函数:
static void usage(void):提示Webbench的用法及命令
static void alarm_handler(int signal) :信号处理函数,时钟结束时进行调用
void build_request(const char *url):创建http连接请求
static int bench(void):创建管道和子进程,调用测试http函数,实现父子进程通信并计算结果
void benchcore(const char *host,const int port,const char *req):对http请求进行测试(子进程的具体工作)
主要流程:
1. 解析命令行参数,根据命令行指定参数设定变量,可以认为是初始化配置。
2.根据指定的配置构造 HTTP 请求报文格式。
3.开始执行 bench 函数,先进行一次 socket 连接建立与断开,测试是否可以正常访问。
4.建立管道,派生根据指定进程数派生子进程。
5.每个子进程调用 benchcore 函数,先通过 sigaction 安装信号,用 alarm 设置闹钟函数,到时间后会产生SIGALRM信号,调用信号处理函数使子进程停止。接着不断建立 socket 进行通信,与服务器交互数据,直到收到信号结束访问测试。子进程将访问测试结果写进管道。
6.父进程读取管道数据,汇总子进程信息,收到所有子进程消息后,输出汇总信息,结束。
程序调用的流程示意图
全局变量:
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请求
每个函数的具体分析:
(1)alarm_handler();
static void alarm_handler(int signal)
{
timerexpired=1;
}
webbench在运行时可以设定压测的持续时间,以秒为单位。例如我们希望测试30秒,也就意味着压测30秒后程序应该退出了。webbench中使用信号(signal)来控制程序结束。函数alarm_handler()是在到达结束时间时运行的信号处理函数。它仅仅是将一个记录是否超时的变量timerexpired标记为true。后面会看到,在程序的while循环中会不断检测此值,只有timerexpired=1,程序才会跳出while循环并返回。
(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"
);
};
其中,-9 -1 -2 分别代表http0.9、http1.0和http1.1协议。webbench支持GET,HEAD,OPTIONS,TRACE四种请求方式。
(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。
(4)主函数main();
int main(int argc, char *argv[])
{
/*函数最开始,使用getopt_long函数读取命令行参数,来设置全局变量的值。
关于getopt_long的具体使用方法,这里有一个配有讲解的小例子,可以帮助学习:
http://blog.youkuaiyun.com/lanyan822/article/details/7692013
在此期间如果出现错误,会调用函数(1)告知用户此工具使用方法,然后退出。
*/
build_request(argv[optind]);
//参数读完后,argv[optind]即放在命令行最后的url
//调用函数(2)建立完整的HTTP request,
//HTTP request存储在全部变量char request[REQUEST_SIZE]
/*接下来的部分,main函数的所有代码都是在网屏幕上打印此次测试的信息,
例如即将测试多少秒,几个并发进程,使用哪个HTTP版本等。
这些信息并非程序核心代码,因此我们也略去。
*/
return bench();
//简简单单一句话,原来,压力测试在这最后一句才真正开始!
//所有的压测都在bench函数(即函数(5))实现
}
(5)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++){...}
}
函数先进行了一次socket连接,确认能连通以后,才进行后续步骤。调用pipe函数初始化一个管道,用于子进行向父进程汇报测试数据。子进程根据clients数量fork出来。每个子进程都调用函数(6)进行测试,并将结果输出到管道,供父进程读取。父进程负责收集所有子进程的测试数据,并汇总输出。
(6)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表示我们是否在发出请求后需要等待服务器的响应结果。
疑问及解答:
1、线程占用的空间比进程要小,而且线程切换的时间开销也小,但为什么程序的实现上采用的是fork进程而不是使用多线程呢?
答:因为默认情况下:主线程+辅助线程<253个自己的线程<=256,含主线程和一个辅助线程,最多255个,即一个用户只能生成253个线程。而进程的最大数目则跟系统本身限制相关。
2、webbench中在多个进程进行写管道的情况下,在代码中没有采取同步措施,程序是如何保持数据正确呢?
答:管道写的数据不大于 PIPE_BUF 时,系统可以保证写的原子性。在2.6.29内核中,\include\linux\limits.h定义:#define PIPE_BUF 4096
涉及到的知识点有:命令行参数解析(getopt_long)、 信号处理(sigaction)、 socket 管道读写 。
源码中的一些函数用法及启示
getoptlong():
在写程序时常常需要对命令行参数进行处理,当命令行参数个数较多时,如果按照顺序一个一个定义参数含义很容易造成混乱,而且如果程序只按顺序处理参数的话,一些“可选参数”的功能将很难实现。
在Linux中,可以使用getopt、getopt_long、getopt_long_only来对处理这个问题。
sleep(1)的功能:
让CPU能够空闲下来,不至于使CPU使用率一直高居不下;本线程放弃cpu时间片,其他线程处理之后,再开始本线程,多线程处理socket接收发送的时候经常这样处理,防止接收发送出现问题。
转自:Webbench源码剖析
webbench安装指南:在CentOS下安装WebBench进行web 性能测试
webbench测试:用Webbench进行网站压力测试