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合法性,合法:
无代理服务器:
将其网络地址–>host[],端口号–>proxyport;
url资源路径–>request[]
有代理服务器:
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
#include
#include
#include
#include
#include
#include
//统计的压力测试最终结果表示
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; //默认无代理服务器
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 Run benchmark for seconds. Default 30.\n"
" -p|--proxy Use proxy server for request.\n"
" -c|--clients Run 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':
//使用代理服务器,设置其代理网络号和端口号:格式:-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");
//输出代理服务器的信息
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) //若无代理服务器
{
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) //若无代理服务器
{
// 得到端口号从主机名
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 //若有代理服务器
{
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);
// 打开管道写:连接请求状态的信息
f = fdopen(mypipe[1],"w"); //将文件描述符转换为文件指针
if(f == NULL)
{
perror("open pipe for writing failed.");
return 3;
}
//写入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;
}
setvbuf(f,NULL,_IONBF,0); //设置f的缓冲区为无缓冲区
speed = 0; //连接成功总次数
failed = 0; //失败请求数
bytes = 0; //传输字节数
while(1)
{
pid = fscanf(f,"%d %d %d",&i,&j,&k);
if(pid<2)
{
fprintf(stderr,"Some of our childrens died.\n");
break;
}
speed += i; //连接成功总次数
failed += j; //连接失败总次数
bytes += k; //传输总字节数
//子进程是否读取完
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;
}
//信号处理函数
static void alarm_handler(int signal)
{
timerexpired = 1;
}
//子进程处理发起请求
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;
sa.sa_flags = 0;
if(sigaction(SIGALRM,&sa,NULL))
exit(3);
//设置闹钟函数
alarm(benchtime);
rlen = strlen(req);
nexttry:
while(1){
//收到信号则使 timerexpired = 1
if(timerexpired)
{
if(failed > 0)
{
failed--;
}
return;
}
//建立 socket连接, 进行 HTTP 请求
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;
}
// -f 选项时未设置时等待读取服务器回复
if(force == 0)
{
while(1)
{
if(timerexpired)
break;
i = read(s,buf,1500);
if(i<0)
{
failed++; //读失败++
close(s);
goto nexttry;
}
else
if(i == 0) //读完退出
break;
else
bytes+=i; //统计服务器回复的字节数
}
}
if(close(s))
{
failed++; //关闭失败++
continue;
}
speed++; //成功连接一次++一次
}
}
Socket.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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);
if (inaddr != INADDR_NONE)
memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));
else
{
// 取得 ip 地址
hp = gethostbyname(host); //得到主机的二进制Ip给hp->h_addr
if (hp == NULL)
return -1;
memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);
}
ad.sin_port = htons(clientPort);
// 建立 socket
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;
}
即以上模拟50个客户端在30秒期间并发请求百度,结果如下:
每分钟平均有1708次请求连接,服务器每秒传输字节为3260321,在30秒期间请求连接成功为847次,失败7次。
其中webbench的优点为:
1.部署简单,适用于小型网站压力测试,(最多可模拟3万并发);
2.它具有静态页面测试能力也支持动态页面(ASP,PHP,JAVA,CGI)进行测试能力;
3.支持对含有SSL的安全网站如电子商务网站进行动态或静态性能测试;
缺点为:
1.不适合中大型网站测试;
2.其并发采用多进程实现并非线程,长时间其会大量占用内存与CPU,所以一般长时间的压力测试不推荐使用webbench.