当前位置:   article > 正文

Webserve(4): HTTP解析

Webserve(4): HTTP解析
  1. // 写HTTP响应
  2. bool http_conn::write()
  3. {
  4. int temp = 0;
  5. if ( bytes_to_send == 0 ) {
  6. // 将要发送的字节为0,这一次响应结束。
  7. modfd( m_epollfd, m_sockfd, EPOLLIN );
  8. init();
  9. return true;
  10. }
  11. while(1) {
  12. // 分散写
  13. temp = writev(m_sockfd, m_iv, m_iv_count);
  14. if ( temp <= -1 ) {
  15. // 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,虽然在此期间,
  16. // 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性。
  17. if( errno == EAGAIN ) {
  18. modfd( m_epollfd, m_sockfd, EPOLLOUT );
  19. return true;
  20. }
  21. unmap();
  22. return false;
  23. }
  24. bytes_have_send += temp;
  25. bytes_to_send -= temp;
  26. if (bytes_have_send >= m_iv[0].iov_len)
  27. {
  28. m_iv[0].iov_len = 0;
  29. m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
  30. m_iv[1].iov_len = bytes_to_send;
  31. }
  32. else
  33. {
  34. m_iv[0].iov_base = m_write_buf + bytes_have_send;
  35. m_iv[0].iov_len = m_iv[0].iov_len - temp;
  36. }
  37. if (bytes_to_send <= 0)
  38. {
  39. // 没有数据要发送了
  40. unmap();
  41. modfd(m_epollfd, m_sockfd, EPOLLIN);
  42. if (m_linger)
  43. {
  44. init();
  45. return true;
  46. }
  47. else
  48. {
  49. return false;
  50. }
  51. }
  52. }
  53. }

这段代码实现了写入 HTTP 响应的功能。让我们逐行解释它的工作原理:

  1. if (bytes_to_send == 0):检查是否还有待发送的字节。如果没有,则说明本次响应已经完成,重新初始化连接并等待下一个请求。

  2. while(1):进入一个无限循环,以确保所有要发送的数据都能够被写入。

  3. temp = writev(m_sockfd, m_iv, m_iv_count);:调用 writev 函数向套接字写入数据。m_iviovec 结构的数组,用于描述要写入的数据块。m_iv_count 表示 m_iv 数组中的元素数量。writev 函数会尽可能地将所有数据一次性写入套接字。

  4. if (temp <= -1):检查 writev 函数的返回值,如果出现错误,则根据错误类型进行处理。

    • 如果错误为 EAGAIN,表示套接字写缓冲区已满,无法继续写入数据,需要等待下一次 EPOLLOUT 事件再次写入。因此,修改套接字的事件监听为 EPOLLOUT,然后返回 true,表示本次写入未完成,需要等待下一次写入机会。

    • 其他错误情况,可能是连接出现了问题,需要取消内存映射,并返回 false

  5. bytes_have_send += temp;:更新已经发送的字节数。

  6. bytes_to_send -= temp;:更新待发送的字节数。

  7. if (bytes_have_send >= m_iv[0].iov_len):检查是否已经发送完 m_iv[0] 描述的数据块。如果是,则更新 m_iv[1] 描述的数据块为下一块要发送的数据。

  8. if (bytes_to_send <= 0):检查是否所有数据均已发送。如果是,则取消内存映射,修改事件监听为 EPOLLIN,并根据是否需要保持连接来重新初始化连接或者关闭连接。

  9. 循环会继续写入剩余的数据,直到所有数据均已发送完成或者发生错误。

这段代码实现了有效的分散写操作,可以高效地将 HTTP 响应数据写入套接字。

这段代码逻辑用于更新 iovec 结构数组 m_iv 中描述的数据块,以便继续发送数据。让我们逐行解释:

  1. if (bytes_have_send >= m_iv[0].iov_len):这个条件判断语句检查是否已经发送完了 m_iv[0] 描述的数据块中的内容。如果 bytes_have_send 大于等于 m_iv[0].iov_len,表示 m_iv[0] 描述的数据块中的内容已经全部发送完成。

  2. m_iv[0].iov_len = 0;:将 m_iv[0] 描述的数据块的长度设置为零,表示该数据块中的内容已经全部发送完成,下一次写入将不再包含这部分内容

  3. m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);:更新 m_iv[1] 描述的数据块的起始地址。m_file_address 是之前使用 mmap 函数映射的文件内存起始地址,bytes_have_send - m_write_idx 表示已经发送的字节数减去 m_write_idx,得到当前需要发送的数据在文件内存中的起始位置

  4. m_iv[1].iov_len = bytes_to_send;:更新 m_iv[1] 描述的数据块的长度,使其等于待发送的字节数 bytes_to_send

  5. else:如果 if 条件不满足,则执行 else 分支,表示 m_iv[0] 描述的数据块中的内容尚未全部发送完成。在这种情况下,需要继续发送 m_iv[0] 描述的数据块的剩余内容

  6. m_iv[0].iov_base = m_write_buf + bytes_have_send;:更新 m_iv[0] 描述的数据块的起始地址,使其指向剩余待发送的缓冲区中的数据

  7. m_iv[0].iov_len = m_iv[0].iov_len - temp;:更新 m_iv[0] 描述的数据块的长度,减去已经发送的字节数 temp,以便指向剩余待发送的数据块。

这段代码的目的是确保 writev 函数能够连续地将所有的数据块一次性发送出去,提高发送效率。

  1. // 往写缓冲中写入待发送的数据
  2. bool http_conn::add_response( const char* format, ... ) {
  3. if( m_write_idx >= WRITE_BUFFER_SIZE ) {
  4. return false;
  5. }
  6. va_list arg_list;
  7. va_start( arg_list, format );
  8. int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );
  9. if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) ) {
  10. return false;
  11. }
  12. m_write_idx += len;
  13. va_end( arg_list );
  14. return true;
  15. }

这段代码是 http_conn 类的 add_response 方法的实现,用于将待发送的数据写入写缓冲区中。让我们逐行解释这段代码的功能:

  1. if (m_write_idx >= WRITE_BUFFER_SIZE)检查当前写缓冲区中的数据是否已经超过了缓冲区的大小。如果超过了,说明没有足够的空间来写入新的数据,返回 false 表示写入失败。

  2. va_list arg_list; va_start(arg_list, format);:声明一个 va_list 类型的变量 arg_list,并使用 va_start 宏初始化该变量。这个宏的作用是初始化 arg_list,使其指向可变参数列表中的第一个参数。

  3. int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);:使用 vsnprintf 函数将可变参数格式化为字符串,并将结果写入写缓冲区中。这个函数类似于 sprintf,但是它使用了可变参数列表来支持不定数量的参数。WRITE_BUFFER_SIZE - 1 - m_write_idx 是限制写入长度的上限,以确保不会溢出写缓冲区。

  4. if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)):检查 vsnprintf 函数的返回值 len 是否大于等于剩余空间的大小。如果是,说明写入的数据超出了剩余空间,因此写入失败,返回 false

  5. m_write_idx += len;:更新写缓冲区的索引,将其向后移动 len 个位置,以便下次写入数据时从正确的位置开始。

  6. va_end(arg_list);:结束可变参数列表的遍历。

  7. return true;:写入数据成功,返回 true

这段代码允许使用类似于 printf 的格式化字符串,将数据格式化并写入到写缓冲区中。如果写入失败,可能是由于缓冲区已满或者格式化错误导致的。

这段代码使用了C语言中的可变参数功能,通过 va_list, va_start, 和 vsnprintf 函数来处理和格式化一个不确定数量的参数。下面是对这些关键部分的详细解释:

  1. va_list arg_list;
    va_list一个用于处理可变参数列表的类型arg_list 是一个变量,用于访问函数的可变参数部分。在这里,它将被用来访问传给 add_response 函数的所有可变参数

  2. va_start(arg_list, format);
    va_start 是一个宏,用于初始化 arg_list 变量,以便它可以用于访问可变参数列表va_start 的第一个参数是之前声明的 va_list 变量,第二个参数是可变参数列表之前的最后一个固定参数,这里是 format。这个调用之后,arg_list 将指向函数的第一个可变参数。

  3. int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
    这一行是整个操作中最核心的部分,它执行多个任务:

    • vsnprintf 是一个标准C函数,功能是将可变参数按照 format 指定的格式格式化为字符串,并将结果存储到指定的缓冲区。这里,缓冲区是 m_write_buf + m_write_idx,即写缓冲区当前的写入位置
    • WRITE_BUFFER_SIZE - 1 - m_write_idx 指定了目标缓冲区的最大长度,以防止写入超出缓冲区大小导致的溢出。这里减去1是为了留出位置给字符串的终结符 \0
    • format格式字符串,指定了后续可变参数应该如何被格式化。
    • arg_list 是之前通过 va_start 初始化的可变参数列表。
    • 函数返回写入字符的数量(不包括终结符\0),并将这个数量赋值给 len 变量。如果格式化后的字符串长度超过了缓冲区的限制,vsnprintf 会截断输出但仍会返回完整的字符串长度(如果不截断会有的长度)

通过这个过程,add_response 函数能够将格式化的字符串安全地追加到写缓冲区中,同时避免缓冲区溢出,这是网络编程中处理字符串时的一种常见且重要的做法。

va_end(arg_list); 是用来结束可变参数列表的使用的宏。在使用 va_start 初始化了 va_list 类型的变量以后,你应当在函数返回之前对其使用 va_end,以进行必要的清理。

虽然在许多实现中 va_end 实际上可能什么也不做,调用它是好的编程实践,也是标准C中的要求。这样做可以确保代码的可移植性,因为某些平台或编译器可能需要 va_end 来执行实际的操作,比如释放分配的内存或重置状态。

简而言之,va_end 的作用是结束对可变参数列表的访问,帮助确保资源得到正确管理和释放。在这个上下文中,它标志着 va_list 类型的 arg_list 的使用周期结束:

  1. bool http_conn::add_status_line( int status, const char* title ) {
  2. return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );
  3. }
  4. bool http_conn::add_headers(int content_len) {
  5. add_content_length(content_len);
  6. add_content_type();
  7. add_linger();
  8. add_blank_line();
  9. }
  10. bool http_conn::add_content_length(int content_len) {
  11. return add_response( "Content-Length: %d\r\n", content_len );
  12. }
  13. bool http_conn::add_linger()
  14. {
  15. return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );
  16. }
  17. bool http_conn::add_blank_line()
  18. {
  19. return add_response( "%s", "\r\n" );
  20. }

这段代码是在构建HTTP响应消息的不同部分,用于在服务器端向客户端发送HTTP响应。每个方法都负责生成响应的一部分,按照HTTP协议格式组织信息。下面是每个方法的详细说明:

add_status_line(int status, const char* title)

此方法用于添加HTTP响应的状态行。状态行是HTTP响应消息的第一行,包含HTTP版本号、状态码和原因短语。例如,“HTTP/1.1 200 OK”表示一个成功的请求。

  • "HTTP/1.1" 表示HTTP版本。
  • status 是一个整数,表示HTTP状态码,如200、404等。
  • title 是状态码的文本描述,如"OK"或"Not Found"。
  • 该方法通过调用add_response函数,将格式化的状态行字符串添加到响应缓冲区中。

add_headers(int content_len)

此方法用于添加HTTP响应头。它依次调用其他几个方法来添加特定的头部信息,包括内容长度、内容类型、连接类型和一个空行,标志着头部结束,接下来是消息体。

  • add_content_length(content_len) 添加Content-Length头,指示响应体的长度。
  • add_content_type() 该方法在代码片段中未显示,但其作用应是添加Content-Type头,指明响应体的媒体类型。
  • add_linger() 添加Connection头,根据m_linger成员变量的值决定是保持连接还是关闭连接。
  • add_blank_line() 添加一个空行,表示头部信息结束,按照HTTP协议,头部和正文之间必须有一个空行。

add_content_length(int content_len)

这个方法添加Content-Length响应头,该头部字段告诉客户端响应体的字节长度。这是一个重要的头部字段,特别是在响应体不为空时,因为它允许客户端正确地读取和解析接收到的数据。

add_linger()

这个方法添加Connection响应头,其值基于m_linger的布尔值。如果m_linger为true,头部值为"keep-alive",指示连接应该保持打开状态,以便客户端可以复用连接发送更多的请求。如果为false,则值为"close",指示处理完当前请求后应关闭连接。

add_blank_line()

这个方法向响应中添加一个空行,这在HTTP响应中是必需的,用来分隔头部和正文。在HTTP协议中,头部信息和正文内容之间必须有一个空行(即\r\n),这个方法正是用来添加这个分隔符的。

总体而言,这些方法共同构成了HTTP响应消息的创建和发送过程的一部分,确保了响应格式符合HTTP/1.1协议的规范。

  1. bool http_conn::add_content( const char* content )
  2. {
  3. return add_response( "%s", content );
  4. }
  5. bool http_conn::add_content_type() {
  6. return add_response("Content-Type:%s\r\n", "text/html");
  7. }

这两个方法都是用于构建HTTP响应的一部分,用来向响应中添加特定的内容和头部信息

add_content(const char* content)

此方法用于向HTTP响应正文中添加具体的内容。这里的content参数是一个字符串,代表了要发送给客户端的数据。这个方法通过调用add_response函数,并使用"%s"格式化字符串,将content直接添加到响应缓冲区中。这个方法通常用于添加HTML页面内容、API的JSON响应等。

add_content_type()

此方法用于添加Content-Type头部到HTTP响应中。Content-Type是一个HTTP头部字段,用于指示资源的MIME类型,客户端可以通过这个信息来解析和处理接收到的数据。在这个方法中,它固定地设置了Content-Type的值为"text/html",表示发送的内容是HTML文本。这是Web服务器常见的做法,因为大多数响应内容都是HTML格式的页面。如果服务器需要发送其他类型的内容,如JSON、图片或视频等,就需要相应地改变Content-Type的值

这两个方法的共同点是都通过add_response函数向响应缓冲区添加内容。add_response负责将格式化后的字符串追加到缓冲区,同时确保不会超过缓冲区的大小限制。通过这种方式,服务器能够构建包含状态行、响应头和响应体的完整HTTP响应消息。

  1. bool http_conn::process_write(HTTP_CODE ret) {
  2. switch (ret)
  3. {
  4. case INTERNAL_ERROR:
  5. add_status_line( 500, error_500_title );
  6. add_headers( strlen( error_500_form ) );
  7. if ( ! add_content( error_500_form ) ) {
  8. return false;
  9. }
  10. break;
  11. case BAD_REQUEST:
  12. add_status_line( 400, error_400_title );
  13. add_headers( strlen( error_400_form ) );
  14. if ( ! add_content( error_400_form ) ) {
  15. return false;
  16. }
  17. break;
  18. case NO_RESOURCE:
  19. add_status_line( 404, error_404_title );
  20. add_headers( strlen( error_404_form ) );
  21. if ( ! add_content( error_404_form ) ) {
  22. return false;
  23. }
  24. break;
  25. case FORBIDDEN_REQUEST:
  26. add_status_line( 403, error_403_title );
  27. add_headers(strlen( error_403_form));
  28. if ( ! add_content( error_403_form ) ) {
  29. return false;
  30. }
  31. break;
  32. case FILE_REQUEST:
  33. add_status_line(200, ok_200_title );
  34. add_headers(m_file_stat.st_size);
  35. m_iv[ 0 ].iov_base = m_write_buf;
  36. m_iv[ 0 ].iov_len = m_write_idx;
  37. m_iv[ 1 ].iov_base = m_file_address;
  38. m_iv[ 1 ].iov_len = m_file_stat.st_size;
  39. m_iv_count = 2;
  40. bytes_to_send = m_write_idx + m_file_stat.st_size;
  41. return true;
  42. default:
  43. return false;
  44. }
  45. m_iv[ 0 ].iov_base = m_write_buf;
  46. m_iv[ 0 ].iov_len = m_write_idx;
  47. m_iv_count = 1;
  48. bytes_to_send = m_write_idx;
  49. return true;
  50. }

process_write 函数的目的是根据处理请求的结果(由ret参数的HTTP_CODE类型表示)准备HTTP响应。这个函数执行几项任务:

  1. 设置响应行和头部:根据HTTP_CODE的值,它会设置适当的状态行(例如,"HTTP/1.1 404 Not Found")和响应的头部。这包括指定内容长度和其他必要的头部,如ConnectionContent-Type

  2. 添加响应内容:对于错误状态(如404未找到、400错误请求等),它会向客户端添加描述错误的HTML正文。这是通过调用add_content实现的,该函数将错误描述的HTML追加到响应缓冲区中。

  3. 处理文件请求:如果请求是获取文件(FILE_REQUEST),它会准备表示成功的头部(200 OK)并设置两个iovec结构,以使用writev高效发送文件。第一个iovec指向存储在缓冲区(m_write_buf)中的HTTP头部,第二个iovec指向内存映射的文件内容(m_file_address)。

  4. 发送准备:最后,它会计算总共要发送的字节数,并根据情况设置m_iv数组(用于writev调用)和bytes_to_send。对于文件请求,bytes_to_send会包括头部和文件内容的总大小;对于错误响应,只包括响应头和错误页面的大小

  1. // 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
  2. void http_conn::process() {
  3. // 解析HTTP请求
  4. HTTP_CODE read_ret = process_read();
  5. if ( read_ret == NO_REQUEST ) {
  6. modfd( m_epollfd, m_sockfd, EPOLLIN );
  7. return;
  8. }
  9. // 生成响应
  10. bool write_ret = process_write( read_ret );
  11. if ( !write_ret ) {
  12. close_conn();
  13. }
  14. modfd( m_epollfd, m_sockfd, EPOLLOUT);
  15. }

这段代码是一个HTTP连接处理流程的概述,展示了在一个典型的基于事件驱动的服务器模型中,如何处理一个HTTP请求。具体来说,这段代码可能是在一个基于epoll的服务器中使用线程池处理HTTP请求的一部分。下面是对这个过程的详细解释:

  1. 解析HTTP请求:首先调用process_read()函数来解析客户端发送的HTTP请求这个函数的目的是读取并分析客户端发送的数据,然后确定这些数据是否构成了一个完整的HTTP请求process_read()函数返回一个HTTP_CODE枚举类型的值,表示请求解析的结果。

  2. 检查请求状态:如果process_read()返回NO_REQUEST,这表示当前读取的数据不足以构成一个完整的HTTP请求,需要继续读取客户端数据。此时,使用modfd()函数重新设置socket为EPOLLIN事件(即可读事件),然后返回,等待更多数据到来。

  3. 生成响应:如果process_read()成功解析出一个完整的HTTP请求,它会返回一个代表请求状态的HTTP_CODE值,而不是NO_REQUEST。随后,process_write(read_ret)函数会被调用,以根据解析的请求生成相应的HTTP响应process_write()函数根据process_read()的返回值来准备HTTP响应数据,并返回一个布尔值,表示响应是否成功生成。

  4. 处理生成响应失败的情况:如果process_write()返回false,表明在准备响应时遇到了问题,可能是资源不足或内部错误。此时,会调用close_conn()来关闭这个连接

  5. 设置为可写事件:无论process_write()是否成功,最后都会调用modfd(m_epollfd, m_sockfd, EPOLLOUT),将这个socket的事件设置为EPOLLOUT(即可写事件)。这是因为无论如何都需要向客户端发送数据(即HTTP响应),哪怕是一个错误消息。设置为EPOLLOUT后,当socket的发送缓冲区可写时,epoll会通知应用程序,应用程序随后可以写入HTTP响应数据。

这个流程展示了处理HTTP请求的典型步骤:读取请求、解析请求、生成响应,并根据请求的处理结果调整socket的事件注册,以便继续进行数据的读取或写入。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/217787
推荐阅读
相关标签
  

闽ICP备14008679号