You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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;
}

修复说明

  1. 请求头读取:循环读取直到获取完整的\r\n\r\n分隔符,确保请求头解析准确;
  2. 请求体读取:根据剩余需要的字节数动态调整recv的读取大小,统一处理所有返回值情况,正确截断多余数据并更新剩余数据缓存;
  3. 错误处理:增加更严谨的边界检查,避免因非法值导致的逻辑异常。

内容的提问来源于stack exchange,提问作者interesting

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 19:51:02