如何在C++ libcurl库中正确使用Keep-Alive头部优化请求
CURL HTTP长连接优化技术问题
问题背景
我们有一个需向后端服务器发起数百万次请求的应用,采用基于TCP的HTTP API。单次请求平均耗时150-200ms,但后端服务器仅耗时1-2ms,大部分时间消耗在DNS查询和SSL三次握手环节。我们希望利用TCP Keep-Alive功能保持与后端的TCP连接不关闭,通过Postman测试发现添加相关请求头部就能缩短请求耗时,但在C++应用中却无此效果。
现有代码
INT CCurlHTTP::HTTPSPost(const CString& endPointUrl, const CString& urlparam,const CString& cookie){ CURL *curl; CURLcode res; struct curl_slist *headers=NULL; char errbuf[CURL_ERROR_SIZE]; curl = curl_easy_init(); CString KeepAlive = "Connection: keep-alive"; CString KeepAliveSettings = "Keep-Alive: timeout=100, max=100"; get_request req; req.buffer =0; req.len =0; req.buflen =0; if(curl) { //add url, headers, and paramaters to the request curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); curl_easy_setopt(curl, CURLOPT_URL, endPointUrl); curl_easy_setopt(curl, CURLOPT_DEFAULT_PROTOCOL, "https"); headers = curl_slist_append(headers, m_httpHeadAccept); headers = curl_slist_append(headers, m_httpContentType); headers = curl_slist_append(headers, KeepAlive); headers = curl_slist_append(headers, KeepAliveSettings); //callback function used to save response curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_String); curl_easy_setopt(curl, CURLOPT_TCP_KEEPALIVE, 1L); curl_easy_setopt(curl, CURLOPT_MAXCONNECTS, 5L); req.buffer = (unsigned char*) malloc(CHUNK_SIZE); req.buflen = CHUNK_SIZE; req.len = 0; curl_easy_setopt(curl,CURLOPT_WRITEDATA, (void *)&req); if (!cookie.IsEmpty()) { headers = curl_slist_append(headers, m_DBAuthCertficate); //What is difference between this and line no 118? CString pCookie = "DBAuthTicket=" + cookie; curl_easy_setopt(curl,CURLOPT_COOKIE, pCookie); } else { headers = curl_slist_append(headers, m_OAuthToken); } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, urlparam); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); errbuf[0] = 0; curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 512000); CFileTime start, end; CFileTimeSpan duration; start = CFileTime::GetCurrentTime(); res = curl_easy_perform(curl); end = CFileTime::GetCurrentTime(); duration = (end - start); CString elapsed, logMsg; elapsed.Format("%ld",duration.GetTimeSpan()/10000); logMsg.Format("[CCurlHTTP::HTTPSPost::%d]Respone time = %s ms",__LINE__, elapsed); UTHelper::Get().WriteLog(logMsg); long res_code; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res_code); if(res_code != 200 && res_code != 201) { res = CURLE_HTTP_RETURNED_ERROR; } m_response = (char*)req.buffer; m_errDescription.Format("%ld", res_code); len = req.len; buflen = req.buflen; //curl_easy_cleanup(curl); free(req.buffer); } return res; }
注:该代码同时用于单线程应用和多线程Web应用(VBScript + C++ COM DLL),属于遗留代码库(C++ 98 + VC7)。
技术问询
- 每次执行新请求时都会创建新的CurlHTTP类,curl_easy_init每次都会返回新的curl句柄还是会返回相同的句柄?
- 如何让该代码配合Keep-Alive头部生效?当前代码添加头部后无效果,请求耗时仍为150-200ms。
- 无法将CurlHTTP设为单例(多线程环境下同步会造成瓶颈),求其他可行的优化方案。
- 应在何处初始化curl_global_init,需传入什么参数?
问题解答
1. curl_easy_init的返回值
每次调用curl_easy_init()都会返回一个全新的CURL句柄,每个句柄独立管理自己的连接状态、配置选项等。当前每次请求都创建新的CCurlHTTP实例,再调用curl_easy_init,自然每次都是新句柄,无法复用之前的连接。
2. 让Keep-Alive生效的关键调整
当前代码的核心问题是没有复用CURL句柄,且存在几个配置错误:
- 不要手动添加Connection和Keep-Alive头部:CURL默认会自动处理HTTP/1.1的持久连接(Keep-Alive),手动添加这些头部可能会和CURL的内部逻辑冲突,甚至被覆盖。直接删除这两行代码:
headers = curl_slist_append(headers, KeepAlive); headers = curl_slist_append(headers, KeepAliveSettings); - 必须复用CURL句柄:只有同一个CURL句柄发起的后续请求,才会复用之前建立的TCP连接。当前每次请求都创建新句柄,执行完后还注释了
curl_easy_cleanup(curl),这会导致句柄泄漏,同时完全无法复用连接。正确做法是让CCurlHTTP实例持有CURL句柄,在实例生命周期内复用,而不是每次请求都创建新句柄。 - 检查CURL版本和SSL配置:确保使用的CURL版本支持持久连接,且SSL层(如OpenSSL)配置正确,允许会话复用(可以设置
CURLOPT_SSL_SESSIONID_CACHE为1L开启会话缓存,减少SSL握手耗时)。 - 启用连接缓存:配合
CURLOPT_MAXCONNECTS设置最大缓存连接数,同时确保没有禁用连接复用的选项(比如CURLOPT_FORBID_REUSE不要设为1)。
3. 多线程环境下的优化方案
既然不能用单例,可采用以下方案:
- 线程本地存储(TLS)维护CURL句柄:每个线程持有自己的CCurlHTTP实例(及对应的CURL句柄),线程内的请求复用该句柄,避免跨线程同步。因为是线程私有,无需锁,不会有性能瓶颈。
- 连接池实现:创建一个CURL句柄池,每个句柄独立管理连接,多线程从池中获取空闲句柄,用完放回。注意:CURL句柄不能跨线程使用,所以池中的句柄要绑定线程,或者保证同一时间只有一个线程使用某一句柄。
- 批量请求优化:如果业务允许,将多个请求打包成批量请求发送,减少请求次数,直接降低握手开销。
- DNS缓存:启用CURL的DNS缓存(设置
CURLOPT_DNS_CACHE_TIMEOUT为大于0的值,比如3600秒),避免重复DNS查询。
4. curl_global_init的初始化位置和参数
- 初始化位置:必须在所有CURL操作之前调用,且整个进程生命周期内只调用一次。对于COM DLL,建议在DLL的
DllMain函数的DLL_PROCESS_ATTACH阶段调用;对于单线程应用,在程序启动初期(比如main函数开头)调用即可。 - 参数选择:
- 若不确定需求,传入
CURL_GLOBAL_ALL即可,它会初始化所有CURL依赖的模块(SSL、DNS等),兼容性最好。 - 若明确不需要某些模块,可传入
CURL_GLOBAL_NOTHING或组合其他标志,但对于HTTPS请求,必须确保初始化SSL相关模块,因此推荐用CURL_GLOBAL_ALL。 - 注意:
curl_global_init不是线程安全的,必须在主线程或所有线程启动前调用。
- 若不确定需求,传入
内容的提问来源于stack exchange,提问作者chargedfever
相关产品推荐
相关产品推荐

