|
前言
考研结束的假期闲来无事,想着把之前学的C++捡起来,于是决定找个小项目练练手,在各种项目推荐帖里面发现了Webbench这个小工具,于是便研究了一下源代码。Webbench是一个简单的网站测压工具,由C语言编写,使用了多进程技术来测试网站并发压力。笔者在学习完源代码后,利用C++对Webbench进行复现,使用线程技术来替换多进程。
本文第一章对Webbench用到的一些前置知识作出补充,第二章介绍Webbench源代码的整体框架,后续几章分别介绍了框架中各个组件的功能。
需要了解的前置知识
- 头文件<strings.h>的使用——处理字符数组
- 头文件<getopt.h>的使用——获取命令行中用户输入的参数
- UNIX网络编程——socket的创建、使用
- 多进程编程——利用fork函数
源代码框架
Webbench可以分为三个部分
- 获取用户输入的参数
- 解析用户所给参数中的URL并根据用户选择的测试方案组装HTTP请求报文
- 利用fork函数模拟多个Client向URL地址发送报文
- 利用管道进行进程间通信,计算出测试方案的各项数据
报文组装
报文组装有如下步骤:
检查用户输入的URL格式
- 是否包含&#34;://&#34; ?
- URL是否过长?——大于1500个字符
- URL是否过短?——小于8个字符
- URL前缀是否为&#34;HTTP://&#34;?
- 结尾是否带有&#34;/&#34;?
- URL是否包含端口号?
获取用户输入的host,若没有端口号,则默认设为HTTP端口号80
根据用户选择的模式组装REQUEST报文
- GET/HEAD/OPTION/TRACE
- ProxyHost
- HTTP版本号(1.0或1.1)
- 长链接
void build_request(const char *url)
{
// wlz:组装请求行和请求头
char tmp[10];
int i;
//bzero(host,MAXHOSTNAMELEN);
//bzero(request,REQUEST_SIZE);
// wlz:初始化host和request数组,用空字符填充host和request
memset(host,0,MAXHOSTNAMELEN);
memset(request,0,REQUEST_SIZE);
// wlz:根据用户偏好,选择使用哪种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;
// wlz:跟据用户选择的模式组装请求方式
switch(method)
{
default:
// wlz:strcpy将一个字符串复制到另一个字符串
case METHOD_GET: strcpy(request,&#34;GET&#34;);break;
case METHOD_HEAD: strcpy(request,&#34;HEAD&#34;);break;
case METHOD_OPTIONS: strcpy(request,&#34;OPTIONS&#34;);break;
case METHOD_TRACE: strcpy(request,&#34;TRACE&#34;);break;
}
/* wzl:以下是对url格式检查
strcat将两个char类型相连*/
strcat(request,&#34; &#34;);
if(NULL==strstr(url,&#34;://&#34;))// wlz:strstr在url查找&#34;://&#34;第一次出现的位置
{
fprintf(stderr, &#34;\n%s: is not a valid URL.\n&#34;,url);
exit(2);
}
if(strlen(url)>1500)
{
fprintf(stderr,&#34;URL is too long.\n&#34;);
exit(2);
}
// wlz:strncasecmp对比str1和str2前几个字符,忽略大小写
if (0!=strncasecmp(&#34;http://&#34;,url,7))
{
fprintf(stderr,&#34;\nOnly HTTP protocol is directly supported, set --proxy for others.\n&#34;);
exit(2);
}
/* protocol/host delimiter */
i=strstr(url,&#34;://&#34;)-url+3;//wlz: 找到://的定位
if(strchr(url+i,&#39;/&#39;)==NULL) {
fprintf(stderr,&#34;\nInvalid URL syntax - hostname don&#39;t ends with &#39;/&#39;.\n&#34;);
exit(2);
}
if(proxyhost==NULL)///wlz: 如果没有走代理服务器
{
/* get port from hostname */
/* wlz:
// index查找字符串首次出现的位置
// 针对有端口号的情况*/
if(index(url+i,&#39;:&#39;)!=NULL && index(url+i,&#39;:&#39;)<index(url+i,&#39;/&#39;))
{
/*wlz
// strncpy 将src所指向的字符串中以src地址开始的前n个字节复制到dest所指的数组中.
// strchr 查找一个串中给定字符的第一个匹配处*/
strncpy(host,url+i,strchr(url+i,&#39;:&#39;)-url-i);
//bzero(tmp,10);
memset(tmp,0,10);
strncpy(tmp,index(url+i,&#39;:&#39;)+1,strchr(url+i,&#39;/&#39;)-index(url+i,&#39;:&#39;)-1);
/* printf(&#34;tmp=%s\n&#34;,tmp); */
proxyport=atoi(tmp);
if(proxyport==0) proxyport=80;// wlz:默认80端口,http端口
}
// wlz:针对没有端口号的情况
else
{
// wlz:strcspn 返回从开头开始的连续子串,并且该子串不带&#39;/&#39;
strncpy(host,url+i,strcspn(url+i,&#34;/&#34;));
}
// printf(&#34;Host=%s\n&#34;,host);
strcat(request+strlen(request),url+i+strcspn(url+i,&#34;/&#34;));// wlz:将host地址插入请求中
}
else// wlz:如果需要走代理服务器
{
// printf(&#34;ProxyHost=%s\nProxyPort=%d\n&#34;,proxyhost,proxyport);
strcat(request,url);
}
if(http10==1)
strcat(request,&#34; HTTP/1.0&#34;);
else if (http10==2)
strcat(request,&#34; HTTP/1.1&#34;);
strcat(request,&#34;\r\n&#34;);
if(http10>0)
strcat(request,&#34;User-Agent: WebBench &#34;PROGRAM_VERSION&#34;\r\n&#34;);
if(proxyhost==NULL && http10>0)
{
strcat(request,&#34;Host: &#34;);
strcat(request,host);
strcat(request,&#34;\r\n&#34;);
}
if(force_reload && proxyhost!=NULL)
{
strcat(request,&#34;Pragma: no-cache\r\n&#34;);
}
if(http10>1)
strcat(request,&#34;Connection: close\r\n&#34;);// wlz:对长链接采取close
/* add empty line at end */
if(http10>0) strcat(request,&#34;\r\n&#34;);
printf(&#34;\nRequest:\n%s\n&#34;,request);
}套接字创建
- 查询用户所给host对应的ip地址
- 连接至ip地址
- 返回socket编号
int Socket(const char *host, int clientPort)
{
int sock;
unsigned long inaddr;
// sockaddr是c-socket编程通用的地址结构,
// 但是sockaddr将地址和端口混用
// 而sockaddr_in将地址和端口分开存储,并且能够转换为sockaddr
// 因此常用的做法是:sockaddr_in用于sock赋值和定义 sockaddr用于传参
struct sockaddr_in ad;
// hostent是gethostbyname的返回值
// gethostbyname用来解析域名,将域名转换为IP地址
// 该接口已经被弃用,因为仅支持ipv4协议。
// 新接口支持ipv4\ipv6与其他协议。
struct hostent *hp;
memset(&ad, 0, sizeof(ad));
ad.sin_family = AF_INET;// sin_family指定地址类型为AF_INET
// 尝试通过inet_addr 将由点和数字组成的主机地址转换成二进制的网络字节顺序流
inaddr = inet_addr(host);
// 如果用户传入的host是域名则会返回INADDR_NONE
if (inaddr != INADDR_NONE)// INADDR_NONE是错误地址的宏
memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));
// 通过gethostbyname解析域名
else
{
hp = gethostbyname(host);
if (hp == NULL)
return -1;
memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);
}
// htons将整形变量从主机字节顺序转为网络字节顺序
ad.sin_port = htons(clientPort);
// 建立socket
// socket返回文件描述符id。fid
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用于测试
- 创一个管道用于进程间通信
- 使用fork创建多个进程(个数为用户指定的参数)
- 在子进程中创建socket并尝试发送报文
- 如果用户指定&#39;-f&#39;,则进程计数++,并且关闭socket。(指定--force/-f 意味着不强制收到服务器返回的消息)
- 如果用户未指定&#39;-f&#39;,则等待服务器返回的消息,将收到的byte数累加,然后关闭socket
- 计算速度、byte数等统计结果。
cstatic int bench(void)
{
int i,j,k;
pid_t pid=0;
FILE *f;
/* check avaibility of target server */
/* wlz
// 对socket链接进行测试
// 返回一个唯一的socket关联值*/
i=Socket(proxyhost==NULL?host:proxyhost,proxyport);
if(i<0) {
fprintf(stderr,&#34;\nConnect to server failed. Aborting benchmark.\n&#34;);
return 1;
}
close(i);// wlz:关闭socket
/* create pipe */
/*wlz:
// 创建管道
// 函数原型int pipe(int fd[2]) fd[2]:管道的两个文件描述符,可以直接操作这两个描述符
// 返回值:成功0 失败 -1
// fd[0]是读端,fd[1]是写端*/
if(pipe(mypipe))
{
perror(&#34;pipe failed.&#34;);
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 */
/* wlz:*/
// fork()返回值:
/* 负值:创建子进程失败
零:返回到新创建的子进程
正值:返回新创建的进程的PID
*/
for(i=0;i<clients;i++)
{
pid=fork();// !!!子进程会fork,clients是不是少开根号?!!!
if(pid <= (pid_t) 0)// 类型强制转换
{
/* child process or error*/
/* wlz:sleep让进程挂起1s */
sleep(1); /* make childs faster */
break;
}
}
if( pid < (pid_t) 0)
{
fprintf(stderr,&#34;problems forking worker no. %d\n&#34;,i);
perror(&#34;fork failed.&#34;);
return 3;
}
if(pid == (pid_t) 0)
{
/* I am a child */
if(proxyhost==NULL)
benchcore(host,proxyport,request);
else
benchcore(proxyhost,proxyport,request);
/* write results to pipe */
f=fdopen(mypipe[1],&#34;w&#34;);
if(f==NULL)
{
perror(&#34;open pipe for writing failed.&#34;);
return 3;
}
/* fprintf(stderr,&#34;Child - %d %d\n&#34;,speed,failed); */
fprintf(f,&#34;%d %d %d\n&#34;,speed,failed,bytes);
fclose(f);
return 0;
}
else
{
f=fdopen(mypipe[0],&#34;r&#34;);
if(f==NULL)
{
perror(&#34;open pipe for reading failed.&#34;);
return 3;
}
setvbuf(f,NULL,_IONBF,0);
speed=0;
failed=0;
bytes=0;
while(1)
{
pid=fscanf(f,&#34;%d %d %d&#34;,&i,&j,&k);
if(pid<2)
{
fprintf(stderr,&#34;Some of our childrens died.\n&#34;);
break;
}
speed+=i;
failed+=j;
bytes+=k;
/* fprintf(stderr,&#34;*Knock* %d %d read=%d\n&#34;,speed,failed,pid); */
if(--clients==0) break;
}
fclose(f);
printf(&#34;\nSpeed=%d pages/min, %d bytes/sec.\nRequests: %d susceed, %d failed.\n&#34;,
(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 */
/* wlz:如果子进程运行时间超过30s,将timerexpired设为true*/
sa.sa_handler=alarm_handler;
sa.sa_flags=0;
if(sigaction(SIGALRM,&sa,NULL))
exit(3);
alarm(benchtime); // after benchtime,then exit
rlen=strlen(req);
nexttry:while(1)
{
if(timerexpired)
{
if(failed>0)
{
/* fprintf(stderr,&#34;Correcting failed by signal\n&#34;); */
failed--;
}
return;
}
s=Socket(host,port);
if(s<0) { failed++;continue;}
if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;}// wlz:write将req中的rlen个字节写入s(socket)中
if(http10==0)
if(shutdown(s,1)) { failed++;close(s);continue;}// wlz: 尝试关闭socket
if(force==0)
{
/* read all available data from socket */
while(1)
{
if(timerexpired) break;
i=read(s,buf,1500);
/* fprintf(stderr,&#34;%d\n&#34;,i); */
if(i<0)
{
failed++;
close(s);
goto nexttry;
}
else
if(i==0) break;
else
bytes+=i;
}
}
if(close(s)) {failed++;continue;}
speed++;
}
}
引用
Webbench implement by cpp
Web Bench Homepage
后记
把五个月以前自己写的代码拿出来看简直是人间折磨。。。(复习密码学也是人间折磨,可能更痛苦) |
|