webbench 压力测试软件

一.webbench简单介绍

Webench是一款轻量级的网站测压工具,最多可以对网站模拟3w左右的并发请求,可以控制时间、是否使用缓存、是否等待服务器回复等等,且对中小型网站有明显的效果,基本上可以测出中小型网站的承受能力,对于大型的网站,如百度、淘宝这些巨型网站没有意义,因为其承受能力非常大。同时测试结果也受自身网速、以及自身主机的性能与内存的限制,性能好、内存大的主机可以模拟的并发就明显要多。

Webbench能测试处在相同硬件上,不同服务的性能以及不同硬件上同一个服务的运行状况。webbench的标准测试可以向我们展示服务器的两项内容:每秒钟相应请求数和每秒钟传输数据量。webbench不但能具有便准静态页面的测试能力,还能对动态页面(ASP,PHP,JAVA,CGI)进 行测试的能力。还有就是他支持对含有SSL的安全网站例如电子商务网站进行静态或动态的性能测试。

Webbench最多可以模拟3万个并发连接去测试网站的负载能力。

二.Webbench实现的核心原理

Webbench实现的核心原理是:父进程fork若干个子进程,每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求,父子进程通过管道进行通信,子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息,父进程通过管道读端读取子进程发来的相关信息,子进程在时间到后结束,父进程在所有子进程退出后统计并给用户显示最后的测试结果,然后退出。

三.安装

1.软件下载&安装:

wget wget http://www.ha97.com/code/webbench-1.5.tar.gz

tar xf  webbench-1.5.tar.gz

yum install gcc*  ctags* -y

make && make install 

2.简单使用:

webbench http://192.168.15.112:8006/             默认一个客户端,执行30S

回显

3.webbench 参数介绍:

-c表示并发数

-t表示运行测试URL的时间(秒)

命令:webbench -c 300 -t 60   http://192.168.15.112:8006/ 

4.总结:

1)webbench 做压力测试时,该软件自身也会消耗CPU和内存资源,为了测试准确,请将 webbench 安装在别的服务器上。

2)压力测试工作应该放到产品上线之前,而不是上线以后

3)测试时并发应当由小逐渐加大,比如并发100时观察一下网站负载是多少、打开页面是否流畅,并发200时又是多少、网站打开缓慢时并发是多少、网站打不开时并发又是多少;

4)更详细的进行某个页面测试,如电商网站可以着重测试购物车、推广页面等,因为这些页面占整个网站访问量比重较大

5.Webbench 是一个可怕的东西,为什么说可怕呢?

1)首先,它仅属于发送 GET 命令、所以无论是 CDN 还是自身防火墙都不会将它当做是 DDOS 或 CC 看待,也就意味着不会起到防御的功能。这样的压力测试工具还包括有 Apache Bench 等比较常见(只要有 SSH 的网站主机、几乎都可以使用)

2)解决办法未必管用,即时你拒绝该测试发起着 IP 的访问、顶多只是起到了缓解的效果。因为你的错误页足够让它成千上万的测试数量吃饱喝足。

3)使用过于简单,对于繁杂且使用人数越来越少的 TFN2K 之类较为专业的工具、Webbench 只要是一个有 VPS 的小白即可使用,带宽越大、性能越好的 VPS 可以起到越大的“攻击”效应

防护方法:

1)防火墙添加规则:iptables -I INPUT -s webbench网段地址/24 -j DROP

2)虚拟主机访问控制:

A、在虚拟主机的开放目录下新建 .htaccess 文件、录入:

Order allow,deny

allow from all

deny from  webbench

四.源码分析:

源代码主要有三个源文件:Socket.c\Socket.h\webbench.c

其中Sokcet.c与Socket.h封装了对于目标网站的TCP套接字的构造,其中Socket函数用于获取连接目标网站TCP套接字,重点详细研究webbench.c,其中有些对于各种错误的处理。

详细代码:

#include"Socket.h"

#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 [选项参数]... URL\n"     

    "  -f|--force              不等待服务器响应\n" 

    "  -r|--reload              重新请求加载(无缓存)\n" 

    "  -t|--time <sec>          运行时间,单位:秒,默认为30秒\n" 

    "  -p|--proxy <server:port> 使用代理服务器发送请求\n" 

    "  -c|--clients <n>        创建多少个客户端,默认为1个\n" 

    "  -9|--http09              使用http0.9协议来构造请求\n" 

    "  -1|--http10              使用http1.0协议来构造请求\n" 

    "  -2|--http11              使用http1.1协议来构造请求\n" 

    "  --get                    使用GET请求方法\n" 

    "  --head                  使用HEAD请求方法\n" 

    "  --options                使用OPTIONS请求方法\n" 

    "  --trace                  使用TRACE请求方法\n" 

    "  -?|-h|--help            显示帮助信息\n" 

    "  -V|--version            显示版本信息\n"  ); 

}; 

//http请求方法

#define METHOD_GET 0

#define METHOD_HEAD 1

#define METHOD_OPTIONS 2

#define METHOD_TRACE 3

//相关参数选项的默认值

int method = METHOD_GET;

int clients = 1;

int force = 0;          //默认需要等待服务器相应

int force_reload = 0;    //默认不重新发送请求

int proxyport = 80;    //默认访问80端口,http国际惯例

char* proxyhost = NULL; //默认无代理服务器,因此初值为空

int benchtime = 30;    //默认模拟请求时间

//所用协议版本

int http = 1;  //0:http0.9 1:http1.0 2:http1.1

//用于父子进程通信的管道

int mypipe[2];

//存放目标服务器的网络地址

char host[MAXHOSTNAMELEN];

//存放请求报文的字节流

#define REQUEST_SIZE 2048

char request[REQUEST_SIZE];

//构造长选项与短选项的对应

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},

    {"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 speed = 0;

int failed = 0;

long long bytes = 0;

int timeout = 0;

void build_request(const char* url);

static int bench();

static void alarm_handler(int signal);

void benchcore(const char* host,const int port,const char* req);

int main(int argc,char* argv[])

{

    int opt = 0;

    int options_index = 0;

    char* tmp = NULL;

    //首先进行命令行参数的处理

    //1.没有输入选项

    if(argc == 1)

    {

        usage();

        return 1;

    }

    //2.有输入选项则一个一个解析

    while((opt = getopt_long(argc,argv,"frt:p:c:?V912",long_options,&options_index)) != EOF)

    {

        switch(opt)

        {

            case 'f':

                force = 1;

                break;

            case 'r':

                force_reload = 1;

                break;

            case '9':

                http = 0;

                break;

            case '1':

                http = 1;

                break;

            case '2':

                http = 2;

                break;

            case 'V':

                printf("WebBench 1.5 covered by fh\n");

                exit(0);


            case 't':

                benchtime = atoi(optarg);  //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)  //端口号在optarg最开头,说明缺失主机名

                {

                    fprintf(stderr,"选项参数错误,代理服务器 %s:缺失主机名",optarg);

                    return 2;

                }

                if(tmp == optarg + strlen(optarg)-1)    //':'在optarg末位,说明缺少端口号

                {

                    fprintf(stderr,"选项参数错我,代理服务器 %s 缺少端口号",optarg);

                    return 2;

                }

                *tmp = '\0';      //将optarg从':'开始截断

                proxyport = atoi(tmp+1);    //把代理服务器端口号设置好

                break;


            case '?':

                usage();

                exit(0);

                break;

            default:

                usage();

                return 2;

                break;

        }

    }

    //选项参数解析完毕后,刚好是读到URL,此时argv[optind]指向URL


    if(optind == argc)    //这样说明没有输入URL,不明白的话自己写一条命令行看看

    {

        fprintf(stderr,"缺少URL参数\n");

        usage();

        return 2;

    }


    if(benchtime == 0)

        benchtime = 30;

    fprintf(stderr,"webbench: 一款轻巧的网站测压工具 1.5 covered by fh\nGPL Open Source Software\n");

    //OK,我们解析完命令行后,首先先构造http请求报文

    build_request(argv[optind]);    //参数当然是URL

    //请求报文构造好了

    //开始测压

    printf("\n测试中:\n");

    switch(method)

    {

        case METHOD_OPTIONS:

            printf("OPTIONS");

            break;


        case METHOD_HEAD:

            printf("HEAD");

            break;

        case METHOD_TRACE:

            printf("TRACE");

            break;


        case METHOD_GET:


        default:

            printf("GET");

            break;

    }

    printf(" %s",argv[optind]);

    switch(http)

    {

        case 0:

            printf("(使用 HTTP/0.9)");

            break;

        case 1:

            printf("(使用 HTTP/1.0)");

            break;

        case 2:

            printf("(使用 HTTP/1.1)");

            break;

    }

    printf("\n");

    printf("%d 个客户端",clients);

    printf(",%d s",benchtime);

    if(force)

        printf(",选择提前关闭连接");

    if(proxyhost != NULL)

        printf(",经由代理服务器 %s:%d  ",proxyhost,proxyport);

    if(force_reload)

        printf(",选择无缓存");


    printf("\n");  //换行不能少!库函数是默认行缓冲,子进程会复制整个缓冲区,

                    //若不换行刷新缓冲区,子进程会把缓冲区的的也打出来!

                    //而换行后缓冲区就刷新了,子进程的标准库函数的那块缓冲区就不会有前面的这些了

    //真正开始压力测试

    return bench();

}

void build_request(const char* url)

{

    char tmp[10];

    int i = 0;

    bzero(host,MAXHOSTNAMELEN);

    bzero(request,REQUEST_SIZE);

    //缓存和代理都是http1.0后才有的

    //无缓存和代理都要在http1.0以上才能使用

    //因此这里要处理一下,不然可能会出问题

    if(force_reload && proxyhost != NULL && http < 1)

        http = 1;

    //HEAD请求是http1.0后才有

    if(method == METHOD_HEAD && http < 1)

        http = 1;

    //OPTIONS和TRACE都是http1.1才有

    if(method == METHOD_OPTIONS && http < 2)

        http = 2;

    if(method == METHOD_TRACE && http < 2)

        http = 2;

    //开始填写http请求

    //请求行

    //填写请求方法

    switch(method)

    {

    case METHOD_HEAD:

            strcpy(request,"HEAD");

            break;

        case METHOD_OPTIONS:

            strcpy(request,"OPTIONS");

            break;

        case METHOD_TRACE:

            strcpy(request,"TRACE");

            break;

        default:

        case METHOD_GET:

            strcpy(request,"GET");

    }

    strcat(request," ");

    //判断URL的合法性

    //1.URL中没有"://"

    if(strstr(url,"://") == NULL)

    {

        fprintf(stderr,"\n%s:是一个不合法的URL\n",url);

        exit(2);

    }

    //2.URL过长

    if(strlen(url) > 1500)

    {

        fprintf(stderr,"URL 长度过过长\n");

        exit(2);

    }


    //3.没有代理服务器却填写错误

    if(proxyhost == NULL)  //若无代理

    {

        if(strncasecmp("http://",url,7) != 0)  //忽略大小写比较前7位

        {

            fprintf(stderr,"\nurl无法解析,是否需要但没有选择使用代理服务器的选项?\n");

            usage();

            exit(2);

        }

    }

    //定位url中主机名开始的位置

    //比如  http://www.xxx.com/

    i = strstr(url,"://") - url + 3;

    //4.在主机名开始的位置找是否有'/',若没有则非法

    if(strchr(url + i,'/') == NULL)

    {

        fprintf(stderr,"\nURL非法:主机名没有以'/'结尾\n");

        exit(2);

    }

    //判断完URL合法性后继续填写URL到请求行


    //无代理时

    if(proxyhost == NULL)

    {

        //有端口号时,填写端口号

        if(index(url+i,':') != NULL && index(url,':') < index(url,'/'))

        {

            //设置域名或IP

            strncpy(host,url+i,strchr(url+i,':') - url - i);

            bzero(tmp,10);

            strncpy(tmp,index(url+i,':')+1,strchr(url+i,'/') - index(url+i,':')-1);

            //设置端口号

            proxyport = atoi(tmp);

            //避免写了':'却没写端口号

            if(proxyport == 0)

                proxyport = 80;

        }

        else    //无端口号

        {

            strncpy(host,url+i,strcspn(url+i,"/"));  //找到url+i到第一个”/"之间的字符个数

        }

    }

    else    //有代理服务器就简单了,直接填就行,不用自己处理

    {

        strcat(request,url);

    }

    //填写http协议版本到请求行

    if(http == 1)

        strcat(request," HTTP/1.0");

    if(http == 2)

        strcat(request," HTTP/1.1");

    strcat(request,"\r\n");

    //请求报头


    if(http > 0)

        strcat(request,"User-Agent:WebBench 1.5\r\n");


    //填写域名或IP

    if(proxyhost == NULL && http > 0)

    {

        strcat(request,"Host: ");

        strcat(request,host);

        strcat(request,"\r\n");

    }

    //若选择强制重新加载,则填写无缓存

    if(force_reload && proxyhost != NULL)

    {

        strcat(request,"Pragma: no-cache\r\n");

    }

    //我们目的是构造请求给网站,不需要传输任何内容,当然不必用长连接

    //否则太多的连接维护会造成太大的消耗,大大降低可构造的请求数与客户端数

    //http1.1后是默认keep-alive的

    if(http > 1)

        strcat(request,"Connection: close\r\n");

    //填入空行后就构造完成了

    if(http > 0)

        strcat(request,"\r\n");

}

//父进程的作用:创建子进程,读子进程测试到的数据,然后处理

static int bench()

{

    int i = 0,j = 0;

    long long k = 0;

    pid_t pid = 0;

    FILE* f = NULL;

    //尝试建立连接一次

    i = Socket(proxyhost == NULL?host:proxyhost,proxyport);

    if(i < 0)

    {

        fprintf(stderr,"\n连接服务器失败,中断测试\n");

        return 3;

    }

    close(i);//尝试连接成功了,关闭该连接

    //建立父子进程通信的管道

    if(pipe(mypipe))

    {

        perror("通信管道建立失败");

        return 3;

    }

    //让子进程去测试,建立多少个子进程进行连接由参数clients决定

    for(i = 0;i < clients;i++)

    {

        pid = fork();

        if(pid <= 0)

        {

            sleep(1);

            break;  //失败或者子进程都结束循环,否则该子进程可能继续fork了,显然不可以

        }

    }

    //处理fork失败的情况

    if(pid < 0)

    {

        fprintf(stderr,"第 %d 子进程创建失败",i);

        perror("创建子进程失败");

        return 3;

    }

    //子进程执行流

    if(pid == 0)

    {

        //由子进程来发出请求报文

        benchcore(proxyhost == NULL?host : proxyhost,proxyport,request);

        //子进程获得管道写端的文件指针

        f = fdopen(mypipe[1],"w");

        if(f == NULL)

        {

            perror("管道写端打开失败");

            return 3;

        }

        //向管道中写入该子进程在一定时间内请求成功的次数

        //失败的次数

        //读取到的服务器回复的总字节数


        fprintf(f,"%d %d %lld\n",speed,failed,bytes);

        fclose(f);  //关闭写端

        return 0;

    }

    else

    {

        //子进程获得管道读端的文件指针

        f = fdopen(mypipe[0],"r");

        if(f == NULL)

        {

            perror("管道读端打开失败");

            return 3;

        }

        //fopen标准IO函数是自带缓冲区的,

        //我们的输入数据非常短,并且要求数据要及时,

        //因此没有缓冲是最合适的

        //我们不需要缓冲区

        //因此把缓冲类型设置为_IONBF

        setvbuf(f,NULL,_IONBF,0);


        speed = 0;  //连接成功的总次数,后面除以时间可以得到速度

        failed = 0; //失败的请求数

        bytes = 0;  //服务器回复的总字节数

        //唯一的父进程不停的读

        while(1)

        {

            pid = fscanf(f,"%d %d %lld",&i,&j,&k);//得到成功读入的参数个数

            if(pid < 3)

            {

                fprintf(stderr,"某个子进程死亡\n");

                break;

            }

            speed += i;

            failed += j;

            bytes += k;

            //我们创建了clients,正常情况下要读clients次

            if(--clients == 0)

                break;

        }

        fclose(f);

        //统计处理结果

        printf("\n速度:%d pages/min,%d bytes/s.\n请求:%d 成功,%d 失败\n",\

                (int)((speed+failed)/(benchtime/60.0f)),\

                (int)(bytes/(float)benchtime),\

                speed,failed);

    }

    return i;

}

//闹钟信号处理函数

static void alarm_handler(int signal)

{

    timeout = 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)

    {

        //只有在收到闹钟信号后会使 time = 1

        //即该子进程的工作结束了

        if(timeout)

        {

            if(failed > 0)

            {

                failed--;

            }

            return;

        }

        //建立到目标网站服务器的tcp连接,发送http请求

        s = Socket(host,port);

        if(s < 0)

        {

            failed++;  //连接失败

            continue;

        }

        //发送请求报文

        if(rlen != write(s,req,rlen))

        {

            failed++;

            close(s);  //写失败了也不能忘了关闭套接字

            continue;

        }

        //http0.9的特殊处理

        //因为http0.9是在服务器回复后自动断开连接的,不keep-alive

        //在此可以提前先彻底关闭套接字的写的一半,如果失败了那么肯定是个不正常的状态,

        //如果关闭成功则继续往后,因为可能还有需要接收服务器的恢复内容

        //但是写这一半是一定可以关闭了,作为客户端进程上不需要再写了

        //因此我们主动破坏套接字的写端,但是这不是关闭套接字,关闭还是得close

        //事实上,关闭写端后,服务器没写完的数据也不会再写了,这个就不考虑了

        if(http == 0)

        {

            if(shutdown(s,1))

            {

                failed++;

                close(s);

                continue;

            }

        }

        //-f没有设置时默认等待服务器的回复

        if(force == 0)

        {

            while(1)

            {

                if(timeout)

                    break;

                i = read(s,buf,1500);  //读服务器发回的数据到buf中

                if(i < 0)

                {

                    failed++;  //读失败

                    close(s);  //失败后一定要关闭套接字,不然失败个数多时会严重浪费资源

                    goto nexttry;  //这次失败了那么继续下一次连接,与发出请求

                }

                else

                {

                    if(i == 0)  //读完了

                        break;

                    else

                        bytes += i; //统计服务器回复的字节数

                }

            }

        }

        if(close(s))

        {

            failed++;

            continue;

        }

        speed++;

    }

}

推荐阅读更多精彩内容