TinyWebSever源码逐行注释(五)_ http_conn.cpp

前言

项目源码地址
项目详细介绍

项目简介:

Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.

  1. 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
  2. 使用状态机解析HTTP请求报文,支持解析GET和POST请求
  3. 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
  4. 实现同步/异步日志系统,记录服务器运行状态
  5. 经Webbench压力测试可以实现上万的并发连接数据交换

http_conn.cpp利用一个主从状态机来处理客户端的htttp连接并生成相应的响应。主要内容如下:

根据状态转移,通过主从状态机封装了http连接类。其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机

  • 客户端发出http连接请求
  • 从状态机读取数据,更新自身状态和接收数据,传给主状态机
  • 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取

原项目地址的注释较少不适合初学者,于是我将每行都加上了注释帮助大家更好的理解:

cpp 复制代码
#include "http_conn.h"
#include <mysql/mysql.h>
#include <fstream>

// 定义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 satisfy.\n";
const char *error_403_title = "Forbidden";   // 客户端没有访问权限
const char *error_403_form = "You do not have permission to get file from 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";

// 用于线程安全操作的锁
locker m_lock;

// 存储用户名和密码的映射,用于验证登录和注册
map<string, string> users;

// 初始化数据库结果,将数据库中的用户信息读取到内存中
void http_conn::initmysql_result(connection_pool *connPool)
{
    // 从连接池中取出一个MYSQL连接
    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);

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

// 设置文件描述符为非阻塞模式
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或LT模式,同时选择是否启用EPOLLONESHOT模式
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;  // 绑定fd

    if (1 == TRIGMode)  // ET模式下,添加EPOLLET标志
        event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;  // EPOLLRDHUP表示对端关闭连接
    else
        event.events = EPOLLIN | EPOLLRDHUP;  // LT模式下

    if (one_shot)  // 是否启用EPOLLONESHOT模式,防止同一个socket被多个线程处理
        event.events |= EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);  // 向epoll实例中注册事件
    setnonblocking(fd);  // 设置非阻塞
}

// 从内核事件表中删除文件描述符
void removefd(int epollfd, int fd)
{
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);  // 删除指定文件描述符的事件
    close(fd);  // 关闭文件描述符
}

// 修改文件描述符上的注册事件,重置EPOLLONESHOT
void modfd(int epollfd, int fd, int ev, int TRIGMode)
{
    epoll_event event;
    event.data.fd = fd;

    if (1 == TRIGMode)  // ET模式
        event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    else  // LT模式
        event.events = ev | EPOLLONESHOT | EPOLLRDHUP;

    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);  // 修改事件
}

int http_conn::m_user_count = 0;  // 用户数量初始化为0
int http_conn::m_epollfd = -1;  // epoll实例文件描述符初始化为-1

// 关闭连接,减少用户计数
void http_conn::close_conn(bool real_close)
{
    if (real_close && (m_sockfd != -1))  // 只有当real_close为true并且socket存在时才关闭连接
    {
        printf("close %d\n", m_sockfd);
        removefd(m_epollfd, m_sockfd);  // 从epoll中移除该文件描述符
        m_sockfd = -1;  // 重置socket描述符
        m_user_count--;  // 用户总量减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;  // 保存传入的socket文件描述符
    m_address = addr;  // 保存客户端的地址信息

    addfd(m_epollfd, sockfd, true, m_TRIGMode);  // 注册epoll事件,并且启用EPOLLONESHOT模式
    m_user_count++;  // 新增一个用户

    // 初始化一些相关参数,如网站根目录、触发模式、是否关闭日志等
    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();  // 调用init()函数初始化其他成员变量
}

// 初始化连接的一些内部状态
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;  // 默认请求方法为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;  // 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);
}

// 从状态机,用于逐行解析读取到的数据
http_conn::LINE_STATUS http_conn::parse_line()
{
    char temp;
    // 遍历缓冲区,从m_checked_idx位置开始逐字符检查
    for (; m_checked_idx < m_read_idx; ++m_checked_idx)
    {
        temp = m_read_buf[m_checked_idx];
        // 如果当前字符为回车符
        if (temp == '\r')
        {
            // 如果回车符是缓冲区最后一个字符,则表示还没有完整的一行,返回LINE_OPEN
            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;  // 返回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;  // 如果前一个字符不是回车符,则返回LINE_BAD
        }
    }
    return LINE_OPEN;  // 如果没有遇到回车换行,表示行不完整,返回LINE_OPEN
}

// 循环读取客户端数据,直到无数据可读或对方关闭连接
bool http_conn::read_once()
{
    if (m_read_idx >= READ_BUFFER_SIZE)  // 如果读缓冲区已满,则返回false
    {
        return false;
    }
    int bytes_read = 0;

    // LT模式读取数据
    if (0 == m_TRIGMode)
    {
        // 从socket中读取数据,存储到读缓冲区
        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)  // 出现错误
            {
                // 如果错误是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请求行,获取请求方法、目标URL及HTTP版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
    m_url = strpbrk(text, " \t");  // 查找请求行中的第一个空格或制表符,后面是URL
    if (!m_url)
    {
        return BAD_REQUEST;  // 如果没有找到空格或制表符,说明请求行格式错误
    }
    *m_url++ = '\0';  // 将空格或制表符替换为字符串结束符,分离出请求方法
    char *method = text;  // 获取请求方法
    if (strcasecmp(method, "GET") == 0)  // 比较请求方法是否为GET
        m_method = GET;
    else if (strcasecmp(method, "POST") == 0)  // 比较请求方法是否为POST
    {
        m_method = POST;
        cgi = 1;  // 如果是POST方法,开启CGI处理
    }
    else
        return BAD_REQUEST;  // 如果不是GET或POST,返回BAD_REQUEST

    m_url += strspn(m_url, " \t");  // 跳过URL前的空格或制表符
    m_version = strpbrk(m_url, " \t");  // 查找URL后的空格或制表符,后面是HTTP版本
    if (!m_version)
        return BAD_REQUEST;  // 如果没有找到,返回BAD_REQUEST
    *m_version++ = '\0';  // 将空格或制表符替换为字符串结束符,分离出URL
    m_version += strspn(m_version, " \t");  // 跳过版本号前的空格或制表符

    if (strcasecmp(m_version, "HTTP/1.1") != 0)  // 检查是否为HTTP/1.1
        return BAD_REQUEST;

    // 如果URL是以"http://"或"https://"开头,跳过协议部分
    if (strncasecmp(m_url, "http://", 7) == 0)
    {
        m_url += 7;
        m_url = strchr(m_url, '/');  // 查找URL路径部分
    }
    if (strncasecmp(m_url, "https://", 8) == 0)
    {
        m_url += 8;
        m_url = strchr(m_url, '/');
    }

    if (!m_url || m_url[0] != '/')  // 如果URL无效或不以'/'开头,返回BAD_REQUEST
        return BAD_REQUEST;

    if (strlen(m_url) == 1)  // 如果URL为"/",显示默认页面
        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)
{
    if (text[0] == '\0')  // 如果当前头部信息为空,表示解析完毕
    {
        if (m_content_length != 0)  // 如果有消息体,切换到解析消息体的状态
        {
            m_check_state = CHECK_STATE_CONTENT;
            return NO_REQUEST;
        }
        return GET_REQUEST;  // 如果没有消息体,说明请求已完整,返回GET_REQUEST
    }
    // 解析Connection头部,判断是否为长连接
    else if (strncasecmp(text, "Connection:", 11) == 0)
    {
        text += 11;
        text += strspn(text, " \t");
        if (strcasecmp(text, "keep-alive") == 0)
        {
            m_linger = true;  // 如果是keep-alive,保持长连接
        }
    }
    // 解析Content-Length头部,获取消息体的长度
    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!unknow header: %s", text);  // 记录未知的头部字段
    }
    return NO_REQUEST;  // 继续解析其他头部
}

// 解析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';  // 将消息体以\0结束
        m_string = text;  // 将消息体存储起来,通常是POST请求的参数
        return GET_REQUEST;  // 消息体解析完成,返回GET_REQUEST
    }
    return NO_REQUEST;  // 消息体还未解析完整,继续读取
}

// 主状态机处理入口,依次调用解析请求行、请求头、消息体的函数
http_conn::HTTP_CODE http_conn::process_read()
{
    LINE_STATUS line_status = LINE_OK;  // 当前行的解析状态
    HTTP_CODE ret = NO_REQUEST;  // HTTP请求的解析结果
    char *text = 0;

    // 循环解析HTTP请求,直到完整解析或遇到错误
    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)  // 根据当前解析状态,处理不同部分
        {
        case CHECK_STATE_REQUESTLINE:  // 解析请求行
        {
            ret = parse_request_line(text);  // 调用parse_request_line()函数解析
            if (ret == BAD_REQUEST)  // 如果解析失败,返回错误
                return BAD_REQUEST;
            break;
        }
        case CHECK_STATE_HEADER:  // 解析请求头
        {
            ret = parse_headers(text);  // 调用parse_headers()函数解析
            if (ret == BAD_REQUEST)  // 如果解析失败,返回错误
                return BAD_REQUEST;
            else if (ret == GET_REQUEST)  // 如果请求完整,执行do_request()
            {
                return do_request();
            }
            break;
        }
        case CHECK_STATE_CONTENT:  // 解析消息体
        {
            ret = parse_content(text);  // 调用parse_content()函数解析
            if (ret == GET_REQUEST)  // 如果解析成功,执行do_request()
                return do_request();
            line_status = LINE_OPEN;  // 如果消息体不完整,继续等待数据
            break;
        }
        default:
            return INTERNAL_ERROR;  // 发生未知错误,返回服务器内部错误
        }
    }
    return NO_REQUEST;  // 如果还未解析完成,返回NO_REQUEST
}

// 处理HTTP请求,生成相应的响应
http_conn::HTTP_CODE http_conn::do_request()
{
    strcpy(m_real_file, doc_root);  // 将网站根目录复制到m_real_file中
    int len = strlen(doc_root);  // 获取根目录路径的长度
    const char *p = strrchr(m_url, '/');  // 查找请求的最后一个'/',区分不同的URL

    // 如果是POST请求,且URL是登录或注册请求
    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);  // 释放动态内存

        // 解析POST请求的用户名和密码
        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");  // 用户已存在,返回错误页面
        }
        // 如果是登录请求
        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");  // 登录失败,跳转到错误页面
        }
    }

    // 根据URL后缀处理不同的页面请求
    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;  // 文件不存在,返回NO_RESOURCE

    // 检查文件是否有读取权限
    if (!(m_file_stat.st_mode & S_IROTH))
        return FORBIDDEN_REQUEST;  // 没有权限,返回FORBIDDEN_REQUEST

    // 检查是否是目录
    if (S_ISDIR(m_file_stat.st_mode))
        return BAD_REQUEST;  // 请求的是目录,返回BAD_REQUEST

    // 打开文件
    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()
{
    if (m_file_address)
    {
        munmap(m_file_address, m_file_stat.st_size);  // 解除文件的内存映射
        m_file_address = 0;  // 重置文件地址指针
    }
}

// 向客户端写入HTTP响应
bool http_conn::write()
{
    int temp = 0;
    if (bytes_to_send == 0)  // 如果要发送的字节为0,表示响应已经发送完毕
    {
        modfd(m_epollfd, m_sockfd, EPOLLIN);  // 修改epoll事件为读事件,准备处理下一次请求
        init();  // 重新初始化连接
        return true;
    }

    // 循环发送响应数据,直到全部发送完成或遇到错误
    while (1)
    {
        temp = writev(m_sockfd, m_iv, m_iv_count);  // 使用writev函数将响应数据发送给客户端
        if (temp < 0)  // 发送过程中遇到错误
        {
            // 如果错误是由于非阻塞写导致的缓冲区已满
            if (errno == EAGAIN)
            {
                modfd(m_epollfd, m_sockfd, EPOLLOUT);  // 重新注册写事件
                return true;
            }
            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;  // 清空头部的iov结构体长度
            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 -= temp;  // 更新头部剩余需要发送的长度
        }

        if (bytes_to_send <= 0)  // 如果所有数据都已发送完成
        {
            unmap();  // 取消文件映射
            modfd(m_epollfd, m_sockfd, EPOLLIN);  // 重新注册读事件
            if (m_linger)  // 如果是长连接
            {
                init();  // 重新初始化连接,等待处理新的请求
                return true;
            }
            else
            {
                return false;  // 如果不是长连接,关闭连接
            }
        }
    }
}

// 将HTTP响应生成并写入缓冲区
bool http_conn::add_response(const char *format, ...)
{
    if (m_write_idx >= WRITE_BUFFER_SIZE)  // 如果写入的响应数据超出缓冲区大小,返回false
    {
        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);  // 格式化输出到缓冲区
    if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))  // 如果格式化后的数据超出缓冲区大小,返回false
    {
        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)
{
    return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);  // 将状态行写入响应中
}

// 向响应中添加头部信息
bool http_conn::add_headers(int content_len)
{
    add_content_length(content_len);  // 添加Content-Length头部,指定响应内容长度
    add_linger();  // 添加Connection头部,指定是否保持连接
    add_blank_line();  // 添加空行,表示头部结束
    return true;
}

// 向响应中添加Content-Length头部
bool http_conn::add_content_length(int content_len)
{
    return add_response("Content-Length:%d\r\n", content_len);  // 写入Content-Length头部
}

// 向响应中添加Connection头部
bool http_conn::add_linger()
{
    return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");  // 根据长连接状态写入Connection头部
}

// 向响应中添加空行
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);  // 将内容写入响应
}

// 处理向客户端返回的完整响应
bool http_conn::process_write(HTTP_CODE ret)
{
    switch (ret)
    {
    case INTERNAL_ERROR:  // 内部错误时的响应
    {
        add_status_line(500, error_500_title);  // 添加状态行,状态码500
        add_headers(strlen(error_500_form));  // 添加响应头
        if (!add_content(error_500_form))  // 添加错误内容
            return false;
        break;
    }
    case BAD_REQUEST:  // 错误请求时的响应
    {
        add_status_line(400, error_400_title);  // 添加状态行,状态码400
        add_headers(strlen(error_400_form));  // 添加响应头
        if (!add_content(error_400_form))  // 添加错误内容
            return false;
        break;
    }
    case NO_RESOURCE:  // 资源不存在时的响应
    {
        add_status_line(404, error_404_title);  // 添加状态行,状态码404
        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);  // 添加状态行,状态码403
        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);  // 添加状态行,状态码200
        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>";  // 如果文件为空,返回一个简单的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;
}

// 主逻辑函数,负责处理HTTP请求并生成响应
void http_conn::process()
{
    HTTP_CODE read_ret = process_read();  // 调用process_read解析HTTP请求
    if (read_ret == NO_REQUEST)  // 如果请求不完整,继续监听
    {
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        return;
    }

    bool write_ret = process_write(read_ret);  // 生成响应
    if (!write_ret)
    {
        close_conn();  // 如果生成响应失败,关闭连接
    }
    modfd(m_epollfd, m_sockfd, EPOLLOUT);  // 修改epoll事件为写事件,准备发送响应
}
相关推荐
lwprain5 分钟前
安装支持ssl的harbor 2.1.4 docker 19.03.8 docker-compose 1.24.0
网络协议·ssl·harbor
软件技术员7 分钟前
Let‘s Encrypt SSL证书:acmessl.cn申请免费3个月证书
服务器·网络协议·ssl
耗同学一米八19 分钟前
2024 年河北省职业院校技能大赛网络建设与运维赛项样题四
运维·网络
hunandede24 分钟前
av_image_get_buffer_size 和 av_image_fill_arrays
c++
速盾cdn1 小时前
速盾:CDN缓存的工作原理是什么?
网络·安全·web安全
hopetomorrow2 小时前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
网络安全-杰克2 小时前
网络安全概论
网络·web安全·php
不是二师兄的八戒2 小时前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
怀澈1222 小时前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++