本文是对github上万star项目源码解析,供大家学习交流
项目地址:
https://github.com/qinguoyi/TinyWebServer
三、http连接处理
http连接处理类
===============
根据状态转移,通过主从状态机封装了http连接类。其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机
=>客户端发出http连接请求
=>从状态机读取数据,更新自身状态和接收数据,传给主状态机
=>主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取
===============
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/uio.h>
#include <map>
#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"
#include "../timer/lst_timer.h"
#include "../log/log.h"
// http_conn.cpp中include
#include "http_conn.h"
#include <mysql/mysql.h>
#include <fstream>
包括头文件http_coon.h声明类的变量与方法、http_conn.cpp类的具体实现两个文件
包括:构造函数threadpool()、析构函数~threadpool()以及append()方法、append_p()方法、worker()方法、run()方法
1、头文件http_conn类的声明
class http_conn{
public:
// 用于定义文件名(路径)的最大长度,限制为200个字节
static const int FILENAME_LEN = 200;
// 用于定义接收缓冲区的大小,即每次从套接字中读取的字节数量,限制为2048字节
static const int READ_BUFFER_SIZE = 2048;
// 用于定义发送缓冲区的大小,即每次向套接字中写入的字节数量,限制为1024字节
static const int WRITE_BUFFER_SIZE = 1024;
// 在 http_conn 类中声明的枚举类型,用于表示 HTTP 请求方法
enum METHOD{
GET = 0, // 表示 GET 请求方法
POST, // 表示 POST 请求方法
HEAD, // 表示 HEAD 请求方法
PUT, // 表示 PUT 请求方法
DELETE, // 表示 DELETE 请求方法
TRACE, // 表示 TRACE 请求方法
OPTIONS, // 表示 OPTIONS 请求方法
CONNECT, // 表示 CONNECT 请求方法
PATH // 表示 PATH 请求方法
};
// 用于标识当前解析请求行和请求头的状态
enum CHECK_STATE{
CHECK_STATE_REQUESTLINE = 0, // 表示正在解析请求行
CHECK_STATE_HEADER, // 表示正在解析请求头
CHECK_STATE_CONTENT // 表示正在解析请求正文
};
// 用于表示处理 HTTP 请求的结果代码
enum HTTP_CODE{
NO_REQUEST, // 表示请求不完整,需要继续等待数据
GET_REQUEST, // 表示获取到一个完整的 HTTP 请求
BAD_REQUEST, // 表示请求有语法错误或者非法内容
NO_RESOURCE, // 表示请求的资源不存在
FORBIDDEN_REQUEST, // 表示请求的资源禁止访问
FILE_REQUEST, // 表示请求合法,可以发送文件
INTERNAL_ERROR, // 表示服务器内部错误
CLOSED_CONNECTION // 表示客户端已经关闭连接
};
// 用于表示解析 HTTP 请求时每行的状态
enum LINE_STATUS{
LINE_OK = 0, // 表示读取到一个完整的行
LINE_BAD, // 表示读取到的行错误
LINE_OPEN // 表示读取到的行数据不完整,需要继续读取
};
public:
http_conn() {}
~http_conn() {}
public:
void init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname); // 初始化连接,外部调用初始化套接字地址
void close_conn(bool real_close = true); // 关闭连接,关闭一个连接,客户总量减一
void process();
bool read_once();
bool write();
sockaddr_in *get_address(){
return &m_address;
}
void initmysql_result(connection_pool *connPool);
int timer_flag;
int improv;
private:
void init();
HTTP_CODE process_read();
bool process_write(HTTP_CODE ret);
HTTP_CODE parse_request_line(char *text);
HTTP_CODE parse_headers(char *text);
HTTP_CODE parse_content(char *text);
HTTP_CODE do_request();
char *get_line() { return m_read_buf + m_start_line; };
LINE_STATUS parse_line();
void unmap();
bool add_response(const char *format, ...);
bool add_content(const char *content);
bool add_status_line(int status, const char *title);
bool add_headers(int content_length);
bool add_content_type();
bool add_content_length(int content_length);
bool add_linger();
bool add_blank_line();
public:
static int m_epollfd;
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];
long m_read_idx;
long 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;
char *m_version;
char *m_host;
long m_content_length;
bool m_linger;
char *m_file_address;
struct stat m_file_stat;
struct iovec m_iv[2];
int m_iv_count;
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];
};
2、定义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";
// 在cpp文件中定义的变量
locker m_lock;
map<string, string> users;
int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;
3、对文件描述符设置非阻塞
setnonblocking方法用来将一个文件描述符设置为非阻塞模式。在非阻塞模式下,当读取或写入数据时,如果没有数据可用或者缓冲区已满,程序会立即返回,而不是等待数据或空间被释放
int setnonblocking(int fd){
// 调用fcntl函数获取fd文件描述符当前文件描述符的状态标志
int old_option = fcntl(fd, F_GETFL);
// 将O_NONBLOCK选项与当前文件描述符进行按位或运算,生成新的文件描述符
int new_option = old_option | O_NONBLOCK;
// 使用fcntl函数设置新的fd文件描述符
fcntl(fd, F_SETFL, new_option);
// 函数返回旧文件描述符,以供需要时恢复文件描述符的状态
return old_option;
}
其中:
fcntl是Unix和类Unix操作系统提供的一个系统调用,用于对文件描述符进行控制。它可以执行各种操作,包括复制文件描述符、获取和设置文件描述符标志、获取和设置锁等。
在Unix中,一切皆文件,每个打开的文件都有一个文件描述符来标识它。fcntl函数可以对这些文件描述符进行多种操作,例如:
- 复制文件描述符(F_DUPFD)
- 获取和设置文件状态标志(F_GETFL、F_SETFL)
- 获取和设置文件记录锁(F_GETLK、F_SETLK、F_SETLKW)
fcntl函数还可以用于非阻塞I/O操作,通过设置文件描述符为非阻塞模式,在读取或写入数据时不会阻塞进程
4、对象的初始化,外部调用初始化套接字地址
用于初始化http_conn对象的各个成员变量。该函数有多个参数,其中包括socket文件描述符、地址信息、根目录等等,用于对http_conn对象进行初始化
函数传入参数包括:
sockfd
:socket文件描述符addr
:客户端的地址信息root
:网站的根目录TRIGMode
:触发模式(Reactor或者Proactor)close_log
:是否关闭日志记录user
:MySQL数据库用户名passwd
:MySQL数据库密码sqlname
:MySQL数据库名
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()函数将socket文件描述符加入到epoll事件表中,并设置为监听可读事件
addfd(m_epollfd, sockfd, true, m_TRIGMode);
m_user_count++;
//当浏览器出现连接重置时,可能是网站根目录出错或http响应格式出错或者访问的文件中内容完全为空
doc_root = root;
m_TRIGMode = TRIGMode;
m_close_log = close_log;
// 使用strcpy()函数将MySQL数据库用户名、密码和数据库名分别复制到对应的成员变量sql_user、sql_passwd和sql_name中
strcpy(sql_user, user.c_str());
strcpy(sql_passwd, passwd.c_str());
strcpy(sql_name, sqlname.c_str());
// 调用另外一个成员函数init(),以完成http_conn对象的其他成员变量的初始化工作
init();
}
5、对象的重置,初始化新接受的连接
此init()是privite方法,以供类内函数调用,重置对象成员变量
//check_state默认为分析请求行状态
void http_conn::init(){
// 重置http_conn对象的各个成员变量
mysql = NULL;
bytes_to_send = 0;
bytes_have_send = 0;
m_check_state = CHECK_STATE_REQUESTLINE;
m_linger = false;
m_method = 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;
m_state = 0;
timer_flag = 0;
improv = 0;
// 清空读缓冲区和写缓冲区,并且关闭MySQL连接等操作
memset(m_read_buf, '\0', READ_BUFFER_SIZE);
memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
memset(m_real_file, '\0', FILENAME_LEN);
}
6、初始化数据库连接池
初始化连接池并从数据库中读取用户信息
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));
}
// 从表中检索完整的结果集,这段代码说将最近一次执行的SELECT查询结果集存储在一个名为result的MYSQL_RES结构体指针中。
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;
}
}
7、关闭连接,关闭一个连接,客户总量减一
void http_conn::close_conn(bool real_close){
if (real_close && (m_sockfd != -1)){
printf("close %d\n", m_sockfd);
// 调用removefd函数将该文件描述符从epoll内核事件表中删除,以便不再关注该连接上的事件
removefd(m_epollfd, m_sockfd);
// 将m_sockfd设置为-1,表示该连接已经不存在
m_sockfd = -1;
// 客户总量减1
m_user_count--;
}
}
8、从状态机方法
该函数实现了对HTTP请求报文中每一行数据的解析,判断该行数据是否符合HTTP协议规范,并将报文中\r\n
替换为\0\0
以便后续处理。如果在报文中发现格式错误,则返回错误代码。如果当前报文中还有未处理完的数据,则需要继续等待数据到达并进行解析
//从状态机,用于分析出一行内容
//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line()
{
// 定义字符变量temp,用于逐个读取当前请求报文中的字符
char temp;
// 使用一个for循环遍历当前请求报文中从已经检查过的位置m_checked_idx到已经读取到的最后一个位置m_read_idx之间的所有字符
for (; m_checked_idx < m_read_idx; ++m_checked_idx)
{
temp = m_read_buf[m_checked_idx];
// 如果当前字符temp是\r,则需要继续判断下一个字符
if (temp == '\r')
{
// 判断当前已经读取的字符数加1是否等于缓冲区中已经读入的字节数,如果是,则说明缓冲区中已经没有更多的数据可供读取,此时返回LINE_OPEN表示需要等待更多的数据到达
if ((m_checked_idx + 1) == m_read_idx)
return LINE_OPEN;
// 如果下一个字符是\n,则表示当前行已经结束,将\r和\n都替换为\0,并返回LINE_OK表示该行处理完毕
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;
}
// 如果下一个字符不是\n,则说明HTTP请求报文格式有误,返回LINE_BAD表示该行不符合规范
return LINE_BAD;
}
// 如果当前字符temp是\n,则需要与前一个字符进行配对
else if (temp == '\n')
{
// 如果前一个字符是\r,则表示当前行已经结束,将\r和\n都替换为\0,并返回LINE_OK表示该行处理完毕
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;
}
// 如果前一个字符不是\r,则说明HTTP请求报文格式有误,返回LINE_BAD表示该行不符合规范
return LINE_BAD;
}
}
return LINE_OPEN;
}
9、HTTP_CODE部分
基础知识:请求消息分为三个部分:请求消息行、请求消息头、消息正文
9.1 解析HTTP请求消息行
请求消息行格式举例:
GET /test/test.html HTTP/1.1
/*
GET为请求方式,请求方式分为:Get(默认)、POST、DELETE、HEAD等
GET:明文传输 不安全,数据量有限,不超过1kb
POST:暗文传输,安全。数据量没有限制。
/test/test.html为URI,统一资源标识符
HTTP/1.1为协议版本
*/
项目代码:
//解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text){
// 首先在请求行中找到第一个以空格或制表符为分隔符的位置,分隔符前面的部分表示HTTP方法,后面的部分表示URL
m_url = strpbrk(text, " \t");
// 没有找到分隔符,则返回BAD_REQUEST,表示解析失败
if (!m_url){
return BAD_REQUEST;
}
// 将URL部分截断,并将指针移到URL字符串开头处
*m_url++ = '\0';
char *method = text;
// 用strcasestr函数比较HTTP方法是否为GET或POST,根据结果设置相应的标记
if (strcasecmp(method, "GET") == 0)
m_method = GET;
else if (strcasecmp(method, "POST") == 0){
m_method = POST;
cgi = 1;
}
else
return BAD_REQUEST;
// 判断完HTTP方法之后,将指针移到URL字符串开头处,并查找最近的空格或制表符,即HTTP版本号所在的位置
m_url += strspn(m_url, " \t");
m_version = strpbrk(m_url, " \t");
// 如果没有找到分隔符,则返回BAD_REQUEST
if (!m_version)
return BAD_REQUEST;
*m_version++ = '\0';
m_version += strspn(m_version, " \t");
// 如果HTTP版本号不为HTTP/1.1,则也返回BAD_REQUEST
if (strcasecmp(m_version, "HTTP/1.1") != 0)
return BAD_REQUEST;
// 判断URL是否以http://或https://开头。如果是,将指针移到URL路径的开头处
if (strncasecmp(m_url, "http://", 7) == 0){
m_url += 7;
m_url = strchr(m_url, '/');
}
if (strncasecmp(m_url, "https://", 8) == 0){
m_url += 8;
m_url = strchr(m_url, '/');
}
// 如果URL路径未以“/”开头,则也返回“BAD_REQUEST”
if (!m_url || m_url[0] != '/')
return BAD_REQUEST;
// 如果URL路径只包含“/”(即根目录),则将其替换为judge.html,即当url为/时,显示判断界面
if (strlen(m_url) == 1)
strcat(m_url, "judge.html");
// 设置解析状态为CHECK_STATE_HEADER,表示需要进一步解析HTTP头部
m_check_state = CHECK_STATE_HEADER;
// 返回值为“NO_REQUEST”,表示解析成功但还需要继续解析
return NO_REQUEST;
}
9.2 解析HTTP请求消息头
从第二行开始到空白行统称为请求消息头,HTTP消息请求头由一系列键值对组成,每个键值对都以一个字段名和相应的值为元素。请求头位于HTTP请求消息的首部,用于描述客户端请求的属性和内容
请求头中的键值对由冒号分隔,每行只能包含一个键值对。每个键值对中的键名和值之间有一个空格分隔
请求头和请求体之间有一个空行分隔,如果请求没有请求体,则直接在请求头后面添加一个空行
请求消息头格式(2-5行为请求消息头):
<method> <resource> HTTP/<version>
<HeaderName1>: <value1>
<HeaderName2>: <value2>
...
<HeaderNameN>: <valueN>
<request body>
请求消息头举例:
GET /index.html HTTP/1.1
// 请求 www.example.com 的 /index.html 页面。请求头部分包含了一些关于浏览器和所需数据类型的信息
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Connection: keep-alive
常见请求头字段:
Accept:浏览器可接受的MIME类型,告诉服务器客户端能接收什么样类型的文件
Accept-Charset:浏览器通过这个头告诉服务器,它支持哪种字符集
Accept-Encoding:浏览器能够进行解码的数据编码方式,比如gzip
Accept-Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到。 可以在浏览器中进行设置
Host:初始URL中的主机和端口
Referrer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面
Content-Type:内容类型,告诉服务器浏览器传输数据的MIME类型,文件传输的类型
If-Modified-Since:利用这个头与服务器的文件进行比对,如果一致,则从缓存中直接读取文件
User-Agent:浏览器类型
Content-Length:表示请求消息正文的长度
Connection:表示是否需要持久连接。如果服务器看到这里的值为“Keep -Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接)
Cookie:用于分辨两个请求是否来自同一个浏览器,以及保存一些状态信息,
Date:请求时间GMT
项目代码:
// 解析http请求的一个头部信息
// 该函数接收一个字符串指针text,该指针指向 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;
}
// 否则返回 GET_REQUEST 表示请求已经全部解析完毕
return GET_REQUEST;
}
// 如果该行不为空,则根据首部字段的名称进行解析,首先判断是否为 Connection 首部字段,如果是则取出其值
else if (strncasecmp(text, "Connection:", 11) == 0){
text += 11;
text += strspn(text, " \t");
// 如果值为 "keep-alive" 则表示客户端希望保持长连接,设置相应的标志位
if (strcasecmp(text, "keep-alive") == 0){
m_linger = true;
}
}
// 然后判断是否为 Content-length 字段,如果是则取出消息体的长度
else if (strncasecmp(text, "Content-length:", 15) == 0){
text += 15;
text += strspn(text, " \t");
m_content_length = atol(text);
}
// 如果是Host字段,则将其值保存在类成员变量m_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);
}
// 最后返回NO_REQUEST表示该行已经解析完毕
return NO_REQUEST;
}
9.3 解析HTTP请求消息正文部分
// 判断http请求是否被完整读入
http_conn::HTTP_CODE http_conn::parse_content(char *text){
// 如果当前已经读取的字节数m_read_idx大于等于HTTP请求中的Content-Length(正文长度)加上已经检查过的字节数m_checked_idx,则认为HTTP请求已经被完整读入
if (m_read_idx >= (m_content_length + m_checked_idx)){
// 将正文部分的字符串以'\0'结尾,并将其保存到成员变量m_string中
text[m_content_length] = '\0';
// POST请求中最后为输入的用户名和密码
m_string = text;
// 最后返回 GET_REQUEST 状态码表示HTTP请求已经完整解析
return GET_REQUEST;
}
// 否则,返回NO_REQUEST状态码,继续读取数据,直至完整读入HTTP请求
return NO_REQUEST;
}
9.4 解析客户端发送的HTTP请求
这个方法用于处理客户端发送的HTTP请求,包括解析请求行、请求头、请求体等,并根据请求内容返回不同的响应状态码。
http_conn::HTTP_CODE http_conn::process_read(){
LINE_STATUS line_status = LINE_OK; // 表示当前解析出来的行状态
HTTP_CODE ret = NO_REQUEST; // 表示当前解析出来的方法执行结果
char *text = 0; // 定义一个指向当前读取行缓冲区的指针text初始化为0
//进入while循环,不断地从socket中读取数据,并对其进行解析和处理
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK)){
// 读取一行数据并保存到text变量中
text = get_line();
// 设置m_start_line变量为当前已经解析过的字符数(即上一次解析完的位置)
m_start_line = m_checked_idx;
// 输出读取到的数据内容
LOG_INFO("%s", text);
// 根据m_check_state的不同值,调用相应的函数对当前行数据进行解析和处理
switch (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;
// 如果解析请求头部发现请求方法是GET,则直接跳转到do_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,以便下一轮循环继续读取下一个请求
line_status = LINE_OPEN;
break;
}
default:
return INTERNAL_ERROR;
}
}
// 返回 NO_REQUEST 状态码,表示HTTP请求未被完整读入
return NO_REQUEST;
}
代码中while循环判断条件详解:
这是一个while循环中的复合判断条件,包含两个部分。
第一个部分是:
(m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK)
它用于判断当前的HTTP报文是否完整地被解析出来了。其中,m_check_state
表示HTTP请求的解析状态,CHECK_STATE_CONTENT
状态表示正在解析HTTP请求体部分。line_status
表示当前行的解析状态,LINE_OK
表示当前行已经被解析成功。
当以上两个状态都满足时,就说明当前的HTTP请求体已经被完整地解析出来了,可以对其进行处理。
第二个部分是:
(line_status = parse_line()) == LINE_OK
它用于判断是否需要继续读取HTTP请求报文的数据。其中,parse_line()
函数用于从 socket 中读取一行数据,并进行解析。line_status
变量表示当前行的解析状态。
当 parse_line()
函数返回 LINE_OK
时,说明当前行已经被成功解析出来了,可以对其进行处理。此时将 line_status
赋值为 LINE_OK
,并与 LINE_OK
进行比较,相等则代表当前行解析成功,需要继续读取下一行数据。
因此,当以上任意一个条件成立时,while循环就会继续执行,直到HTTP请求的所有数据都被成功解析出来,或者出现错误。
9.4 将客户端请求的URL分析并进行相应的处理
主要功能是根据URL中的内容进行相关处理,并返回具体的错误码或成功标识。
http_conn::HTTP_CODE http_conn::do_request(){
// 将doc_root(文档根目录)拷贝到m_real_file中
strcpy(m_real_file, doc_root);
// 计算出doc_root的长度
int len = strlen(doc_root);
//printf("m_url:%s\n", m_url);
// 通过查找URL中最后一个斜杠后面的字符来判断该请求是否需要进行CGI处理
const char *p = strrchr(m_url, '/');
// 如果需要,处理cgi,则会进一步提取出用户名和密码信息,
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3')){
//根据标志判断是登录检测还是注册检测,并根据检测结果修改URL
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);
//将用户名和密码提取出来
//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");
}
}
// 如果不需要CGI处理,则根据URL中的数字编号来确定具体的请求类型,并将对应的HTML页面路径存储到m_real_file中
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);
// 通过调用stat()函数获取m_real_file所指向的文件状态,并对文件的读取权限进行判断
if (stat(m_real_file, &m_file_stat) < 0)
// 如果stat()操作失败,则返回NO_RESOURCE错误代码
return NO_RESOURCE;
if (!(m_file_stat.st_mode & S_IROTH))
// 如果该文件不可读,则返回FORBIDDEN_REQUEST错误码
return FORBIDDEN_REQUEST;
if (S_ISDIR(m_file_stat.st_mode))
// 如果该文件为目录,则返回BAD_REQUEST错误码
return BAD_REQUEST;
// 否则,函数会通过open()函数打开该文件,并通过mmap()函数将文件映射到内存中
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);
// 函数最终返回FILE_REQUEST错误码,表示成功处理了文件请求
return FILE_REQUEST;
}
10、向客户端发送 HTTP 响应报文
用于向客户端发送 HTTP 响应报文,write()
方法实现了向客户端发送 HTTP 响应报文的功能,并在过程中采用了多次写入以及事件类型修改等措施,以确保能够将数据完整地发送出去。
bool http_conn::write(){
int temp = 0;
// 如果待发送的数据量已经为0
if (bytes_to_send == 0){
// 通过modfd()更新事件类型为EPOLLIN
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
// 并通过init()方法重置HTTP连接对象
init();
return true;
}
while (1){
// 使用writev()函数将m_iv数组中的数据写入到套接字缓冲区中,temp变量记录本次写入的字节数量
temp = writev(m_sockfd, m_iv, m_iv_count);
// temp < 0,说明出现错误
if (temp < 0){
// errno等于 EAGAIN,则表示当前不可写
if (errno == EAGAIN){
// 修改事件类型为EPOLLOUT并返回true
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
return true;
}
// 否则就释放资源并返回false
unmap();
return false;
}
bytes_have_send += temp; // 更新已经发送的字节数
bytes_to_send -= temp; // 更新待发送的字节数
// 根据已经发送的字节数和第一个缓冲区的长度判断第一个缓冲区是否已经全部发送
if (bytes_have_send >= m_iv[0].iov_len){
// 如果已经全部发送,则将第一个缓冲区的长度置为0
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;
}
// 待发送的数据量小于等于0
if (bytes_to_send <= 0){
// 调用unmap()方法释放资源
unmap();
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
// 根据m_linger标志位判断是否需要重置该连接
if (m_linger){
// 如果需要重置,则调用init()方法,并返回true
init();
return true;
}
else{
// 否则直接返回false
return false;
}
}
}
}
11、HTTP连接读操作
这段代码实现了一个HTTP连接的读取操作
//循环读取客户数据,直到无数据可读或对方关闭连接
//非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once(){
// 判断缓冲区是否已满,如果已满,则返回false表示读取失败
if (m_read_idx >= READ_BUFFER_SIZE){
return false;
}
// 如果缓冲区未满,则根据触发模式(m_TRIGMode)进行不同的读取方式
int bytes_read = 0;
// 当触发模式为LT(Level Triggered)时
if (0 == m_TRIGMode){
// 使用阻塞式的recv函数读取数据,将读到的数据存放在缓冲区中,并更新m_read_idx表示已经读取的字节数
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
m_read_idx += bytes_read;
// 如果读取失败(bytes_read<=0),则返回false表示读取失败
if (bytes_read <= 0){
return false;
}
// 则返回true表示读取成功
return true;
}
// 当触发模式为ET(Edge Triggered)时
else{
// 使用非阻塞式的recv函数进行读取操作,循环调用recv函数
while (true){
// 将读到的数据存放在缓冲区中,并更新m_read_idx表示已经读取的字节数
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
// 如果recv函数返回值为-1
if (bytes_read == -1){
// 需要根据errno来判断是否是EAGAIN或EWOULDBLOCK错误,如果是则说明暂时没有数据可以读取,退出循环
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
// 如果不是,则返回false表示读取失败
return false;
}
// 如果recv函数返回值为0,则说明对端已经关闭了连接,也返回false表示读取失败
else if (bytes_read == 0){
return false;
}
m_read_idx += bytes_read;
}
// 如果读取成功,则返回true表示读取成功
return true;
}
}
12、HTTP连接写操作
HTTP服务器中处理写操作的函数
// 写操作
bool http_conn::process_write(HTTP_CODE ret){
// 参数ret是一个枚举类型的值,表示服务器对请求的处理结果。使用switch语句根据ret的值进行不同的处理
switch (ret){
// 表示内部错误,服务器会向客户端返回500错误码和相应的错误页面
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;
}
// 表示请求无效,服务器会向客户端返回404错误码和相应的错误页面
case BAD_REQUEST:{
add_status_line(404, error_404_title);
add_headers(strlen(error_404_form));
if (!add_content(error_404_form))
return false;
break;
}
// 表示请求被禁止,服务器会向客户端返回403错误码和相应的错误页面
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;
}
// 表示成功获取到文件内容,服务器会向客户端返回200成功码,并将文件内容添加到响应报文中
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数组的成员变量,使其指向响应报文的头部和文件内容的地址和长度
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;
}
// 如果没有进入任何case分支,则说明出现了未知错误,函数返回false
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数组的第一个元素设置为响应报文的头部地址
m_iv[0].iov_len = m_write_idx; // 将m_iv数组的第一个元素设置为响应报文的头部长度
m_iv_count = 1;
bytes_to_send = m_write_idx; // 将需要发送的字节数设置为头部长度
// 返回true表示处理写操作成功
return true;
}
13、fd方法
这三个方法是在 Linux 中使用 epoll I/O 模型编程时常用的方法,具体功能如下:
- addfd() 方法:将一个文件描述符添加到 epoll 监听中,并设置对应的事件类型。其中 one_shot 参数表示是否开启 EPOLLONESHOT 模式,TRIGMode 参数表示触发模式(LT 或 ET)。
- removefd() 方法:从 epoll 监听中删除一个文件描述符,并关闭相应的文件描述符。
- modfd() 方法:修改一个已经在 epoll 监听中的文件描述符的事件类型,同时重置为 EPOLLONESHOT 模式。其中 ev 表示新设置的事件类型,TRIGMode 参数表示触发模式(LT 或 ET)。
13.1 addfd()方法
//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode){
/*
epollfd:epoll实例的文件描述符;
fd:需要添加到epoll实例中的文件描述符;
one_shot:表示是否开启EPOLLONESHOT模式;
TRIGMode:表示触发模式,0表示LT(Level Triggered)模式,1表示ET(Edge Triggered)模式
*/
// 创建一个epoll_event结构体实例event,并将其data.fd字段设置为需要添加的文件描述符fd
epoll_event event;
event.data.fd = fd;
// 根据传入的触发模式TRIGMode,设置event的events字段
if (1 == TRIGMode)
// 如果TRIGMode为1,则设置为EPOLLIN | EPOLLET | EPOLLRDHUP,即边缘触发模式
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
else
// 否则设置为EPOLLIN | EPOLLRDHUP,即水平触发模式
event.events = EPOLLIN | EPOLLRDHUP;
if (one_shot)
// 如果one_shot为true,则加上EPOLLONESHOT标志位
event.events |= EPOLLONESHOT;
/*
调用epoll_ctl()函数向epoll实例中添加文件描述符
第一个参数epollfd是epoll实例的文件描述符;
第二个参数EPOLL_CTL_ADD表示添加操作;
第三个参数fd是需要添加到epoll监听的文件描述符;
第四个参数&event是指向epoll_event结构体实例的指针,传递了需要添加到epoll实例中的事件类型
*/
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
// 调用setnonblocking(fd)函数将文件描述符设置为非阻塞模式
setnonblocking(fd);
}
13.2 removefd()方法
用于从指定的内核事件表中删除一个文件描述符
//从内核时间表删除描述符
void removefd(int epollfd, int fd){
// 使用Linux系统调用epoll_ctl来完成删除文件描述符fd
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
// 关闭指定的文件描述符
close(fd);
}
13.3 modfd()方法
修改指定的文件描述符在内核事件表中的关注事件
//将事件重置为EPOLLONESHOT
void modfd(int epollfd, int fd, int ev, int TRIGMode){
// // 创建一个epoll_event结构体实例event,并将其data.fd字段设置为需要添加的文件描述符fd
epoll_event event;
event.data.fd = fd;
// 判断是否需要使用边沿触发模式
if (1 == TRIGMode)
// 如果需要,则将相关的标志位EPOLLET、EPOLLONESHOT和EPOLLRDHUP设置到event.events字段中
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
else
// 否则,只需将EPOLLONESHOT和EPOLLRDHUP标志位设置进去
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
// 通过调用epoll_ctl函数,并传入EPOLL_CTL_MOD操作类型和event变量来修改内核事件表中的文件描述符关注事件
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}
14、add部分
这些方法用于向响应报文中添加不同的内容信息,以构造完整的HTTP响应
// 用于将格式化字符串添加到m_write_buf缓冲区中
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);
// 使用vsnprintf函数来将输入的字符串和参数格式化为缓冲区中从当前写索引开始的位置
int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
// 剩余的缓冲区空间通过计算WRITE_BUFFER_SIZE - 1 - m_write_idx来获得,其中m_write_idx表示当前的写索引
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)){
// 如果格式化后的字符串超过了剩余的缓冲区空间,则返回 false
va_end(arg_list);
return false;
}
// 否则,它会将写索引更新为新的长度,并返回true
m_write_idx += len;
va_end(arg_list);
LOG_INFO("request:%s", m_write_buf);
return true;
}
// 用于添加响应状态行,即HTTP协议版本号、状态码和状态消息
bool http_conn::add_status_line(int status, const char *title){
return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}
// 用于添加响应头部信息,包括Content-Length、Connection和空白行等
bool http_conn::add_headers(int content_len){
return add_content_length(content_len) && add_linger() &&
add_blank_line();
}
// 用于添加Content-Length字段,指定响应正文的长度
bool http_conn::add_content_length(int content_len){
return add_response("Content-Length:%d\r\n", content_len);
}
// 用于添加Content-Type字段,指定响应正文的类型
bool http_conn::add_content_type(){
return add_response("Content-Type:%s\r\n", "text/html");
}
// 用于添加Connection字段,指明是否启用长连接
bool http_conn::add_linger(){
return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}
// 用于添加一个空白行,表示头部信息结束
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);
}
15、数据区域释放
unmap()
方法用于将之前通过mmap()
系统调用映射到内存中的数据区域释放,并将指向该区域的指针m_file_address
清零。
void http_conn::unmap(){
if (m_file_address){
munmap(m_file_address, m_file_stat.st_size);
m_file_address = 0;
}
}
16、HTTP请求-响应周期的核心处理函数
处理客户端请求的核心函数 process()
。该函数的作用是根据读取到的数据进行处理并返回对应的 HTTP_CODE 枚举值,该函数完成了对客户端请求的处理和响应的发送,并通过 epoll 实现了高效的事件驱动模型,提高了服务器的吞吐量和性能
void http_conn::process(){
// 调用process_read()函数,从客户端socket中读取数据并解析出HTTP请求信息
HTTP_CODE read_ret = process_read();
// 当前没有接收到完整的请求数据,函数直接结束
if (read_ret == NO_REQUEST){
// 将客户端socket加入epoll监听列表,等待下一次可读事件触发
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
return;
}
// 如果返回值不为NO_REQUEST,则表示已经成功解析出HTTP请求信息,并准备好回复客户端
// 调用process_write()函数进行响应报文的构建和发送,并将返回值赋给write_ret变量
bool write_ret = process_write(read_ret);
// 如果process_write()函数返回false,则说明响应发送失败
if (!write_ret){
// 关闭客户端连接
close_conn();
}
// 将客户端socket加入epoll监听列表,并等待下一次可写事件触发,以继续发送响应数据
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
}