对于像我这样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);
}