美文网首页
自制Web服务器(2) 建立TCP连接&解析HTTP请求

自制Web服务器(2) 建立TCP连接&解析HTTP请求

作者: JianlingZhou | 来源:发表于2018-06-03 13:17 被阅读396次

    四月初就开始着手写Web服务器,因为一些事耽搁了一个月,最近又在提交代码了,文章列表:

    这不是一个写Web服务器的教程,只是做的过程的记录,因为是先写代码后写文章的,有些过程我直接凭记忆在这里写下来,可能会有疏漏。

    建立 TCP 连接

    这一部分参考 Liso Project 的 start_code、深入理解计算机系统 第二版 10-12 章、UNIX网络编程 卷1 第三版 1-4 章。

    首先作为一个 Web服务器,要能够监听端口、等待TCP连接、建立TCP连接,这是基本要求。因为使用C语言开发,所以要用 UNIX 的套接字API,TCP连接建立与释放的流程如下:


    其中:

    • socket() 用于创建一个套接字结构体,这里需要使用的协议族、协议类型等。这一步一般不会出问题。
    • bind() 用于绑定到一个本地端口。出错的原因一般有端口已经被占用、非 Root 用户绑定知名端口。
    • listen() 用于说明这个套接字用于被动接收连接请求。
    • accept() 会导致程序陷入睡眠,直到系统中断提醒程序有新连接建立。
    • read() 也是一个 I/O操作,会导致程序陷入睡眠,这个时候内核开始从网卡的Buffer里面拷贝数据到内存里,一旦拷贝完了,系统中断让进程切回来继续执行。

    这部分对应的代码:

    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/socket.h>
    #include <sys/stat.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <string.h>
    
    #define BUFFER_SIZE 4096
    #define FORK_CHILD_PID 0
    
    typedef struct http_mod {
        int sockfd;
        struct sockaddr_in addr;
    } http_mod;
    
    http_mod* http_init(uint16_t port) {
        http_mod* m = (http_mod*) malloc(sizeof(http_mod*));
        m->sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (m->sockfd == -1) {
           fprintf(stderr, "Create socket failed.");
           free(m);
           return NULL;
        }
    
        m->addr.sin_family = AF_INET;
        m->addr.sin_port = htons(port);
        m->addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        int bind_ret = bind(m->sockfd, (struct sockaddr*) &(m->addr), sizeof(m->addr));
        if (bind_ret != 0) {
            fprintf(stderr, "bind to %d failed!", port);
            if(close(m->sockfd)) {
                fprintf(stderr, "Close socked failed!\n");
            }
            free(m);
            return NULL;
        }
    
        int listen_ret = listen(m->sockfd, 5);
        if (listen_ret != 0) {
            fprintf(stderr, "Failed to listen!\n");
            if(close(m->sockfd)) {
                fprintf(stderr, "Close socked failed!\n");
            }
            free(m);
            return NULL;
        }
        fprintf(stdout, "start http mod successfully! \n"); 
    
        return m;
    }
    
    //TODO: replace fork with IO multiplex
    int start_receive_conn(http_mod *http) {
        struct sockaddr_in client_sock_addr;
        socklen_t cs_size = sizeof(client_sock_addr);
        fprintf(stdout, "Waiting for a connection, localport: %d\n", ntohs(http->addr.sin_port));
    
        while (1) {
            int new_sock = accept(http->sockfd, (struct sockaddr*)(&client_sock_addr), &cs_size);
            if (new_sock == -1) {
                fprintf(stderr, "Accept connections failed!\n");
                if (close(http->sockfd)) {
                    fprintf(stderr, "Close socket failed!\n");
                }
                return -1;
            } else {
                fprintf(stdout, "Receive connection from %s\n", inet_ntoa(client_sock_addr.sin_addr));
            }
            if (fork() == FORK_CHILD_PID) {
                handle_request_loop(new_sock); 
                if (close(new_sock)) {
                    fprintf(stderr, "Close real sock failed! \n'");
                }
                fprintf(stdout, "Close connection from %s", inet_ntoa(client_sock_addr.sin_addr));
                exit(0);
            }
    
            if (close(new_sock)) {
                fprintf(stderr, "Close real sock failed! \n'");
                return -1;
            }
    
        }
    
        if (close(http->sockfd)) {
            fprintf(stderr, "Close sock failed! \n'");
        }
        fprintf(stdout, "Liso has stopped.\n");
        return 0;
    }
    
    void liso_init(struct arguments* arg) {
        http_mod* hm = http_init(arg->port);
        
        if (hm == NULL) {
            fprintf(stderr, "Initialize http module failed!\n");
            return;
        } 
        start_receive_conn(hm);
    }
    
    

    因为程序在调用 accept() 建立了新连接后就不处于监听状态了,此时别的客户端是无法和服务端建立连接的。所以必须要处理并发问题,一个最简单的处理办法是用 fork() 的方式支持并发:

    每次建立了一个TCP连接后,就调用fork() 派生出一个子进程,此时子进程和父进程所有的内存数据、文件打开列表都是一样的,这时可以让子进程继续处理数据,而父进程把刚刚建立的连接关闭掉,继续调用 accept() 等待客户端连接就可以支持并发了。

    但是这样弊端也很大,那就是支持并发所需要的开销太大了。更好的方法是用线程和IO多路复用,但是出于快速写一个架子的考虑,我先让这个“服务器”可以踉踉跄跄地跑起来再说,多路复用先加入到 TODO List 里面去。

    解析 HTTP 请求

    这一部分参考 编译原理(龙书) 3到4 章、Flex&Bison 开发文档、RFC 2616(HTTP/1.1 标准)、RFC 2396。

    解决一个计算机问题,建立一个稳定可复现、可调试的一个观察点还是很必要的,尤其我在TCP协议之上做数据传输,如果无法观察到实际传输的数据是很恼火的,所以我要抓包看数据。一开始我想用 WireShark,后来发现那玩意从源码编译起来有点麻烦,直接用 apt 安装了一个 tcpdump。

    RFC文档看的是 2616,这是 CS-15-441/641 Project1 的项目要求给的参考文档,似乎这个文档已经被新的标准所替代,但是绝大部分内容还是可以参考的。

    HTTP请求的格式如下:


    仔细研究了一下格式,发现其实用C语言代码解析一下就很方便了,完全没有必要用Flex和Bison,但是既然文档那么要求,就试一下这两个工具。

    Flex 是用于词法分析的工具,它把一个输入的字符串分割成一个个词法单元送给语法分析工具 Bison。Flex 和 Bison 在 Ubuntu 中都可以通过 apt 安装。在 Flex 中,我需要定义一些正则表达式来匹配词法单元,Flex 文件格式如下:

    %{
    定义词法单元,可以用 #define 表示
    %}
    声明部分
    %%
    转换规则
    %%
    辅助函数
    

    Flex 和 Bison 结合起来使用的时候, %{ %} 中的词法单元可以不定义,放到 Bison 文件中去定义。转换规则是重点,每行规则以一个正则表达式开始,后面再跟一个代码块。代码块里可以执行一些逻辑,比如调用辅助函数,返回词法单元给 Bison。

    转换规则这里有几个坑点

    • 我一开始每行加了\t来indent一下,这里不能加,不要从行首开始写正则表达式
    • Flex支持的正则表达式和我平时用的正则表达式不太一样,有些符号如 \w 是不支持的
    • 正斜杠(forward slash)符号 / 会被转义,要想不转义前面加上 \ 是没有用的,得用双引号括起来:"/"

    这里放一部分规则:

    辅助函数那里,如果不使用 Bison 的话,需要定义一个 yywrap() 函数:

    int yywrap() {
        return 1;
    }
    

    使用 flex xxx.l 命令就可以把 xxx.l 文件编译为 c 文件,然后调用 yylex() 函数就会开始解析输入的数据。 默认是从 stdin 解析的。

    Bison文件的格式和 Flex 类似。需要定义产生式。Bison 会根据产生式自动生成语法解析器。

    未完待续

    相关文章

      网友评论

          本文标题:自制Web服务器(2) 建立TCP连接&解析HTTP请求

          本文链接:https://www.haomeiwen.com/subject/szfjsftx.html