为何recv在接收完Content-Length前阻塞?C++ HTTP服务器问题
HTTP服务器请求体接收不完整问题排查与修复
问题背景
用C++开发HTTP服务器,通过检查请求头的Content-Length字段提取请求体,但测试不同大小的请求体(8MB、1.9MB、31MB)时,始终无法接收完整内容,接收的body长度小于Content-Length指定的值。现象如下:
recv持续读取1024字节直到total接近len,随后读取字节数变小;- 当
total与len差值在400-600字节左右时,recv在total等于len前阻塞(无数据可读); - 更换Postman、Insomnia等客户端,结果一致。
核心实现代码
_recv函数
req_t *Webserver::_recv(int client_fd, bool *closed) { string req; static string rest; char buff[1024]; int n = recv(client_fd, buff, 1024, 0); if (n == -1) { _set_error_code("500", "Internal Server Error"); return NULL; } if (n == 0) { *closed = true; return NULL; } buff[n] = '\0'; req += buff; req_t *extracted_req = _extract_req(client_fd, req, rest, closed); return extracted_req; }
_extract_req函数
req_t *Webserver::_extract_req(int client_fd, const string &req, string &rest, bool *closed) { req_t *ret; try { ret = new req_t; } catch (std::bad_alloc &e) { std::cerr << "\033[1;31mError:\033[0m " << e.what() << std::endl; exit(1); } string status_line = req.substr(0, req.find("\r\n")); string headers = req.substr(req.find("\r\n") + 2, req.find("\r\n\r\n") - req.find("\r\n") - 2); rest = req.substr(req.find("\r\n\r\n") + 4, req.size() - req.find("\r\n\r\n") - 4); ret->status_line = status_line; ret->headers = headers; if (headers.find("Content-Length") != string::npos) { long long content_length = _get_content_len(headers); if (content_length == -1) { _set_error_code("400", "Bad Request"); return NULL; } ret->body = _extract_body_len(client_fd, rest, content_length, closed); } return ret; }
extract_body_len函数
string extract_body_len(int client_fd, string& rest, unsigned long long len) { string body; unsigned long long total = 0; body = rest; total += rest.size(); if (total >= len) { body = rest.substr(0, len); rest = rest.substr(len); return body; } else { while (total < len) { char buf[1024]; int ret = recv(client_fd, buf, 1024, 0); if (ret != 1024) { if ((total + ret) >= len) { body += string(buf).substr(0, len - total); rest = string(buf).substr(len - total); break; } } if (ret == 0) { if (total == len) { rest = ""; break; } else { res->status_code = "400"; res->status_message = "Bad Request"; return NULL; } } else if (ret == -1) { res->status_code = "500"; res->status_message = "Internal Server Error"; return body; } total += ret; body += string(buf, ret); } } return body; }
问题分析
1. 请求头解析不完整
_recv函数仅执行一次recv就尝试解析请求,未处理HTTP请求分多个TCP包到达的情况。若第一次recv仅收到部分请求头(未包含\r\n\r\n),req.find("\r\n\r\n")会返回string::npos,导致headers和rest的提取逻辑出错:
headers会包含不属于请求头的内容,导致Content-Length解析错误;rest的初始值错误,后续读取body的起始点偏移。
2. 请求体读取逻辑缺陷
extract_body_len函数的分支处理逻辑存在漏洞:
- 仅在
ret != 1024时检查是否读取到足够数据,忽略了ret == 1024但total + ret >= len的情况; - 当
ret != 1024且满足total + ret >= len时,直接break循环但未更新total,虽返回的body长度正确,但逻辑不严谨; - 未根据剩余需要读取的字节数调整
recv的读取大小,可能读取多余数据或重复阻塞等待。
修复方案
修复_recv函数:循环读取直到获取完整请求头
req_t *Webserver::_recv(int client_fd, bool *closed) { char buff[1024]; static string rest; // 保存上一次未处理完的数据 while (true) { // 先检查已有数据中是否包含完整请求头 size_t header_end = rest.find("\r\n\r\n"); if (header_end != string::npos) { req_t *extracted_req = _extract_req(client_fd, rest, rest, closed); return extracted_req; } // 无完整请求头,继续读取数据 int n = recv(client_fd, buff, 1024, 0); if (n == -1) { _set_error_code("500", "Internal Server Error"); return NULL; } if (n == 0) { *closed = true; return NULL; } rest += string(buff, n); } }
修复_extract_req函数:安全解析请求头与初始body数据
req_t *Webserver::_extract_req(int client_fd, const string &req, string &rest, bool *closed) { req_t *ret = nullptr; try { ret = new req_t; } catch (std::bad_alloc &e) { std::cerr << "\033[1;31mError:\033[0m " << e.what() << std::endl; exit(1); } // 解析状态行 size_t status_line_end = req.find("\r\n"); if (status_line_end == string::npos) { _set_error_code("400", "Bad Request"); delete ret; return nullptr; } ret->status_line = req.substr(0, status_line_end); // 解析请求头 size_t header_end = req.find("\r\n\r\n", status_line_end + 2); if (header_end == string::npos) { _set_error_code("400", "Bad Request"); delete ret; return nullptr; } ret->headers = req.substr(status_line_end + 2, header_end - (status_line_end + 2)); string current_rest = req.substr(header_end + 4); // 处理请求体 if (ret->headers.find("Content-Length") != string::npos) { long long content_length = _get_content_len(ret->headers); if (content_length == -1 || content_length < 0) { _set_error_code("400", "Bad Request"); delete ret; return nullptr; } ret->body = _extract_body_len(client_fd, current_rest, static_cast<unsigned long long>(content_length), closed); rest = current_rest; // 保存剩余未处理数据 } else { ret->body = ""; rest = current_rest; } return ret; }
修复_extract_body_len函数:统一处理所有读取场景
string Webserver::_extract_body_len(int client_fd, string& rest, unsigned long long len, bool *closed) { string body; unsigned long long total = rest.size(); body = rest; if (total >= len) { body = rest.substr(0, len); rest = rest.substr(len); return body; } while (total < len) { char buf[1024]; // 计算剩余需要读取的字节数,避免读取多余数据 unsigned long long need = len - total; int read_size = static_cast<int>(std::min(need, static_cast<unsigned long long>(1024))); int ret = recv(client_fd, buf, read_size, 0); if (ret == -1) { _set_error_code("500", "Internal Server Error"); *closed = false; return ""; } if (ret == 0) { // 客户端提前关闭连接,数据不完整 _set_error_code("400", "Bad Request"); *closed = true; return ""; } total += ret; body += string(buf, ret); if (total >= len) { // 处理多余读取的数据 if (total > len) { unsigned long long extra = total - len; rest = string(buf + ret - extra, extra); body.resize(len); // 截断body到指定长度 } else { rest.clear(); } break; } } return body; }
修复说明
- 请求头读取:循环读取直到获取完整的
\r\n\r\n分隔符,确保请求头解析准确; - 请求体读取:根据剩余需要的字节数动态调整
recv的读取大小,统一处理所有返回值情况,正确截断多余数据并更新剩余数据缓存; - 错误处理:增加更严谨的边界检查,避免因非法值导致的逻辑异常。
内容的提问来源于stack exchange,提问作者interesting
相关产品推荐
相关产品推荐

