IE盒子

搜索
查看: 127|回复: 0

C语言开源项目——Webbench源码剖析

[复制链接]

3

主题

10

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2023-1-15 16:26:03 | 显示全部楼层 |阅读模式
前言

考研结束的假期闲来无事,想着把之前学的C++捡起来,于是决定找个小项目练练手,在各种项目推荐帖里面发现了Webbench这个小工具,于是便研究了一下源代码。Webbench是一个简单的网站测压工具,由C语言编写,使用了多进程技术来测试网站并发压力。笔者在学习完源代码后,利用C++对Webbench进行复现,使用线程技术来替换多进程。
本文第一章对Webbench用到的一些前置知识作出补充,第二章介绍Webbench源代码的整体框架,后续几章分别介绍了框架中各个组件的功能。
需要了解的前置知识


  • 头文件<strings.h>的使用——处理字符数组
  • 头文件<getopt.h>的使用——获取命令行中用户输入的参数
  • UNIX网络编程——socket的创建、使用
  • 多进程编程——利用fork函数
源代码框架

Webbench可以分为三个部分

  • 获取用户输入的参数
  • 解析用户所给参数中的URL并根据用户选择的测试方案组装HTTP请求报文
  • 利用fork函数模拟多个Client向URL地址发送报文
  • 利用管道进行进程间通信,计算出测试方案的各项数据
报文组装

报文组装有如下步骤:
检查用户输入的URL格式


  • 是否包含"://" ?
  • URL是否过长?——大于1500个字符
  • URL是否过短?——小于8个字符
  • URL前缀是否为"HTTP://"?
  • 结尾是否带有"/"?
  • 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,"GET");break;
        case METHOD_HEAD: strcpy(request,"HEAD");break;
        case METHOD_OPTIONS: strcpy(request,"OPTIONS");break;
        case METHOD_TRACE: strcpy(request,"TRACE");break;
    }

    /* wzl:以下是对url格式检查
    strcat将两个char类型相连*/
    strcat(request," ");

    if(NULL==strstr(url,"://"))// wlz: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);
    }
    // wlz:strncasecmp对比str1和str2前几个字符,忽略大小写
    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;//wlz: 找到://的定位

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

    if(proxyhost==NULL)///wlz: 如果没有走代理服务器
    {
        /* get port from hostname */
                                /* wlz:
                                // index查找字符串首次出现的位置
                                // 针对有端口号的情况*/
        if(index(url+i,':')!=NULL && index(url+i,':')<index(url+i,'/'))
        {
        /*wlz
        // strncpy 将src所指向的字符串中以src地址开始的前n个字节复制到dest所指的数组中.
        // strchr 查找一个串中给定字符的第一个匹配处*/
            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;// wlz:默认80端口,http端口
        }
        // wlz:针对没有端口号的情况
        else
        {
        // wlz:strcspn 返回从开头开始的连续子串,并且该子串不带'/'
            strncpy(host,url+i,strcspn(url+i,"/"));
        }
        // printf("Host=%s\n",host);
        strcat(request+strlen(request),url+i+strcspn(url+i,"/"));// wlz:将host地址插入请求中
    }
    else// wlz:如果需要走代理服务器
    {
        // 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");// wlz:对长链接采取close
   
    /* add empty line at end */
    if(http10>0) strcat(request,"\r\n");
   
    printf("\nRequest:\n%s\n",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并尝试发送报文
  • 如果用户指定'-f',则进程计数++,并且关闭socket。(指定--force/-f 意味着不强制收到服务器返回的消息)
  • 如果用户未指定'-f',则等待服务器返回的消息,将收到的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,"\nConnect to server failed. Aborting benchmark.\n");
        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("pipe failed.");
        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,"problems forking worker no. %d\n",i);
        perror("fork failed.");
        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],"w");
        if(f==NULL)
        {
            perror("open pipe for writing failed.");
            return 3;
        }
        /* fprintf(stderr,"Child - %d %d\n",speed,failed); */
        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;
        }
        
        setvbuf(f,NULL,_IONBF,0);
        
        speed=0;
        failed=0;
        bytes=0;
   
        while(1)
        {
            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;
        
            /* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */
            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;
}




/// 在子进程中调用的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,"Correcting failed by signal\n"); */
                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,"%d\n",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
后记

把五个月以前自己写的代码拿出来看简直是人间折磨。。。(复习密码学也是人间折磨,可能更痛苦)
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表