前言
项目简介:
Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.
- 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
- 使用状态机解析HTTP请求报文,支持解析GET和POST请求
- 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
- 实现同步/异步日志系统,记录服务器运行状态
- 经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事件为写事件,准备发送响应
}