TinyWebServer项目小白逐行解析第五集——http
对于像我这样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 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 机制的一个结构体,在 头文件中定义。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 函数:是一个系统调用,定义在 头文件中。其作用是关闭指定的文件描述符 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++ 标准库中的一个函数,它的原型定义在 头文件中,用于将一段内存区域的每个字节都设置为指定的值。
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 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 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 = (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 语言标准库 中的一个函数,把 src 指向的字符串(包含 '\0')复制到 dest 所指向的数组中,并且返回 dest 的指针。
char *strcpy(char *dest, const char *src);
2.strchr:是 C 语言标准库中的一个函数,用于在字符串中查找指定字符的第一次出现位置
char *strchr(const char *str, int c);
3.strcpy:C 语言标准库 (在 C++ 中为 )里的一个函数,其主要功能是将一个字符串复制到另一个字符串。
char *strcpy(char *dest, const char *src);
4.strncpy 是 C 语言标准库 (在 C++ 中为 )中的一个函数,用于将一个字符串的一部分复制到另一个字符串中,可以限制复制的数组大小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 #include int stat(const char *pathname, struct stat *statbuf);
7.open 是一个系统调用函数,主要用于打开或者创建文件。此函数定义于 头文件之中。
#include 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 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(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)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效率。它的原型定义在 头文件中,其原型如下:
成功时,writev 返回实际写入的字节数。
#include 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++ 标准库中定义的一个全局变量,用于表示系统调用或库函数执行过程中发生的错误。它在 头文件中声明,其值会在函数调用失败时被设置为一个特定的错误码,每个错误码都对应着一种特定的错误类型。
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 = 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 = 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 = ""; 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); }