TinyHttpSever

来源:互联网 发布:工时记录软件 编辑:程序博客网 时间:2024/06/03 14:21

Across the great wall, we can reach every corner in the world.

TinyHttpd源码分析

        • TinyHttpd源码分析
      • 背景
      • 源码解析
        • 主体框架 - main
        • 基础通讯实现 - startup
        • 请求处理 - accept_request
        • 执行CGI
        • 文件发送实现
        • 相关函数实现

1. 背景

一直很好奇web的工作原理,加之这阵子也在学习Python爬虫,就有想法了解这部分的知识,所以买了一本图解HTTP。这本书简洁清晰也很形象地介绍了HTTP协议的工作流程,对零基础了解HTTP协议有着不错的引导作用。书也很薄,可以很快看完。不过纯粹通过看书学习一个协议难免会浮于表面,因此,我找了TinyHttpd的source code来了解http协议的实现和实际工作场景。

2. 源码解析

声明:这篇里面的代码并不是TinyHttpd的源码,是我自己手动临摹一遍的代码,实测跑通了。一直相信代码自己码一遍会比纯看加注释收获多一些。同时,TinyHttpd只有几百行,自己码一遍也不算什么。关于阅读tinyhttpd的source code,个人觉得可以以如下顺序展开:main –> startup –> accept_request –> execute_cgi –>了解cgi实现,因此本文就按照此顺序展开分享。

主体框架 -> main()

main函数是整个httpd的工作框架,具体的实现流程如下, startup创建socket通信并建立端口监听 –> accept等待客户端连接请求 –> accept_request处理客户端http请求 –> cleanup释放资源

int main(int argc,char *argv[]){    int sever_sock = -1;    u_short port = 5277;    int client_sock = -1;    struct sockaddr_in client_name;    unsigned int client_name_len = sizeof(client_name);    pthread_t newthread;    sever_sock = startup(&port); //建立socket通讯,并进行端口监听    printf("httpd running on port %d\n", port);    while(1)    {       client_sock = accept(sever_sock,                            (struct sockaddr *)&client_name,                            &client_name_len); // 接受客户端请求       if(client_sock == -1)       {           error_die("accept failed");       }       if(pthread_create(&newthread, NULL, accept_request, (void *)&client_sock) != 0) // 创建子线程处理客户端请求       {           perror("pthread_create failed");       }    }    cleanup(sever_sock); // 关闭socket,释放相关资源    printf("httpd stopped\n");    return 0;}

基础通讯实现 -> startup()

HTTP是一个应用层协议,通过TCP/IP进行传输的。HTTP协议规定,连接请求从客户端发起,服务端提供资源响应。在客户端无请求的情况下,服务端不会主动发送响应。服务端通讯建立过程: socket创建套接字 –> bind绑定套接字 –> listen监听套接字 –> accept等待客户端连接请求。

int startup(u_short *port){    int httpd = 0;    struct sockaddr_in name;    // 创建socket描述符:采用TCP通讯方式,在第二个参数确定的情况下,第三个参数可以传0由函数自动匹配对应协议    httpd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);    if( httpd == -1 )    {        error_die("socket failed");    }    // 绑定套接字:绑定IP地址和端口号    memset(&name, 0, sizeof(name));    name.sin_family = AF_INET;    name.sin_port   = htons(*port); // 指定端口:若端口为0,则自动分配一个端口。将端口转换为网络字节序    name.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址:INADDR_ANY -> 服务器上所有的IP对应端口号都监听    if( bind(httpd,(const struct sockaddr *)&name, sizeof(name) ) < 0 )    {        error_die("bind failed");    }    // 若端口为0,获取自动分配的端口号    if(*port == 0)    {        int namelen = sizeof(name);        if( getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1 ) // 获取套接字信息        {            error_die("getsockname failed");        }        *port = ntohs(name.sin_port); // 获取端口号: 网络字节序转主机字节序    }    // 监听socket    if( listen(httpd, 5) < 0 ) // 监听httpd,等待客户端连接请求,并设置最大可排队连接数为5个    {        error_die("listen failed");    }    return httpd;}

请求处理 -> accept_request()

accept_request是这个httpd的主体。通过解析http请求,对应发送资源和响应。http请求报文主要由三部分组成: 报文首部(分请求起始行和可选的请求首部字段)、空行、报文主体。通常并不一定要有报文主体。请求报文中每一行都以回车换行(CRLF,即”\r\n”)作为结束标志。

Method URL HTTP_Version<CRLF>    // 请求起始行Header_Name: Header_Value<CRLF>  // 请求首部字段,可选... ...Header_Name: Header_Value<CRLF><CRLF>                           // 空行,表示报文首部结束BODY                             // 报文主体

下文我们用来分析的报头首部是用wireshark抓chrome访问httpd时发出的,只有报文首部,没有报文主体。不同浏览器可能有所差异,具体可用wireshark尝试分析。
TinyHttpd主要是针对请求起始行进行处理。请求起始行由Method、Request-Url和Http版本信息组成,三者通过空格隔开。如下请求起始行中”GET”就是method,表示请求访问服务器的类型,用于告知服务器访问意图。”/”为URL,表示请求访问的资源,也称作Request-URL,”HTTP/1.1”表示http版本信息,用来提示客户端使用的http协议功能。
下面的内容为请求首部字段,是可选的,在accept_request的execute_cgi中,我们只有在处理POST请求时才会去解析这部分的内容,对于GET,我们解析请求起始行后会去清除buf中的这部分数据,避免对后续处理或者下次通讯请求造成影响。

GET / HTTP/1.1                // 请求起始行Host: 192.168.179.145:5277    // 以下为可选首部字段,格式为Header-Name: Header-Value<CRLF>Connection: keep-aliveCache-Control: max-age=0Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-Encoding: gzip, deflate, sdchAccept-Language: zh-CN,zh;q=0.8,en;q=0.6

解析请求的具体实现。

void *accept_request(void *pclient){    int client = *(int*)pclient;    char buf[1024];    char method[255] = {0, };    char url[255] = {0, };    char path[255] = {0, };    char *query_string = NULL;    struct stat st;    int i = 0, j = 0, cgi = 0;    unsigned int numofchars = 0;    numofchars = sock_getline(client, buf, sizeof(buf)); // 获取一行请求报文,以LF(\n)作为结尾。 #if DEBUG_ENABLE    printf("recieve : %s", numofchars == 0 ? "NULL\r\n" : buf);#endif     // 对于http报文来说,第一行即为请求起始行:method url http-version    while( !isspace((int)buf[i]) && (i < sizeof(method) - 1) // 获取请求方法        method[j++] = buf[i++];    method[i] = '\0';    // strcasecmp为忽略大小写,比较字符串是否相同,相同则返回0,否则参数1长度大于参数2时返回正值,反之返回负值。    // TinyHttpd只支持GET和POST两种方法    if( strcasecmp(method, "GET") && strcasecmp(method, "POST") )    {        bad_request(client);        return ;    }    // 检测请求是POST还是GET,若为POST则需要CGI处理,置起对应标志    cgi = strcasecmp(method, "POST") == 0 ? 1 : 0;     //清除多余空格    while( isspace((int)buf[j]) && (j++ < sizeof(buf)) )          ;    i = 0;    //获取URL,用于确定访问什么资源    while( !isspace((int)buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)) )    {        url[i++] = buf[j++];    }    url[i] = '\0';#if DEBUG_ENABLE    printf("Request-URL: %s\r\n", url);#endif    /* process the request */    if(cgi == 0) /* method : GET */    {        query_string = url;        // 若GET请求的URL带?,则表明有查询参数,须CGI处理        while( (*query_string != '?') && (*query_string != '\0') )             query_string++;        if (*query_string == '?') /* should be process by CGI */        {            cgi = 1;            *query_string = '\0';            query_string++; //截取查询的字符串        }    }    /*以上为请求起始行的解析过程。*/    // 将URL转化为本地资源路径path    sprintf(path, "htdocs%s", url);    // 如果path为目录则返回首页路径    if(path[strlen(path) - 1] == '/')    {        strcat(path, "index.html");    } #if DEBUG_ENABLE    printf("request path: %s\r\n", path);#endif    //检测请求文件是否存在    if(stat(path, &st) == -1)    {            //文件不存在则清除剩余header信息,即可选首部字段部分。        while( (numofchars > 0) && strcmp("\n", buf) )        {            numofchars = sock_getline(client, buf, sizeof(buf));        }        not_found(client); // 向浏览器声明没有相应资源    }    else    {        // 若请求URL为路径,则返回首页        // warning: 这里有一个bug,假设URL为"htdocs/index",本地存在这个目录,        // 但不存在"htdocs/index/index.html"这里会合成之后的路径就是错的        if( (st.st_mode & S_IFMT) == S_IFDIR )        {            strcat(path, "/index.html");        }       // 检测到文件具备可执行权限,当请求文件为可执行程序,则应执行对应程序获取执行结果        if( (st.st_mode & S_IXUSR ) ||    // 文件所有者具备执行权限            (st.st_mode & S_IXGRP ) ||    // 用户组具备执行权限            (st.st_mode & S_IXOTH ) )     // 其他用户具备可执行权限        {            cgi = 1;        }#if DEBUG_ENABLE        printf("cgi[%d]: goto %s\r\n", cgi, cgi == 0 ? "serve_file":"execute_cgi");#endif        if (cgi == 0)        {            serve_file(client, path); // 请求文件存在且非执行,则发送文件内容        }        else        {            execute_cgi(client, path, method, query_string); // 需执行CGI获取内容的        }    }    close(client);     //释放客户端套接字,通讯结束}

执行CGI

void execute_cgi( int client, const char *path, const char *method, const char *query_string ){    char buf[1024]= {'A', 0,};    int cgi_in[2]={0,0}, cgi_out[2] = {0,0}; //声明管道通讯,用于父子进程之间的通讯    unsigned int content_length = -1, numofchars = 1;    char ch = '\0';    pid_t pid = -1;    int i = 0, status;    if (strcasecmp(method, "GET") == 0)    {        //如果是GET方法,则清除剩余http头        while( (numofchars > 0) && strcmp("\n",buf) ) //clean the header        {            sock_getline(client, buf, sizeof(buf));        }    }    else    {        while((numofchars > 0) && strcmp(buf, "\n")) // 解析终止条件:HTTP请求头部解析完        {            buf[14] = '\0';  //strlen("content-length") == 14            // 解析http头请求字段,获取content-length字段值,即实体主体大小            if( 0 == strcasecmp(buf, "content-length") )             {                content_length = atoi(&buf[16]);            }            numofchars = sock_getline(client, buf, sizeof(buf));        }        if(content_length == -1)         {            //如果没有成功解析到,则表明这是一个错误请求            bad_request(client);            return ;        }    }    // 响应报文,返回正确响应码200    send_str(client, "HTTP/1.0 200 OK\r\n"); // 响应报文起始行组成: HTTP-Version Status-Code Reason-Phrase    //pipe操作必须在fork之前,这边子进程才能继承到两组文件描述符,实现父子进程之间的通讯    if( (pipe(cgi_out) < 0) || (pipe(cgi_in) < 0) )    {        //创建管道,fd[0]-->读 fd[1]<--写,创建失败则返回信息给客户端        cannot_execute(client);        return ;    }    if( (pid = fork()) < 0 )    {        cannot_execute(client);        return ;    }//为方便理解和阅读代码,加的定义#define DEFINE_STDIN    (0)#define DEFINE_STDOUT   (1)#define DEFINE_STDERR   (2)    if(pid == 0)    {        char meth_env[255], query_env[255], length_env[255];        dup2(cgi_out[1], DEFINE_STDOUT); // dup2将系统标准输出定义到cgi_out[1]        close(cgi_out[0]);               // 关闭cgi_out[0],避免误操作        dup2(cgi_in[0], DEFINE_STDIN);   // 将系统标准输入定义到cgi[0]上        close(cgi_out[1]);        sprintf(meth_env, "REQUEST_METHOD=%s", method); //将请求方法保存在进程所在的环境变量中        putenv(meth_env);        if( strcasecmp(method,"GET") == 0 )        {            sprintf(query_env, "QUERY_STRING=%s", query_string); // GET方法需提供查询的信息            putenv(query_env);        }else{            sprintf(length_env, "CONTENT_LENGTH=%d", content_length); // POST方法提供主题的大小            putenv(length_env);        }        execl(path, path, NULL); // 执行CGI程序,同时继承了子进程的文件描述符        exit(0);    }else{        // 关闭两个不会操作到的pipe,避免误操作        close(cgi_in[0]);        close(cgi_out[1]);        if(strcasecmp(method, "POST") == 0)        {            for(i = 0; i < content_length; i++)            {                recv(client, &ch, 1, 0); // POST方法需要解析报文主体实体,然后发给CGI程序                write(cgi_in[1], &ch, 1);                #if DEBUG_ENABLE                    printf("%c", ch);                #endif            }        }        while(read(cgi_out[0], &ch, 1) > 0) // 获取CGI执行结果,并通过Socket返回客户端        {        #if DEBUG_ENABLE            printf("%c", ch);        #endif            send(client, &ch, 1, 0);        }        close(cgi_out[0]);        close(cgi_in[1]);        waitpid(pid, &status, 0); // 等待所有子进程执行完毕    }}

文件发送实现

void cat( int client, FILE *resource ){    char buf[1024];    fgets( buf, sizeof(buf), resource );      // 读取1024bytes数据    while(!feof(resource))                    // 如果文件未EOF则继续读    {        send(client, buf, strlen(buf), 0);    // socket传输数据        fgets(buf, sizeof(buf), resource);    }}void serve_file( int client, const char *filename ){    FILE *resource = NULL;    int numofchars = 1;    char buf[1024] = {'A', '\0',};    /* read & discard headers */    while( (numofchars > 0) && strcmp("\n", buf) )    {        numofchars = sock_getline( client, buf, sizeof(buf) ); // 清除请求头。    }    resource = fopen(filename, "r");    // 打开文件读取    if( resource == NULL )    {        not_found(client);              // 资源未找到或无法访问    }    else    {        headers(client, filename);      // 发送服务器响应报文首部        cat(client, resource);          // 发送服务器响应实体主体    }    fclose(resource);                   // 释放资源}

相关函数实现

1、获取客户端请求报文的一行内容

int sock_getline(int sock, char *buf, unsigned int size){    int i = 0;    char ch = '\0';    int n = 0;    if((buf == NULL) && (size == 0) && (sock == -1)) // 参数合法性检查    {        printf("parameter error, please check %s[%d]\n", __func__, __LINE__);        return -1;    }    while( (i < size - 1) && (ch != '\n') ) // \n是行结束标志    {        n = recv(sock, &ch, 1, 0);        if(n > 0)        {            if(ch == '\r')            {                n = recv(sock, &ch, 1, MSG_PEEK);    // MSG_PEEK可实现下次读到的,仍是此次读取到的内容                if( (n > 0) && (ch == '\n') )        // 若读取到的\r\n,则此次读取结束,读取到的字符为\n                {                    recv(sock, &ch, 1, 0);                }                else                {                    ch = '\n';                       // 否则设定读取的字符为\n,读取结束                }            }            buf[i] = ch;            i++;        }        else        {            ch = '\n';        }    }    buf[i] = '\0';    return i;}

2、服务器响应报文实现
为方便代码编写和阅读,我在tinyhttpd的基础上实现了下面这个函数,专门用于发送字符到socket

void send_str(int client, const char *str){    unsigned int ret = send(client, str, strlen(str), 0);#if DEBUG_ENABLE    ret == strlen(str) ?  0 : printf("send_str error[ret = 0x%02x].\r\n", ret);#endif}
/*发送文件前的响应头*/void headers( int client, const char *filename ){    (void)filename;    send_str(client, "HTTP/1.0 200 OK\r\n");          // 2**表示执行成功,200表示请求被正常处理    send_str(client, SERVER_STRING);    send_str(client, "Content-Type: text/html\r\n");  // 发送资源的MIME为text/html,即文本类型    send_str(client, "\r\n");}/* 未找到文件或无法访问文件的响应报文 */ void not_found(int client){#if DEBUG_ENABLE        printf("not found.\r\n");#endif    // 4**的状态码表明错误是客户端引发的    send_str(client, "HTTP/1.0 404 NOT FOUND\r\n"); //404表示请求的资源不存在或服务器不提供此资源访问     send_str(client, SERVER_STRING);    send_str(client, "Content-Type: text/html\r\n");    send_str(client, "\r\n");    send_str(client, "<HTML><TITLE>NOT FOUND</TITLE>"             // 发送一个简单页面用于提示                     "<BODY><P> the sever couldn't fullfill"                     "your request because the resource specified"                     "is unavailable or nonexistence."                     "</BODY></HTML>\r\n");}/* 错误请求响应报文*/void bad_request(int client){#if DEBUG_ENABLE    printf("bad request.\r\n");#endif    // 服务器不支持对应的方法或者报文语法时,会发出错误请求报文    send_str(client, "HTTP/1.0 400 BAD REQUEST\r\n");  // 400表示请求错误或者请求的报文中存在语法错误    send_str(client, "Content-Type: text/html\r\n");    send_str(client, "\r\n");    send_str(client, "<P> Your browse sent a bad request,"            "such as a POST without a Content-Length.\r\n");}/*服务器内部异常响应报文*/void cannot_execute(int client){#if DEBUG_ENABLE        printf("can not execute.\r\n");#endif    send_str(client, "HTTP/1.0 500 Internal Server Error\r\n"); // 5**为服务器错误,500表示服务器在执行请求时发生错误    send_str(client, "Content-Type: text/html\r\n");    send_str(client, "\r\n");    send_str(client, "<p>error prohibited CGI execution.\r\n");}
原创粉丝点击