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警报关闭了写入连接。
需要解决两个问题:
- 如何排查并解决该问题?
- 如何启用完整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
相关产品推荐
相关产品推荐

