项目中HTTP协议处理部分(续)

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_bufm_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 且错误码为 EAGAINEWOULDBLOCK 时,表明当前数据已全部读完(非阻塞模式下的正常返回),退出循环。
  • 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 请求解析的第二阶段(继请求行之后),通过识别关键头部字段(ConnectionContent-lengthHost),获取连接方式、请求体长度等核心信息,并根据是否存在请求体决定下一步解析请求体还是结束解析。该方法确保服务器正确理解客户端的请求细节,为后续处理(如读取请求体、返回响应)提供必要的元数据。

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 请求解析状态管理的核心变量。

各变量含义与作用:

  1. m_read_idx

    • 表示读取缓冲区中已有效存储的数据长度(即已从 socket 读取到缓冲区的字节总数)。
    • 缓冲区 m_read_buf 的有效数据范围是 [0, m_read_idx - 1],新读取的数据会从 m_read_idx 位置开始存储。
    • 例如:若从 socket 读取了 500 字节数据,则 m_read_idx 会被更新为 500,标识缓冲区前 500 字节为有效数据。
  2. m_checked_idx

    • 表示已解析校验过的缓冲区数据位置(即已完成解析的字节数)。
    • 解析过程中,从 m_checked_idx 开始处理未解析的数据,解析完成后更新该值。
    • 例如:若已解析完前 300 字节,则 m_checked_idx 为 300,下一次解析从 300 位置开始。
  3. 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 同步更新。
相关推荐
xixixi777773 小时前
SOC(安全运营中心)
网络·安全·soc·安全运营中心
失散133 小时前
分布式专题——25 深入理解网络通信和TCP、IP协议
java·分布式·网络协议·tcp/ip·架构
想成为大佬的每一天6 小时前
Linux驱动之V4L2
网络
Lowjin_10 小时前
计算机网络-RIP协议
网络·计算机网络·智能路由器
问道飞鱼12 小时前
【数据库知识】TxSQL 主从数据库同步底层原理深度解析
网络·数据库·半同步复制·txsql
粟悟饭&龟波功14 小时前
【网络安全】一、入门篇:读懂 HTTP 协议
安全·web安全·http
骥龙14 小时前
粤港澳全运会网络安全防御体系深度解析:威胁态势与实战防护
网络·安全·web安全
漫谈网络15 小时前
InfiniBand 深度解析
网络·rdma·infiniband·roce v2
海域云赵从友15 小时前
从直播卡顿到流畅带货:SD-WAN网络专线如何优化阿联酋TikTok体验?
网络