C++ Webserver从零开始:代码书写(十四)——http连接处理

前言

HTTP类是Webserver到目前为止最为庞大的类。其实最开始我是只想分析它的部分代码,但是最后我还咬咬牙将http连接处理的全代码分析写完了。因此,本文会特别的长,我相信没人可以把它一口气全部读完。不过我在本文中进行了细致的目录划分,除了头文件,我将http连接处理分为了 接收连接 处理连接 响应连接,大家如果嫌长,就一个部分一个部分看吧。手机端用户可以直接用下方的目录功能,PC端用户按住ctrl + 鼠标滚轮 把页面缩小一点,目录就出来了


概述

在上一章中,我们写好了线程池的代码,当时我们说说的是请求队列里面添加的元素的类型是http解析类型。具体什么是http解析,今天我们就来了解一下。

我就不在文章里给出http的原理了,感兴趣可以的同学可以去网上找。我们应该知道的是,在服务器项目中,http很重要。如果说先前我们写的线程,数据库池或者底层的小工具如locker锁等东西是地基,是骨架,那么http解析部分就是主体,是血肉。那么可以预见是,http解析部分的内容会相当地长,它应该是目前为止最长的一篇文章,我会尽可能地分段地来介绍每一部分地功能和代码,也会使用更多的拓扑图来梳理结构。如果阅读起来太累,我推荐你按分段一段一段地读,读完一段歇口气,然后再看后面的部分。我也非常推荐你把笔纸拿出来,解析一下各部分的代码,写一写它们的运行逻辑,我相信这对你帮助会非常大。


这是我画的简易的拓扑图,它的左边是浏览器,右边整个大框可以理解为部分服务器。可以看到右边的工作队列和工作线程在上一章也有体现,这里它们进入了一个更大的环境,成为了一个局部模块。

头文件

原谅我一次性把这个非常庞大的头文件丢了了上来,按一个模块一个模块地发头文件实在太费时了,而且每个知识点的具体实现大都体现在定义里,声明大家看看就好。头文件中比较重要的内容有这些:

  1. 主/状态机的状态枚举
  2. 报文解析的结果枚举
  3. io向量及buffer控制部分的变量
#ifndef HTTP_CONN_H
#define HTTP_CONN_H
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include <map>

#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"
#include "../timer/lst_timer.h"
#include "../log/log.h"

class http_conn{
public:
    /*设置读取文件的名称m_rea_file大小*/
    static const int FILENAME_LEN = 200;
    /*设置读缓冲区m_read_buf大小*/
    static const int READ_BUFFER_SIZE = 2048;
    /*设置写缓冲区m_write_buf大小*/
    static const int WRITE_BUFFER_SIZE = 1024;

    /*报文的请求方法,本项目只用到GET和Post*/
    enum METHOD{GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATH};
    /*主状态机的状态*/
    enum CHECK_STATE{CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT};
    /*报文解析结果*/
    enum HTTP_CODE{NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTIONS};
    /*从状态机的状态*/
    enum LINE_STATUS{LINE_OK = 0, LINE_BAD, LINE_OPEN};

public:
    http_conn(){}
    ~http_conn(){}

public:
    /*初始化套接字地址,函数内部调用私有方法init*/
    void init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname);
    /*关闭http连接*/
    void close_conn(bool real_close = true);
    void process();
    /*读取浏览器端发来的全部数据*/
    bool read_once();
    /*响应报文写入函数*/
    bool write();
    sockaddr_in *get_address() {
        return &m_address;
    }
    /*同步线程初始化数据库读取表*/
    void initmysql_result(connection_pool *connPool);
    int timer_flag;
    int improv;

private:
    void init();
    /*从m_read_buf读取,并处理请求报文*/
    HTTP_CODE process_read();
    /*向m_write_buf写入响应报文*/
    bool process_write(HTTP_CODE ret);
    /*主状态机解析报文中的请求行数据*/
    HTTP_CODE parse_request_line(char *text);
    /*主状态机解析请求报文中的请求头数据*/
    HTTP_CODE parse_headers(char *text);
    /*主状态机解析报文中的请求内容*/
    HTTP_CODE parse_content(char *text);
    /*生成响应报文*/
    HTTP_CODE do_request();

    /*m_start_line是已解析的字符*/
    /*get_line用于将指针往后偏移,指向未处理的字符*/
    char *get_line() {return m_read_buf + m_start_line;};
    
    /*从状态机读取一行,分析是请求报文的哪一部分*/
    LINE_STATUS parse_line();

    void unmap();

    /*根据响应报文的格式,生成对应的8个部分,以下函数均由do_request调用*/
    bool add_response(const char *format,...);
    bool add_content(const char *content);
    bool add_status_line(int status, const char *title);
    bool add_headers(int content_length);
    bool add_content_type();
    bool add_content_length(int content_length);
    bool add_linger();
    bool add_blank_line();

public:
    static int m_epollfd;
    static int m_user_count;
    MYSQL *mysql;
    int m_state;//读事件0,写事件1

private:
    int m_sockfd;
    sockaddr_in m_address;

    /*存储读取的请求报文*/
    char m_read_buf[READ_BUFFER_SIZE];
    /*缓冲区中m_read_buf中数据的最后一个字节的下一个位置*/
    long m_read_idx;
    /*m_read_buf读取的位置m_checked_idx*/
    long m_checked_idx;
    /*m_read_buf中已经解析的字符个数*/
    int m_start_line;

    /*存储发送的响应报文数据*/
    char m_write_buf[WRITE_BUFFER_SIZE];
    /*指示buffer中的长度*/
    int m_write_idx;

    /*主状态机的状态*/
    CHECK_STATE m_check_state;
    /*请求方法*/
    METHOD m_method;

    /*以下为解析请求报文中对应的6个变量*/
    char m_real_file[FILENAME_LEN];
    char *m_url;
    char *m_version;
    char *m_host;
    long m_content_length;
    bool m_linger;

    char *m_file_address;   //读取服务器上的文件地址
    struct stat m_file_stat;
    struct iovec m_iv[2];   //io向量机制iovec
    int m_iv_count;
    int cgi;                //是否启用的POST
    char *m_string;         //存储请求头数据
    int bytes_to_send;      //剩余发送的字节
    int bytes_have_send;    //已发送的字节
    char *doc_root;         //网站根目录

    map<string, string> m_users;
    int m_TRIGMode;         //m_TRIGMode == 1时epoll为ET触发模式
    int m_close_log;

    char sql_user[100];
    char sql_passwd[100];
    char sql_name[100];
};
#endif


定义文件

process()函数是http最为核心的函数,将process()函数的调用栈画成目录格式见下图,帮助各位同学理解各代码所处的位置。


接收连接

init

http类的初始化工作我们单独定义了一个init函数对私有成员初始化,而不是使用构造函数。我们定义两个init(),一个有参一个无参,有参init()初始化的内容是一些 根据连接进来的用户动态变化的变量,不同的连接这部分内容都不相同,而无参init()则负责将类的内部用于实现各模块功能的变量初始化,具体代码见下

int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;

void http_conn::init(int sockfd, const sockaddr_in &addr, char *root, int TRIGMode,
                    int close_log, string user, string passwd, string sqlname) {
    m_sockfd = sockfd;
    m_address = addr;

    addfd(m_epollfd, sockfd, true, m_TRIGMode);
    m_user_count++;

    /*当浏览器出现连接重置时,可能是网站根目录出错或http响应格式出错或者访问的文件中内容完全为空*/
    doc_root = root;
    m_TRIGMode = TRIGMode;
    m_close_log = close_log;

    strcpy(sql_user, user.c_str());
    strcpy(sql_passwd, passwd.c_str());
    strcpy(sql_name, sqlname.c_str());

    /*私有无参初始化*/
    init();
}

addfd


/*设置fd非阻塞*/
int setnonblocking(int fd) {
    int old_option = fcntl(fd, F_GETFL);
    int new_option =old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

/*将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT*/
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode) {
    epoll_event event;
    event.data.fd = fd;

    if (TRIGMode ==1) {
        event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
    } else {
        event.events = EPOLLIN | EPOLLRDHUP;
    }

    if (one_shot) {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

无参init()

/*初始化接收新的连接,check_state默认为分析请求行状态*/
void http_conn::init() {
    mysql = NULL;
    bytes_to_send = 0;
    bytes_have_send = 0;
    m_check_state = CHECK_STATE_REQUESTLINE;
    
    m_linger = false;
    m_method = GET;
    m_url = 0;
    m_version = 0;
    m_content_length = 0;
    m_host = 0;

    m_start_line = 0;
    m_checked_idx = 0;
    m_read_idx = 0;
    m_write_idx = 0;

    cgi = 0;
    m_state = 0;
    timer_flag = 0;
    improv = 0;

    memset(m_read_buf, '\0', READ_BUFFER_SIZE);
    memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
    memset(m_real_file, '\0', FILENAME_LEN);
}


removefd, modfd

在init里面我们调用了addfd(),addfd函数实现的功能是将传进来的sockfd(也就是真正的客户端连接)注册在epoll内核事件表上,这里就是我们项目第一次开始涉及epoll了。在基础知识部分我们介绍了三种I/O复用方式:C++ Webserver从零开始:基础知识(四)——I/O复用-CSDN博客

既然使用了epoll_ctl,那epoll_create去哪了呢?其实我们的epoll_create调用还在最顶层使用,最顶层调用了epoll_create后将得到的epollfd作为参数传递给HTTP类里的init函数,init将其用以设置事件的属性。其实我们在lst_timer类里实现过一次addfd,但是lit_timer中并没有实现removefd,modfd,大家可以想一想这之中的原理。

/*从内核事件表删除描述符*/
void removefd(int epollfd, int fd) {
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

/*将事件重置为ONESHOT*/
void modfd(int epollfd, int fd, int ev, int TRIGMode) {
    epoll_event event;
    event.data.fd = fd;

    if (TRIGMode == 1) {
        event.events = ev | EPOLLIN | EPOLLET | EPOLLRDHUP;
    } else {
        event.events = ev | EPOLLIN | EPOLLRDHUP;
    }
    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}


read_once()

初始化部分完成后,我们来看看http类流程图中做的第一个非常重要的事——将数据读到缓冲区。在这里我们定义了一块缓冲区m_read_buf专门用来存放从浏览器发送来的请求报文,并用一个指针m_read_idx记录、维护这个读缓冲区。这里就涉及到了服务器的一个重要知识点:LT模式和ET模式。

在LT模式下,epoll_wait会无数次地通知应用程序读事件的发生,直到应用程序去取。这里的应用程序是什么呢?很显然就是下面这个read_once()代码,在if ( m_TRIGMode == 0) 程序块里,应用程序用recv去将sockfd内的内容取到m_readbuf里面,如果没有取完,程序是无所谓的,它会继续往下执行,直到下一次epoll_wait再次通知,它便再进行recv操作。

而与此相对应的是另一个程序控制块,即ET模式,在ET模式下,epoll_wait只会通知应用程序一次,应用程序被要求在这一次就把sockfd中全部的数据取出,即read_once,可以看到在ET模式下,代码执行一个永不结束的循环while(true),唯有当数据全部取完(即recv返回-1并设置errno为EAGAIN或EWOULDBLOCK)时,程序才会退出,不然程序就一直循环下去。(其实当对方关闭了sock连接也会退出,但这就属于异常处理的流程,而非常规流程了)

bool http_conn::read_once() {
    if (m_read_idx >= READ_BUFFER_SIZE) {
        return false;
    }
    int bytes_read = 0;

    /*LT模式*/
    if (m_TRIGMode == 0) {
        bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
        m_read_idx += bytes_read;

        if (bytes_read <= 0) {
            return false;
        }
        return true;
    } else {
        /*ET模式*/
        while (true)
        {
            bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
            if (bytes_read == -1) {
                /*当errno == EAGAIN或EWOULDBLOCK时,表示没有数据可读,需要稍后尝试*/
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    break;
                }
                return false;
            }
            /*对方关闭连接*/
            else if (bytes_read == 0) {
                return false;
            }
            m_read_idx += bytes_read;
        }
        return true;
    }
}

到此为止,http类接收浏览器连接的代码就结束了,接下来介绍http处理连接部分的代码。


处理连接

在处理连接部分,我们设计了两个状态机主状态机/从状态机来进行报文解析。下图是处理连接的拓扑图:

  • 从状态机负责读取一行的数据
  • 当从状态机成功读完一行后,就将这行数据交给主状态机
  • 主状态机会根据报文格式以及自身状态对该请求行进行解析
  • 解析完后主状态机的状态改变,切换到下一个状态再等待从状态机的数据循环

如果是GET请求,则在CHECK_STATE_HEADER状态中调用do_request

如果是POST请求,则在CHECK_STATE_CONTENT状态中调用do_request


从状态机设计

在上面的read_once()函数中我们获得了一个字符数组m_read_buf,和一个指针m_read_idx。在从状态机中,我们开始读取m_read_buf至今为止保存的内容。这时我们需要创建一个新的指针m_checked_idx来记录每一行报文的结束地址。这是因为http报文是按行来分开不同的信息的,所以我们在确定m_checked_idx的结束地址时,可以根据C中的换行转义符 \r\n 来进行判断。下面是从状态机的运行逻辑:

  • 从状态机从m_read_buf中逐字节地读,判断当前字符是否为\r
    • 判断\r下一个字符是否为\n,若是,将\r\n改为\0\0,返回LINE_OK
    • 若下一个字符是m_read_idx说明未读完(LT),则返回LINE_OPEN
    • 否则,返回LINE_BAD
  • 判断当前字符是否是\n
    • 判断上一个字符是否是\r,若是,将\r\n改为\0\0,返回LINE_OK

大家可以思考一下,什么情况下当前字符会是\n,而上一个字符是\r(若有不清楚的,可以在评论区提问)

/*从状态机,用于分析出一行的内容
  返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN*/
http_conn::LINE_STATUS http_conn::parse_line() {
    char temp;
    for (; m_checked_idx < m_read_idx; ++m_checked_idx) {
        temp = m_read_buf[m_checked_idx];
        if (temp == '\r') {
            if ((m_checked_idx + 1) == m_read_idx) {
                return LINE_OPEN;
            } 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;
        }
        else if (temp == '\n') {
            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;
        }
    }
    return LINE_OPEN;
}

主状态机-parse_request_line函数

分析http报文格式,我们得知第一行里记录了http报文的类型,url(GET类型),版本号。我们便可以开始着手写主状态机的第一个函数了。

/*解析http请求行,获得请求方法,目标url及http版本号*/
http_conn::HTTP_CODE http_conn::parse_request_line(char *text) {
    m_url = strpbrk(text, " \t");//将找到的第一个空格或者水平制表符返回
    if (!m_url) {
        return BAD_REQUEST;
    }

    /*将该位置改为0,并将前面的数据取出*/
    *m_url++ = '\0';

    /*取出数据,通过与GET和POST比较,确定请求方式*/
    char *method = text;
    if (strcasecmp(method, "GET") == 0) {
        m_method = GET;
    } else if (strcasecmp(method, "POST") == 0) {
        m_method = POST;
        cgi = 1;
    } else 
        return BAD_REQUEST;
    
    /*m_url此时跳过了第一个空格或\t字符,但不知道后面是否还有
      将m_url向后偏移,通过查找,继续跳过空格和\t,指向请求资源的第一个字符*/
    m_url += strspn(m_url, " \t");
    /*同样的方法判断HTTP版本号*/
    m_version = strpbrk(m_url, " \t");
    if (!m_version) {
        return BAD_REQUEST;
    }
    *m_version++ = '\0';
    m_version += strspn(m_version, " \t");
    if (strcasecmp(m_version, "HTTP/1.1") != 0) {
        return BAD_REQUEST;
    }
    if (strncasecmp(m_url, "http://", 7) == 0) {
        m_url += 7;
        m_url = strchr(m_url, '/');
    }

    if (strncasecmp(m_url, "https://", 8) == 0) {
        m_url += 8;
        m_url = strchr(m_url, '/');
    }

    if (!m_url || m_url[0] != '/') {
        return BAD_REQUEST;
    }

    if (strlen(m_url) == 1) {
        strcat(m_url, "judge.html");
    }
    m_check_state = CHECK_STATE_HEADER;
    return NO_REQUEST;
}


主状态机--parse_headers函数

解析完请求行,我们再来解析请求头部,根据报文格式,以及头部字段名的种类,我们有下面的代码

/*解析http请求的一个头部信息*/
http_conn::HTTP_CODE http_conn::parse_headers(char *text) {
    /*判断是空行还是请求头*/
    if (text[0] == '\0') {
        if (m_content_length != 0) {
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        return GET_REQUEST;
    }
    /*解析请求头部连接字段*/
    else if (strncasecmp(text, "Connection:", 11) == 0) {
        text += 11;
        /*跳过空格和\t字符*/
        text += strspn(text, " \t");
        if (strcasecmp(text, "keep-alive") == 0) {
            /*如果是长连接,把m_linger设为true*/
            m_linger = true;
        }
    }
    /*解析请求头部内容长度字段*/
    else if (strncasecmp(text, "Content-length:", 15) == 0) {
        text += 15;
        text += strspn(text, " \t");
        m_content_length = atol(text);
    }
    /*解析请求头部HOST字段*/
    else if (strncasecmp(text, "Host:", 5) == 0) {
        text += 5;
        text += strspn(text, " \t");
        m_host = text;
    }
    else {
        LOG_INFO("oop!unkonw header: %s", text);
    }
    return NO_REQUEST;
}

主状态机--parse_content函数

最后解析请求数据,在我们这个项目中,POST请求发送的是用户名和密码。以此来实现注册登录功能,实际Web中parse_content的内容要多得多得多。

/*判断http请求是否被完整读入*/
http_conn::HTTP_CODE http_conn::parse_content(char *text) {
    if (m_read_idx >= (m_content_length + m_checked_idx)) {
        text[m_content_length] = '\0';
        /*POST请求中最后为输入的用户名和密码*/
        m_string = text;
        return GET_REQUEST;
    }
    return NO_REQUEST;
}

主状态机实现--process_read

实现了三个parse函数,我们现在来完成主状态机的设计。主状态机的设计主要是根据我们的拓扑图。

这里的while(循环)进入条件:

((line_status = parse_line()) == LINE_OK)

当从状态机为LINE_OK时,主状态机循环就可以继续

m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK

在POST请求报文中,因为消息体结尾没有 \r\n ,所以不会触发parse_line()解析,所以我们只能根据主状态机进行条件判断进入循环。但这会有个问题,等POST请求报文全部解析完后,m_check_state依然是CHECK_STATE_CONTENT,还是不会退出循环。这不是我们所希望的,所以我们让它加上 line_status == LINE_OK,这样当POST消息体全部解析完后,line_status会被赋值为LINE_OPEN,就不再进入主循环

有限状态机部分,比较简单,具体的逻辑在拓扑图中也有,就不细说了

http_conn::HTTP_CODE http_conn::process_read() {
    /*初始化从状态机状态,HTTP请求解析结果*/
    LINE_STATUS line_status = LINE_OK;
    HTTP_CODE ret = NO_REQUEST;
    char* text = 0;

    /*parse_line为从状态机的具体实现*/
    while( ((line_status = parse_line()) == LINE_OK) || (m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) ) {
        text = get_line();

        /*m_checked_idx是每一个数据行在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:
            {
                /*解析请求行*/
                ret = parse_request_line(text);
                if (ret == BAD_REQUEST) {
                    return BAD_REQUEST;
                }
                break;
            }
            case CHECK_STATE_HEADER:
            {
                /*解析请求头*/
                ret = parse_headers(text);
                if (ret == BAD_REQUEST) {
                    return BAD_REQUEST; 
                }
                /*完整解析GET请求后,跳转到报文响应函数*/
                else if (ret == GET_REQUEST) {
                    return do_request();
                }
                break;
            }
            case CHECK_STATE_CONTENT:
            {
                /*解析消息体*/
                ret = parse_content(text);

                /*完整解析POST请求后,跳转到报文响应函数*/
                if (ret == GET_REQUEST) {
                    return do_request();
                }

                /*解析完消息体即完成了报文解析,为了避免再次进入循环,更新line_status*/
                line_status = LINE_OPEN;
                break;
            }
        
        default:
            return INTERNAL_ERROR;
        }
    }
    return NO_REQUEST;
}


响应连接

OK,我们的http解析流程进入最后一步,响应连接。

do_request

在do_request部分,我们实现对POST请求的响应连接。同时我们在这里实现一个简易的注册登录功能。

我们要实现注册功能,就需要使用数据库,根据POST消息体中的写好的账户和密码,我们将其保存在数据库中(保存前先验证是否重复)。然后在GET请求中,我们再看数据库中的账户和密码与用户登录用的账户密码是否匹配(真正的登录注册功能实现比这要复杂),如果匹配,才返回用户请求的资源。

除此之外,我们根据m_url中最后一个‘/’的后一位的字符来判断返回给用户什么资源

http_conn::HTTP_CODE http_conn::do_request() {
    strcpy(m_real_file, doc_root);
    int len = strlen(doc_root);
    /*找到m_url中/的位置*/
    const char *p = strrchr(m_url, '/');

    /*处理CGI*/
    if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) =='3')) {
        /*根据标志判断是登录检测还是注册检测*/
        char flag = m_url[1];

        char *m_url_real = (char *)malloc(sizeof(char)* 200);
        strcpy(m_url_real, "/");
        strcat(m_url_real, m_url + 2);
        strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
        free(m_url_real);

        /*提取用户名和密码*/
        char name[100], password[100];
        int i;
        for (i = 5; m_string[i] != '&'; ++i)
            name[i - 5] = m_string[i];
        name[i - 5] = '\0';

        int j = 0;
        for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
            password[j] = m_string[i];
        password[j] = '\0';

        if (*(p + 1) == '3') {
            /*如果是注册,先检测数据库中是非有重名的
              没有重名,增加数据*/
            char *sql_insert = (char *)malloc(sizeof(char) * 200);
            strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
            strcat(sql_insert, "'");
            strcat(sql_insert, name);
            strcat(sql_insert, "', '");
            strcat(sql_insert, password);
            strcat(sql_insert, "')");

            if (users.find(name) == users.end()) {
                m_lock.lock();
                /*SQL查询*/
                int res = mysql_query(mysql,sql_insert);
                users.insert(pair<string, string>(name, password));
                m_lock.unlock();

                if (!res) {
                    strcpy(m_url, "/log.html");
                } else {
                    strcpy(m_url, "/registerError.html");
                }
            } else {
                strcpy(m_url, "/registerError.html");
            }
        }
        /*如果是登录,直接判断
        若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0*/
        else if (*(p + 1) == '2') {
            if (users.find(name) != users.end() && users[name] == password) {
                strcpy(m_url, "/welcome.html");
            } else {
                strcpy(m_url, "/logError.html");
            }
        }
    }

    /*如果请求资源为/0,表示跳转注册界面*/
    if (*(p + 1) == '0') {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/register.html");

        /*将网站目录和/register.html进行拼接,更新到m_real_file中*/
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));
        free(m_url_real);
    }
    /*请求资源为/1,表示跳转登录界面*/
    else if (*(p + 1) == '1') {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/log.html");

        strncpy(m_real_file + len, m_url_real, sizeof(m_url_real));
        free(m_url_real);
    }
    else if (*(p + 1) == '5')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/picture.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    else if (*(p + 1) == '6')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/video.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    else if (*(p + 1) == '7')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/fans.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    else {
        strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
    }

    if (stat(m_real_file, &m_file_stat) < 0) {
        return NO_RESOURCE;
    }
    /*如果文件不可读,返回FORBIDDEN_REQUEST*/
    if (!(m_file_stat.st_mode & S_IROTH)) {
        return FORBIDDEN_REQUEST;
    }
    /*如果是目录,返回BAD_REQUEST*/
    if (S_ISDIR(m_file_stat.st_mode)) {
        return BAD_REQUEST;
    }
    /*以只读方式获取文件描述符,通过mmap将该文件描述符映射到内存*/
    int fd = open(m_real_file, O_RDONLY);
    m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

    close(fd);
    /*请求文件存在,可以访问*/
    return FILE_REQUEST;

}

add_response

我们定义一个基础的往HTTP响应的缓冲区添加格式化的数据的函数,并且使用可变参数va_list增加它的可复用性。这样我们后面的各种写数据就可以直接调用add_response了。

bool http_conn::add_response(const char *format, ...) {
    /*若写入内容超出m_write_buf大小则报错*/
    if (m_write_idx >= WRITE_BUFFER_SIZE) {
        return false;
    }
    /*定义可变参数列表*/
    va_list arg_list;
    va_start(arg_list, format);

    /*将数据format从可变参数列表写入缓冲区,返回写入数据长度*/
    int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
    /*若写入的数据长度比缓冲区剩下的空间大,则报错*/
    if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)) {
        va_end(arg_list);
        return false;
    }
    /*更新m_write_idx位置*/
    m_write_idx += len;
    va_end(arg_list);
    LOG_INFO("request:%s", m_write_buf);
    return true;
}

调用add_response的函数系列

/*添加状态行*/
bool http_conn::add_status_line(int status, const char *title) {
    return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}

/*添加消息报头,具体的添加长度文本 连接状态 和空行*/
bool http_conn::add_headers(int content_len) {
    return add_content_length(content_len) && add_linger() && add_blank_line();
}
bool http_conn::add_content_length(int content_len) {
    return add_response("Content-Length:%d\r\n", content_len);
}
bool http_conn::add_content_type() {
    return add_response("Content-Type:%s\r\n", "text/html");
}
bool http_conn::add_linger() {
    return add_response("Connection:%s\r\n",(m_linger == true) ? "keep-alive" : "close");
}
bool http_conn::add_blank_line() {
    return add_response("%s", "\r\n");
}
bool http_conn::add_content(const char *content) {
    return add_response("%s", content);
}


process_write

我们在决定好要给浏览器返回什么数据前,还需要根据服务器状态的不同,将不同的数据填入写缓冲区。这与接受连接时,服务器将数据从读缓冲区读出解析相对应。在http协议中定义有状态码,这些状态码给浏览器一些信息以表示服务器资源不同的状态。其中,正常访问的状态以200开头,则在下面的代码中,唯有当HTTP_CODE状态码为FILE_REQUEST(对应200状态码)时,应用程序才正常地往缓冲区写浏览器请求的数据。

const char *ok_200_title = "OK";
const char *error_400_title = "Bad Request";
const char *error_400_form = "Your request has bad syntax or is inherently impossible to staisfy.\n";
const char *error_403_title = "Forbidden";
const char *error_403_form = "You do not have permission to get file form this server.\n";
const char *error_404_title = "Not Found";
const char *error_404_form = "The requested file was not found on this server.\n";
const char *error_500_title = "Internal Error";
const char *error_500_form = "There was an unusual problem serving the request file.\n";
bool http_conn::process_write(HTTP_CODE ret) {
    switch (ret)
    {
    case INTERNAL_ERROR:
    {
        add_status_line(500, error_500_title);
        add_headers(strlen(error_500_form));
        if (!add_content(error_500_form)) {
            return false;
        }
        break;
    }
    case BAD_REQUEST:
    {
        add_status_line(404, error_404_title);
        add_headers(strlen(error_404_form));
        if (!add_content(error_404_form)) {
            return false;
        }
        break;
    }
    case FORBIDDEN_REQUEST:
    {
        add_status_line(403, error_403_title);
        add_headers(strlen(error_403_form));
        if (!add_content(error_403_form))
            return false;
        break;
    }
    case FILE_REQUEST:
    {
        add_status_line(200, ok_200_title);
        if (m_file_stat.st_size != 0) {
            add_headers(m_file_stat.st_size);
            m_iv[0].iov_base = m_write_buf;
            m_iv[0].iov_len = m_write_idx;
            m_iv[1].iov_base = m_file_address;
            m_iv[1].iov_len = m_file_stat.st_size;
            m_iv_count = 2;
            bytes_to_send = m_write_idx + m_file_stat.st_size;
            return true;
        } else {
            const char *ok_string = "<html><body></body></html>";
            add_headers(strlen(ok_string));
            if (!add_content(ok_string)) {
                return false;
            }
        }
    }
    default:
        return false;
    }

    m_iv[0].iov_base = m_write_buf;
    m_iv[0].iov_len = m_write_idx;
    m_iv_count = 1;
    bytes_to_send = m_write_idx;
    return true;
}

write

在写缓冲区写满要发送的数据后,我们最后调用write将其发送给浏览器。这里要注意的API是writev,writev用于一次函数调用写多个非连续缓冲区。在循环里面调用writev时,需要重新处理iovec向量中的指针和长度,writev不会对它们作任何处理。

在write函数中,服务器的子线程注册epollout事件,将缓冲区中的数据存入sock文件描述符,发给浏览器,浏览器进行解析,然后把画面渲染出来,一个网络地址访问的一生就结束了。

bool http_conn::write() {
    int temp = 0;
    
    /*响应报文为空*/
    if (bytes_to_send == 0) {
        modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
        init();
        return true;
    }

    while (1) {
        /*将响应报文状态行,消息头,空行和响应正文发给浏览器端*/
        temp = writev(m_sockfd, m_iv, m_iv_count);

        /*未发送*/
        if (temp < 0) {
            /*判断缓冲区是否满了*/
            if (errno == EAGAIN) {
                modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
                return true;
            }
            unmap();
            return false;
        }

        bytes_have_send += temp;
        bytes_to_send -= temp;
        /*第一个iovec头部信息已发完,发第二个*/
        if (bytes_have_send >= m_iv[0].iov_len) {
            m_iv[0].iov_len = 0;
            m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
            m_iv[1].iov_len = bytes_to_send;
        } else {
            /*继续发第一个*/
            m_iv[0].iov_base = m_write_buf + bytes_have_send;
            m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
        }
        /*数据已全部发完*/
        if (bytes_to_send <= 0) {
            unmap();
            /*epoll上重置EPOLLONESHOT事件*/
            modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
            /*请求为长连接*/
            if (m_linger) {
                /*重新初始化HTTP对象*/
                init();
                return true;
            } else {
                return false;
            }
        }
    }
}

封装运行函数--process

最后,我们将http内部的代码封装成一个简单的process函数,将我们所有的设计,细节,心血藏于这短短11行普普通通的代码,这也许就是封装的魅力吧。

void http_conn::process() {
    HTTP_CODE read_ret = process_read();
    if (read_ret == NO_REQUEST) {
        modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
        return;
    }
    bool write_ret = process_write(read_ret);
    if (!write_ret) {
        close_conn();
    }
    modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}

结束语

HTTP类我们就完成了,下一章我们开始写最顶层的一个server封装类。将我们至今为止写的所有代码,封装到一个类里,去实现它们的使命。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/413283.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

部署roop实现视频人脸替换

roop只需要一张人脸的图像&#xff0c;就可以替换视频中的脸。不需要数据集和模型训练。 下载对应版本的cudnn https://developer.nvidia.com/rdp/cudnn-archivehttps://developer.nvidia.com/rdp/cudnn-archive解压后的三个文件夹拷贝到cuda的目录 C:\Program Files\NVIDIA…

【非递归版】归并排序算法(2)

目录 MergeSortNonR归并排序 非递归&归并排序VS快速排序 整体思想 图解分析​ 代码实现 时间复杂度 归并排序在硬盘上的应用&#xff08;外排序&#xff09; MergeSortNonR归并排序 前面的快速排序的非递归实现&#xff0c;我们借助栈实现。这里我们能否也借助栈去…

uniapp实现单选框

采用uniapp-vue3实现的一款单选框组件&#xff0c;提供丝滑的动画选中效果&#xff0c;支持不同主题配置&#xff0c;适配web、H5、微信小程序&#xff08;其他平台小程序未测试过&#xff0c;可自行尝试&#xff09; 可到插件市场下载尝试&#xff1a; https://ext.dcloud.net…

SpringCloudAlibaba全家桶介绍

Spring Cloud Alibaba Spring Cloud Alibaba 是什么&#xff1f;微服务全景图核心特色 大家好&#xff0c;我叫阿明。下面我会为大家准备Spring Cloud Alibaba系列知识体系&#xff0c;结合实战输出案列&#xff0c;让大家一眼就能明白得技术原理&#xff0c;应用于各公司得各…

二次供水物联网:HiWoo Cloud助力城市水务管理升级

随着城市化的快速推进&#xff0c;二次供水系统作为城市基础设施的重要组成部分&#xff0c;其稳定运行和高效管理显得至关重要。然而&#xff0c;传统的二次供水管理方式在应对复杂多变的城市供水需求时&#xff0c;显得力不从心。为了破解这一难题&#xff0c;HiWoo Cloud平台…

VsCode的leetcode插件无法登录

前提 想使用VsCode的leetcode插件进行刷题&#xff0c;然后按照网上的教程进行安装下载&#xff0c;但是到了登录这一步&#xff0c;死活也登录不了&#xff0c;然后查看log一直报的错误是invalid password。 解决方法 首先确定在插件中设置的站点是Leetcode中国&#xff0c…

【Java EE初阶二十五】简单的表白墙(一)

1. 前端部分 1.1 前端代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"wid…

继电器测试中需要注意的安全事项有哪些?

继电器广泛应用于电气控制系统中的开关元件&#xff0c;其主要功能是在输入信号的控制下实现输出电路的断开或闭合。在继电器测试过程中&#xff0c;为了确保测试的准确性和安全性&#xff0c;需要遵循一定的安全事项。以下是在进行继电器测试时需要注意的安全事项&#xff1a;…

【代码随想录python笔记整理】第十三课 · 链表的基础操作 1

前言:本笔记仅仅只是对内容的整理和自行消化,并不是完整内容,如有侵权,联系立删。 一、链表 在之前的学习中,我们接触到了字符串和数组(列表)这两种结构,它们具有着以下的共同点:1、元素按照一定的顺序来排列。2、可以通过索引来访问数组中的元素和字符串中的字符。由此,…

go环境安装-基于vscode的Windows安装

1、vscode安装 官网链接&#xff1a;https://code.visualstudio.com/ 选择相应的版本&#xff0c;这里选择Windows下的 下载得到一个VSCodeUserSetUp-x64的可执行文件&#xff0c;双击执行&#xff0c;选择要安装的路径&#xff0c;下一步。 2、go语言安装 官网链接&#x…

后端程序员入门react笔记(五)ajax请求

常见的ajax Ajax 最原始的方式&#xff0c;基于原生的js XmlHttpRequest 多个请求之间如果有先后关系&#xff0c;会存在很多层回调的问题&#xff0c;也是基于原生js Jquery Ajax 基于原生XHR封装&#xff0c;依赖Jquery框架&#xff0c;由jquery 框架去封装原生的XML(Xml)封…

git commit 后,本地远端都没有记录,消失不见

今天git commit 之后发现远端没有记录&#xff0c;本地没有最新代码记录 git commit 后&#xff0c;提交记录会消失不见的原因可能是&#xff1a; git只git commit了&#xff0c;没有push到远程分支&#xff0c;切换到其他分支时丢失。而且看不到提交记录&#xff0c;和找不到…

命令执行 [UUCTF 2022 新生赛]ez_rce

打开题目 得到题目源码 居然都不输入参数&#xff0c;可恶!!!!!!!!!<?php ## 放弃把&#xff0c;小伙子&#xff0c;你真的不会RCE,何必在此纠结呢&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#…

2024首更,Smartbi产品功能更新,用户体验更丝滑

Smartbi用户可以在官网下载Smartbi V11最新版本&#xff08;https://www.smartbi.com.cn/download&#xff09;&#xff08;PC端下载&#xff09;更新后可以使用相关功能&#xff0c;也可以在思迈特官网体验中心体验相关功能。 交互仪表盘 ▍指标拆解树组件支持从右到左展开指标…

应用回归分析:弹性网络回归

弹性网络回归&#xff1a;原理、优势与应用 弹性网络回归&#xff08;Elastic Net Regression&#xff09;是一种广泛使用的线性回归方法&#xff0c;它结合了岭回归&#xff08;Ridge Regression&#xff09;和套索回归&#xff08;Lasso Regression&#xff09;的特点。通过…

搭建Facebook直播网络对IP有要求吗?

在当今数字化时代&#xff0c;Facebook直播已经成为了一种极具吸引力的社交形式&#xff0c;为个人和企业提供了与观众直接互动的机会&#xff0c;成为推广产品、分享经验、建立品牌形象的重要途径。然而&#xff0c;对于许多人来说&#xff0c;搭建一个稳定、高质量的Facebook…

算法竞赛--对拍

对拍需要 loop.bat、makedate.exe、a.in、a.exe、a.out、std.exe、std.out ,注意这几个文件要全部在同一文件夹下。 loop.bat 比较代码&#xff08;在记事本里写&#xff0c;后缀改成.bat) :loopmakedataastdfc std.out a.outif %errorlevel%0 goto loop pause makedata.exe…

面试redis篇-09redis分布式锁

原理 Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则 SET)的简写 Redis实现分布式锁如何合理的控制锁的有效时长? 根据业务执行时间预估 给锁续期 redisson实现的分布式锁-可重入 利用hash结构记录线程id和重入次数 redisson实现的分…

Programming Abstractions in C阅读笔记:p303-p305

《Programming Abstractions in C》学习第74天&#xff0c;p303-p305总结&#xff0c;总计3页。 一、技术总结 1.时间复杂度分类(complexity classes) ClassNotationExampleconstantO(1)Returning the first element in an arraylogarithmicO(logN)Binary search in a sorte…

防火墙的内容安全

目录 1. 内容安全 1.1 IAE引擎 DPI---深度包检测技术 DFI---深度流检测技术 结论(优缺点)&#xff1a; 1.2 入侵防御&#xff08;检测&#xff09;(IPS) IPS的优势: 入侵检测的方法: 入侵检测的流程 签名 查看预定义签名的内容 新建自定义签名 入侵防御的检测…