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

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

  • 自制Web服务器(1) 目标&环境&初步
  • 自制Web服务器(2) 建立TCP连接&解析HTTP请求

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

建立 TCP 连接

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

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


自制Web服务器(2) 建立TCP连接&解析HTTP请求_第1张图片

其中:

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

这部分对应的代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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请求的格式如下:


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

仔细研究了一下格式,发现其实用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请求)