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 响应的功能。让我们逐行解释它的工作原理:
if (bytes_to_send == 0)
:检查是否还有待发送的字节。如果没有,则说明本次响应已经完成,重新初始化连接并等待下一个请求。
while(1)
:进入一个无限循环,以确保所有要发送的数据都能够被写入。
temp = writev(m_sockfd, m_iv, m_iv_count);
:调用writev
函数向套接字写入数据。m_iv
是iovec
结构的数组,用于描述要写入的数据块。m_iv_count
表示m_iv
数组中的元素数量。writev
函数会尽可能地将所有数据一次性写入套接字。
if (temp <= -1)
:检查writev
函数的返回值,如果出现错误,则根据错误类型进行处理。
如果错误为
EAGAIN
,表示套接字写缓冲区已满,无法继续写入数据,需要等待下一次 EPOLLOUT 事件再次写入。因此,修改套接字的事件监听为EPOLLOUT
,然后返回true
,表示本次写入未完成,需要等待下一次写入机会。其他错误情况,可能是连接出现了问题,需要取消内存映射,并返回
false
。
bytes_have_send += temp;
:更新已经发送的字节数。
bytes_to_send -= temp;
:更新待发送的字节数。
if (bytes_have_send >= m_iv[0].iov_len)
:检查是否已经发送完m_iv[0]
描述的数据块。如果是,则更新m_iv[1]
描述的数据块为下一块要发送的数据。
if (bytes_to_send <= 0)
:检查是否所有数据均已发送。如果是,则取消内存映射,修改事件监听为 EPOLLIN,并根据是否需要保持连接来重新初始化连接或者关闭连接。循环会继续写入剩余的数据,直到所有数据均已发送完成或者发生错误。
这段代码实现了有效的分散写操作,可以高效地将 HTTP 响应数据写入套接字。
这段代码逻辑用于更新
iovec
结构数组m_iv
中描述的数据块,以便继续发送数据。让我们逐行解释:
if (bytes_have_send >= m_iv[0].iov_len)
:这个条件判断语句检查是否已经发送完了m_iv[0]
描述的数据块中的内容。如果bytes_have_send
大于等于m_iv[0].iov_len
,表示m_iv[0]
描述的数据块中的内容已经全部发送完成。
m_iv[0].iov_len = 0;
:将m_iv[0]
描述的数据块的长度设置为零,表示该数据块中的内容已经全部发送完成,下一次写入将不再包含这部分内容。
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
,得到当前需要发送的数据在文件内存中的起始位置。
m_iv[1].iov_len = bytes_to_send;
:更新m_iv[1]
描述的数据块的长度,使其等于待发送的字节数bytes_to_send
。
else
:如果if
条件不满足,则执行else
分支,表示m_iv[0]
描述的数据块中的内容尚未全部发送完成。在这种情况下,需要继续发送m_iv[0]
描述的数据块的剩余内容。
m_iv[0].iov_base = m_write_buf + bytes_have_send;
:更新m_iv[0]
描述的数据块的起始地址,使其指向剩余待发送的缓冲区中的数据。
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
方法的实现,用于将待发送的数据写入写缓冲区中。让我们逐行解释这段代码的功能:
if (m_write_idx >= WRITE_BUFFER_SIZE)
:检查当前写缓冲区中的数据是否已经超过了缓冲区的大小。如果超过了,说明没有足够的空间来写入新的数据,返回false
表示写入失败。
va_list arg_list; va_start(arg_list, format);
:声明一个va_list
类型的变量arg_list
,并使用va_start
宏初始化该变量。这个宏的作用是初始化arg_list
,使其指向可变参数列表中的第一个参数。
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
是限制写入长度的上限,以确保不会溢出写缓冲区。
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
:检查vsnprintf
函数的返回值len
是否大于等于剩余空间的大小。如果是,说明写入的数据超出了剩余空间,因此写入失败,返回false
。
m_write_idx += len;
:更新写缓冲区的索引,将其向后移动len
个位置,以便下次写入数据时从正确的位置开始。
va_end(arg_list);
:结束可变参数列表的遍历。
return true;
:写入数据成功,返回true
。这段代码允许使用类似于
printf
的格式化字符串,将数据格式化并写入到写缓冲区中。如果写入失败,可能是由于缓冲区已满或者格式化错误导致的。
这段代码使用了C语言中的可变参数功能,**通过va_list
,va_start
, 和vsnprintf
函数来处理和格式化一个不确定数量的参数。**下面是对这些关键部分的详细解释:
va_list arg_list;
va_list
是一个用于处理可变参数列表的类型 ,arg_list
是一个变量,用于访问函数的可变参数部分 。在这里**,它将被用来访问传给add_response
函数的所有可变参数**。
va_start(arg_list, format);
va_start
是一个宏,用于初始化arg_list
变量,以便它可以用于访问可变参数列表 。va_start
的第一个参数是之前声明的va_list
变量,第二个参数是可变参数列表之前的最后一个固定参数,这里是format
。这个调用之后,arg_list
将指向函数的第一个可变参数。
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
的使用周期结束:
cppbool 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协议的规范。
cppbool 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响应消息。
cppbool 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响应。这个函数执行几项任务:
设置响应行和头部 :根据
HTTP_CODE
的值,它会设置适当的状态行(例如,"HTTP/1.1 404 Not Found")和响应的头部。这包括指定内容长度和其他必要的头部,如Connection
和Content-Type
。添加响应内容 :对于错误状态(如404未找到、400错误请求等),它会向客户端添加描述错误的HTML正文。这是通过调用
add_content
实现的,该函数将错误描述的HTML追加到响应缓冲区中。处理文件请求 :如果请求是获取文件(
FILE_REQUEST
),它会准备表示成功的头部(200 OK)并设置两个iovec
结构,以使用writev
高效发送文件。第一个iovec
指向存储在缓冲区(m_write_buf
)中的HTTP头部,第二个iovec
指向内存映射的文件内容(m_file_address
)。发送准备 :最后,它会计算总共要发送的字节数,并根据情况设置
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请求的一部分。下面是对这个过程的详细解释:
解析HTTP请求 :首先调用
process_read()
函数来解析客户端发送的HTTP请求 。这个函数的目的是读取并分析客户端发送的数据,然后确定这些数据是否构成了一个完整的HTTP请求 。process_read()
函数返回一个HTTP_CODE
枚举类型的值,表示请求解析的结果。检查请求状态 :如果
process_read()
返回NO_REQUEST
,这表示当前读取的数据不足以构成一个完整的HTTP请求,需要继续读取客户端数据。此时,使用modfd()
函数重新设置socket为EPOLLIN事件(即可读事件),然后返回,等待更多数据到来。生成响应 :如果
process_read()
成功解析出一个完整的HTTP请求,它会返回一个代表请求状态的HTTP_CODE
值,而不是NO_REQUEST
。随后,process_write(read_ret)
函数会被调用,以根据解析的请求生成相应的HTTP响应 。process_write()
函数根据process_read()
的返回值来准备HTTP响应数据,并返回一个布尔值,表示响应是否成功生成。处理生成响应失败的情况 :如果
process_write()
返回false
,表明在准备响应时遇到了问题,可能是资源不足或内部错误。此时,会调用close_conn()
来关闭这个连接。设置为可写事件 :无论
process_write()
是否成功,最后都会调用modfd(m_epollfd, m_sockfd, EPOLLOUT)
,将这个socket的事件设置为EPOLLOUT(即可写事件)。这是因为无论如何都需要向客户端发送数据(即HTTP响应),哪怕是一个错误消息。设置为EPOLLOUT后,当socket的发送缓冲区可写时,epoll会通知应用程序,应用程序随后可以写入HTTP响应数据。这个流程展示了处理HTTP请求的典型步骤:读取请求、解析请求、生成响应,并根据请求的处理结果调整socket的事件注册,以便继续进行数据的读取或写入。