Webserve(4): HTTP解析

cpp 复制代码
// 写HTTP响应
bool http_conn::write()
{
    int temp = 0;
    
    if ( bytes_to_send == 0 ) {
        // 将要发送的字节为0,这一次响应结束。
        modfd( m_epollfd, m_sockfd, EPOLLIN ); 
        init();
        return true;
    }

    while(1) {
        // 分散写
        temp = writev(m_sockfd, m_iv, m_iv_count);
        if ( temp <= -1 ) {
            // 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,虽然在此期间,
            // 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性。
            if( errno == EAGAIN ) {
                modfd( m_epollfd, m_sockfd, EPOLLOUT );
                return true;
            }
            unmap();
            return false;
        }

        bytes_have_send += temp;
        bytes_to_send -= temp;

        if (bytes_have_send >= m_iv[0].iov_len)
        {
            m_iv[0].iov_len = 0;
            m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
            m_iv[1].iov_len = bytes_to_send;
        }
        else
        {
            m_iv[0].iov_base = m_write_buf + bytes_have_send;
            m_iv[0].iov_len = m_iv[0].iov_len - temp;
        }

        if (bytes_to_send <= 0)
        {
            // 没有数据要发送了
            unmap();
            modfd(m_epollfd, m_sockfd, EPOLLIN);

            if (m_linger)
            {
                init();
                return true;
            }
            else
            {
                return false;
            }
        }

    }

    
}

这段代码实现了写入 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 函数能够连续地将所有的数据块一次性发送出去,提高发送效率。

cpp 复制代码
// 往写缓冲中写入待发送的数据
bool http_conn::add_response( const char* format, ... ) {
    if( m_write_idx >= WRITE_BUFFER_SIZE ) {
        return false;
    }
    va_list arg_list;
    va_start( arg_list, format );
    int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );
    if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) ) {
        return false;
    }
    m_write_idx += len;
    va_end( arg_list );
    return true;
}

这段代码是 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 的使用周期结束:

cpp 复制代码
bool http_conn::add_status_line( int status, const char* title ) {
    return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );
}

bool http_conn::add_headers(int content_len) {
    add_content_length(content_len);
    add_content_type();
    add_linger();
    add_blank_line();
}

bool http_conn::add_content_length(int content_len) {
    return add_response( "Content-Length: %d\r\n", content_len );
}

bool http_conn::add_linger()
{
    return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );
}

bool http_conn::add_blank_line()
{
    return add_response( "%s", "\r\n" );
}

这段代码是在构建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协议的规范。

cpp 复制代码
bool http_conn::add_content( const char* content )
{
    return add_response( "%s", content );
}

bool http_conn::add_content_type() {
    return add_response("Content-Type:%s\r\n", "text/html");
}

这两个方法都是用于构建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响应消息。

cpp 复制代码
bool http_conn::process_write(HTTP_CODE ret) {
    switch (ret)
    {
        case INTERNAL_ERROR:
            add_status_line( 500, error_500_title );
            add_headers( strlen( error_500_form ) );
            if ( ! add_content( error_500_form ) ) {
                return false;
            }
            break;
        case BAD_REQUEST:
            add_status_line( 400, error_400_title );
            add_headers( strlen( error_400_form ) );
            if ( ! add_content( error_400_form ) ) {
                return false;
            }
            break;
        case NO_RESOURCE:
            add_status_line( 404, error_404_title );
            add_headers( strlen( error_404_form ) );
            if ( ! add_content( error_404_form ) ) {
                return false;
            }
            break;
        case FORBIDDEN_REQUEST:
            add_status_line( 403, error_403_title );
            add_headers(strlen( error_403_form));
            if ( ! add_content( error_403_form ) ) {
                return false;
            }
            break;
        case FILE_REQUEST:
            add_status_line(200, ok_200_title );
            add_headers(m_file_stat.st_size);
            m_iv[ 0 ].iov_base = m_write_buf;
            m_iv[ 0 ].iov_len = m_write_idx;
            m_iv[ 1 ].iov_base = m_file_address;
            m_iv[ 1 ].iov_len = m_file_stat.st_size;
            m_iv_count = 2;

            bytes_to_send = m_write_idx + m_file_stat.st_size;

            return true;
        default:
            return false;
    }

    m_iv[ 0 ].iov_base = m_write_buf;
    m_iv[ 0 ].iov_len = m_write_idx;
    m_iv_count = 1;
    bytes_to_send = m_write_idx;
    return true;
}

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会包括头部和文件内容的总大小;对于错误响应,只包括响应头和错误页面的大小

cpp 复制代码
// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process() {
    // 解析HTTP请求
    HTTP_CODE read_ret = process_read();
    if ( read_ret == NO_REQUEST ) {
        modfd( m_epollfd, m_sockfd, EPOLLIN );
        return;
    }
    
    // 生成响应
    bool write_ret = process_write( read_ret );
    if ( !write_ret ) {
        close_conn();
    }
    modfd( m_epollfd, m_sockfd, EPOLLOUT);
}

这段代码是一个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的事件注册,以便继续进行数据的读取或写入。

相关推荐
熊的猫32 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
城南vision1 小时前
计算机网络——HTTP篇
网络协议·计算机网络·http
‍。。。2 小时前
使用Rust实现http/https正向代理
http·https·rust
科技探秘人2 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人2 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
守城小轩9 小时前
Chromium127编译指南 Mac篇(五)- 编译Chromium
chrome·chrome devtools·指纹浏览器·浏览器开发
点点滴滴的记录12 小时前
RPC核心实现原理
网络·网络协议·rpc
程思扬13 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节
海绵波波10714 小时前
Webserver(4.8)UDP、广播、组播
单片机·网络协议·udp
很透彻17 小时前
【网络】传输层协议TCP(下)
网络·c++·网络协议·tcp/ip