TinyWebServer项目小白逐行解析第五集——http_conn

对于像我这样0项目经验的小白想学习Linux Web服务器来说,WebServer项目无疑是最好的一个Web服务器项目。我站在各位大神的肩膀上记录一步步剖析这个项目的过程,也给其他跟我一样苦于看源码头疼的初学者一点帮助。

Github链接:qinguoyi/TinyWebServer   Linux下C++轻量级WebServer服务器


http_conn图例 

均为个人绘制,仅用于学习交流

前置知识

1.主从状态机

在 http_conn 类中使用主从状态机来解析 HTTP 请求,主要是为了将复杂的 HTTP 请求解析过程进行模块化拆分,以提高代码的可读性、可维护性和执行效率。下面详细解释使用主从状态机的原因:

        1.降低复杂度:HTTP 请求通常由请求行、请求头和请求体组成,每个部分都有自己的格式和规则。主从状态机将解析过程拆分成多个小的状态,使得每个状态只负责处理特定的任务。从状态机则专注于解析每一行的内容。这

        2.提高可读性和可维护性:使用状态机可以将不同的解析逻辑封装在不同的状态中,使得代码结构更加清晰。

        3.增强灵活性:状态机的设计使得代码具有更好的灵活性。可以根据需要添加、删除或修改状态,以适应不同的 HTTP 协议版本或自定义的请求格式。

        4.提高解析效率:通过状态机的方式,可以在解析过程中及时发现错误并进行处理,避免不必要的解析操作。根据不同状态进行不同的处理

在该项目中,以报文解析状态和行读取状态为从状态机,以正在检查报文的哪一部分作为主状态机。

2.HTTP报文

http_conn类就是被设计用来处理用户/客户端发来的报文请求,最主要的报文为POST和GET报文,其中所有的通信细节都被隐藏在了socket通信中,它使用了TCP协议进行通信,并利用了Epoll,在此类中,先不讨论socket,先聚焦于http报文的解析,把socket当成是一个报文消息的载体,从客户端这边将消息/报文传递给了客户端。

暂且不讨论socket编程的细节


总体概述

http_conn.h相关函数

public:
    http_conn() {} //http_conn类初始化
    ~http_conn() {}

    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);
    //其主要功能是关闭 HTTP 连接,并对相关资源进行清理。
    void process();
    //处理 HTTP 请求和响应。其主要功能由socket和epoll完成。
    bool read_once();
    //从客户端套接字读取数据到 m_read_buf 缓冲区中。
    bool write();
    //该函数的主要功能是将 HTTP 响应数据通过 writev 函数写入套接字 m_sockfd,
    //支持分散写(writev 可以将多个缓冲区的数据一次性写入),实现HTTP响应的发送。
    
    sockaddr_in *get_address() 
    {
        return &m_address;
    }
    //用于返回 http_conn 类中私有成员变量 m_address 的地址。
    //m_address 是一个 sockaddr_in 类型的变量,
    //通常用于存储客户端的网络地址信息,如 IP 地址和端口号。
    
    void initmysql_result(connection_pool *connPool); 
    //初始化(获取)账号密码,用于用户登录。
    
    int timer_flag;
    //该变量一般作为一个标志位,用于指示是否需要触发定时器相关的操作。
    int improv;
    //这个变量可能用于记录事件处理的改进状态。


private:
    void init();
    //默认初始化函数
    HTTP_CODE process_read();
    //解析 HTTP 请求的主函数
    bool process_write(HTTP_CODE ret);
    //依据 HTTP 请求处理结果(HTTP_CODE 类型的 ret)来构建 HTTP 响应,并将响应数据准备好以便发送。
    HTTP_CODE parse_request_line(char *text);
    //解析 HTTP 请求信息,从中提取请求方法、目标 URL 以及 HTTP 版本号。
    HTTP_CODE parse_headers(char *text);
    //主要功能是解析 HTTP 请求的头部带有的信息。这里不是提取请求方法和URL的函数。
    HTTP_CODE parse_content(char *text);
    //解析 HTTP 请求的请求体内容。
    HTTP_CODE do_request();
    //该函数的主要作用是处理 HTTP 请求,依据请求的 URL 进行不同的处理
    //比如处理 CGI 请求(注册、登录),然后确定要返回给客户端的文件,最后将文件映射到内存中。
    
    char *get_line() { return m_read_buf + m_start_line; };
    
    LINE_STATUS parse_line();
    //是从已读取的 HTTP 请求数据中解析出一行内容。HTTP 请求由多行文本组成,
    //每行以 \r\n(回车换行)结尾。
    
    void unmap();
    //该函数的主要功能是解除之前通过 mmap 函数建立的文件映射。
    bool add_response(const char *format, ...);
    //该函数用于将格式化的字符串添加到 m_write_buf 缓冲区中,支持可变参数。
    bool add_content(const char *content);
    //添加 HTTP 响应的内容。
    bool add_status_line(int status, const char *title);
    //添加 HTTP 响应的状态行
    bool add_headers(int content_length);
    //添加 HTTP 响应的头部信息。
    bool add_content_type();
    //添加 Content-Type 头部字段,用于指定响应内容的类型。
    bool add_content_length(int content_length);
    //添加 Content-Length 头部字段,用于指定响应内容的长度。
    bool add_linger();
    //添加 Connection 头部字段,用于指定连接的状态。
    bool add_blank_line();
    //添加一个空行,用于分隔 HTTP 头部和响应内容。


};

#endif

http_conn.h相关成员变量

static const int FILENAME_LEN = 200; //文件名最大长度
static const int READ_BUFFER_SIZE = 2048; //读缓冲区大小
static const int WRITE_BUFFER_SIZE = 1024; //写缓冲区大小

public:
    static int m_epollfd; //存储 epoll 实例的文件描述符。
    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]; //读缓冲区
    int m_read_idx;//读缓冲区中当前已读取的字节数
    int m_checked_idx;//已检查的字节数
    int m_start_line;//请求行的起始位置
    char m_write_buf[WRITE_BUFFER_SIZE];//写缓冲区
    int m_write_idx;//写缓冲区中当前已写入的字节数
    
    CHECK_STATE m_check_state;//主状态机的当前状态
    METHOD m_method;//请求方法
    char m_real_file[FILENAME_LEN];//请求的真实文件路径
    char *m_url;//请求的URL
    char *m_version;//HTTP协议版本
    char *m_host;//主机名
    int m_content_length;//请求体的长度
    bool m_linger;//是否保持连接
    char *m_file_address;//文件映射的内存起始地址
    struct stat m_file_stat;//文件状态信息
    struct iovec m_iv[2];//iovec结构体数组,用于分散读取和写入
    int m_iv_count;//iovec结构体数组的元素个数
    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;//触发模式
    int m_close_log;//是否关闭日志
    char sql_user[100];//数据库用户名
    char sql_passwd[100];//数据库密码
    char sql_name[100];//数据库名称


http请求方法

enum METHOD     //http请求方法
{
    GET = 0,    // GET 请求方法,用于获取资源
    POST,       // POST 请求方法,用于向服务器提交数据
    HEAD,       // HEAD 请求方法,类似于 GET 请求,但只返回响应头
    PUT,        // PUT 请求方法,用于更新资源
    DELETE,     // DELETE 请求方法,用于删除资源
    TRACE,      // TRACE 请求方法,用于测试服务器的可达性
    OPTIONS,    // OPTIONS 请求方法,用于获取服务器支持的请求方法
    CONNECT,    // CONNECT 请求方法,用于建立隧道连接
    PATH        
};

主从状态机变量

enum CHECK_STATE    //主状态机的状态
{
    CHECK_STATE_REQUESTLINE = 0,    //当前正在分析请求行
    CHECK_STATE_HEADER,             //当前正在分析头部字段
    CHECK_STATE_CONTENT             //当前正在解析请求体
};
enum HTTP_CODE    // 从状态机的可能状态,报文解析的结果
{
    NO_REQUEST,     // 未收到完整的 HTTP 请求
    GET_REQUEST,    // 成功解析出一个完整的 HTTP GET 请求
    BAD_REQUEST,    // 请求格式错误,不符合 HTTP 协议规范
    NO_RESOURCE,    // 请求的资源在服务器上不存在
    FORBIDDEN_REQUEST, // 客户端没有权限访问请求的资源
    FILE_REQUEST,   // 请求的是一个文件资源,且服务器可以找到该文件
    INTERNAL_ERROR, // 服务器在处理请求时发生内部错误
    CLOSED_CONNECTION // 客户端已关闭连接或服务器决定关闭连接
};
enum LINE_STATUS    //从状态机的可能状态,行的读取状态
{
    LINE_OK = 0, //值为 0,表示当前行已经完整读取,并且格式正确
    LINE_BAD, //表示当前行的格式错误,不符合 HTTP 请求的规范。可能是在读取过程中发现了非法字符
    LINE_OPEN //表示当前行还未读取完整,可能是因为数据还没有全部到达,需要继续等待更多的数据
};

函数详解

void http_conn::initmysql_result(connection_pool connPool)

功能:初始化(获取)账号密码,用于用户登录。

1.其中使用到了connectionRAII,mysql这个变量再http_conn线程销毁时将自动释放其mysql资源。

2.传入connPool是为了获取线程池单例,进而得到mysql连接

void http_conn::initmysql_result(connection_pool *connPool)
{
    MYSQL *mysql = NULL;
    connectionRAII mysqlcon(&mysql, connPool);//从连接池中取一个连接
    //在user表中检索username,passwd数据
    if (mysql_query(mysql, "SELECT username,passwd FROM user"))
    {
        LOG_ERROR("SELECT error:%s\n", mysql_error(mysql));
    }
    MYSQL_RES *result = mysql_store_result(mysql);    //从表中检索完整的结果集
    int num_fields = mysql_num_fields(result);    //返回结果集中的列数
    MYSQL_FIELD *fields = mysql_fetch_fields(result);    //返回所有字段结构的数组,其中包括了result中的各种信息

    while (MYSQL_ROW row = mysql_fetch_row(result)) //从结果集中获取下一行,将对应的用户名和密码,存入map中
    {
        string temp1(row[0]);
        string temp2(row[1]);
        users[temp1] = temp2;
    }
}

int setnonblocking(int fd)

功能:将文件描述符设置为非阻塞状态。在非阻塞模式下,对该文件描述符的读写操作将不会阻塞线程,而是立即返回,这样可以提高程序的并发性能。

1.按位或操作 | :如果两个对应位中至少有一个为1,则结果位为1;只有当两个对应位都为0时,结果位才为0。

int setnonblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL); //得到文件描述符fd当前的状态标志位
    int new_option = old_option | O_NONBLOCK; //按位或操作,计算得到新状态标志
    fcntl(fd, F_SETFL, new_option);  //修改当前文件描述符
    return old_option;
}

void addfd(int epollfd, int fd, bool one_shot, int TRIGMode)

功能:是将指定的文件描述符 fd 添加到 epoll 实例(由 epollfd 表示)中,并设置相应的事件监听和属性。

1.epoll_event是 Linux 系统中用于 epoll 机制的一个结构体,在 <sys/epoll.h> 头文件中定义。epoll 是一种高效的 I/O 多路复用机制,用于同时监控多个文件描述符的 I/O 事件。epoll_event 结构体用于描述要监听的事件以及与该事件相关的数据

2.使用了epoll_ctl函数,其中EPOLL_CTL_ADD参数将文件描述符fd添加到epoll实例中,并设置要监听的事件。其中的epollfd为已经初始化的epoll实例。

3.fd 参数:fd 代表要关闭的文件描述符,它是一个非负整数,是操作系统为每个打开的文件、套接字等资源分配的唯一标识符

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

    if (1 == TRIGMode)
        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); //设置为非阻塞状态
}

epoll_event

struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

events字段表示要监听的事件类型,可以是以下值之一:

EPOLLIN:表示对应的文件描述符上有数据可读

EPOLLOUT:表示对应的文件描述符上可以写入数据

EPOLLRDHUP:表示对端已经关闭连接,或者关闭了写操作端的写入

EPOLLPRI:表示有紧急数据可读

EPOLLERR:表示发生错误

EPOLLHUP:表示文件描述符被挂起

EPOLLET:表示将epoll设置为边缘触发模式

EPOLLONESHOT:表示将事件设置为一次性事件

epoll_data

表示用户数据,它的类型是一个union,可以存放一个指针或文件描述符等数据。

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

epoll_ctl

 Linux 系统中用于控制 epoll 实例的系统调用函数,它允许你向 epoll 实例中添加、修改或删除文件描述符及其对应的事件监听。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd:epoll 实例的文件描述符。 op:操作类型,有以下三种:

EPOLL_CTL_ADD:将指定的文件描述符 fd 添加到 epoll 实例中,并设置要监听的事件。

EPOLL_CTL_MOD:修改已经存在于 epoll 实例中的文件描述符 fd 的监听事件。

EPOLL_CTL_DEL:从 epoll 实例中移除指定的文件描述符 fd,此时 event 参数可以为 NULL。

fd:要操作的文件描述符

event:指向 epoll_event 结构体的指针,用于指定要监听的事件和关联的数据。在 EPOLL_CTL_DEL 操作中,此参数可以为 NULL。

void removefd(int epollfd, int fd)

功能:功能是将指定的文件描述符fd从epoll实例中移除,并关闭该文件描述符。

1.使用了epoll_ctl函数,EPOLL_CTL_DEL为删除epollfd实例中的fd文件描述符。

2.close 函数:是一个系统调用,定义在 <unistd.h> 头文件中。其作用是关闭指定的文件描述符 fd,释放系统为该文件描述符分配的资源。当一个文件描述符被关闭之后,就不能再用它来进行 I/O 操作了。

void removefd(int epollfd, int fd)
{
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

void modfd(int epollfd, int fd, int ev, int TRIGMode)

功能:修改 epoll 实例中指定文件描述符的监听事件。

1.利用局部变量event,存储和设置需要修改的指定文件描述符的监听的事件信息。

2.EPOLLET为边缘触发

void modfd(int epollfd, int fd, int ev, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;

    if (1 == TRIGMode)
        event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    else
        event.events = ev | EPOLLONESHOT | EPOLLRDHUP;

    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

根据 TRIGMode 的值来设置 event.events

若 TRIGMode 为 1,使用按位或操作(|)将 ev、EPOLLET、EPOLLONESHOT 和 EPOLLRDHUP 组合起来。EPOLLET 表示边缘触发模式,EPOLLONESHOT 表示该文件描述符上的事件只触发一次,

POLLRDHUP 表示对端关闭连接或半关闭连接。 若 TRIGMode 不为 1,使用按位或操作(|)将 ev、EPOLLONESHOT 和 EPOLLRDHUP 组合起来,即使用水平触发模式。

void http_conn::close_conn(bool real_close)

功能:其主要功能是关闭 HTTP 连接,并对相关资源进行清理。

1.m_sockfd 是连接的套接字文件描述符

2.调用 removefd 函数,将该套接字文件描述符从 epoll 实例中移除,并关闭该文件描述符。m_epollfd 是 epoll 实例的文件描述符

void http_conn::close_conn(bool real_close)
{
    if (real_close && (m_sockfd != -1))
    {
        printf("close %d\n", m_sockfd);
        removefd(m_epollfd, m_sockfd);
        m_sockfd = -1;  //将 m_sockfd 设置为 -1,表示该连接已经关闭
        m_user_count--; //将当前连接的用户数量减 1,m_user_count 是 http_conn 类的静态成员变量,用于记录当前的连接用户数量。
    }
}

void http_conn::init(int sockfd, const sockaddr_in &addr, char root, int TRIGMode,int close_log, string user, string passwd, string sqlname)

功能:作用是初始化一个新的 HTTP 连接。

sockfd:新连接的套接字文件描述符,用于后续的网络 I/O 操作。

addr:客户端的地址信息,类型为 sockaddr_in。

root:网站的根目录,用于定位请求的文件。

TRIGMode:触发模式,用于指定 epoll 的触发方式(如边缘触发或水平触发)。

close_log:是否关闭日志记录的标志。

user:数据库用户名。

passwd:数据库用户密码。

sqlname:数据库名

1.调用addfd函数,使用EPOLLONESHOT 标志,以确保每个连接的事件只触发一次

2.调用init()初始化函数。

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++; //连接的客户数量+1

    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();
}


void http_conn::init()

功能:对 http_conn 对象的各个成员变量进行初始化操作,让对象处于一个已知的初始状态,以便后续处理 HTTP 请求。

1.memset 是 C/C++ 标准库中的一个函数,它的原型定义在 <string.h> 头文件中,用于将一段内存区域的每个字节都设置为指定的值。

void http_conn::init()
{
    mysql = NULL;    // 将 MySQL 连接指针置为 NULL,表示当前没有连接到 MySQL 数据库
    bytes_to_send = 0;    // 初始化要发送的字节数为 0
    bytes_have_send = 0;    // 初始化已发送的字节数为 0
    m_check_state = CHECK_STATE_REQUESTLINE;    // 设置检查状态为分析请求行状态,意味着从解析请求行开始处理 HTTP 请求
    m_linger = false;    // 初始化连接保持标志为 false,即默认不保持连接
    m_method = GET;    // 初始化请求方法为 GET,默认处理 GET 请求
    m_url = 0;    // 初始化请求 URL 指针为 0,表示还未解析到请求 URL
    m_version = 0;    // 初始化 HTTP 版本指针为 0,表示还未解析到 HTTP 版本
    m_content_length = 0;    // 初始化请求体长度为 0
    m_host = 0;    // 初始化请求头中的主机地址指针为 0,表示还未解析到主机地址
    m_start_line = 0;    // 初始化起始行索引为 0
    m_checked_idx = 0;    // 初始化已检查的索引为 0,用于标记在读取缓冲区中已经检查过的位置
    m_read_idx = 0;    // 初始化读取索引为 0,用于标记在读取缓冲区中已经读取的位置
    m_write_idx = 0;    // 初始化写入索引为 0,用于标记在写入缓冲区中已经写入的位置
    cgi = 0;    // 初始化 CGI 标志为 0,表示默认不使用 CGI 程序
    m_state = 0;    // 初始化状态变量为 0
    timer_flag = 0;    // 初始化定时器标志为 0
    improv = 0;    // 初始化改进标志为 0

    memset(m_read_buf, '\0', READ_BUFFER_SIZE);    // 将读取缓冲区的内容全部置为 '\0',清空读取缓冲区
    memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);    // 将写入缓冲区的内容全部置为 '\0',清空写入缓冲区
    memset(m_real_file, '\0', FILENAME_LEN);    // 将存储真实文件路径的缓冲区内容全部置为 '\0',清空该缓冲区

}

static const int FILENAME_LEN = 200; //文件名最大长度

static const int READ_BUFFER_SIZE = 2048; //读缓冲区大小

static const int WRITE_BUFFER_SIZE = 1024; //写缓冲区大小

CR(Carriage Return):即回车符,对应的 ASCII 码值是 13,在代码里用 \r 表示。

LF(Line Feed):也就是换行符,对应的 ASCII 码值是 10,在代码里用 \n 表示。

SP(space):表示空格字符。空格字符的 ASCII 码值是 32,' '表示

http_conn::LINE_STATUS http_conn::parse_line()

功能:是从已读取的 HTTP 请求数据中解析出一行内容。HTTP 请求由多行文本组成,每行以 \r\n(回车换行)结尾。该函数通过遍历读取缓冲区,查找 \r 和 \n 字符来确定行的结束位置

1.返回值是LINE_STATUS,即每一行的读取状态(从状态机)。将逐个字符读出read_buf中的字符,并判断当前的行状态。

enum LINE_STATUS    //从状态机的可能状态,行的读取状态
{
    LINE_OK = 0,
    LINE_BAD,
    LINE_OPEN
};

2.在C和C++里,字符串是以字符数组的形式存储的,并且用 '\0' 来标记字符串的结束,这样子所读出的read_buf就能分割字符串

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') //如果 temp 为 \r,表示可能遇到了行的结束。
        {
            if ((m_checked_idx + 1) == m_read_idx) //如果 \r 是读取缓冲区的最后一个字符,说明行还未读取完整,返回 LINE_OPEN。
                return LINE_OPEN;
            else if (m_read_buf[m_checked_idx + 1] == '\n') //如果 \r 后面紧跟着 \n,说明找到了完整的行结束符 \r\n。
            {
                m_read_buf[m_checked_idx++] = '\0';//将 \r 和 \n 替换为 \0,以便将该行作为一个以 \0 结尾的字符串处理。
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;//返回 LINE_OK 表示行读取完整。
            }
            return LINE_BAD; //如果 \r 后面不是 \n,说明行格式错误,返回 LINE_BAD。
        }
        else if (temp == '\n')  //如果 temp 为 \n,表示可能遇到了行的结束。
        {
            if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r') //如果 \n 前面是 \r,说明找到了完整的行结束符 \r\n。
            {
                m_read_buf[m_checked_idx - 1] = '\0'; //将 \r 和 \n 替换为 \0,以便将该行作为一个以 \0 结尾的字符串处理。
                m_read_buf[m_checked_idx++] = '\0';
                return LINE_OK;
            }
            return LINE_BAD;
        }
    }
    return LINE_OPEN; //如果遍历完整个读取缓冲区都没有找到完整的行结束符,说明行还未读取完整,返回 LINE_OPEN。
}

bool http_conn::read_once()

功能:从客户端套接字读取数据到 m_read_buf 缓冲区中。该函数会根据不同的触发模式(LT 或 ET)采用不同的读取策略。

recv函数

用于网络编程的系统调用函数,主要用于从套接字接收数据。

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

sockfd:这是一个整数类型的参数,代表要接收数据的套接字描述符。

buf:这是一个指向缓冲区的指针,用于存储接收到的数据。

len:这是一个 size_t 类型的参数,代表缓冲区的最大容量。

flags:这是一个整数类型的参数,用于指定接收数据的额外选项。当设置为 0 时,表示使用默认行为。

如果函数调用成功,recv 会返回实际接收到的字节数。

若函数调用失败,recv 会返回 -1,并且会设置 errno 来指示具体的错误类型。

2.在边缘触发模式中,将数据一次性读出,如果 bytes_read 为 -1,表示读取出错。检查 errno 的值,如果是 EAGAIN 或 EWOULDBLOCK,说明当前没有数据可读,跳出循环;否则返回 false。

bool http_conn::read_once()
{
    if (m_read_idx >= READ_BUFFER_SIZE) 
    {
        return false;//缓冲区已满,无法再读取数据,函数返回 false。
    }
    int bytes_read = 0; //每次读取的字节数

    //LT读取数据
    if (0 == m_TRIGMode) //根据触发模式判断是LT还是ET
    {
        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;
    }
    //ET读数据
    else
    {
        while (true)
        {
            bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
            if (bytes_read == -1)
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK)//说明当前没有数据可读,跳出循环
                    break;
                return false;
            }
            else if (bytes_read == 0)
            {
                return false;
            }
            m_read_idx += bytes_read;
        }
        return true;
    }
}

在请求方法和URL之间存在sp以隔开两者。

http_conn::HTTP_CODE http_conn::parse_request_line(char text)

功能:解析 HTTP 请求信息,从中提取请求方法、目标 URL 以及 HTTP 版本号。

1.返回值类型为 HTTP_CODE,表示解析结果的状态码

enum HTTP_CODE    // 从状态机的可能状态,报文解析的结果
{
    NO_REQUEST,     // 未收到完整的 HTTP 请求
    GET_REQUEST,    // 成功解析出一个完整的 HTTP GET 请求
    BAD_REQUEST,    // 请求格式错误,不符合 HTTP 协议规范
    NO_RESOURCE,    // 请求的资源在服务器上不存在
    FORBIDDEN_REQUEST, // 客户端没有权限访问请求的资源
    FILE_REQUEST,   // 请求的是一个文件资源,且服务器可以找到该文件
    INTERNAL_ERROR, // 服务器在处理请求时发生内部错误
    CLOSED_CONNECTION // 客户端已关闭连接或服务器决定关闭连接
};

2.strpbrk 是 C 语言标准库 <string.h> 中的一个函数,用于在一个字符串中查找另一个字符串中任意字符首次出现的位置。

strpbrk 函数用于解析 HTTP 请求行,找出请求方法和 URL 之间的分隔符(空格或制表符)。

char *strpbrk(const char *str1, const char *str2);

3.strcasecmp:用于字符串比较的函数,它定义在 <strings.h> 头文件中。这个函数与 strcmp 类似,但它在比较时会忽略字符串中字母的大小写。

int strcasecmp(const char *s1, const char *s2);

4.strspn:C语言标准库 <string.h> 中的一个函数,用于计算字符串开头连续包含指定字符集中字符的长度。

size_t strspn(const char *str1, const char *str2);

5.strchr:是 C 语言标准库中的一个函数,用于在字符串中查找指定字符的第一次出现位置

char *strchr(const char *str, int c);

6.strcat 是一个标准库函数,其作用是把一个字符串连接到另一个字符串的末尾。

char *strcat(char *dest, const char *src);
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
    m_url = strpbrk(text, " \t");
    if (!m_url)
    {
        return BAD_REQUEST; //找不到URL说明这个HTTP请求有误
    }
    *m_url++ = '\0'; 
    //把找到的分隔符替换成字符串结束符 '\0',这样 text 就只包含请求方法,
    //同时将 m_url 指针后移一位,使其指向 URL 的起始位置。
    char *method = text;  //得到请求方法
    if (strcasecmp(method, "GET") == 0)
        m_method = GET;
    else if (strcasecmp(method, "POST") == 0)
    {
        m_method = POST;  //如果是 POST 请求,将 cgi 标志设置为 1,表示需要进行 CGI 处理。
        cgi = 1;
    }
    else
        return BAD_REQUEST;  //方法找不到,HTTP请求有误
    m_url += strspn(m_url, " \t"); //计算 m_url 字符串中开头连续的空格或制表符的数量
                                //然后,m_url 指针会向后移动这个数量,从而跳过这些空格和制表符。
    m_version = strpbrk(m_url, " \t"); //找出版本号和 URL 之间的分隔符(空格或制表符)。
    if (!m_version)
        return BAD_REQUEST;
    *m_version++ = '\0';
       //把找到的分隔符替换成字符串结束符 '\0',这样 m_url就只含有URL,
      //同时将 m_version指针后移一位,使其指向 version的起始位置。
    m_version += strspn(m_version, " \t");//计算 m_version 字符串中开头连续的空格或制表符的数量
                            //然后,m_version 指针会向后移动这个数量,从而跳过这些空格和制表符
    if (strcasecmp(m_version, "HTTP/1.1") != 0) //版本号
        return BAD_REQUEST;
    if (strncasecmp(m_url, "http://", 7) == 0) //URL
    {
        m_url += 7;
        m_url = strchr(m_url, '/');  //跳过"http://baidu.com/path"中的baidu.com并拿到path
    }

    if (strncasecmp(m_url, "https://", 8) == 0)
    {
        m_url += 8;
        m_url = strchr(m_url, '/'); //跳过"https://baidu.com/path"中的baidu.com并拿到path
    }

    if (!m_url || m_url[0] != '/')
        return BAD_REQUEST;
    //当url为/时,显示判断界面
    if (strlen(m_url) == 1)
        strcat(m_url, "judge.html"); //链接字符串
    m_check_state = CHECK_STATE_HEADER;  //返回正在分析头部字段的主状态机字段
    return NO_REQUEST;  //从状态机状态为未得到完整HTTP请求
}

http_conn::HTTP_CODE http_conn::parse_headers(char text)

**功能:主要功能是解析 HTTP 请求的头部带有的信息。**这里不是提取请求方法和URL的函数。

提取首部字段名和字段值。

1.strcasecmp:用于字符串比较的函数,它定义在 <strings.h> 头文件中。这个函数与 strcmp 类似,但它在比较时会忽略字符串中字母的大小写。

int strcasecmp(const char *s1, const char *s2);

2.strspn:C语言标准库 <string.h> 中的一个函数,用于计算字符串开头连续包含指定字符集中字符的长度。

size_t strspn(const char *str1, const char *str2);

3.atol:原型定义于 <cstdlib> 头文件中,主要用于将字符串转换为长整型(long)数值

long atol(const char* str);
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
    if (text[0] == '\0')
    //当 text 的第一个字符为 '\0' 时,意味着当前行是空行。在 HTTP 请求中,空行标志着头部信息结束。
    
    //如果 m_content_length 不为 0,说明请求包含请求体,
    //将主状态机状态 m_check_state 设置为 CHECK_STATE_CONTENT,
    //表示接下来要解析请求体,返回 NO_REQUEST 表示还未完成整个请求的解析。
    
    //如果 m_content_length 为 0,说明请求没有请求体,
    //返回 GET_REQUEST 表示已经成功获取到完整的请求。

    {
        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;
        text += strspn(text, " \t"); //计算 text字符串中开头连续的空格或制表符的数量
                                //然后,text指针会向后移动这个数量,从而跳过这些空格和制表符。
        if (strcasecmp(text, "keep-alive") == 0)
        {
            m_linger = true;//用于表示 HTTP 连接是否使用持久连接(Keep - Alive)。
        }
    }
    else if (strncasecmp(text, "Content-length:", 15) == 0)
    {
        text += 15;
        text += strspn(text, " \t");
        m_content_length = atol(text);  //数据长度
    }
    else if (strncasecmp(text, "Host:", 5) == 0)
    {
        text += 5;
        text += strspn(text, " \t");
        m_host = text;
    }
    else
    {
        LOG_INFO("oop!unknow header: %s", text);
    }
    return NO_REQUEST;
}

http_conn::HTTP_CODE http_conn::parse_content(char text)

功能:解析 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;
}

http_conn::HTTP_CODE http_conn::process_read()

功能:解析 HTTP 请求的主函数

1.get_line()

功能:该函数通过将读缓冲区的起始地址加上当前行的起始偏移量,来返回当前正在解析的行的起始地址。

char *get_line() { return m_read_buf + m_start_line; };

2.parse_line( )

功能:返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN

3.parse_request_line()

功能:解析http请求行,获得请求方法,目标url及http版本号。并将主状态机状态改为CHECK_STATE_HEADER,从而开始分析头部字段

4.parse_headers()

功能:解析http请求的一个头部信息。如果内容长度不为0,则将主状态机状态改为CHECK_STATE_CONTENT,从而开始分析报文内容

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_checked_idx;
        LOG_INFO("%s", text); //写日志
        switch (m_check_state)   //http初始化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;
            else if (ret == GET_REQUEST)
            {
                return do_request();
            }
            break;
        }
        case CHECK_STATE_CONTENT:
        {
            ret = parse_content(text);
            if (ret == GET_REQUEST)
                return do_request();
            line_status = LINE_OPEN;
            break;
        }
        default:
            return INTERNAL_ERROR;
        }
    }
    return NO_REQUEST;
}

http_conn::HTTP_CODE http_conn::do_request()

功能:该函数的主要作用是处理 HTTP 请求,依据请求的 URL 进行不同的处理,比如处理 CGI 请求(注册、登录),然后确定要返回给客户端的文件,最后将文件映射到内存中。

!!这里使用了很多my_real_file,其中my_real_file等于root(网站root地址)+URL(统一资源定位符),并且使用my_real_url来重定向my_real_file,进而访问不同的资源。

1.strcpy 是 C 语言标准库 <string.h> 中的一个函数,把 src 指向的字符串(包含 '\0')复制到 dest 所指向的数组中,并且返回 dest 的指针。

char *strcpy(char *dest, const char *src);

2.strchr:是 C 语言标准库中的一个函数,用于在字符串中查找指定字符的第一次出现位置

char *strchr(const char *str, int c);

3.strcpy:C 语言标准库 <string.h>(在 C++ 中为 <cstring>)里的一个函数,其主要功能是将一个字符串复制到另一个字符串。

char *strcpy(char *dest, const char *src);

4.strncpy 是 C 语言标准库 <string.h>(在 C++ 中为 <cstring>)中的一个函数,用于将一个字符串的一部分复制到另一个字符串中,可以限制复制的数组大小n。

char *strncpy(char *dest, const char *src, size_t n);

5.strcat 是一个标准库函数,其作用是把一个字符串连接到另一个字符串的末尾。

char *strcat(char *dest, const char *src);

6.stat 是一个标准的 C 库函数,stat 函数用于获取指定文件的状态信息,并将这些信息存储在 struct stat 结构体中其原型如下:

pathname:指向要获取状态信息的文件路径的字符串指针。

statbuf:指向 struct stat 结构体的指针,用于存储获取到的文件状态信息。

#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);

7.open 是一个系统调用函数,主要用于打开或者创建文件。此函数定义于 <fcntl.h> 头文件之中。

#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

pathname:这是一个指向要打开或者创建的文件路径的字符串指针。

flags:此参数为位掩码,用于指定文件的打开方式。以下是一些常用的标志:

O_RDONLY:以只读模式打开文件。

O_WRONLY:以只写模式打开文件。

O_RDWR:以读写模式打开文件。

O_CREAT:若文件不存在,则创建该文件。

O_TRUNC:若文件已经存在,且以只写或者读写模式打开,那么将文件长度截断为 0。

O_APPEND:以追加模式打开文件,也就是每次写入时都会将数据追加到文件末尾。

mode:当使用 O_CREAT 标志创建文件时,这个参数用于指定文件的权限。它是一个八进制数,例如 0644 代表文件所有者拥有读写权限,而组用户和其他用户只有读权限。

8.mmap 是一个在 Linux 系统编程中非常重要的函数,它用于将一个文件或者设备映射到进程的地址空间,这样进程可以像访问内存一样直接访问文件或设备,从而避免了传统的文件 I/O 操作(如 read 和 write)带来的额外开销。

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:指定映射的起始地址,通常设置为 0 或 NULL,让系统自动选择合适的地址。

length:映射区域的大小,即要映射的文件或设备的字节数。

prot:映射区域的保护方式,常用的取值有:

PROT_READ:映射区域可读。

PROT_WRITE:映射区域可写。

PROT_EXEC:映射区域可执行。

PROT_NONE:映射区域不可访问。

flags:映射的标志,常用的取值有:

MAP_SHARED:对映射区域的写入操作会反映到文件中,其他映射该文件的进程也能看到这些变化。

MAP_PRIVATE:对映射区域的写入操作不会反映到文件中,而是创建一个该文件的私有副本。

fd:要映射的文件描述符,通过 open 函数打开文件后得到。

offset:映射的文件偏移量,通常设置为 0,表示从文件的起始位置开始映射。

http_conn::HTTP_CODE http_conn::do_request()
{
    strcpy(m_real_file, doc_root);//在init()函数中doc_root = root为网站根目录
    int len = strlen(doc_root);
    //printf("m_url:%s\n", m_url);
    const char *p = strrchr(m_url, '/'); //构建文件路径
    //把网站根目录 doc_root 复制到 m_real_file 中,并且找到 URL 中最后一个 / 的位置。
    
    //处理cgi
    if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))
    //若 cgi 标志为 1,并且 URL 中 / 后面的字符是 2 或者 3,就处理 CGI 请求。
    {
        //根据标志判断是登录检测还是注册检测
        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); //得到需要请求的URl资源的位置

        //将用户名和密码提取出来
        //user=123&passwd=123
        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(); //互斥锁
                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");
        }
    }
    
    //处理除了登录和注册以外的其他事务
    
    if (*(p + 1) == '0')
    {
        char *m_url_real = (char *)malloc(sizeof(char) * 200);
        strcpy(m_url_real, "/register.html");
        strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

        free(m_url_real);
    }
    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, strlen(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;//请求的资源在服务器上不存在

    if (!(m_file_stat.st_mode & S_IROTH))
        return FORBIDDEN_REQUEST; //客户端没有权限访问请求的资源

    if (S_ISDIR(m_file_stat.st_mode))
        return BAD_REQUEST; //请求格式错误,不符合 HTTP 协议规范

    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;// 请求的是一个文件资源,且服务器可以找到该文件
}

void http_conn::unmap()

功能:该函数的主要功能是解除之前通过 mmap 函数建立的文件映射。在使用 mmap 函数将文件映射到内存后,当不再需要使用该映射时,就需要调用 munmap 函数来释放映射的内存区域,避免内存泄漏。

void http_conn::unmap()
{
    if (m_file_address)
    {
        munmap(m_file_address, m_file_stat.st_size);
        m_file_address = 0;
    }
}

bool http_conn::write()

功能:该函数的主要功能是将 HTTP 响应数据通过 writev 函数写入套接字 m_sockfd,支持分散写(writev 可以将多个缓冲区的数据一次性写入),实现HTTP响应的发送。

1.writev:进行分散写(scatter write)操作的系统调用函数,将 m_iv 数组中描述的多个内存块的数据一次性写入到套接字 m_sockfd 中,避免多次调用write函数,提高I/O效率。它的原型定义在 <sys/uio.h> 头文件中,其原型如下:

成功时,writev 返回实际写入的字节数。

#include <sys/uio.h>

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

fd:文件描述符,表示要写入数据的目标文件或套接字。在当前代码中,m_sockfd 是一个套接字描述符,用于向客户端发送数据。

iovcnt:iov 数组的元素个数,即要写入的内存块的数量。在当前代码中,m_iv_count 表示 m_iv 数组的元素个数。 iov:一个指向 struct iovec 数组的指针。struct iovec 是一个结构体,用于描述一个内存块,其定义如下:

struct iovec {
    void  *iov_base;    /* Starting address */
    size_t iov_len;     /* Number of bytes to transfer */
};

iov_base 指向内存块的起始地址,iov_len 表示该内存块的长度。

2.errno:是 C 和 C++ 标准库中定义的一个全局变量,用于表示系统调用或库函数执行过程中发生的错误。它在 <errno.h> 头文件中声明,其值会在函数调用失败时被设置为一个特定的错误码,每个错误码都对应着一种特定的错误类型。

if (errno == EAGAIN)
//其中EAGAIN代表套接字暂时不可写,(发送缓冲区已满等),将重新修改套接字属性并稍后可再尝试写

bool http_conn::write()
{
    int temp = 0;

    if (bytes_to_send == 0)  //检查是否有数据要发送
    {
        modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
        //如果 bytes_to_send 为 0,说明没有数据需要发送了,
        //将套接字重新注册为可读事件,然后调用 init() 函数初始化连接,最后返回 true。
        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;
            }
            //果 errno 等于 EAGAIN,表示当前套接字暂时不可写(例如,发送缓冲区已满)
            //这种情况通常是暂时的,程序可以稍后再尝试写入。
            //因此,代码会将套接字重新注册为可写事件(EPOLLOUT),
            //并返回 true 表示后续还可以继续处理写入操作。
            //如果 errno 不等于 EAGAIN,则表示发生了其他严重的错误,
            //代码会调用 unmap() 函数解除内存映射,并返回 false 表示写入操作失败。
            unmap();
            return false;
        }

        bytes_have_send += temp;
        bytes_to_send -= temp;
        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)
        {    
            //如果 bytes_to_send 小于等于 0,说明数据已经发送完毕,
            //调用 unmap() 函数解除内存映射,将套接字重新注册为可读事件。
            unmap();
            modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);

            if (m_linger)  //如果设置了keep-alive的话就再重新初始化连接
            {
                init();
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

bool http_conn::add_response(const char format, ...)

功能:该函数用于将格式化的字符串添加到 m_write_buf 缓冲区中,支持可变参数。

format:格式化字符串,类似于 printf 函数的第一个参数。... :可变参数列表,用于替换 format 中的占位符。

1.可变参数列表va_list:va_list 本质上是一个指针类型,用于指向可变参数列表。借助 va_list 和相关宏,函数能够逐个访问可变参数。

va_start:这个宏用于初始化 va_list 类型的变量,让它指向可变参数列表的起始位置。它需要两个参数,第一个是 va_list 类型的变量,第二个是可变参数函数里最后一个固定参数的名称

va_arg:此宏用于获取可变参数列表中的下一个参数。它接受两个参数,第一个是 va_list 类型的变量,第二个是要获取的参数的类型。

va_end:该宏用于结束对可变参数列表的访问,通常在函数结束前调用,目的是释放相关资源。

bool http_conn::add_response(const char *format, ...)
{
    if (m_write_idx >= WRITE_BUFFER_SIZE)   //检查缓冲区是否已满
        return false;
    va_list arg_list;
    va_start(arg_list, format);
    int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
    //使用 vsnprintf 函数将格式化后的字符串写入到 m_write_buf 缓冲区中,
    //从 m_write_idx 位置开始写入,最多写入 WRITE_BUFFER_SIZE - 1 - m_write_idx 个字符。
    //vsnprintf 函数返回格式化后的字符串长度(不包括终止符)。
    
    if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)) //写长度超过缓冲区,越界
    {
        va_end(arg_list);
        return false;
    }
    m_write_idx += len;
    va_end(arg_list);
    //关闭可变参数,释放资源
    LOG_INFO("request:%s", m_write_buf);

    return true;
}

bool http_conn::add_status_line(int status, const char title)

功能:添加 HTTP 响应的状态行。状态行的格式为 HTTP/版本号 状态码 状态描述,其中以空格隔开,最后以 \r\n 结尾

参数:status:HTTP 状态码,例如 200、404 等。title:状态码对应的描述信息,例如 "OK"、"Not Found" 等。

返回值:调用 add_response 函数添加状态行,如果添加成功则返回 true,否则返回 false。

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)

功能:添加 HTTP 响应的头部信息。它会依次调用 add_content_length、add_linger 和 add_blank_line 函数。

参数:content_len:响应内容的长度。

返回值:如果所有头部信息都添加成功,则返回 true,否则返回 false。

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)

功能:添加 Content-Length 头部字段,用于指定响应内容的长度。

参数:content_len:响应内容的长度。

返回值:调用 add_response 函数添加 Content-Length 头部字段,如果添加成功则返回 true,否则返回 false。

bool http_conn::add_content_length(int content_len)
{
    return add_response("Content-Length:%d\r\n", content_len);
}

bool http_conn::add_linger()

功能:添加 Connection 头部字段,用于指定连接的状态。如果 m_linger 为 true,则设置为 keep-alive,表示保持连接;否则设置为 close,表示关闭连接。

返回值:调用 add_response 函数添加 Connection 头部字段,如果添加成功则返回 true,否则返回 false。

bool http_conn::add_linger()
{
    return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}

bool http_conn::add_blank_line()

功能:添加一个空行,用于分隔 HTTP 头部和响应内容。

返回值:调用 add_response 函数添加空行,如果添加成功则返回 true,否则返回 false。

bool http_conn::add_blank_line()
{
    return add_response("%s", "\r\n");
}

bool http_conn::add_content_type()

功能:添加 Content-Type 头部字段,用于指定响应内容的类型。这里默认设置为 text/html。

返回值:调用 add_response 函数添加 Content-Type 头部字段,如果添加成功则返回 true,否则返回 false。

bool http_conn::add_content_type()
{
    return add_response("Content-Type:%s\r\n", "text/html");
}

bool http_conn::add_content(const char content)

功能:添加 HTTP 响应的内容。

参数:content:响应内容的字符串。

返回值:调用 add_response 函数添加响应内容,如果添加成功则返回 true,否则返回 false。

bool http_conn::add_content(const char *content)

{
  return add_response("%s", content);
}

bool http_conn::process_write(HTTP_CODE ret)

功能:依据 HTTP 请求处理结果(HTTP_CODE 类型的 ret)来构建 HTTP 响应,并将响应数据准备好以便发送。

1.一些http响应的状态信息

//定义http响应的一些状态信息
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";

2.**HTTP_CODE **从状态机的可能的状态,即报文解析的结果

enum HTTP_CODE    // 从状态机的可能状态,报文解析的结果
{
    NO_REQUEST,     // 未收到完整的 HTTP 请求
    GET_REQUEST,    // 成功解析出一个完整的 HTTP GET 请求
    BAD_REQUEST,    // 请求格式错误,不符合 HTTP 协议规范
    NO_RESOURCE,    // 请求的资源在服务器上不存在
    FORBIDDEN_REQUEST, // 客户端没有权限访问请求的资源
    FILE_REQUEST,   // 请求的是一个文件资源,且服务器可以找到该文件
    INTERNAL_ERROR, // 服务器在处理请求时发生内部错误
    CLOSED_CONNECTION // 客户端已关闭连接或服务器决定关闭连接
};
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:// 请求格式错误,不符合 HTTP 协议规范
    {
        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;
}

void http_conn::process()

**功能:处理 HTTP 请求和响应。**首先尝试解析客户端的请求,若请求未完成则继续监听可读事件;若请求处理完成,则构建并准备响应,若响应准备失败则关闭连接,最后监听可写事件以发送响应。

void http_conn::process()
{
    HTTP_CODE read_ret = process_read(); //1.处理读取请求
    //2.处理未完成请求
    //如果 read_ret 的值为 NO_REQUEST,意味着当前请求还未完整读取
    //或者需要更多的数据才能继续处理。此时,调用 modfd 函数将套接字 m_sockfd 重新注册到 epoll 实例 
    //m_epollfd 中,监听可读事件(EPOLLIN),然后函数返回。
    if (read_ret == NO_REQUEST)
    {
        modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
        return;
    }
    //3.处理写入响应
    bool write_ret = process_write(read_ret);
    //4.写入失败
    if (!write_ret)
    {
        close_conn();
    }
    //5.更新事件监听,将套接字重新注册到 epoll 实例 m_epollfd 中,
    //监听可写事件(EPOLLOUT),以便后续将响应数据发送给客户端。
    modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值