Webbench實現與詳解

Webbench是一款輕量級的網站測試工具,最多可以對網站模擬3w左右的併發請求,可以控制時間、是否使用緩存、是否等待服務器回覆等等,且對中小型網站有明顯的效果,基本上可以測出中小型網站的承受能力,對於大型的網站,如百度、淘寶這些巨型網站沒有意義,因爲其承受能力非常大。同時測試結果也受自身網速、以及自身主機的性能與內存的限制,性能好、內存大的主機可以模擬的併發就明顯要多。

Webbench用C語言編寫,運行於linux平臺,下載源碼後直接編譯即可使用,非常迅速快捷,對於中小型網站的製作者,在上線前用webbench進行系列併發測試不失爲一個好的測試方法。

使用方法也是非常的簡單,例如對百度進行測試。

Webbench實現的核心原理是:父進程fork若干個子進程,每個子進程在用戶要求時間或默認的時間內對目標web循環發出實際訪問請求,父子進程通過管道進行通信,子進程通過管道寫端向父進程傳遞在若干次請求訪問完畢後記錄到的總信息,父進程通過管道讀端讀取子進程發來的相關信息,子進程再時間到後結束,父進程在所有子進程退出後統計並給用戶顯示最後的測試結果,然後退出。

源代碼主要有三個源文件:Socket.c\Socket.h\webbench.c

其中Socket.c與Socket.h封裝了對於目標網站的TCP套接字的構造,其中Socket函數用於獲取連接目標網站TCP套接字,這一部分後面就不做說明了,重點講解webbench.c,其中有些對於各種錯誤的處理就不在此加以說明了,詳見末尾的源碼。

詳細實現過程


命令行參數解析

該模塊由build_request函數實現,該函數流程如下

這個模塊需要進行大量的錯誤分析,分析用戶有可能出現的錯誤填寫,並及時終止,如果在這一步不做好這件事,後面會出現幾乎找不出的錯誤。然後就是在填寫報文時,一定要嚴格的注意格式,不要隨意少空格、回車換行,保證報文填寫的正確。

壓力測試

這一模塊由bench函數來完成

這一過程中關鍵是恰當的利用管道在父子進程間通信,其中函數benchcore函數時每個子進程要求時間內發送請求報文的函數,該函數會記錄請求的成功次數、失敗次數、以及服務器回覆的字數。

過程分析得再清楚,不如代碼清除來得好:

#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 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)) != NULL) {
        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 convered 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: 一款輕巧的網站測壓工具");
    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();
        }
    }
    //定位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, proxyhost);

    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, proxyhost, 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++;
    }
}

源碼

git clone https://github.com/l-f-h/webbench-1.5.git

make && make clean

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章