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
枚举值,用于表示解析的状态或结果。代码逻辑
完整性检查:
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
是之前已经处理过的数据的位置。设置字符串结束标志:
- 如果已经接收到了完整的请求体,即条件判断为真,那么就在请求体的末尾加上字符串结束符
'\0'
,以便后续处理时可以当作字符串来操作。这里假设请求体是文本数据。text[m_content_length] = '\0';
这行代码是在请求体的正文内容后面加上结束符,确保文本字符串的正确结束。返回处理结果:
- 如果请求体已完整接收,函数返回
GET_REQUEST
,表示已经获取到了一个完整的 HTTP 请求,可以进行下一步的处理。- 如果当前读取的内容还不足以构成完整的请求体,函数返回
NO_REQUEST
,表示需要继续读取数据。总结
这个函数主要用于处理带有内容体的 HTTP 请求(例如 POST 请求),通过检查已接收数据的长度与
Content-Length
标头指定的长度是否匹配来确定请求体是否完整。如果请求体接收完整,就准备好了进行后续的请求处理,比如解析请求体中的数据。这是处理 HTTP 请求的一个重要步骤,确保了服务器能够正确处理完整的请求数据。
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请求的整个过程。方法通过解析请求行、头部和内容,最终决定如何响应客户端的请求。下面是对该方法的逐步解析:方法逻辑
初始化状态:
LINE_STATUS line_status = LINE_OK;
初始化行状态为LINE_OK
,表示读取行的操作是成功的。HTTP_CODE ret = NO_REQUEST;
初始化HTTP响应码为NO_REQUEST
,表示尚未接收到完整的HTTP请求。char* text = 0;
初始化文本指针为nullptr
,用于后续指向解析得到的文本行。循环读取行:
- 使用
while
循环来读取每一行数据。循环条件包括两部分:
- 如果当前状态是解析内容(
CHECK_STATE_CONTENT
)并且行状态为LINE_OK
。- 或者,调用
parse_line()
方法解析行并检查返回的行状态是否为LINE_OK
。处理各种状态:
- 根据当前的检查状态(
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
,表示内容可能还未完全接收完毕。返回处理结果:
- 最后,如果在循环中未能进一步处理(例如,未接收到完整的请求或正在等待更多的数据),则返回
NO_REQUEST
,表示继续等待和接收数据。方法的调用逻辑
process_read
方法通过逐行解析HTTP请求并根据请求的不同部分(请求行、头部、内容)采取相应的动作,是处理HTTP请求的核心逻辑之一。- 对于每一部分的解析,该方法依赖于其他辅助方法(如
parse_request_line
,parse_headers
, 和parse_content
),这些方法专注于处理HTTP请求的具体一个环节。- 该方法展示了一个状态机的实现方式,通过不同的状态控制流程,直到收集和解析了所有必要的数据,最终决定如何响应客户端请求。
错误处理和状态转换
- 方法中的错误处理非常直接,一旦遇到无法解析的请求行或头部,就立即返回
BAD_REQUEST
,表示客户端的请求存在语法错误或不被服务器支持。- 状态转换确保了服务器能够按照HTTP请求的结构逐步处理每个部分,直至完成请求的处理或确定请求无法被处理。
这段代码中的
while
循环主要用于解析 HTTP 请求的内容。让我们逐步解释为什么要这样写:while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK)) || ((line_status = parse_line()) == LINE_OK))
解释循环条件
检查状态和行状态:
m_check_state == CHECK_STATE_CONTENT
:此条件检查当前状态是否为解析内容(CHECK_STATE_CONTENT
)。line_status == LINE_OK
:此条件检查行状态是否为LINE_OK
,表示上一行已成功解析。解析行:
line_status = parse_line()
:这部分是parse_line()
函数的调用,它负责解析 HTTP 请求的每一行。((line_status = parse_line()) == LINE_OK)
:这部分检查解析行的返回状态是否为LINE_OK
,表示当前行已经成功解析。组合条件:
(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)
// 当得到一个完整、正确的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 找到对应的文件,并进行相关的权限和文件类型检查,最后将文件映射到内存中以便读取。下面是对这段代码的逐行解释:
strcpy(m_real_file, doc_root);
:将服务器的根目录路径doc_root
复制到m_real_file
字符数组中。m_real_file
用于存储请求的实际文件路径。
int len = strlen(doc_root);
:获取服务器根目录路径的长度。
strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);
:将请求的 URL 路径拼接到m_real_file
后面,组成完整的文件路径。这里使用strncpy
函数来保证不超出数组范围。
if (stat(m_real_file, &m_file_stat) < 0) { return NO_RESOURCE; }
:使用stat
函数获取文件的状态信息,并存储在m_file_stat
结构体中。如果获取失败,则表示请求的资源不存在,返回NO_RESOURCE
。
if (!(m_file_stat.st_mode & S_IROTH)) { return FORBIDDEN_REQUEST; }
:检查文件的权限,如果当前用户没有读取文件的权限,则返回FORBIDDEN_REQUEST
,表示禁止访问。
if (S_ISDIR(m_file_stat.st_mode)) { return BAD_REQUEST; }
:检查文件是否是目录,如果是目录,则返回BAD_REQUEST
,表示请求的资源无效。
int fd = open(m_real_file, O_RDONLY);
:以只读方式打开文件,获取文件描述符fd
。
m_file_address = (char*) mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
:使用mmap
函数将文件映射到内存中,以便快速读取文件内容。参数包括内存起始地址、文件大小、访问权限、映射方式、文件描述符和偏移量。映射成功后,返回的地址存储在m_file_address
中。
close(fd);
:关闭文件描述符,释放资源。
return FILE_REQUEST;
:表示成功处理了文件请求,可以进行文件读取操作。这段代码是处理客户端 HTTP 请求的核心部分,完成了根据请求的 URL 找到对应文件、权限检查、文件映射等操作,为后续文件读取操作做好了准备。
strcpy
和strncpy
是 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
的指定位置。让我们逐步解释这句代码的含义:
m_real_file + len
:这个表达式表示m_real_file
字符串的地址偏移了len
个字符。因为m_real_file
是一个字符数组(字符串),所以它指向字符串的起始位置。通过+ len
的操作,将指针移动到m_real_file
字符串的第len
个字符处,即指针指向了字符串的末尾位置(不包括字符串结束符\0
)。
m_url
:这个参数是源字符串,即要被复制的字符串。它是一个以\0
结尾的字符串。
FILENAME_LEN - len - 1
:这个表达式表示最大允许复制的字符数。FILENAME_LEN
是目标字符串m_real_file
的最大长度,len
是已经占用的长度,1
是为了留出空间放置字符串结束符\0
。因此,这个表达式计算出剩余的可用空间。
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
字符以确保字符串的终止。
// 对内存映射区执行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
方法的实现,用于取消内存映射区。让我们逐行解释代码的含义:
if (m_file_address)
:检查m_file_address
是否为非空指针,即是否已经进行了内存映射操作。
munmap(m_file_address, m_file_stat.st_size);
:调用munmap
函数取消内存映射,将之前映射的内存区域释放。参数m_file_address
是要取消映射的起始地址,m_file_stat.st_size
是要取消映射的长度。
m_file_address = 0;
:将m_file_address
设置为零,表示取消了内存映射。总结
这段代码的主要功能是取消之前创建的内存映射区,释放已经映射的内存。这在 HTTP 服务器中是很常见的操作,特别是在处理大文件时,为了释放内存和资源而取消内存映射非常重要。