cpp
// 循环读取客户数据,直到无数据可读或对方关闭连接
// 非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once()
{
LOG_TRACE << "in read_once: m_read_idx: " << m_read_idx << " m_TRIGMode: " << m_TRIGMode;
if (m_read_idx >= READ_BUFFER_SIZE)
{
return false;
}
int bytes_read = 0;
// LT读取数据
if (0 == m_TRIGMode)
{
LOG_TRACE << "0 == m_TRIGMODE LT读取数据";
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
LOG_INFO << "call recv: m_sockfd: " << m_sockfd;
LOG_INFO << " bytes_read: " << bytes_read;
LOG_INFO << "m_read_buf : " << m_read_buf;
LOG_INFO << "m_read_idx: " << m_read_idx;
LOG_INFO << "m_read_buf + m_read_idx : " << m_read_buf + m_read_idx;
m_read_idx += bytes_read;
if (bytes_read <= 0)
{
return false;
}
return true;
}
else
{
// ET读数据
LOG_TRACE << "1 == m_TRIGMODE" << " ET读数据 ";
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;
}
}
这段代码是 http_conn
类中的 read_once
方法,用于从客户端套接字读取 HTTP 请求数据到缓冲区,并根据配置的触发模式(LT 或 ET)处理非阻塞 I/O 读取逻辑,确保高效且完整地获取请求数据。
核心功能与设计背景
在 HTTP 服务器中,客户端的请求数据通过套接字传输,由于使用非阻塞 I/O,需要通过该方法读取数据并存储到 m_read_buf
缓冲区中。该方法针对水平触发(LT) 和边缘触发(ET) 两种模式做了不同处理,以适配 epoll 的两种工作模式。
关键变量说明
m_read_buf
:用于存储读取到的请求数据的缓冲区。m_read_idx
:记录缓冲区中已读取数据的末尾位置(即下一次读取的起始点)。READ_BUFFER_SIZE
:缓冲区的最大容量(预定义常量),避免缓冲区溢出。m_sockfd
:当前连接的客户端套接字文件描述符。m_TRIGMode
:触发模式标记(0 为 LT 模式,1 为 ET 模式)。bytes_read
:单次recv
调用读取到的字节数
代码逻辑详解
1. 缓冲区溢出检查
if (m_read_idx >= READ_BUFFER_SIZE) {
return false;
}
- 若当前已读取数据长度(
m_read_idx
)达到缓冲区上限,无法继续读取,返回false
。
2. 水平触发(LT)模式读取(m_TRIGMode == 0
)
LT 模式下,epoll 会持续通知有数据可读,直到数据被读完。因此只需读取一次:
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;
- 调用
recv
从套接字读取数据,存储到m_read_buf
中m_read_idx
之后的位置,读取长度为缓冲区剩余空间(READ_BUFFER_SIZE - m_read_idx
)。 - 若
bytes_read <= 0
:bytes_read == 0
:客户端关闭连接。bytes_read == -1
:读取出错(如连接异常)。- 均返回
false
表示读取失败。
- 否则更新
m_read_idx
并返回true
,表示读取成功。
3. 边缘触发(ET)模式读取(m_TRIGMode == 1
)
ET 模式下,epoll 仅在数据到达时通知一次,需一次性读完所有可用数据:
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;
- 使用
while
循环持续调用recv
,直到无数据可读。 - 当
bytes_read == -1
且错误码为EAGAIN
或EWOULDBLOCK
时,表明当前数据已全部读完(非阻塞模式下的正常返回),退出循环。 - 若
bytes_read == 0
或其他错误,返回false
。 - 循环结束后返回
true
,表示已读完所有可用数据。
总结
read_once
方法是 HTTP 服务器处理请求数据的关键步骤,通过区分 LT/ET 模式实现了高效的非阻塞读取:
- LT 模式下按需单次读取,依赖 epoll 持续通知。
- ET 模式下通过循环一次性读完所有数据,减少 epoll 通知次数,提升性能。
该方法确保了请求数据被完整存入缓冲区,为后续的 HTTP 协议解析(如解析请求行、请求头)提供了完整的输入数据。
cpp
// 解析http请求行,获得请求方法,目标url及http版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{
LOG_TRACE << "text: " << text;
m_url = strpbrk(text, " \t");
LOG_INFO << "m_url: " << m_url;
if (nullptr == m_url || *m_url == '\0')
{
return BAD_REQUEST;
}
*m_url++ = '\0';
char *method = text;
if (strcasecmp(method, "GET") == 0)
{
m_method = GET;
}
else if (strcasecmp(method, "POST") == 0)
{
m_method = POST;
cgi = 1;
}
else
{
return BAD_REQUEST;
}
LOG_INFO << "m_method: " << m_method << " " << getMethod(m_method);
m_url += strspn(m_url, " \t");//检索匹配连续的\t
m_version = strpbrk(m_url, " \t");//查找从m_url开始后第一个\t的位置
LOG_INFO << "m_version : " << m_version;
if (nullptr == m_version)
{
return BAD_REQUEST;
}
*m_version++ = '\0';
m_version += strspn(m_version, " \t");//可能不止一个\t,GET https://www.baidu.com/ HTTP/1.1
LOG_INFO << "m_version: " << m_version;
if (strcasecmp(m_version, "HTTP/1.1") != 0)
{
return BAD_REQUEST;
}
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, '/');
}
LOG_INFO << "m_version: " << m_version;
// for(int i = 0;m_url[i] != '\0'; ++i)
// {
// printf("%d => %d => %c \n",i,m_url[i],m_url[i]);
// }
if (nullptr == m_url || m_url[0] != '/')
{
return BAD_REQUEST;
}
// 当url为/时,显示判断界面
if (strlen(m_url) == 1)
{
strcat(m_url, "judge.html");
}
LOG_INFO << "m_url : " << m_url;
m_check_state = CHECK_STATE_HEADER;
return NO_REQUEST;
//return
}
cpp
int main()
{
const char* str1 = "importent siglificant improve";
const char* str2 = "si";
auto x = strpbrk(str1, str2);//在第一个字符串str1中查找第一个匹配第二个字符串str2中任一字符的字符,并返回该字符在str1中的位置
cout << *x << endl;
}
这段代码是 http_conn
类中的 parse_request_line
方法,用于解析 HTTP 请求行 ,提取请求方法(如 GET/POST)、目标 URL 和 HTTP 版本号,并进行格式校验。请求行是 HTTP 请求的第一行,格式为 方法 URL 版本号
(如 GET /index.html HTTP/1.1
)。
核心功能与流程
1. 定位 URL 位置
m_url = strpbrk(text, " \t");
strpbrk(text, " \t")
在请求行字符串text
中查找第一个空格或制表符,定位到 URL 的起始位置(请求方法后的分隔符)。- 若未找到分隔符(
m_url
为空),说明请求行格式错误,返回BAD_REQUEST
。
2. 提取并校验请求方法
*m_url++ = '\0'; // 将分隔符替换为字符串结束符,拆分出请求方法
char *method = text; // text 此时指向请求方法(如 "GET")
if (strcasecmp(method, "GET") == 0) {
m_method = GET;
} else if (strcasecmp(method, "POST") == 0) {
m_method = POST;
cgi = 1; // POST 方法通常需要 CGI 处理
} else {
return BAD_REQUEST; // 不支持其他方法
}
- 通过替换分隔符为
\0
,将text
截断为纯请求方法字符串(如text
变为 "GET")。 - 仅支持 GET 和 POST 方法,其他方法返回错误。
3. 定位并校验 HTTP 版本号
m_url += strspn(m_url, " \t"); // 跳过 URL 前的空格/制表符
m_version = strpbrk(m_url, " \t"); // 查找 URL 后的分隔符,定位版本号
if (nullptr == m_version) {
return BAD_REQUEST; // 无版本号,格式错误
}
*m_version++ = '\0'; // 拆分出 URL
m_version += strspn(m_version, " \t"); // 跳过版本号前的空格/制表符
if (strcasecmp(m_version, "HTTP/1.1") != 0) {
return BAD_REQUEST; // 仅支持 HTTP/1.1 版本
}
- 先跳过 URL 前的空白字符,确保
m_url
指向 URL 实际内容。 - 再通过分隔符定位版本号,拆分后校验是否为
HTTP/1.1
(仅支持该版本)。
4. 处理 URL 格式
// 移除 URL 中的协议前缀(http:// 或 https://)
if (strncasecmp(m_url, "http://", 7) == 0) {
m_url += 7;
m_url = strchr(m_url, '/'); // 定位到域名后的路径(如 /index.html)
}
if (strncasecmp(m_url, "https://", 8) == 0) {
m_url += 8;
m_url = strchr(m_url, '/');
}
// 校验 URL 格式(必须以 / 开头)
if (nullptr == m_url || m_url[0] != '/') {
return BAD_REQUEST;
}
// 特殊处理:若 URL 为根路径 /,默认指向 judge.html
if (strlen(m_url) == 1) {
strcat(m_url, "judge.html");
}
- 移除可能存在的
http://
或https://
前缀,仅保留路径部分(如将http://example.com/index.html
处理为/index.html
)。 - 确保 URL 以
/
开头(HTTP 规范要求),否则返回错误。 - 根路径
/
自动映射到 judge.html 页面。
5. 状态更新与返回
m_check_state = CHECK_STATE_HEADER; // 解析完请求行后,下一步解析请求头
return NO_REQUEST; // 表示需要继续解析(尚未获取完整请求)
关键变量说明
text
:指向请求行的字符串(如GET /index.html HTTP/1.1
)。m_url
:存储解析后的 URL 路径(如/index.html
)。m_method
:存储请求方法(GET 或 POST)。m_version
:存储 HTTP 版本号(需为HTTP/1.1
)。m_check_state
:状态机变量,更新为CHECK_STATE_HEADER
表示下一步解析请求头。cgi
:标记是否需要 CGI 处理(POST 方法设为 1)。
总结
parse_request_line
是 HTTP 请求解析的第一步,通过字符串处理函数拆分请求行,校验格式合法性,并提取关键信息(方法、URL、版本)。解析成功后,将状态机切换到请求头解析阶段,为后续处理(如解析请求头、处理 CGI 等)奠定基础。
cpp
// 解析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;
LOG_INFO<<"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");
if (strcasecmp(text, "keep-alive") == 0)
{
m_linger = true;
}
}
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: " << text;
}
return NO_REQUEST;
}
这段代码是 http_conn
类中的 parse_headers
方法,用于解析 HTTP 请求头(Header),提取关键头部信息(如连接方式、内容长度、主机名等),并根据解析结果更新状态机,决定下一步处理逻辑。
核心功能与流程
HTTP 请求头由多行键值对组成(如 Connection: keep-alive
),每行以 \r\n
结尾,所有头部结束后以空行(\r\n
)标识。该方法逐行解析这些头部,提取必要信息并更新状态。
1. 处理头部结束标志(空行)
if (text[0] == '\0') {
if (m_content_length != 0) {
m_check_state = CHECK_STATE_CONTENT; // 需要继续解析请求体
LOG_INFO << "m_check_state = CHECK_STATE_CONTENT";
return NO_REQUEST;
}
return GET_REQUEST; // 头部解析完成,且无请求体
}
- 当
text
为空字符串(text[0] == '\0'
)时,表示已读到头部结束的空行。- 若
m_content_length != 0
(存在请求体):状态机切换到CHECK_STATE_CONTENT
,返回NO_REQUEST
表示需要继续解析请求体。 - 若
m_content_length == 0
(无请求体):返回GET_REQUEST
表示整个请求解析完成。
- 若
2. 解析 Connection
头部
else if (strncasecmp(text, "Connection:", 11) == 0) {
text += 11; // 跳过 "Connection:"
text += strspn(text, " \t"); // 跳过头部值前的空格/制表符
if (strcasecmp(text, "keep-alive") == 0) {
m_linger = true; // 标记为长连接
}
}
strncasecmp
不区分大小写比较,判断是否为Connection
头部。- 提取头部值(如
keep-alive
),若为keep-alive
则设置m_linger = true
,表示客户端希望保持连接。
3. 解析 Content-length
头部
else if (strncasecmp(text, "Content-length:", 15) == 0) {
text += 15; // 跳过 "Content-length:"
text += strspn(text, " \t"); // 跳过空格/制表符
m_content_length = atol(text); // 转换为整数,记录请求体长度
}
- 提取
Content-length
的值(请求体的字节数),存储到m_content_length
,用于后续判断请求体是否完整。
4. 解析 Host
头部
else if (strncasecmp(text, "Host:", 5) == 0) {
text += 5; // 跳过 "Host:"
text += strspn(text, " \t"); // 跳过空格/制表符
m_host = text; // 记录主机名(如 `example.com`)
}
Host
头部是 HTTP/1.1 必需的,用于指定请求的主机名(尤其在虚拟主机场景中),将其存储到m_host
。
5. 处理未知头部
else {
LOG_INFO << " oop!unknow header: " << text; // 打印未知头部,不做处理
}
- 对不认识的头部仅打印日志,不影响整体解析(HTTP 允许扩展头部)。
6. 默认返回值
return NO_REQUEST; // 表示仍需继续解析其他头部
- 除头部结束的情况外,解析完一行头部后返回
NO_REQUEST
,告知调用者需要继续解析下一行。
关键变量说明
text
:指向当前待解析的请求头行(已被parse_line
方法处理为以\0
结尾的字符串)。m_check_state
:状态机变量,用于标记当前解析阶段(此处可能切换到CHECK_STATE_CONTENT
)。m_linger
:标记是否为长连接(keep-alive
)。m_content_length
:记录请求体的长度(用于判断请求体是否完整)。m_host
:存储请求的主机名。HTTP_CODE
返回值:NO_REQUEST
(需继续解析)、GET_REQUEST
(解析完成)。
总结
parse_headers
方法是 HTTP 请求解析的第二阶段(继请求行之后),通过识别关键头部字段(Connection
、Content-length
、Host
),获取连接方式、请求体长度等核心信息,并根据是否存在请求体决定下一步解析请求体还是结束解析。该方法确保服务器正确理解客户端的请求细节,为后续处理(如读取请求体、返回响应)提供必要的元数据。
cpp
// 判断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 请求数据在读取缓冲区中的位置和解析进度,是 HTTP 请求解析状态管理的核心变量。
各变量含义与作用:
-
m_read_idx
- 表示读取缓冲区中已有效存储的数据长度(即已从 socket 读取到缓冲区的字节总数)。
- 缓冲区
m_read_buf
的有效数据范围是[0, m_read_idx - 1]
,新读取的数据会从m_read_idx
位置开始存储。 - 例如:若从 socket 读取了 500 字节数据,则
m_read_idx
会被更新为 500,标识缓冲区前 500 字节为有效数据。
-
m_checked_idx
- 表示已解析校验过的缓冲区数据位置(即已完成解析的字节数)。
- 解析过程中,从
m_checked_idx
开始处理未解析的数据,解析完成后更新该值。 - 例如:若已解析完前 300 字节,则
m_checked_idx
为 300,下一次解析从 300 位置开始。
-
m_start_line
- 表示当前正在解析的请求行 / 请求头的起始位置(在缓冲区中的索引)。
- HTTP 请求的行(如请求行、请求头)以
\r\n
结尾,解析一行时,m_start_line
指向该行的第一个字符,解析完成后更新为下一行的起始位置。 - 例如:解析完一行后,通过
\r\n
定位到下一行的开头,将m_start_line
设为该位置。
三者的协同关系:
- 缓冲区数据的处理流程:
m_start_line
(当前行起始)→ 解析该行数据 → 更新m_checked_idx
(已解析到的位置)→ 读取新数据时更新m_read_idx
(总有效数据长度)。 - 例如:
- 初始时
m_read_idx = 0
(无数据),m_checked_idx = 0
(未解析),m_start_line = 0
(准备解析第一行)。 - 读取数据后
m_read_idx
增加,解析时从m_checked_idx
开始查找\r\n
分隔符,确定行的范围[m_start_line, 分隔符位置]
,解析完成后将m_start_line
设为分隔符后一位,m_checked_idx
同步更新。
- 初始时