本文适合人群:对WebBench实现感兴趣的人
WebBench原理:
Linux下使用的服务器压力测试工具,利用fork建立多个子进程,每个子进程在测试时间内不断发送请求报文,建立多个连接,然后由父进程统计:TCP连接成功次数,TCP连接失败次数,从服务器接收的数据量
WebBench适用于小,中型网站的服务器压力测试(对淘宝,百度这种大型网站不存在测压作用)
WebBench支持的并行连接数:32768
进程号pid是short类型的,short类型最大为32768
所以WebBench最多可以模拟3万多个并发连接去测试网站的负载能力
1.clients参数
//创建子进程进行测试,子进程数量和clients有关 for(i=0; i<clients; i++) { // pid 为 pid_t 类型 表示进程号 pid=fork();//建立子进程 //fork失败 子进程错误 if(pid <= (pid_t) 0) { sleep(1); //当前进程挂起1毫秒,将cpu时间交给其他进程 break; //跳出去,阻止子进程继续fork } }
子进程数量=1+2+3+……+(clients)
关键是的fork函数的理解:fork一个子进程,该子进程将要执行的指令和父进程继续执行的指令是一模一样的
2.benchtime参数
一个子进程在benchtime时间内,不断发送http请求,建立多个连接进行测试,到达benchtime时间则停止测试,返回测试结果(连接成功次数,连接失败次数,服务器响应内容字节数)
针对原版的WebBench所作的改进:
1.弃用了TRACE请求方法:回显服务器收到的请求
因为一般服务器都不支持这个方法,支持这个方法的服务器存在跨站脚本漏洞,攻击者可以此漏洞欺骗合法用户并得到他们的私人信息
2.增加了连接失败类型的统计,结果更加直观
一共两个文件socket.c和webbench.c
加上注释,代码不超过一千行
sorcket.c:
#include <sys/types.h> #include <sys/socket.h> #include <fcntl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <sys/time.h> #include <string.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <stdarg.h> /* sockaddr_in分析: #include <netinet/in.h>和#include <arpa/inet.h>定义的 struct sockaddr { __SOCKADDR_COMMON (sa_); //协议族 char sa_data[14]; //地址+端口号 }; sockaddr缺陷:把目标地址和端口号混在一起了 而sockaddr_in就解决了这一缺陷 将端口号和IP地址分开存储 struct sockaddr_in { sa_family_t sin_family; //地址族 uint16_t sin_port; //16位TCP/UDP端口号 struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不使用,只为了内存对齐 }; */ /* hostent分析: host entry的缩写 记录主机信息包括主机名,别名,地址类型,地址长度和地址列表 struct hostent { char *h_name; //正式主机名 char **h_aliases; //主机别名 int h_addrtype; //主机IP地址类型:IPV4-AF_INET int h_length; //主机IP地址字节长度,对于IPv4是四字节,即32位 char **h_addr_list; //主机的IP地址列表 }; #define h_addr h_addr_list[0] //保存的是IP地址 主机的的地址是列表形式的原因: 当一个主机又多个网络接口时,自然有多个地址 */ //host ip地址或者主机名 //clientPort 端口 int Socket(const char *host, int clientPort) { int sock; unsigned long inaddr; struct sockaddr_in ad;//地址信息 struct hostent *hp;//主机信息 /* 因为host可能是ip地址或者主机名 所以当host为主机名的时候需要通过主机名得到IP地址 */ //初始化地址 memset(&ad, 0, sizeof(ad)); //采用TCP/IP协议族 ad.sin_family = AF_INET; //点分十进制IP转化为二进制IP inaddr = inet_addr(host); //输入为IP地址 if (inaddr != INADDR_NONE) //将IP地址复制给ad的sin_addr属性 memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr)); //输入不是IP地址,是主机名 else { //通过主机名得到主机信息 hp = gethostbyname(host); //没有得到主机信息 if (hp == NULL) return -1; //将IP地址复制给ad的sin_addr属性 memcpy(&ad.sin_addr, hp->h_addr, hp->h_length); } /* 将端口号从主机字节顺序变成网络字节顺序 就是整数在地址空间存储方式变为高字节存放在内存低字节处 网络字节顺序是TCP/IP中规定好的一种数据表示格式,与CPU和操作系统无关 从而可以保证数据在不同主机之间传输时能够被正确解释 网络字节顺序采用大尾顺序:高字节存储在内存低字节处 */ ad.sin_port = htons(clientPort); /* AF_INET: IPV4网络协议 SOCK_STRAM: 提供面向连接的稳定数据传输,即TCP协议 */ //创建一个采用IPV4和TCP的socket sock = socket(AF_INET, SOCK_STREAM, 0); //创建socket失败 if (sock < 0) return sock; //建立连接 连接失败返回-1 if (connect(sock, (struct sockaddr *)&ad, sizeof(ad)) < 0) return -1; //创建成功 返回socket return sock; }
webbench.c
#include "socket.c" #include <unistd.h> #include<stdio.h> #include <sys/param.h> #include <rpc/types.h> #include <getopt.h> #include <strings.h> #include <time.h> #include <signal.h> #include<string.h> #include<error.h> //用法和各参数的详细意义 static void usage(void) { fprintf(stderr, "webbench [parameter]... URL " " -f|--force No waiting for server response " " -r|--reload Re-request loading (no caching) " " -t|--time <sec> Set run time in seconds, default 30 seconds " " -p|--proxy <server:port> Setting the number of proxy servers " " -c|--clients <n> How many clients are created, default is 1 " " -9|--http09 Using HTTP 0.9 protocol " " -1|--http10 Using HTTP 1.0 protocol " " -2|--http11 Using HTTP 1.1 protocol " " -G|--get Using GET request method " " -H|--head Using HEAD request method " " -O|--options Using OPTIONS request method " " -?|-h|--help Display help information " " -V|--version Display program version information " ); }; //支持的http请求方法 #define METHOD_GET 0 #define METHOD_HEAD 1 #define METHOD_OPTIONS 2 #define METHOD_TRACE 3 //默认参数设置,一般需要自己传入命令行参数设置 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 //支持的http版本号 int http10=1; /* 0表示http0.9 1表示http1.0 2表示http1.1 */ /* 内部 */ int mypipe[2]; //管道用于父子进程通信 char host[MAXHOSTNAMELEN]; //存储服务器网络地址 #define REQUEST_SIZE 2048 //最大请求次数 char request[REQUEST_SIZE]; //存放http请求报文信息数组 //判断测试时长是否已经到达设定时间 volatile int timeout=0; /* volatile: 类型修饰符,作为指令关键字, 确保本指令不会因为编译器优化而省略 且每次要求重新读值, 编译器在用到这个变量的时候都必须小心的重新读取这个变量的值, 而不是使用保存在寄存器里的备份,保证每次读到的都是最新的 */ //测试结果 int speed=0; //成功得到服务器响应的子进程数量 int failed=0; //没有成功得到服务器响应的子进程数量 int bytes=0; //所有子进程读取到服务器回复的总字节数 int connect_failed=0; int send_failed=0; int wclose_failed=0; int read_failed=0; int sclose_failed=0; //程序版本号 #define PROGRAM_VERSION "1.5" /* 函数声明 */ //子进程真正相服务器发出请求报文并以其得到此期间的相关数据 static void benchcore(const char* host,const int port, const char *req); //父进程创建子进程,读取子进程测试得到的数据,然后统计处理 static int bench(void); //构造http请求报文 static void build_request(const char *url); //闹钟信号处理函数 static void alarm_handler(int signal) { //到达设定的测压时间,则调用闹钟信号处理函数 timeout=1;//timerexpired为1则会在循环中跳出测试 } //构造长选项和短选项的对应 static const struct option long_options[]= { {"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}, {"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[]) { //argc表示参数个数 //argv[0]表示自身运行的路径和程序名 //argv[1]指向第1个参数 //argv[n]指向第n个参数 int opt=0; int options_index=0; char *tmp=NULL; //进行命令行参数的处理 //1.命令行没有输入参数 if(argc==1) { usage();//显示提示信息 return 2; } //命令行有输入参数则一个个解析 //"frt:p:c:?V912"中一个字符后面加一个冒号代表该命令后面接一个参数 //比如t,p,c命令,后面都要接一个参数 //连续两个冒号则表示参数可有可无 while((opt=getopt_long(argc,argv,"frt:p:c:?V912GHO",long_options,&options_index))!=EOF ) { switch(opt) { case 'f': force=1;//不等待服务器响应 printf("No waiting for server response "); break; case 'r'://重新请求加载(无缓存) force_reload=1; printf("Re-request loading (no caching) "); break; case '9'://使用http/0.9协议来构造请求 http10=0; printf("Using HTTP/0.9 "); break; case '1': http10=1;//使用http/1.0协议来构造请求 printf("Using HTTP/1.0 "); break; case '2': http10=2;//使用http/1.1协议来构造请求 printf("Using HTTP/1.1 "); break; case 'V': printf(PROGRAM_VERSION" ");//显示程序版本信息 exit(0); case 't'://设置运行时间,单位:秒,默认为30秒 benchtime=atoi(optarg);//optarg指向选项后的参数 printf("benchtime=%d ",benchtime); break; case 'c'://创建多少个客户端,默认为1个 clients=atoi(optarg);//同上 printf("clients=%d ",clients); break; case 'p'://使用代理服务器,则设置其代理网络号和端口号,格式:-p server:port //server:port是一个参数,下面把这个字符串解析成服务器地址和端口两个参数 tmp=strrchr(optarg,':');//在optagr中找到':'最后出现的位置 proxyhost=optarg; if(tmp==NULL)//没有端口号 { break; } if(tmp==optarg)//端口号在optarg最开头,说明缺失主机地址 { fprintf(stderr,"Option parameter error,Proxy server %s: Missing host name ",optarg); return 2; } if(tmp==optarg+strlen(optarg)-1)//':'在最末尾,说明缺失端口号 { fprintf(stderr,"Option parameter error,Proxy server %s: Missing port number ",optarg); return 2; } *tmp='