从零开始:编写一个Web服务器---HTTP部分详细讲解以及代码实现(二)

HTTP部分详细讲解以及代码实现(二)

  • void process()的减负实现
  • Web服务器中的有限状态机
  • HTTP有限状态机
    • 从状态机
      • 如何理解从状态机
      • 流程如下:
      • 从状态机实现代码
    • 主状态机
      • 主状态机三种状态,标识解析位置
      • do_request() 具体处理函数
  • 总结

本文介绍了Web服务器中的状态机

参考书籍《后台开发 核心技术与应用实践》《Linux高性能服务器编程》
参考代码 https://github.com/qinguoyi/TinyWebServer
参考博文 两猿社

感谢以上大佬
注意:本系列完结之前,所写代码不具有运行完备性,每一部分的文章都在为整个项目完成部分实现,如果你想阅读完整代码,参考引用给出的gayhub地址

void process()的减负实现

//http_conn.cpp
void http_conn::process()
{
   
    HTTP_CODE read_ret = process_read();
    //NO_REQUEST,表示请求不完整,需要继续接收请求数据
    if (read_ret == NO_REQUEST)
    {
   
        return;
    }
    bool write_ret = process_write(read_ret);
    if (!write_ret)
    {
   
    	return;
    }
}

可以看出,这里将process()的实现做了两个划分,我们将response改为write(因为回应的本质是向回写)然后并入process()中,使整个逻辑更加完整。

void的修饰不是强制的,你可以写成bool,int,或者自己宏定义,用于判断处理成功与否。

read和write分工操作其实是为了以后的线程池和epoll,这里按下不表。接下来,讲一下如何实现两个不同的功能

在此之前,我希望你可以读一下我的另一篇文章:Linux网络编程:状态机

Web服务器中的有限状态机

Web服务器中的有限状态机体现在两个方面:http和tcp

  • http有限状态机体现在业务逻辑处理
  • tcp有限状态机体现在对于连接情况的判断(代码层面不突出,本篇文章不涉及)

HTTP有限状态机

首先理解这个问题。HTTP协议并未提供头部字段的长度,判断头部结束依据是遇到一个空行,该空行只包含一对回车换行符。同时,如果一次读操作没有读入整个HTTP请求的头部,我们必须等待用户继续写数据再次读入(比如读到 GET /index.html HTT就结束了,必须维护这个状态,下一次必须继续读‘P’)。
即我们需要判定当前解析的这一行是什么(请求行?请求头?消息体?),还需要判断解析一行是否结束!

首先根据上篇文章,HTTP报文解析需要的大致解析代码以及我们枚举出的情况,可以分为两个状态机(主状态机解决前半部分问题,从状态机解决后半部分问题)

从状态机

从状态机负责读取报文的一行

在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析。从状态机负责读取buffer中的数据,将每行数据末尾的\r\n置为\0\0,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析。

三种状态,标识解析一行的读取状态

  • LINE_OK,完整读取一行
  • LINE_BAD,报文语法有误
  • LINE_OPEN,读取的行不完整

如何理解从状态机

从状态机就是更加聚焦于一行行,着眼更加细致,他关心的是当前这一行读完整了/不完整/格式有误,亦即从状态机关注一行的解析状态。(那么显然这里可以想到其实每一个主状态机状态可能对应多轮的从状态改变,类似与包含关系),所以从状态机的函数会关注当前字符是什么,根据这个字符判断当前是读完了吗是格式错误吗等等。

流程如下:

  • 从状态机从m_read_buf中逐字节读取,判断当前字节是否为\r

    • 接下来的字符是\n,将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
    • 接下来达到了buffer末尾,表示buffer还需要继续接收,返回LINE_OPEN
    • 否则,表示语法错误,返回LINE_BAD
  • 当前字节不是\r,判断是否是\n (一般是上次读取到\r就到了buffer末尾,没有接收完整,再次接收时会出现这种情况)

    • 如果前一个字符是\r,则将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
  • 当前字节既不是\r,也不是\n

    • 表示接收不完整,需要继续接收,返回LINE_OPEN
      从零开始:编写一个Web服务器---HTTP部分详细讲解以及代码实现(二)_第1张图片

从状态机实现代码

//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line()
{
   
    char temp;
    //m_read_idx指向缓冲区m_read_buf的数据末尾的下一个字节
    //m_checked_idx指向从状态机当前正在分析的字节
    for (; m_checked_idx < m_read_idx; ++m_checked_idx)
    {
   
        //temp为将要分析的字节
        temp = m_read_buf[m_checked_idx];
        //如果当前是\r字符,则有可能会读取到完整行
        if (temp == '\r')
        {
   
            //下一个字符达到了buffer结尾,则接收不完整,需要继续接收
            if ((m_checked_idx + 1) == m_read_idx)
                return LINE_OPEN;
            //下一个字符是\n,将\r\n改为\0\0
            else if (m_read_buf[m_checked_idx + 1] == '\n')
            {
   
                m_read_buf[m_checked_idx++] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            //如果都不符合,则返回语法错误
            return LINE_BAD;
        }
        //如果当前字符是\n,也有可能读取到完整行
        //一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况
        else if (temp == '\n')
        {
   
            //前一个字符是\r,则接收完整
            if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r')
            {
   
                m_read_buf[m_checked_idx - 1] = '\0';
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    //并没有找到\r\n,需要继续接收
    return LINE_OPEN;
}

主状态机

主状态机负责对该行数据进行解析

主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。

//通过while循环 封装主状态机 对每一行进行循环处理
//此时 从状态机已经修改完毕 主状态机可以取出完整的行进行解析
http_conn::HTTP_CODE http_conn::process_read()
{
   
    //初始化从状态机的状态
    LINE_STATUS line_status = LINE_OK;
    HTTP_CODE ret = NO_REQUEST;
    char *text = 0;

    //判断条件,这里就是从状态机驱动主状态机
    while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
    {
   
        text = get_line();

        //m_start_line 是每一个数据行在m_read_buf中的起始位置
        //m_checked_idx 表示从状态机在m_read_buf中的读取位置
        m_start_line = m_checked_idx;
        LOG_INFO("%s", text);
        //三种状态转换逻辑
        switch (m_check_state)
        {
   
        case CHECK_STATE_REQUESTLINE

你可能感兴趣的:(Web服务器,c++,web服务器,后端)