Webserve(3): HTTP解析

cpp 复制代码
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';
        return GET_REQUEST;
    }
    return NO_REQUEST;
}

这段代码是处理 HTTP 请求消息体(Content)的函数部分。它的目的是根据之前从 HTTP 头部解析得到的 Content-Length 值来确定是否已经接收到了完整的请求体。下面是对这段代码的详细解释:

函数原型解释

http_conn::HTTP_CODE http_conn::parse_content(char* text)

  • 这是 http_conn 类的一个成员函数,负责解析 HTTP 请求的内容部分。
  • 函数接收一个指向字符数组的指针 text,这个数组包含了请求的消息体。
  • 返回值是一个 HTTP_CODE 枚举值,用于表示解析的状态或结果。

代码逻辑

  1. 完整性检查

    • if (m_read_idx >= (m_content_length + m_checked_idx)) 这行代码检查是否已经读取了足够的数据,即从开始检查到目前为止的索引 m_read_idx 是否大于或等于预期的内容长度 m_content_length 加上之前已经检查过的索引 m_checked_idx
    • 这里的 m_read_idx 表示当前已经读取到的数据的位置,m_content_length 是从 HTTP 头部 Content-Length 字段解析得到的请求体长度,m_checked_idx 是之前已经处理过的数据的位置。
  2. 设置字符串结束标志

    • 如果已经接收到了完整的请求体,即条件判断为真,那么就在请求体的末尾加上字符串结束符 '\0',以便后续处理时可以当作字符串来操作。这里假设请求体是文本数据。
    • text[m_content_length] = '\0'; 这行代码是在请求体的正文内容后面加上结束符,确保文本字符串的正确结束。
  3. 返回处理结果

    • 如果请求体已完整接收,函数返回 GET_REQUEST,表示已经获取到了一个完整的 HTTP 请求,可以进行下一步的处理。
    • 如果当前读取的内容还不足以构成完整的请求体,函数返回 NO_REQUEST,表示需要继续读取数据。

总结

这个函数主要用于处理带有内容体的 HTTP 请求(例如 POST 请求),通过检查已接收数据的长度与 Content-Length 标头指定的长度是否匹配来确定请求体是否完整。如果请求体接收完整,就准备好了进行后续的请求处理,比如解析请求体中的数据。这是处理 HTTP 请求的一个重要步骤,确保了服务器能够正确处理完整的请求数据。

cpp 复制代码
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;
        printf( "got 1 http line: %s\n", text );

        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;
                } 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 类的 process_read 方法的实现,用于处理读取HTTP请求的整个过程。方法通过解析请求行、头部和内容,最终决定如何响应客户端的请求。下面是对该方法的逐步解析:

方法逻辑

  1. 初始化状态

    • LINE_STATUS line_status = LINE_OK; 初始化行状态为 LINE_OK,表示读取行的操作是成功的。
    • HTTP_CODE ret = NO_REQUEST; 初始化HTTP响应码为 NO_REQUEST,表示尚未接收到完整的HTTP请求。
    • char* text = 0; 初始化文本指针为 nullptr,用于后续指向解析得到的文本行。
  2. 循环读取行

    • 使用**while 循环来读取每一行数据。循环条件包括两部分** :
      • 如果当前状态是解析内容(CHECK_STATE_CONTENT)并且行状态为 LINE_OK
      • 或者,调用 parse_line() 方法解析行并检查返回的行状态是否为 LINE_OK
  3. 处理各种状态

    • 根据当前的检查状态(m_check_state),使用 switch 语句对不同的状态进行处理:
      • CHECK_STATE_REQUESTLINE:处理请求行。使用 parse_request_line 方法解析请求行,如果请求行有问题,则直接返回 BAD_REQUEST
      • CHECK_STATE_HEADER:处理头部。使用 parse_headers 方法解析头部,如果头部有问题,返回 BAD_REQUEST;如果获取到一个完整的HTTP请求,则调用 do_request 方法处理请求。
      • CHECK_STATE_CONTENT:处理内容。使用 parse_content 方法解析内容,如果内容完整,则调用 do_request 方法处理请求。此时,将行状态设置为 LINE_OPEN,表示内容可能还未完全接收完毕。
  4. 返回处理结果

    • 最后,如果在循环中未能进一步处理(例如,未接收到完整的请求或正在等待更多的数据),则返回 NO_REQUEST,表示继续等待和接收数据。

方法的调用逻辑

  • process_read 方法通过逐行解析HTTP请求并根据请求的不同部分(请求行、头部、内容)采取相应的动作,是处理HTTP请求的核心逻辑之一。
  • 对于每一部分的解析,该方法依赖于其他辅助方法(如 parse_request_line, parse_headers, 和 parse_content),这些方法专注于处理HTTP请求的具体一个环节。
  • 该方法展示了一个状态机的实现方式,通过不同的状态控制流程,直到收集和解析了所有必要的数据,最终决定如何响应客户端请求。

错误处理和状态转换

  • 方法中的错误处理非常直接,一旦遇到无法解析的请求行或头部,就立即返回 BAD_REQUEST,表示客户端的请求存在语法错误或不被服务器支持
  • 状态转换确保了服务器能够按照HTTP请求的结构逐步处理每个部分,直至完成请求的处理或确定请求无法被处理。
    这段代码中的 while 循环主要用于解析 HTTP 请求的内容。让我们逐步解释为什么要这样写:
cpp 复制代码
while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK))
        || ((line_status = parse_line()) == LINE_OK))

解释循环条件

  1. 检查状态和行状态

    • m_check_state == CHECK_STATE_CONTENT :此条件检查当前状态是否为解析内容CHECK_STATE_CONTENT)。
    • line_status == LINE_OK:此条件检查行状态是否为 LINE_OK,表示上一行已成功解析。
  2. 解析行

    • line_status = parse_line():这部分是 parse_line() 函数的调用,它负责解析 HTTP 请求的每一行。
    • ((line_status = parse_line()) == LINE_OK):这部分检查解析行的返回状态是否为 LINE_OK表示当前行已经成功解析
  3. 组合条件

    • (m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK):如果当前状态为解析内容,并且上一行解析成功,说明继续解析内容。
    • ((line_status = parse_line()) == LINE_OK):如果上一行解析成功,继续解析下一行。

解释循环逻辑

  • 这个 while 循环用于不断地解析 HTTP 请求的每一行,直到解析完成整个 HTTP 请求。
  • 当前状态为 CHECK_STATE_CONTENT 时,并且上一行解析成功,表示当前正在解析请求体内容。
  • 当前状态不是 CHECK_STATE_CONTENT 时,或者上一行解析失败,说明当前需要继续解析请求的其他部分(请求行、头部等)。
  • 循环条件保证了在每一轮迭代中,要么继续解析请求体内容,要么继续解析其他部分的行。

循环条件的细节

  • 循环条件中的 || 表示逻辑或关系,即只要满足其中一个条件,循环就会继续执行
  • 如果当前状态是解析内容并且行状态是 LINE_OK或者解析行的返回状态是 LINE_OK循环将继续执行。
  • parse_line() 解析到请求结束时,返回 LINE_OPEN,循环条件不满足,循环退出。

总结

这样的循环逻辑确保了 HTTP 请求的每一行都会被正确地解析,并根据解析结果做出相应的处理。通过这样的设计,服务器能够在接收到完整的 HTTP 请求之后,准确地进行请求的处理和响应。

问题:这个循环为什么不直接写成while(line_status == LINE_OK)

cpp 复制代码
// 当得到一个完整、正确的HTTP请求时,我们就分析目标文件的属性,
// 如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其
// 映射到内存地址m_file_address处,并告诉调用者获取文件成功
http_conn::HTTP_CODE http_conn::do_request()
{
    // "/home/nowcoder/webserver/resources"
    strcpy( m_real_file, doc_root );
    int len = strlen( doc_root );
    strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 );
    // 获取m_real_file文件的相关的状态信息,-1失败,0成功
    if ( stat( m_real_file, &m_file_stat ) < 0 ) {
        return NO_RESOURCE;
    }

    // 判断访问权限
    if ( ! ( m_file_stat.st_mode & S_IROTH ) ) {
        return FORBIDDEN_REQUEST;
    }

    // 判断是否是目录
    if ( S_ISDIR( m_file_stat.st_mode ) ) {
        return BAD_REQUEST;
    }

    // 以只读方式打开文件
    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 );
    return FILE_REQUEST;
}

这段代码是 http_conn 类的 do_request 方法的实现 ,用于处理客户端的 HTTP 请求。主要功能是根据请求的 URL 找到对应的文件,并进行相关的权限和文件类型检查,最后将文件映射到内存中以便读取。下面是对这段代码的逐行解释:

  1. strcpy(m_real_file, doc_root);:将服务器的根目录路径 doc_root 复制到 m_real_file 字符数组中。m_real_file 用于存储请求的实际文件路径

  2. int len = strlen(doc_root);:获取服务器根目录路径的长度。

  3. strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);:将请求的 URL 路径拼接到 m_real_file 后面,组成完整的文件路径。这里使用 strncpy 函数来保证不超出数组范围。

  4. if (stat(m_real_file, &m_file_stat) < 0) { return NO_RESOURCE; }:使用 stat 函数获取文件的状态信息,并存储在 m_file_stat 结构体中。如果获取失败,则表示请求的资源不存在,返回 NO_RESOURCE

  5. if (!(m_file_stat.st_mode & S_IROTH)) { return FORBIDDEN_REQUEST; }:检查文件的权限,如果当前用户没有读取文件的权限,则返回 FORBIDDEN_REQUEST,表示禁止访问。

  6. if (S_ISDIR(m_file_stat.st_mode)) { return BAD_REQUEST; }:检查文件是否是目录,如果是目录,则返回 BAD_REQUEST,表示请求的资源无效。

  7. int fd = open(m_real_file, O_RDONLY);:以只读方式打开文件,获取文件描述符 fd

  8. m_file_address = (char*) mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);:使用 mmap 函数将文件映射到内存中,以便快速读取文件内容。参数包括内存起始地址、文件大小、访问权限、映射方式、文件描述符和偏移量。映射成功后,返回的地址存储在 m_file_address 中。

  9. close(fd);:关闭文件描述符,释放资源。

  10. return FILE_REQUEST;:表示成功处理了文件请求,可以进行文件读取操作。

这段代码是处理客户端 HTTP 请求的核心部分,完成了根据请求的 URL 找到对应文件、权限检查、文件映射等操作,为后续文件读取操作做好了准备。
strcpystrncpy 是 C 语言中用于字符串操作的函数,它们的作用是将一个字符串复制到另一个字符串中。主要的区别在于复制的方式和对目标字符串的处理。

strcpy 函数

strcpy 函数用于将一个字符串复制到另一个字符串中,直到遇到空字符 \0 为止。

char* strcpy(char* destination, const char* source);

  • destination:目标字符串的指针,表示将要被复制到的字符串。
  • source:源字符串的指针,表示要复制的字符串。

示例:

char destination[20];

char source[] = "Hello, World!";

strcpy(destination, source);

strncpy 函数

strncpy 函数用于将一个字符串的一部分复制到另一个字符串中,最多复制指定数量的字符,如果源字符串不足,则用空字符 \0 填充剩余的空间。

char* strncpy(char* destination, const char* source, size_t num);

  • destination:目标字符串的指针,表示将要被复制到的字符串。
  • source:源字符串的指针,表示要复制的字符串。
  • num:要复制的最大字符数。

示例:

char destination[20]; char source[] = "Hello, World!"; strncpy(destination, source, 5); destination[5] = '\0'; // Ensure null-termination

区别和注意事项

  • strcpy 不会检查源字符串的长度,如果源字符串比目标字符串长,可能会导致缓冲区溢出问题。
  • strncpy 会复制指定数量的字符,并在必要时添加空字符 \0,以确保目标字符串不会溢出。
  • strncpy 在复制结束后不会自动添加空字符 \0,因此在使用后需要手动添加,以确保目标字符串以空字符结尾。

在使用这两个函数时,应根据具体的需求选择适当的函数,并确保目标字符串有足够的空间来容纳源字符串的内容。

strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 );

这句代码是将字符串 m_url 的内容复制 到字符串 m_real_file 的指定位置。让我们逐步解释这句代码的含义:

  1. m_real_file + len:这个表达式表示 m_real_file 字符串的地址偏移了 len 个字符。因为 m_real_file 是一个字符数组(字符串),所以它指向字符串的起始位置。通过 + len 的操作,将指针移动到 m_real_file 字符串的第 len 个字符处,即指针指向了字符串的末尾位置 (不包括字符串结束符 \0)。

  2. m_url这个参数是源字符串,即要被复制的字符串。它是一个以 \0 结尾的字符串

  3. FILENAME_LEN - len - 1:这个表达式表示最大允许复制的字符数FILENAME_LEN 是目标字符串 m_real_file 的最大长度,len 是已经占用的长度,1 是为了留出空间放置字符串结束符 \0。因此,这个表达式计算出剩余的可用空间。

  4. strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);:这是调用 strncpy 函数,将 m_url 字符串的内容复制到 m_real_file 字符串的指定位置。它从 m_url 的首字符开始复制,复制到 m_real_file 的地址偏移了 len 个字符处,最多复制 FILENAME_LEN - len - 1 个字符。这个函数会确保复制的内容不超过指定的长度,并且会在目标字符串末尾添加 \0 字符以确保字符串的终止。

cpp 复制代码
// 对内存映射区执行munmap操作
void http_conn::unmap() {
    if( m_file_address )
    {
        munmap( m_file_address, m_file_stat.st_size );
        m_file_address = 0;
    }
}

这段代码是 http_conn 类的 unmap 方法的实现,用于取消内存映射区。让我们逐行解释代码的含义:

  1. if (m_file_address):检查 m_file_address 是否为非空指针,即是否已经进行了内存映射操作。

  2. munmap(m_file_address, m_file_stat.st_size);:调用 munmap 函数取消内存映射,将之前映射的内存区域释放。参数 m_file_address 是要取消映射的起始地址,m_file_stat.st_size 是要取消映射的长度。

  3. m_file_address = 0;:将 m_file_address 设置为零,表示取消了内存映射。

总结

这段代码的主要功能是取消之前创建的内存映射区,释放已经映射的内存。这在 HTTP 服务器中是很常见的操作,特别是在处理大文件时,为了释放内存和资源而取消内存映射非常重要。

相关推荐
前端李易安41 分钟前
什么是HTTP,什么是HTTPS?HTTP和HTTPS都有哪些区别?
网络协议·http·https
胎粉仔42 分钟前
网络初阶——应用层:HTTPS 协议
网络协议·http·https
Koi慢热1 小时前
信息收集合集
网络·安全·web安全·网络安全
轩轩曲觞阁1 小时前
Linux网络——网络初识
linux·网络
摘星星ʕ•̫͡•ʔ2 小时前
计算机网络 第二章:物理层
网络·计算机网络
linnux领域2 小时前
使用ensp配置单臂路由、静态路由,实现PC互相通信。
网络
hgdlip2 小时前
本机ip地址和网络ip地址一样吗
网络·网络协议·tcp/ip·网络ip地址·本机ip地址
EasyCVR2 小时前
ISUP协议视频平台EasyCVR视频设备轨迹回放平台智慧农业视频远程监控管理方案
服务器·网络·数据库·音视频
长安11086 小时前
前后端、网关、协议方面补充
网络
hzyyyyyyyu9 小时前
隧道技术-tcp封装icmp出网
网络·网络协议·tcp/ip