Tinyhttp源码分析

Posted Dream_yz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tinyhttp源码分析相关的知识,希望对你有一定的参考价值。

Tinyhttp源码分析

  • 简介

    Tinyhttp是一个轻量型Http Server,使用C语言开发,全部代码只500多行,还包括一个简单Client。

  • 源码剖析

    Tinyhttp程序的逻辑为:一个无线循环,一个请求,创建一个线程,之后线程函数处理每个请求,然后解析HTTP请求,做一些判断,之后判断文件是否可执行,不可执行,打开文件,输出给客户端(浏览器),可执行就创建管道,父子进程进行通信。其整体处理流程如下:

    每个函数的作用如下:

    // accept_request函数:处理从套接字上监听到的一个HTTP请求,此函数很大部分体现服务器处理请求流程。
    void accept_request(void *);
    // bad_request函数:返回给客户端这是个错误请求,HTTP状态码400 Bad Request。
    void bad_request(int);
    // cat函数:读取服务器上某个文件写到socket套接字。
    void cat(int, FILE *);
    // cannot_execute函数:处理发生在执行cgi程序时出现的错误。
    void cannot_execute(int);
    // error_die函数:把错误信息写到perror并退出。
    void error_die(const char *);
    // execute_cgi函数:运行cgi程序的处理,是主要的函数。
    void execute_cgi(int, const char *, const char *, const char *);
    // get_line函数:读取套接字的一行,把回车换行等情况都统一为换行符结束。
    int get_line(int, char *, int);
    // headers函数:把HTTP响应的头部写到套接字。
    void headers(int, const char *);
    // not_found函数:处理找不到请求的文件时的情况。
    void not_found(int);
    // serve_file函数:调用cat函数把服务器文件返回给浏览器
    void serve_file(int, const char *);
    // startup函数:初始化httpd服务,包括建立套接字,绑定端口,进行监听等。
    int startup(u_short *);
    // unimplemented函数:返回给浏览器表明收到的HTTP请求所用的method不被支持。
    void unimplemented(int);
    

    分析其程序,流程为:main()——>startup()——>accept_request()——>execute_cgi()等。

  • 核心函数

    1)main()函数

    // 服务器main函数
    int main(void)
    {
        int server_sock = -1;
        u_short port = 4000;
        int client_sock = -1;
        struct sockaddr_in client_name;
        socklen_t  client_name_len = sizeof(client_name);
        pthread_t newthread;
    
        // 建立一个监听套接字,在对应的端口建立httpd服务
        server_sock = startup(&port);
        printf("httpd running on port %d\\n", port);
        // 进入循环,服务器通过调用accept等待客户端的连接,Accept会以阻塞的方式运行,直到
        // 有客户端连接才会返回。连接成功后,服务器启动一个新的线程来处理客户端的请求,处理
        // 完成后,重新等待新的客户端请求。
        while (1)
        {
            // 返回一个已连接套接字,套接字收到客户端连接请求
            client_sock = accept(server_sock,
                    (struct sockaddr *)&client_name,
                    &client_name_len);
            if (client_sock == -1)
                error_die("accept");
            // 派生线程用accept_request函数处理新请求。
            /* accept_request(client_sock); */
            if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)&client_sock) != 0)
                perror("pthread_create");
        }
        // 出现意外退出的时候,关闭socket
        close(server_sock);
    
        return(0);
    }
    

    2)startup()函数

    // startup函数:按照TCP连接的正常流程依次调用socket,bind,listen函数。
    // 监听套接字端口既可以指定也可以动态分配一个随机端口
    int startup(u_short *port)
    {
        int httpd = 0;
        struct sockaddr_in name;
        // 创建一个socket,建立socket连接
        httpd = socket(PF_INET, SOCK_STREAM, 0);
        if (httpd == -1)
            error_die("socket");
        // 填充结构体
        memset(&name, 0, sizeof(name));
        name.sin_family = AF_INET;
        name.sin_port = htons(*port);
        name.sin_addr.s_addr = htonl(INADDR_ANY);
        // 将socket绑定到对应的端口上
        if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
            error_die("bind");
        // 如果当前指定的端口是0,则动态随机分配一个端口
        if (*port == 0)  /* if dynamically allocating a port */
        {
            socklen_t namelen = sizeof(name);
            // 1.getsockname()可以获得一个与socket相关的地址
            //  1)服务器端可以通过它得到相关客户端地址
            //  2)客户端可以得到当前已连接成功的socket的IP和端口
            // 2.在客户端不进行bind而直接连接服务器时,且客户端需要知道当前使用哪个IP地址
            //   进行通信时比较有用(如多网卡的情况)
            if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
                error_die("getsockname");
            *port = ntohs(name.sin_port);
        }
        // 开始监听
        if (listen(httpd, 5) < 0)
            error_die("listen");
        // 返回socket id
        return(httpd);
    }
    

    3)accept_request()函数

    // 线程处理函数
    void accept_request(void *arg)
    {
        int client = *(int*)arg;
        char buf[1024];       // 读取行数据时的缓冲区
        size_t numchars;      // 读取了多少字符
        char method[255];     // 存储HTTP请求名称(字符串)
        char url[255];
        char path[512];
        size_t i, j;
        struct stat st;
        int cgi = 0;      /* becomes true if server decides this is a CGI
                           * program */
        char *query_string = NULL;
    
        // 一个HTTP请求报文由请求行(requestline)、请求头部(header)、空行和请求数据4个部分
        // 组成,请求行由请求方法字段(get或post)、URL字段和HTTP协议版本字段3个字段组成,它们
        // 用空格分隔。如:GET /index.html HTTP/1.1。
        // 解析请求行,把方法字段保存在method变量中。
        // 读取HTTP头第一行:GET/index.php HTTP 1.1
        numchars = get_line(client, buf, sizeof(buf));
        i = 0; j = 0;
    
        // 把客户端的请求方法存到method数组
        while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
        {
            method[i] = buf[i];
            i++;
        }
        j=i;
        method[i] = '\\0';
    
        // 只能识别get和post
        if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
        {
            unimplemented(client);
            return;
        }
    
        // POST的时候开启cgi
        if (strcasecmp(method, "POST") == 0)
            cgi = 1;
    
        // 解析并保存请求的URL(如有问号,也包括问号及之后的内容)
        i = 0;
        // 跳过空白字符
        while (ISspace(buf[j]) && (j < numchars))
            j++;
        // 从缓冲区中把URL读取出来
        while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
        {
            // 存在url
            url[i] = buf[j];
            i++; j++;
        }
        url[i] = '\\0'; // 保存URL
    
        // 先处理如果是GET请求的情况
        // 如果是get方法,请求参数和对应的值附加在URL后面,利用一个问号(“?”)代表URL的结
        // 尾与请求参数的开始,传递参数长度受限制。如index.jsp?10023,其中10023就是要传递
        // 的参数。这段代码将参数保存在query_string中。
        if (strcasecmp(method, "GET") == 0)
        {
            // 待处理请求为url
            query_string = url;
            // 移动指针,去找GET参数,即?后面的部分
            while ((*query_string != '?') && (*query_string != '\\0'))
                query_string++;
            // 如果找到了的话,说明这个请求也需要调用脚本来处理
            // 此时就把请求字符串单独抽取出来
            // GET方法特点,?后面为参数
            if (*query_string == '?')
            {
                // 开启cgi
                cgi = 1;
                // query_string指针指向的是真正的请求参数
                *query_string = '\\0';
                query_string++;
            }
        }
    
        // 保存有效的url地址并加上请求地址的主页索引。默认的根目录是htdocs下
        // 这里是做以下路径拼接,因为url字符串以'/'开头,所以不用拼接新的分割符
        // 格式化url到path数组,html文件都早htdocs中
        sprintf(path, "htdocs%s", url);
        // 如果访问路径的最后一个字符时'/',就为其补全,即默认访问index.html
        if (path[strlen(path) - 1] == '/')
            strcat(path, "index.html");
    
        // 访问请求的文件,如果文件不存在直接返回,如果存在就调用CGI程序来处理
        // 根据路径找到对应文件
        if (stat(path, &st) == -1) {
            // 如果不存在,就把剩下的请求头从缓冲区中读出去
            // 把所有headers的信息都丢弃
            while ((numchars > 0) && strcmp("\\n", buf))  /* read & discard headers */
                numchars = get_line(client, buf, sizeof(buf));
            // 然后返回一个404错误,即回应客户端找不到
            not_found(client);
        }
        else
        {
            // 如果文件存在但却是个目录,则继续拼接路径,默认访问这个目录下的index.html
            if ((st.st_mode & S_IFMT) == S_IFDIR)
                strcat(path, "/index.html");
            // 如果文件具有可执行权限,就执行它
            // 如果需要调用CGI(CGI标志位置1)在调用CGI之前有一段是对用户权限的判断,对应
            // 含义如下:S_IXUSR:用户可以执行
            //          S_IXGRP:组可以执行
            //          S_IXOTH:其它人可以执行
            if ((st.st_mode & S_IXUSR) ||
                    (st.st_mode & S_IXGRP) ||
                    (st.st_mode & S_IXOTH)    )
                cgi = 1;
            // 不是cgi,直接把服务器文件返回,否则执行cgi
            if (!cgi)
                serve_file(client, path);
            else
                execute_cgi(client, path, method, query_string);
        }
    
        // 断开与客户端的连接(HTTP特点:无连接)
        close(client);
    }
    

    4)execute_cgi()函数

    此函数执行流程如下:

    void execute_cgi(int client, const char *path,
            const char *method, const char *query_string)
    {
        char buf[1024];
        int cgi_output[2];
        int cgi_input[2];
        pid_t pid;
        int status;
        int i;
        char c;
        int numchars = 1;
        int content_length = -1;
    
        // 首先需要根据请求是Get还是Post,来分别进行处理
        buf[0] = 'A'; buf[1] = '\\0';
        // 如果是Get,那么就忽略剩余的请求头
        if (strcasecmp(method, "GET") == 0)
            // 把所有的HTTP header读取并丢弃
            while ((numchars > 0) && strcmp("\\n", buf))  /* read & discard headers */
                numchars = get_line(client, buf, sizeof(buf));
        // 如果是Post,那么就需要读出请求长度即Content-Length
        else if (strcasecmp(method, "POST") == 0) /*POST*/
        {
            // 对POST的HTTP请求中找出content_length
            numchars = get_line(client, buf, sizeof(buf));
            while ((numchars > 0) && strcmp("\\n", buf))
            {
                // 使用\\0进行分割
                buf[15] = '\\0';
                // HTTP请求的特点
                if (strcasecmp(buf, "Content-Length:") == 0)
                    content_length = atoi(&(buf[16]));
                numchars = get_line(client, buf, sizeof(buf));
            }
            // 如果请求长度不合法(比如根本就不是数字),那么就报错,即没有找到content_length
            if (content_length == -1) {
                // 错误请求
                bad_request(client);
                return;
            }
        }
        else/*HEAD or other*/
        {
        }
    
        // 建立管道
        if (pipe(cgi_output) < 0) {
            // 错误处理
            cannot_execute(client);
            return;
        }
        // 建立管道
        if (pipe(cgi_input) < 0) {
            // 错误处理
            cannot_execute(client);
            return;
        }
    
        // fork自身,生成两个进程
        if ( (pid = fork()) < 0 ) {   // 复制一个线程
            // 错误处理
            cannot_execute(client);
            return;
        }
        sprintf(buf, "HTTP/1.0 200 OK\\r\\n");
        send(client, buf, strlen(buf), 0);
        // 子进程要调用CGI脚本
        if (pid == 0)  /* child: CGI script */
        {
            // 环境变量缓冲区,会存在溢出风险
            char meth_env[255];
            char query_env[255];
            char length_env[255];
            // 重定向管道
            // 把父进程读写管道的描述符分别绑定到子进程的标准输入和输出
            // dup2功能与freopen()函数类似
            dup2(cgi_output[1], STDOUT);   // 把STDOUT重定向到cgi_output的写入端
            dup2(cgi_input[0], STDIN);     // 把STDIN重定向到cgi_input的读取端
            // 关闭不必要的描述符
            close(cgi_output[0]);          // 关闭cgi_inout的写入端和cgi_output的读取端
            close(cgi_input[1]);
    
            // 服务器设置环境变量,即request_method的环境变量
            // 设置基本的CGI环境变量,请求类型、参数、长度之类
            sprintf(meth_env, "REQUEST_METHOD=%s", method);
            putenv(meth_env);
            if (strcasecmp(method, "GET") == 0) {
                // 设置query_string的环境变量
                sprintf(query_env, "QUERY_STRING=%s", query_string);
                putenv(query_env);
            }
            else {   /* POST */
                // 设置content_length的环境变量
                sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
                putenv(length_env);
            }
    
            // 用execl运行cgi程序
            execl(path, NULL);
            exit(0);
        } else {    /* parent */
    
            // 父进程代码
            // 关闭cgi_input的读取端和cgi_output的写入端
            close(cgi_output[1]);
            close(cgi_input[0]);
            // 对于Post请求,要直接write()给子进程
            // 这样子进程所调用的脚本就可以从标准输入取得Post数据
            if (strcasecmp(method, "POST") == 0)
                // 接收POST过来的数据
                for (i = 0; i < content_length; i++) {
                    recv(client, &c, 1, 0);
                    // 把POST数据写入cgi_input,现在重定向到STDIN
                    write(cgi_input[1], &c, 1);
                }
            // 然后父进程再从输出管道里面读出所有结果,返回给客户端
            while (read(cgi_output[0], &c, 1) > 0)
                send(client, &c, 1, 0);
    
            // 关闭管道
            close(cgi_output[0]);
            close(cgi_input[1]);
            // 最后等待子进程结束,即等待子进程
            waitpid(pid, &status, 0);
        }
    }
    
  • 参考文献

    http://armsword.com/2014/10/29/tinyhttpd-code-analyse/

    http://blog.csdn.net/jcjc918/article/details/42129311

    http://techlog.cn/article/list/10182680

以上是关于Tinyhttp源码分析的主要内容,如果未能解决你的问题,请参考以下文章

C开源项目-TinyHttp解读(中)

C开源项目-TinyHttp解读(中)

C开源项目-TinyHttp解读(下)

C开源项目-TinyHttp解读(下)

C开源项目-TinyHttp解读(下)

C开源项目-TinyHttp解读(上)