Webbench是一个在linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL,测试网站在压力下工作的性能,最多可以模拟3万个并发连接去测试网站的负载能力。

仓库地址

项目结构

代码细节

main

  1. 首先使用getopt_long解析参数并初始化程序。
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;
case 'p':
/* proxy server parsing 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';
proxyport=atoi(tmp+1);break;
case ':':
case 'h':
case '?': usage();return 2;break;
case 'c': clients=atoi(optarg);break;
}
}

optarg:
这个是getoptgetopt_long函数族中的全局变量,用于存储命令行选项的参数值。如果某个选项有参数值(如-o output.txt),那optarg会设置为指向output.txt字符串的指针。

  1. 利用optind找到命令行参数中网站的url,然后调用build_request向对应网站发送请求
if(optind==argc) {
fprintf(stderr,"webbench: Missing URL!\n");
usage();
return 2;
}

if(clients==0) clients=1;
if(benchtime==0) benchtime=30;

/* Copyright */
fprintf(stderr,"Webbench - Simple Web Benchmark "PROGRAM_VERSION"\n"
"Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.\n"
);

build_request(argv[optind]);

optind:
下一个待处理参数的索引,默认从1开始,跳过args[0]程序名。

关于这里的fprintf为什么使用stderr输出:
stdout通常用于程序的实际结果输出,这些内容可能是用户需要重定向到文件或管道的数据。
stderr用于日志、错误、调试信息或非核心内容。即使 stdout 被重定向,stderr 仍会直接显示在终端。

build_request

通过用户参数构造请求头。

void build_request(const char *url)
{
char tmp[10];
int i;

//bzero(host,MAXHOSTNAMELEN);
//bzero(request,REQUEST_SIZE);
// 清空地址内容
memset(host,0,MAXHOSTNAMELEN);
memset(request,0,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;

// 拼接请求头中的请求方法
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," ");

// 一些验证
if(NULL==strstr(url,"://"))
{
fprintf(stderr, "\n%s: is not a valid URL.\n",url);
exit(2);
}
if(strlen(url)>1500)
{
fprintf(stderr,"URL is too long.\n");
exit(2);
}
// 验证是否以http:// 开头,不区分大小写
if (0!=strncasecmp("http://",url,7))
{
fprintf(stderr,"\nOnly HTTP protocol is directly supported, set --proxy for others.\n");
exit(2);
}

/* protocol/host delimiter */
// 分离协议和主机地址
i=strstr(url,"://")-url+3;

if(strchr(url+i,'/')==NULL) {
fprintf(stderr,"\nInvalid URL syntax - hostname don't ends with '/'.\n");
exit(2);
}

// 获取代理主机地址
// 如果代理主机地址为空,就需要从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);
//bzero(tmp,10);
memset(tmp,0,10);
strncpy(tmp,index(url+i,':')+1,strchr(url+i,'/')-index(url+i,':')-1);
/* printf("tmp=%s\n",tmp); */
proxyport=atoi(tmp);
if(proxyport==0) proxyport=80;
}
else
{
//strcspn 从字符串 `url + i` 的位置开始,查找第一个出现 `/` 字符的位置,
//并返回从起始位置到该字符的 **长度(字节数)**
strncpy(host,url+i,strcspn(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);
}

// 拼接协议类型
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");
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");

/* add empty line at end */
if(http10>0) strcat(request,"\r\n");

printf("\nRequest:\n%s\n",request);
}

strcspn(url + i, "/") 的功能解析:
这个函数调用用于 从字符串 url + i 的位置开始,查找第一个出现 / 字符的位置,并返回从起始位置到该字符的 长度(字节数)

最终效果大概如下:

GET /test.jpg HTTP/1.1 
User-Agent: WebBench 1.5
Host:192.168.10.1
Pragma: no-cache
Connection: close
\r\n(这里有一个空行,用\r\n表示)

bench

  1. 该函数首先调用自己封装的接口Socket创建目标主机间的套接字并使用connect连接,测试是否成功,如果没有成功就退出程序;否则关闭连接。
  2. 使用pipe()创建pipe用于子进程和主进程之间通信。
  3. 创建子进程调用benchcore进行测试,将测试结果写入pipe,主进程读取结果。
static int bench(void)
{
int i,j,k;
pid_t pid=0; // 进程id
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();
// fork error,剩下的子进程不创建了
if(pid <= (pid_t) 0)
{
sleep(1); /* make childs faster */
break;
}
}

// 循环创建子进程过程中,只要有一个创建失败,跳出该函数
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;
}
// 写入结果
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;
}

// 不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。
setvbuf(f,NULL,_IONBF,0);

// 给与结果相关的变量置0
speed=0;
failed=0;
bytes=0;

while(1)
{
// 从流 stream 读取格式化输入。
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;
}


fdopen():
FILE *fdopen(int fd, const char *mode); 将 已有的文件描述符 包装成 标准文件流,从而允许使用 fgetsfread 等高级函数

benchcore

  1. 自定义超时信号处理函数,如果超时就设置timerexpired = 1;用alarm()设置超时时间,如果超时就会发送SIGALRM信号
  2. 进行压力测试。如果没有超时,就一直访问这个网页。
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);

// 超过 benchtime 秒后,产生一个 SIGALRM 信号
alarm(benchtime); // after benchtime,then exit

rlen=strlen(req);
nexttry:while(1)
{
// 定时器到,退出循环
if(timerexpired)
{
if(failed>0)
{
failed--;
}
return;
}

// 与目标服务器建立连接
s=Socket(host,port);

// 连接失败,则失败数量 failed++
if(s<0)
{
failed++;
continue;
}
// write 会返回的实际字节数,如果不能把请求消息完全发送,那也是失败了
if(rlen!=write(s,req,rlen))
{
failed++;
close(s);
continue;
}

if (http10 == 0)
{
// 关闭连接s的写端,即不在发送数据,但还能接收数据的意思,没有错误发送则返回 0
if (shutdown(s, 1)) {
failed++;
close(s);
continue;
}
}

// 如果需要等待结果返回
if(force==0)
{
/* read all available data from socket */
while(1)
{
// 定时器到,退出循环
if(timerexpired)
break;
// 从连接 s 中每次读取1500字节数据到 buf,返回实际读取的字节数
i=read(s,buf,1500);
// i < 0,表示有错误发生
if(i<0)
{
failed++;
close(s);
goto nexttry;
}
else {
if (i == 0)
break; // i为0,表示数据已经读取完毕
else
bytes += i; // 加上读取的字节数
}

}
}

// 关闭套接字失败
if(close(s)) {
failed++;
continue;

}

speed++;// 成功访问,成功数量 speed++
}
}

相关函数

getopt_long

函数原型:

#include <getopt.h> // 必须包含此头文件

int getopt_long(
int argc, // main() 的 argc(参数计数)
char *const argv[], // main() 的 argv(参数数组)
const char *optstring, // 短选项字符串(如 "ho:v")
const struct option *longopts, // 长选项结构体数组
int *longindex // 返回长选项的索引(可设为 NULL)
);

getopt_long是一个用于命令行解析的C语言库函数,它是对getopt的扩展,支持长选项和短选项。

该函数有以下解析功能:

  • 短选项
    单字符选项,通常以 - 开头,例如 -h-v

    ./program -h -v
  • 长选项
    多字符选项,以 -- 开头,例如 --help--version

    ./program --help --version
  • 带参数的选项
    选项可以接受参数,例如 --output=file.txt 或 -o file.txt

    ./program -o output.txt
    ./program --output=output.txt