项目中HTTP协议处理部分
EPOLLIN
:表示监控文件描述符(如套接字)的可读事件。当该文件描述符有数据可读时(如客户端发送了 HTTP 请求),epoll
会将其标记为就绪状态,触发后续处理。EPOLLET
:启用边缘触发(Edge Triggered)模式。这是epoll
的高效工作模式,仅在文件描述符状态从 “不可读” 变为 “可读” 时触发一次事件(而非水平触发的持续触发)。配合非阻塞 IO 使用,可减少事件通知次数,提升性能(需在代码中一次性读完所有数据)。EPOLLRDHUP
:监控对方关闭连接或半关闭连接的事件。当客户端主动关闭连接时,epoll
会捕获该事件,便于及时清理连接资源,避免无效的读写操作。
这三个标志通过位或(|
)组合,表示 epoll
实例需要同时监控:
- 目标文件描述符的可读数据(
EPOLLIN
); - 以边缘触发模式处理事件(
EPOLLET
); - 对方关闭连接的事件(
EPOLLRDHUP
)。
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode){LOG_INFO << "epollfd: " << epollfd << " fd: " << fd << " one_shot: " << one_shot << " TRIGMode: " << TRIGMode;struct epoll_event event;event.data.fd = fd;if (1 == TRIGMode){event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;}else{event.events = EPOLLIN | EPOLLRDHUP;}if (one_shot){event.events |= EPOLLONESHOT;}LOG_TRACE << "call epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event)";epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);}
addfd
函数,用于将文件描述符(通常是套接字)注册到 epoll
实例中,实现对该文件描述符的事件监控。
函数作用
将指定的文件描述符(fd
)添加到 epoll
内核事件表(由 epollfd
标识),并配置监控的事件类型、触发模式等,同时将文件描述符设置为非阻塞模式,是 epoll
事件驱动模型中的核心配置函数。
参数说明
epollfd
:epoll
实例的文件描述符(通过epoll_create
创建),代表要操作的内核事件表。fd
:需要添加到epoll
监控的目标文件描述符(如客户端连接的套接字)。one_shot
:布尔值,标识是否启用EPOLLONESHOT
模式(事件只触发一次)。TRIGMode
:触发模式(0 表示水平触发 LT,1 表示边缘触发 ET)。
代码逻辑解析
初始化
epoll_event
结构体struct epoll_event event; event.data.fd = fd; // 绑定要监控的文件描述符,方便后续处理时识别
epoll_event
是epoll
机制的核心结构体,data.fd
用于存储待监控的文件描述符,后续epoll_wait
返回就绪事件时,可通过该字段定位具体的文件描述符。配置事件类型(根据触发模式
TRIGMode
)边缘触发模式(ET,
TRIGMode=1
):event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
EPOLLIN
:监控文件描述符的可读事件(如客户端发送数据)。EPOLLET
:启用边缘触发模式,仅在文件描述符状态从 “不可读” 变为 “可读” 时触发一次事件(需配合非阻塞 IO 一次性读完数据)。EPOLLRDHUP
:监控对方关闭连接的事件(如客户端断开连接)。
水平触发模式(LT,
TRIGMode=0
):event.events = EPOLLIN | EPOLLRDHUP;
不启用
EPOLLET
,默认水平触发模式:只要文件描述符有数据可读,就会持续触发事件。
启用
EPOLLONESHOT
模式(可选)if (one_shot) {event.events |= EPOLLONESHOT; // 位或操作添加该事件标志 }
EPOLLONESHOT
确保某个文件描述符的事件只会被一个线程处理一次。若需要再次监控,需通过epoll_ctl
重新设置事件(避免多线程同时处理同一连接的事件,导致数据混乱)。将文件描述符添加到
epoll
实例epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
调用
epoll_ctl
系统调用,以EPOLL_CTL_ADD
操作为标志,将配置好的event
注册到epollfd
对应的内核事件表中,完成监控注册。设置文件描述符为非阻塞模式
setnonblocking(fd);
调用自定义函数
setnonblocking
将fd
设为非阻塞模式,配合epoll
的 ET 模式使用时,可确保高效读取所有数据(避免阻塞在recv
/read
调用上)。
MYSQL
结构体:
- 定义在 MySQL 客户端开发库的头文件
<mysql/mysql.h>
中,是 MySQL C API 的核心结构体,用于表示一个数据库连接的上下文信息(如连接状态、服务器信息、查询结果等)。 - 通过该结构体指针,可调用 MySQL 提供的 API 函数(如
mysql_query
执行 SQL 语句、mysql_store_result
获取查询结果等)完成数据库操作。
// 从状态机,用于分析出一行内容// 返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPENhttp_conn::LINE_STATUS http_conn::parse_line(){LOG_TRACE << "in parse_line";char temp = '\0';LOG_TRACE << "m_checked_idx: " << m_checked_idx << " m_read_idx: " << m_read_idx;LOG_TRACE << "m_read_buf: " << m_read_buf;for (; m_checked_idx < m_read_idx; ++m_checked_idx){temp = m_read_buf[m_checked_idx];//LOG_INFO << "m_checked_idx: " << m_checked_idx << " temp: " << static_cast<int>(temp) << " " << temp;if (temp == '\r'){LOG_TRACE << " if (temp == '\r')";LOG_TRACE << " if (m_read_buf[m_checked_idx + 1])" << static_cast<int>(m_read_buf[m_checked_idx + 1]);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';LOG_TRACE << "ret LINE_OK: "<<m_read_buf;return LINE_OK;}}else if (temp == '\n'){LOG_TRACE << "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;}LOG_TRACE << "return LINE_BAD";return LINE_BAD;}}return LINE_OPEN;}/*
这段代码是 http_conn 类中的 parse_line 方法,作用是解析 HTTP 请求中的一行数据,判断当前读取的缓冲区内容是否构成完整的 HTTP 协议行(以 \r\n 为结束标志),并返回行的解析状态(LINE_OK、LINE_BAD、LINE_OPEN)。
核心逻辑与解析规则
HTTP 协议规定,请求行和请求头的每行数据必须以 \r\n(回车 + 换行)作为结束符。parse_line 方法通过遍历读取缓冲区(m_read_buf),根据字符判断行的完整性和合法性,具体流程如下:
遍历缓冲区:通过循环遍历 m_read_buf 中从 m_checked_idx 到 m_read_idx 的数据(m_checked_idx 是已检查的位置,m_read_idx 是缓冲区中实际数据的末尾位置)。
判断 \r 字符:当遇到 \r(回车)时:
若下一个位置(m_checked_idx + 1)已到达缓冲区末尾(m_read_idx),说明数据不完整,返回 LINE_OPEN(需要继续读取)。
若下一个字符是 \n(换行),则符合 \r\n 结束符规范:
将 \r 替换为 \0(字符串结束符),m_checked_idx 后移一位。
将 \n 替换为 \0,m_checked_idx 再后移一位。
返回 LINE_OK(成功解析一行)。
判断 \n 字符:当遇到 \n(换行)时:
若前一个字符是 \r(即实际是 \r\n,但可能因缓冲区拆分导致 \r 在上一次读取中),则同样替换 \r 和 \n 为 \0,返回 LINE_OK。
若前一个字符不是 \r,则不符合 HTTP 行结束规范,返回 LINE_BAD(行格式错误)。
遍历结束仍无完整行:若遍历完当前缓冲区数据仍未找到完整的 \r\n,返回 LINE_OPEN(需要继续读取更多数据)。
关键变量作用
m_read_buf:存储从客户端读取的 HTTP 请求数据的缓冲区。
m_checked_idx:记录已检查过的缓冲区位置,避免重复解析。
m_read_idx:记录缓冲区中已读取数据的末尾位置(有效数据长度)。
temp:当前遍历到的字符。
返回值含义
LINE_OK:成功解析出一行完整的 HTTP 数据(符合 \r\n 结束规范)。
LINE_BAD:行格式错误(如仅以 \n 结束,无 \r)。
LINE_OPEN:数据不完整,需继续读取更多数据才能判断行的完整性。
总结
parse_line 是 HTTP 请求解析的基础工具方法,通过识别 \r\n 结束符判断行的完整性,为后续解析请求行(parse_request_line)、请求头(parse_headers)提供了结构化的输入(以 \0 结尾的字符串),确保 HTTP 协议解析的正确性。
*/
m_start_line
、m_checked_idx
和 m_read_buf
的关系,我们可以通过一个具体的 HTTP 请求解析过程来举例说明:
假设场景
客户端发送的 HTTP 请求数据(存储在 m_read_buf
中)如下:
plaintext
GET /index.html HTTP/1.1\r\n
Host: localhost:8080\r\n
Connection: keep-alive\r\n
\r\n
(注:\r\n
是 HTTP 协议中每行的结束标志)
变量含义与解析过程
m_read_buf
:存储从客户端读取的原始请求数据的缓冲区,上述请求数据会被完整存入其中。m_checked_idx
:记录已经解析过的位置(当前检查到的索引),初始值为 0,随着解析推进不断递增。m_start_line
:记录当前正在解析的 “行” 在m_read_buf
中的起始索引,每解析完一行后会更新到下一行的起始位置。
解析步骤演示
初始状态
m_read_buf
:存储上述 HTTP 请求数据(假设索引从 0 开始)。m_checked_idx = 0
(尚未开始解析)。m_start_line = 0
(当前行的起始位置为 0)。
第一步:解析请求行(第一行 GET /index.html HTTP/1.1\r\n
)
- 程序通过
parse_line()
函数查找行结束标志\r\n
,发现第一行的\r\n
位于索引 23-24(假设)。 - 解析完成后,
m_checked_idx
会移动到25
(跳过\r\n
,指向第二行的起始位置)。 - 此时
m_start_line
仍为0
(表示当前解析的行从 0 开始)。 - 解析完这一行后,
m_start_line
被更新为25
(下一行的起始位置)。
第二步:解析头部字段(第二行 Host: localhost:8080\r\n
)
parse_line()
继续从m_checked_idx=25
开始查找,发现第二行的\r\n
位于索引 45-46。- 解析完成后,
m_checked_idx
移动到47
。 - 此时
m_start_line=25
(当前行的起始位置),解析完成后更新为47
。
第三步:解析头部字段(第三行 Connection: keep-alive\r\n
)
- 同理,解析完成后
m_checked_idx
移动到该行\r\n
之后的位置(假设为 68),m_start_line
更新为68
。
第四步:解析空行(\r\n
)
- 最后一行是
\r\n
(表示头部结束),解析完成后m_checked_idx
移动到整个请求的末尾,m_start_line
也随之更新。
总结三者关系
m_read_buf
是整个请求数据的 “容器”。m_start_line
标记 “当前正在解析的行” 的起点。m_checked_idx
标记 “已经解析到的位置”,也是下一行解析的起点。
通过这三个变量的配合,程序能够从 m_read_buf
中 “逐行” 提取数据(请求行、头部字段等),实现对 HTTP 请求的分步解析。例如,当需要获取当前行的内容时,直接通过 m_read_buf + m_start_line
即可定位到该行的起始地址,直到 m_checked_idx
所标记的结束位置。
struct iovec m_iv[2];
这段代码在 http_conn
类的私有成员中声明了一个包含 2 个 struct iovec
元素的数组 m_iv
,主要用于高效地集中管理 HTTP 响应数据的缓冲区,配合 writev
系统调用一次性发送多个不连续的缓冲区数据。
关键说明:
struct iovec
结构体:- 定义于
<sys/uio.h>
,包含两个成员:iov_base
(指向数据缓冲区的起始地址)和iov_len
(缓冲区的字节长度)。 - 作用是将分散的内存缓冲区 “拼接” 成一个逻辑上连续的缓冲区,便于通过
writev
系统调用一次性写入(避免多次系统调用的开销)。
- 定义于
m_iv[2]
的具体用途:- 数组长度为 2,通常用于分别存储 HTTP 响应的头部信息和文件内容(或响应体):
m_iv[0]
:关联响应头部缓冲区(如m_write_buf
),存储状态行、响应头、空行等元数据。m_iv[1]
:关联响应体数据(如通过mmap
映射的文件内容m_file_address
),存储实际的资源数据(如 HTML 内容、图片二进制数据等)。
- 数组长度为 2,通常用于分别存储 HTTP 响应的头部信息和文件内容(或响应体):
与发送流程的关联:
- 在构建响应时,
m_iv_count
会记录实际使用的iovec
数量(通常为 2,即头部 + 体部)。 - 调用
writev(m_sockfd, m_iv, m_iv_count)
时,系统会按顺序发送m_iv[0]
和m_iv[1]
指向的数据,实现 “零拷贝” 或高效批量发送,减少 I/O 操作次数,提升性能。
- 在构建响应时,
m_iv
是 HTTP 服务器优化响应发送效率的关键结构,通过整合分散的响应数据缓冲区,实现一次系统调用完成多段数据的发送。
http_conn
类的私有成员中声明了整数变量 m_iv_count
,用于记录 m_iv
数组中实际使用的 struct iovec
结构体元素数量。
具体说明:
与
m_iv
的关联:m_iv
是一个包含 2 个struct iovec
元素的数组,用于管理 HTTP 响应的分散缓冲区(如响应头部和响应体)。m_iv_count
则标记当前实际使用了m_iv
中的多少个元素(通常为 1 或 2)。
取值场景:
- 当响应仅包含头部信息(如某些错误响应)时,
m_iv_count
可能为 1(仅使用m_iv[0]
存储头部)。 - 当响应包含头部和文件内容(如正常的静态资源请求)时,
m_iv_count
为 2(m_iv[0]
存头部,m_iv[1]
存文件数据)。
- 当响应仅包含头部信息(如某些错误响应)时,
在发送流程中的作用:
- 调用
writev
系统调用发送响应时,m_iv_count
作为第三个参数传入,告知系统需要发送的缓冲区数量,确保只发送有效数据,避免处理未使用的缓冲区,提升 I/O 效率。
- 调用
m_iv_count
是 m_iv
数组的 “有效长度标记”,用于准确控制多缓冲区数据的发送范围,是高效处理 HTTP 响应发送的重要辅助变量。
enum LINE_STATUS //用于表示 HTTP 请求数据行的解析状态,主要在解析请求行和请求头时判断一行数据是否完整或有效。{LINE_OK = 0,LINE_BAD, // 坏的LINE_OPEN // 当前读取的缓冲区数据不完整};
枚举类型 LINE_STATUS
,用于表示 HTTP 请求数据行的解析状态,主要在解析请求行和请求头时判断一行数据是否完整或有效。
各枚举值的含义:
LINE_OK
:表示成功解析出一行完整的 HTTP 数据(以\r\n
作为行结束符)。例如,请求行GET /index.html HTTP/1.1\r\n
或请求头Host: example.com\r\n
被完整解析后,会返回此状态。LINE_BAD
:表示解析到的行格式错误(不符合 HTTP 协议规范的行结束格式)。例如,行结束符不是\r\n
(如仅\n
或其他字符),此时服务器可判定为无效请求,后续可能返回BAD_REQUEST
错误。LINE_OPEN
:表示当前读取的缓冲区数据中,一行数据尚未完整(未检测到\r\n
结束符)。这种情况通常需要继续读取更多数据到缓冲区,直到凑齐完整的一行或判定为错误。
应用场景:
在 parse_line
方法中,会根据 m_read_buf
中的数据判断行的状态:
- 若检测到完整的
\r\n
,返回LINE_OK
,并更新解析索引(m_checked_idx
和m_start_line
),以便后续处理该行数据(如解析请求方法、URL 等)。 - 若未检测到
\r\n
且缓冲区已读满,可能返回LINE_BAD
或LINE_OPEN
(取决于是否仍有数据待读取)。 - 若数据不完整(未读满缓冲区且无
\r\n
),返回LINE_OPEN
,等待下一次数据到达后继续解析。
LINE_STATUS
是 HTTP 请求解析过程中用于跟踪行数据完整性的状态标记,是逐步解析请求行、请求头的基础。