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

C++98下libcurl偶发SSL_ERROR_ZERO_RETURN错误排查及日志配置求助

问题描述

我们使用libcurl从设备向云端发送数据,多数场景运行正常,但偶发SSL_ERROR_ZERO_RETURN错误,且错误均出现在首次发送由JSON文件+二进制/文档文件组成的gzip格式数据时。
运行环境:定制Linux系统Gaia,开发语言C++98。

以下是CurlSender函数的部分代码(TE_EXT_FUNC为curl命令调用包装器):

static bool Send (  IN void* curl,
                    IN const std::map<std::string,std::string> & headersMap,
                    IN HttpClient::InitParams & initParams,
                    IN std::string& write_buffer,
                    IN char* err_buf,
                    IN std::string& unix_socket,
                    IN std::string& url,
                    IN const std::string& api_path,
                    IN const std::string& payload,
                    IN int timeout_secs,
                    IN bool is_multipart,
                    IN const std::string& file_path,
                    IN const std::string& target_file_path,
                    IN HttpClient::REQUEST_TYPE request_type,
                    IN bool use_fresh_connection,
                    OUT NAC::IS::SmartPtr<HttpClient::HttpResponse>& httpResponse )
{

    void* slist = NULL;
    
    void *post=NULL;
    void *last=NULL;
    // clear write buffer leftovers (if any)
    write_buffer.clear();
    std::string cookie_string;
...
switch (request_type)
    {
        case HttpClient::POST_REQUEST:
        {
            DM_SLOG( TD::All, "creating POST request");

                if (!file_path.empty())
                {
                    if (!payload.empty())
                    {
                        DM_SLOG(TE_IS, TD::Surprise, "uploading data from buffer and file is supported only when using multipart");
                        return false;
                    }

                    TE_EXT_FUNC(curl_easy_setopt_long)(curl, te_CURLOPT_POST, 1);
                    upload_file = true;
                }
                else
                {
                    TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_POSTFIELDS, payload.c_str());
                    TE_EXT_FUNC(curl_easy_setopt_long)(curl, te_CURLOPT_POSTFIELDSIZE, payload.size());
                }
            }
        }
        break;
TE_EXT_FUNC(curl_easy_setopt_write_callback)(curl, te_CURLOPT_WRITEFUNCTION, &HttpClient::WriteToMemoryFunc);
            TE_EXT_FUNC(curl_easy_setopt_ptr)(curl, te_CURLOPT_WRITEDATA, &write_buffer);

//set data
        TE_EXT_FUNC(curl_easy_setopt_ptr)(curl, te_CURLOPT_HTTPHEADER, slist);
        TE_EXT_FUNC(curl_easy_setopt_PROXYAUTH_BASIC_DIGEST_NTLM)(curl);

TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_ENCODING, ""); //tells the server that it can send the response compressed
        TE_EXT_FUNC(curl_easy_setopt_long)(curl, te_CURLOPT_NOSIGNAL, 1); //needed to run in a MT env otherwise process will crush :-(
        TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_ERRORBUFFER, err_buf);
        TE_EXT_FUNC(curl_easy_setopt_write_callback)(curl, te_CURLOPT_HEADERFUNCTION,&HttpClient::HeaderFunc);
        TE_EXT_FUNC(curl_easy_setopt_ptr)(curl, te_CURLOPT_WRITEHEADER, &cookie_string);

httpResponse->m_url = full_url;

        //set connection timeout
        if (timeout_secs != -1)
        {
            TE_EXT_FUNC(curl_easy_setopt_long)(curl, te_CURLOPT_TIMEOUT, timeout_secs);
        }

        //client certs (if needed)
        if (!initParams.m_client_cert_params.m_client_cert_path.empty())
        {
            TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_SSLCERT, initParams.m_client_cert_params.m_client_cert_path.c_str());
            TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_SSLCERTTYPE, initParams.m_client_cert_params.m_client_cert_type.c_str());
            if (!initParams.m_client_cert_params.m_client_cert_private_key_path.empty())
            {
                TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_SSLKEY, initParams.m_client_cert_params.m_client_cert_private_key_path.c_str());
                TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_SSLKEYTYPE, initParams.m_client_cert_params.m_client_cert_private_key_type.c_str());             
            }
            if (!initParams.m_client_cert_params.m_client_cert_private_key_password.empty())
            {
                TE_EXT_FUNC(curl_easy_setopt_str)(curl, te_CURLOPT_KEYPASSWD, initParams.m_client_cert_params.m_client_cert_private_key_password.c_str());
            }
            
        }

        if(initParams.m_verbose_logs && !TE_EXT_FUNC(curl_easy_setopt_debug_callback)(curl, &HttpClient::s_CurlDebugCb))
        {
            DM_SLOG(TE_IS, TD::Surprise, "failed to set verbose looging");
        }

        if (use_fresh_connection)
        {
            TE_EXT_FUNC(curl_easy_setopt_long)(curl, te_CURLOPT_FRESH_CONNECT, 1L);
        }
        
        if (payload.size() < PAYLOAD_DEBUG_OUTPUT_SIZE)
        {
            DM_SLOG(TE_IS, TD::All, "<request> url: " << full_url << ", data:\n" << payload);
        }
        else
        {   
            std::string part_of_data =  payload.substr(0, PAYLOAD_DEBUG_OUTPUT_SIZE);
            DM_SLOG(TE_IS, TD::All, "<request> url: " << full_url << ", data:\n" << part_of_data);
        }

        httpResponse->m_is_send_success = true;
        
        bool res = TE_EXT_FUNC(curl_easy_perform )(curl);

        if (post != NULL) 
        {
            TE_EXT_FUNC(curl_formfree)(post);
        }
        if (!res)
        {
            DM_SLOG(TE_IS, TD::Surprise, "send ended with error: " << err_buf);
            httpResponse->m_send_error = err_buf;
            httpResponse->m_is_send_success = false;
            cleanup(curl, slist, fd_upload, fd_download);
            return false;
        }

错误发生在调用TE_EXT_FUNC(curl_easy_perform )(curl)后进入if(!res)分支。根据OpenSSL官方说明,SSL_ERROR_ZERO_RETURN表示TLS/SSL对等端发送close_notify警报关闭了写入连接。

需要解决两个问题:

  1. 如何排查并解决该问题?
  2. 如何启用完整curl日志,以便更精准定位该问题及未来同类错误?

解答

1. 排查与解决SSL_ERROR_ZERO_RETURN问题

排查方向

  • 请求格式合法性:当前代码中,上传文件+gzip数据时未启用multipart模式,可能导致请求格式错误,云端无法解析后主动关闭连接。
  • 连接与超时配置:首次请求若强制CURLOPT_FRESH_CONNECT,可能导致握手后服务器提前关闭;总超时设置过短,会让首次大体积gzip数据的传输时间不足,触发超时关闭。
  • SSL/TLS兼容性:定制系统Gaia的OpenSSL版本与云端服务器可能存在协议版本不兼容(比如服务器禁用了客户端默认的旧TLS版本),导致握手后连接被强制终止。
  • 数据完整性:首次发送的gzip数据可能存在损坏或格式不规范,导致云端无法处理而关闭连接。

解决措施

  • 修复请求格式:发送JSON+二进制的gzip数据时,必须启用is_multipart参数,使用curl_formadd构建multipart/form-data请求,避免普通POST无法解析混合数据的问题。
  • 调整连接与超时参数:
    • 首次请求不要强制CURLOPT_FRESH_CONNECT,允许连接复用;
    • 单独设置CURLOPT_CONNECTTIMEOUT(连接超时),并延长CURLOPT_TIMEOUT(总超时),给首次大文件传输留出足够时间。
  • 指定兼容SSL/TLS版本:通过CURLOPT_SSLVERSION设置明确的TLS版本(如TLSv1.2或TLSv1.3),避免协议协商不兼容。
  • 验证gzip数据:发送前用zlib库验证gzip文件的合法性,确保数据无损坏、格式正确。
  • 添加重试逻辑:针对这类偶发的连接关闭错误,添加1-2次重试机制,重试时重置curl句柄状态。

2. 启用完整curl日志

步骤

  • 开启VERBOSE模式:在代码中添加TE_EXT_FUNC(curl_easy_setopt_long)(curl, te_CURLOPT_VERBOSE, 1L);,输出curl从DNS解析到响应接收的全流程日志。
  • 完善调试回调:确保s_CurlDebugCb回调处理所有调试类型,输出详细的请求/响应头、数据长度等信息,示例实现如下:
static int s_CurlDebugCb(CURL* curl, curl_infotype type, char* data, size_t size, void* userptr)
{
    switch(type) {
        case CURLINFO_TEXT:
            DM_SLOG(TE_IS, TD::All, "[CURL DEBUG] TEXT: %.*s", (int)size, data);
            break;
        case CURLINFO_HEADER_OUT:
            DM_SLOG(TE_IS, TD::All, "[CURL DEBUG] OUT HEADER: %.*s", (int)size, data);
            break;
        case CURLINFO_HEADER_IN:
            DM_SLOG(TE_IS, TD::All, "[CURL DEBUG] IN HEADER: %.*s", (int)size, data);
            break;
        case CURLINFO_DATA_OUT:
            DM_SLOG(TE_IS, TD::All, "[CURL DEBUG] OUT DATA: %zu bytes", size);
            break;
        case CURLINFO_DATA_IN:
            DM_SLOG(TE_IS, TD::All, "[CURL DEBUG] IN DATA: %zu bytes", size);
            break;
        default:
            break;
    }
    return 0;
}
  • 输出SSL详细日志:若使用OpenSSL,可通过调用SSL_CTX_set_msg_callback函数,输出SSL握手和数据传输的详细报文;也可通过环境变量配置OpenSSL的debug日志。
  • 日志安全处理:生产环境需过滤日志中的敏感信息(如证书、请求明文数据),避免泄露。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 07:38:00