致敬科比,实现查询科比每赛季数据的Web服务器

前言:我从2007-2008赛季,加索尔来湖人之后,湖人三进总决赛的第一个赛季开始喜欢科比,那时候我五年级,现在十年过去了,我大三了,科比已经退役两年了。
我目前的技能足以支持我实现一个简单的关于科比的Web服务器,这是我两前,2016-4-14日(科比最后一场比赛)结束以后,就想完成的事情。

使用技术
网络编程
多线程
cgi机制
shell脚本

开发环境
centos 6.5
vim/gcc/gdb
c

项目描述
采用C/S(客户端/服务器)模型,实现支持中小型应用的http。
(.使用http/1.0版本)

http分层解剖
这里写图片描述

与http相关的一些协议
我在之前的博客都有总结,附上链接:
TCP:https://blog.csdn.net/han8040laixin/article/details/81300018
IP:https://blog.csdn.net/han8040laixin/article/details/81354588
DNS:https://blog.csdn.net/han8040laixin/article/details/81389509

介绍http协议

特点
客户端/服务器模式;
简单快速,因为http服务器规模小,通信速度快;
灵活,可传输任意类型数据,正在传输的类型由Content-Type标记;
无连接,基于请求响应(客户端发起http请求,服务器响应http请求,连接断开);
无状态,不会保留之前的一切请求或响应;

了解URI,URL的区别
URI:用来唯一的标识一个资源。
URL:是一种具体的URI,不仅唯一的标识了一个资源,还指明了如何定位该资源。
总的来说,如果可以把一个资源唯一的标识出来,就可以说该标识是URI,如果这个标识还可以定位该资源,那么它也可以是一个URL。

URL格式
http://host[“:”port][url]
若没有给url,则浏览器自动以”/”的形式输出
这里写图片描述

http请求与响应格式

这里写图片描述

请求
请求行:包括方法,URL和版本,用空格作间隔,以换行符分隔请求报头。
请求报头:属性采用name: vlaue形式,(注意value前面有空格),每个属性以换行符分隔;遇到空行就表示走完了头部,所以空行有效的划分了头部与数据。
请求正文:可以为空,如果正文存在,则在报头里有一个Content-Length字段来标识正文的大小。
响应
状态行:包括版本,状态码和状态码描述,以换行符结束。
响应报头:格式与请求报头相同。
响应正文:可以为空,如果正文存在,则在报头里有一个Content-Length字段来标识正文的大小。

http请求方法
GET:获取被URL标识的资源,通过URL传参。
POST:传输实体主体,通过正文传参。
PUT:传输文件,会有安全问题,大多web不会使用。
DELETE:和PUT相反,删除资源,一样不安全。
HEAD:获取报文首部,和GET类似只是不要正文。
OPTIONS:询问服务器支持的方法。
……
我在该项目中只实现了GET和POST方法,关于GET和POST,代码中会细细说明。

http状态码
状态码分5类,分别为1开头,2开头,3开头,4开头,5开头;
这里写图片描述

总结常见状态码
200:客户端的请求被正确处理
204:请求被正确处理,但是请求正文为空
206:表示客户单对服务器进行了范围请求,服务器成功处理,响应报头中有一个属性Content-Range指明范围

301:永久性重定向,表示客户端请求的资源已经分配了新的URI
302:临时性重定向,表示客户端请求的资源已被分配了新的URI,让用户本次使用新的URI访问
307:也是临时性重定向


301和302的区别,举个例子:
一个饭店,换地方开了,那就是301;
如果因为一些原因,暂时搬到了别的地方,过段时间又回来继续开,就是302.
________________________________________________________4
303:也是临时性重定向,但明确应使用GET方法定向获取请求的资源;
当301,302,303作为状态码返回,几乎所有浏览器都会把POST改成GET方法,即使301,302禁止将POST改成GET方法
307:也是临时性重定向,但是307会按规矩办事,不会把POST变成GET

400:表示请求报文中存在语法错误,需修改之后再请求
403:表示客户端请求的资源被服务器拒绝
404:not found,表示服务器上没有你要请求的资源

500:表示服务器在响应时发生了错误
503:表示服务器处于超负载或正在维护,无法处理请求

难点:HTTP CGI机制
cgi机制是外部应用程序(cgi程序)与服务器之间的桥梁。
因为客户端不仅要在服务器上拿资源,还要往服务器里上传一些东西(比如说注册),为了让服务器实现交互式,所以有了cgi技术。
注意cgi机制和cgi程序是两回事。
真正解释清楚cgi,需要知道GET方法和POST方法的区别
GET方法如果不传参,就不是cgi,返回资源就会,如果传参,就是cgi,参数在url里
POST方法一定是cgi,参数在正文,也就是说POST方法一定有参数

图解我的服务器
这里写图片描述
如图,蓝线表示普通http请求响应流,红线表示cgi模式下http请求响应流。

普通模式很好理解,由于我只支持GET和POST,所以普通模式一定是GET方法且GET方法不带参数,所以只要根据格式构建一个响应就行;
cgi模式有点棘手,首先cgi模式一定是POST方法或者GET方法带参,然后需要提参:GET方法直接从url里拿到参数,而POST方法只能通过请求报头中的Content-Length字段拿到正文长度;
由于要执行可执行程序,所以用到了exec进程替换的技术,由于我的服务器跑的是多线程,所以直接替换就知道把我的服务器替换了,所以得fork子进程,让子进程替换;
现在我需要把父进程的参数传给子进程,子进程跑完之后再把结果返回给父进程。
首先来看父进程给子进程传参,如果是GET,我可以把参数设为环境变量,是全局的,那么就实现了通信,然后如果是POST,我则需要一个匿名管道把正文传过去;
再看子进程把结果返给父进程,直接用匿名管道传就好了。
父进程拿到了执行结果,就可以构建响应返回给客户端了。

服务器源码

mian函数讲解:
我想把端口号拿到命令行参数,IP地址用INADDR_ANY,所以先判断一下命令行参数个数是否为2,不是的话打印使用手册,并退出;
然后通过StartUp函数来创建监听套接字;
之后开始三次握手建立连接,然后创建一个线程来处理这个请求,最后把线程分离,因为如果让主线程如果等待的话,会使服务器阻塞。

int main(int argc,char* argv[])
{
    if(argc != 2){
        //printf("./server [port]\n");
        usage(argv[0]);
        return 1;
    }

    int listen_sock = StartUp(argv[1]);
    while(1){
        struct sockaddr_in client;
        socklen_t len = sizeof(len);

        int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
        if(new_sock < 0){
            perror("accept");
            continue;
        }
        printf("get a new client!\n");

        pthread_t tid;
        pthread_create(&tid,NULL,handler_request,(void*)new_sock);
        pthread_detach(tid);//pthread_join是阻塞的等待,所以分离
    }

    return 0;
}

使用手册讲解:通过命令行第一个参数(比如./server)+ 端口号,告之使用方法

static void usage(const char* proc){
    printf("Usage:\n%s [port]\n",proc);
}

创建监听套接字函数讲解:这个没什么好说的,先socket,再bind,再listen

int StartUp(const char* port){
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0){
        perror("socket");
        return 2;
    }

    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htons(INADDR_ANY);
    local.sin_port = htons(atoi(port));

    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
        perror("bind");
        return 3;
    }
    if(listen(sock,5) < 0){
        perror("listen");
        return 4;
    }
    return sock;
}

处理请求函数讲解:通过参数拿到与客户端通信的sock之后,首先要拿到请求的第一行,也就是请求行,之前说过,请求行是以换行符结尾的,然后不同客户端,他们的换行符可能不同,常见的换行符有三种:\n \r \r\n,所以get_line函数的功能就是:拿到请求报文的一行,并且把换行符统一为’\n’;
所以第一步就是,通过get_line函数把请求行拿到line数组里;
现在拿到了请求行,需要拿到具体的方法,既然是以空格作分隔,那就很简单了,挨个拷贝到我的method数组里;
现在拿到了方法,通过字符串比较,如果是GET方法,先不做处理,因为不知道是否带参,如果是POST方法,先把cgi设为1,如果是其他方法,直接把退出码设为400(客户端报文语法出错),然后通过我的echo_error函数给出具体的做法;
现在用同样的方法取到url,就需要直到url里是否带参,我的做法是再创建一个quert_string指针,一直走到指着url走,如果遇见’?’就说明有参数,就把‘?’变为’\0’,分离资源(?之前)和参数(?之后),然后把cgi设为1,跳出循环,如果一直没碰见’?’就说明无参,那cgi还是0;
走到这里,已经知道了方法(method),资源(url),参数(query_string),以及是否为cgi;
然而浏览器访问页面的时候,是默认会带上’/’的,而’/’是linux的根目录,我的服务器根目录为:wwwroot这里写图片描述
所以要在url里加上wwwroot,利用sprintf函数,把wwwroot+url放在path数组里;
path有两种,举个例子,一种是wwwroot/a/b/c.html的形式,一种是wwwroot/的形式;
为了能简单一点,如果是以请求是以‘/’结尾的,我就通过exe_www函数把我写好的首页响应给客户,否则需要判断:
通过stat函数提取到文件的属性,如果文件不存在,错误码设为404(服务器没有该资源),然后通过我的echo_error函数给出具体的做法;
如果请求的是目录,我还是通过exe_www函数把写好的首页给客户端;
如果它是可执行程序,那一定是cgi,通过exe_cgi函数来完成;
如果不是目录也不是可执行程序,那就直接把资源给通过exe_www函数给客户端;

void* handler_request(void* arg)
{
    int sock = (int)arg;
    int status_code = 0;//响应状态码
    char line[MAX] = {0};//放请求的第一行
    int cgi = 0;//后续处理客户端的数据,默认没有cgi

    //应该先处理请求(第一行),要知道客户端向我怎么用什么方法,发起什么请求,请求什么资源等
    //怎么拿到一行,以换行符分隔,换行符可能为: \
    \n \r\n \r等三种,如果不统一,可能出现黏包问题
    //所以要统一,把这三种全部转化为\n,然后再获取第一行
    get_line(sock,line,sizeof(line));//把读入的第一行放在line里,统一三种换行符为\n 

    //GET /a/b/c HTTP/1.1

    char method[MAX/10];//方法
    char url[MAX];//url
    int i = 0;
    int j = 0;
    while(i < sizeof(method)-1 && j < sizeof(line) && !isspace(line[j])){
        method[i] = line[j];
        i++;
        j++;
    }
    method[i] = '\0';
    //只作GET和POST方法
    //GET方法和POST方法的区别\
            GET和POST的传参形式不同:GET通过url传参,POST通过请求正文传参

    if(strcasecmp(method,"GET") == 0){//strcasecmp忽略大小写比较,GET,Get都行
    }
    else if(strcasecmp(method,"POST") == 0){
        cgi = 1;//POST方法必须要以cgi方式运行
    }
    else{//不是GET方法也不是POST方法,就不支持
        clear_header(sock);//把头部信息清理,要把这一行数据读完才能给客户端响应
        status_code = 400;//响应状态码
        goto end;
    }

    //此时,已经提取了方法,要么是GET,要么是POST,下一步该提url了
    i = 0;
    //j先把metod走完,到url部分
    while(j < sizeof(line) && isspace(line[j])){
            j++;
    }
    while(i < sizeof(url)-1 && j < sizeof(line) && !isspace(line[j])){
        url[i] = line[j];
        i++;
        j++;
    }
    url[i] = '\0';

#ifdef DEBUG//调试信息
    printf("method: %s,url: %s\n",method,url);
    printf("%s",line);
#endif

    //已经知道了method和url以及是否为cgi
    // url: /a/b/c.html?wd=世界杯&rsv_spt=1&rsv_iqid=0xdc00b0d4000084ae&issp=1&f=8
    // 以问号分隔资源和参数,参数之间用&分隔

    char* query_string = NULL;//url指向资源,query_string指向后面的内容
    if(strcasecmp(method,"GET") == 0){//
        query_string = url;
        while(*query_string){
            if(*query_string == '?'){//有参数
                *query_string = '\0';//让url指向前面的资源
                query_string++;//query_string指向后面的参数
                cgi = 1;//
                break;
            }
            query_string++;
        }
        //如果没有参数,那cgi就还是0,url还是以'\0'结尾,不影响
    }

    //现在已经知道了method url已经被分离 
    //GET的参数放在(query_string)里,如果有参数,cgi
    //POST为cgi

    char path[MAX] = {0};//  /为linux跟目录,要把根目录变为wwwroot
    //int sprintf(char *str, const char *format, ...);
    sprintf(path,"wwwroot%s",url);//此时path里有wwwroot+url
    // path有两种风格:
    // wwwroot/ 首页   
    // wwwroot/a/b/c.html

    //判断最后一个字符是否是/,是的话给首页
    if(path[strlen(path)-1] == '/'){
        strcat(path,HOME_PAGE);
    }
        //int stat(const char *path, struct stat *buf); 提取文件属性信息,第一个参数为资源,第二个为输出型
    struct stat st;
    if(stat(path,&st) < 0){//资源不存在
        clear_header(sock);
        status_code = 404;
        goto end;
    }
    else{
        //如果访问的是目录,就把对应的界面返回给用户;
        //如果访问的是二进制可执行程序,则把跑完的结果给用户,cgi模式下跑
        if(S_ISDIR(st.st_mode)){//是目录
            strcat(path,HOME_PAGE);
        }
        else if((st.st_mode & S_IXUSR) || 
                (st.st_mode & S_IXGRP) || 
                (st.st_mode & S_IXOTH)){//是可执行文件
            cgi = 1;
        }
        else{//不是目录,不是可执行,为普通文件
            //do nothing
        }

        if(cgi){//path后面表示的是可执行程序,要传参:
                //GET:参数在query_string  POST在请求正文
            status_code = exe_cgi(sock,method,path,query_string);
        }
        else{//非cgi,一定是GET方法,且没有传参
            //第三个参数为文件的大小,通过st的size获取
            printf("path: %s\n", path);
            status_code = echo_www(sock,path,st.st_size);
        }
    }

end:
    if(status_code != 200){
        echo_error(sock,status_code);
    }
    close(sock);
}

非cgi响应函数讲解:首先注意,通过返回值把状态码传到处理函数,因为是一个文件,不管是首页还是其他文件,所以我们先把文件打开;接下来先通过clear_header函数清掉报头(其实就是读到正文),然后通过line数组逐行把资源返回给客户端,我写的是构建一行,发一行,最后文件是通过sendfile函数发送的。

int echo_www(int sock,char* path,int size)
{
    //打开这个普通文件
    int fd = open(path,O_RDONLY);
      if(fd < 0){
          return 404;  
      }


    clear_header(sock);
    //构建响应
    char line[MAX];

    //报头
    sprintf(line,"HTTP/1.0 200 OK\r\n");
    //ssize_t send(int sockfd, const void *buf, size_t len, int flags);  flags设为0,用法和write一样
    send(sock,line,strlen(line),0);

    sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");
    send(sock,line,strlen(line),0);

    sprintf(line,"\r\n");
    send(sock,line,strlen(line),0);

    //正文
    //ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
    //out_fd:为了读 in_fd:为了写
    sendfile(sock,fd,NULL,size);
    close(fd);
    return 200;
}

cgi响应函数讲解:同样通过返回值把状态码传给处理函数;
与客户端通信的sock,方法method,资源path,GET的参数query_string都作为参数拿了过来;
所以接下来先判断方法,如果是GET方法,则直接清掉报头,因为资源和参数都拿到了;
如果是POST方法,参数在正文里,而请求报头中有一个Content_Length字段描述正文长度,所以在清掉报头的同时,把Content_Length的大小拿到;
读完之后如果content-length还是-1,则没有抓到,返回400(客户端语法错了);
走到这里已经读到正文了,我们知道path为可执行程序,GET的参数在query_string,POST的参数知道了长度,长度为content-length,之后的操作就是http的cgi机制最核心的部分:
我要把可执行程序path的执行结果返回给客户端,就需要exec进程替换,因为我用的是多线程,直接替换就把我的服务器替换了,所以要fork子进程,让子进程替换;
然后进程之间是独立的,父进程要把参数传给子进程,子进程拿到参数,要把程序跑完的结果返回给父进程,所以用到了进程间通信,我的方法是,对于父进程传参数给子进程:如果是GET,就把参数用环境变量传,如果是POST,既然知道了正文大小,就通过匿名管道把请求正文传给子进程;对于子进程,直接通过匿名管道把跑完的结果传给父进程,所以我创建了两个管道,之后fork子进程,如果fork失败,返回503(服务器超负载了);
最后一个问题:进程替换之后,就把input和output替换掉了,没办法通信了,所以在exec之前,用dup2把标准输入0重定向到input,把标准输出1重定向到output,而exec不会替换fd。

int exe_cgi(int sock,char* method,char* path,char* query_string)
{
    char line[MAX];
    int content_length = -1;
    if(strcasecmp(method,"GET") == 0){//程序在path,参数在query_string,所以直接清报头
        clear_header(sock);
    }
    else{//POST,需要读到正文获取参数(空行下一个)
         //但是得要直到正文大小,不然读多读少容易黏包
         //就得从报头中的[Content-Legth]字段获取正文长度
         //一行一行读,一直读到Content-Length字段,然后读完报头
        do{
            get_line(sock,line,sizeof(line));
            if(strncmp(line,"Content-Legth: ",16) == 0){
                //Content-Legth后面一定有长度,比如Conent-Legth: 13
                //所以只用看数字之前的前16个
                content_length = atoi(line+16);//line+16到数字

            }
        }while(strcmp(line,"\n"));
        //已经到正文了
        if(content_length = -1){
            //没有抓到content-length
            return 400;
        }
    }
    //path为可执行程序,GET的参数在query_string,POST的参数知道了长度

    //现在要执行指定路径的程序,用exec替换
    //线程不能直接exec,得fork子进程来exec
    //子进程跑的结果要返给父进程,父进程的参数要给子进程,需要匿名管道来处理进程间通信
    //而管道只能单向通信,所以得两个管道

    //子进程的视角
    int input[2];//子进程读,父进程写
    int output[2];//子进程写,父进程读
    pipe(input);
    pipe(output);

    pid_t id = fork();
    if(id < 0){
        return 503;
    }
    else if(id == 0){//child
        close(sock);
        close(input[1]);
        close(output[0]);

        //**************************************************************************************************
        //环境变量为全局,可以传给子进程
        //所以通过环境变量把method传给子进程,如果是GET,再把query_string给过去,POST则把content_length给过去
        char method_env[MAX/10] = {0};
        sprintf(method_env,"METHOD=%s",method);//METHOD=GET/POST
        putenv(method_env);

        if(strcasecmp(method,"GET") == 0){
            char query_string_env[MAX] = {0};
            sprintf(query_string_env,"QUERY_STRING=%s",query_string);
            putenv(query_string_env);
        }
        else{//POST
            char content_length_env[MAX/10];
            sprintf(content_length_env,"CONTENT-LENGTH=%s",content_length);
            putenv(content_length_env);
        }
        //***************************************************************************************************



        //进程替换之后,就把input和output替换掉了,没办法通信了
        //所以在exec之前,用dup2把标准输入0重定向到input,把标准输出1重定向到output
        //而exec不会替换fd
        dup2(input[0],0);
        dup2(output[1],1);
        execl(path,path,NULL);//执行path路径下的可执行文件


        exit(1);
    }
    else{//parent
        close(input[0]);
        close(output[1]); 

        //若是POST,则只知道了content-length,要把正文给子进程
        char c;
        if(strcasecmp(method,"POST") == 0){
            int i = 0;
            for( ; i<content_length; i++){
                recv(sock,&c,1,0);//读请求正文
                write(input[1],&c,1);//给子进程
            }
        }

        //构建响应报头
        sprintf(line,"HTTP/1.0 200 OK\r\n");
        send(sock,line,strlen(line),0);
        sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");
        send(sock,line,strlen(line),0);
        sprintf(line,"\r\n");
        send(sock,line,strlen(line),0);


        //已经把程序参数都给子进程了,父进程该拿执行结果了
        while(read(output[0],&c,1) > 0){
            send(sock,&c,1,0);//从子进程拿到执行结果,给客户端响应正文
        }

        waitpid(id,NULL,0);//阻塞等子进程没问题,因为是线程阻塞,不影响主线程
    }
    return 200;
}

读一行并且把换行符统一为’\n’函数,读至正文函数,处理错误函数,以及404响应函数:我暂时只处理了404,把一个出错的页面返回给客户端;

int get_line(int sock,char* line,int size)
{
    assert(line);
    assert(size > 0);

    //一次从sock里读一个字符,读到\n,则读到了换行符;
    //若读到了\r,就再检测下一个

    int i = 0;
    char c = 'A';
    while(c != '\n' && i < size-1){//读到\n就是换行符,一定读完了第一行
        //ssize_t recv(int sockfd, void *buf, size_t len, int flags);
        //当flags为0时,则recv和read一样;当flags为 MSG_PEEK 时,则不读,只是窥探一下
        ssize_t s = recv(sock,&c,1,0);//每次从sock里读一个字符到c
        if(s > 0){
            if(c == '\r'){
                //要么是\r,要么是\r\n
                //所以要把\r和\r\n都转化为\n

                //所以此时要再读下一个来判断,下一个如果是\n,则把\r\n转为\n
                //下一个若是正常字符(以\r换行),那么就会把下一行的第一个字符读走,多读了一个
                //所以就需要先知道下一个是什么,用MSG_PEEK选项窥探
                recv(sock,&c,1,MSG_PEEK);
                if(c == '\n')//读到了\r\n
                    recv(sock,&c,1,0);//就再读一次,读到\n,那么就读到了完整的一行
                else//以\r换行
                    c = '\n';//直接把\r变为\n
            }
            //不是\r,要么是正常字符,要么是\n
            line[i++] = c;//放入line
        }
    }
    line[i++] = '\0';
    return i;
}

void clear_header(int sock)
{
    char line[MAX];
    do{
        get_line(sock,line,sizeof(line));
    }while(strcmp(line,"\n"));
}

void show_404(int sock)
{
    char line[1024];
    sprintf(line,"HTTP/1.0 404 NOT Found\r\n");
    send(sock,line,strlen(line),0);

    sprintf(line,"Content-Type:text/html;charset=ISO-8859-1\r\n");
    send(sock,line,strlen(line),0);

    sprintf(line,"\r\n");
    send(sock,line,strlen(line),0);

    struct stat st;
    stat(PAGE_404,&st);
    int fd = open(PAGE_404,O_RDONLY);
    sendfile(sock,fd,NULL,st.st_size);
    close(fd);
}

void echo_error(int sock,int code)
{
    switch(code){
        case 400:
            //show_404();
            break;
        case 403:
            //show_404();
            break;
        case 404:
            show_404(sock);
            break;
        case 500:
            //show_404();
            break;
        case 503:
            //show_404();
            break;
        default:
            break;
    }
}

cgi程序讲解:由于在cgi机制里,把method,content-length,query_string都设为了环境变量,所以在cgi程序里,直接拿就好了,cgi程序的目的就是拿到参数放入buf,然后处理参数。

void data_begin(char* buf)
{
    //Season=19961997&Type=0
    //int sscanf(const char *str, const char *format, ...);//把str里格式化输入到某个变量
    int x = 0;
    int y = 0;
    sscanf(buf,"Season=%d&Type=%d",&x,&y);
    //printf("x: %d,y: %d\n",x,y);
    switch(x){
        case 19961998:
            if(y == 0)
                printf("pts:7.6 ass:1.3 rebs:1.9 \n");
            else
                printf("pts:8.2 ass:1.2 rebs:1.2 \n\n");
            break;
        //中间是老科二十个赛季的数据,我就不在文章里放了,有点长

        case 20152016:
            if(y == 0)
                printf("pts:17.6 ass:2.8 rebs:3.7 \n");
            else
                printf("Lakers haven't been in the playoffs this season\n");
            break;
        default:
            printf("enter again!\n");
    }
}

int main()
{
    char method[64];
    char buf[1024];//放参数
    if(getenv("METHOD")){
        strcpy(method,getenv("METHOD"));
        if(strcasecmp(method,"GET") == 0){
            //GET方法直接从QUERY_STRING里获取参数
            strcpy(buf,getenv("QUERY_STRING"));
        }
        else{
            int content_length = -1;
            content_length = atoi(getenv("CONTENT-LENGTH"));

            //读content_length个长度获取参数
            int i = 0;
            for( ; i < content_length; i++){
                read(0,buf+i,1);
            }
            buf[i] = '\0';
        }
        //参数已经在buf里了
        //printf("cgi arg: %s\n",buf);
        data_begin(buf);
    }

    return 0;
}

项目文件
这里写图片描述
build.sh:利用shell脚本执行make clean;make
start.sh:利用shell脚本启动服务器
http.c:我的服务器程序
comm.h:头文件以及宏

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>

#include <ctype.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/sendfile.h>
#include <sys/wait.h>

#define MAX 1024
#define HOME_PAGE "index.html"
#define PAGE_404 "wwwroot/404.html"

wwwroot:服务器的根目录,内容:
这里写图片描述
404.html:404页面
cgi:里面放的cgi程序
imag:存的我的图片
index.html:首页

Makefile:一旦make,生成的是http服务器程序以及cgi程序的可执行文件。

WORK_PATH=$(shell pwd)  #当前工作目录放在WORK_PATH
BIN=httpd  #目标
SRC=httpd.c  #依赖
CC=gcc #方法
LDFLAGS=-lpthread -DDEBUG#链接选项
#gcc的-D选项,指定宏

.PHONY:all
all:$(BIN) cgi

$(BIN):$(SRC)
    $(CC) -o $@ $^ $(LDFLAGS)
cgi:
    cd wwwroot/cgi/; make clean;make; cd -

.PHONY:output
output:
    mkdir -p output/wwwroot/cgi #-p:批量化生成路径
    cp $(BIN) output
    cp wwwroot/*.html output/wwwroot
    cp -rf wwwroot/imag output/wwwroot
    cp wwwroot/cgi/data_cgi output/wwwroot/cgi

.PHONY:clean
clean:
    rm -f $(BIN)
    rm -rf output
    cd wwwroot/cgi/; make clean; cd-

服务器效果:
首页:输入赛季以及季后赛还是常规带来获取科比的数据
这里写图片描述

cgi:这里写图片描述

404界面:
这里写图片描述

项目里遇到的问题
1.服务器应答时,没有将html格式的页面发送,而是将代码展示在浏览器:
原因:检查代码无误,发现是浏览器兼容问题,换一个浏览器就解决了问题。

2.首页可以打开了,html页面展现了出来,但是虽然文字无误,但是图片无法打开:
原因:这里写图片描述
构建响应,send时发送了写错了,写成了strlen(path),而不是line。

3.POST方法无法执行cgi:
原因:这里写图片描述
父进程确认方法之后,如果是POST就要把正文发给子进程,这里if判断写错了,少写了==0。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值