libcurl 共享句柄指南:CURLOPT_SHARE 原理、多句柄数据共享与线程安全实践
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
导读
CURLOPT_SHARE是 libcurl 中用于把多个 easy handle(CURL *)绑定到同一个共享句柄(CURLSH *)的核心选项。通过它,应用可以让多个请求共享 DNS 缓存、Cookie、TLS 会话、连接池等数据,从而显著减少重复握手与解析开销。读完本文,你将掌握CURLOPT_SHARE的完整用法、可共享的数据类型、源码层面的实现机制,以及多线程场景下必须配合使用的锁回调的正确姿势。
CURLOPT_SHARE 是什么
CURLOPT_SHARE用于让某个 easy handle 使用由curl_share_init(3)创建的共享句柄(share handle)中的数据,而不是各自维护一份私有数据。该选项自 libcurl 7.10 起引入,适用于所有协议,其官方声明位于 docs/libcurl/opts/CURLOPT_SHARE.md。
核心语义有三点:
- 按需共享:共享句柄只会共享通过
CURLSHOPT_SHARE显式声明的那部分数据,未声明共享的数据仍然由每个 easy handle 以常规方式自行管理。 - 可解除:将该选项再次设置为
NULL,即可让 easy handle 停止使用该共享对象。 - 生命周期风险:在传输进行中把共享句柄设置为
NULL属于不鼓励行为,可能引发未定义行为(undefined behavior)。
API 原型与参数说明
#include <curl/curl.h> CURLcode curl_easy_setopt(CURL *handle, CURLOPT_SHARE, CURLSH *share);| 参数 | 类型 | 说明 |
|---|---|---|
handle | CURL * | 由curl_easy_init()创建的 easy handle |
CURLOPT_SHARE | 选项标识 | 将 easy handle 与一个共享句柄绑定 |
share | CURLSH * | 由curl_share_init()返回的共享句柄;传NULL表示解除绑定 |
在 include/curl/curl.h 中,CURLSH即struct Curl_share的公开别名,而 lib/easyoptions.c 把CURLOPT_SHARE归类为指针类选项(/* CURLSH * */),与CURLOPT_STDERR、CURLOPT_CURLU走同一条setopt_pointers分发路径。
默认值为NULL,即 easy handle 默认不共享任何数据。
可共享的数据类型
共享内容由配套的curl_share_setopt(share, CURLSHOPT_SHARE, type)声明,type取自 include/curl/curl.h 中的curl_lock_data枚举:
| 类型 | 共享内容 | 说明 |
|---|---|---|
CURL_LOCK_DATA_COOKIE | Cookie 数据 | 多个请求共享 HTTP Cookie 状态(依赖 HTTP/Cookie 支持) |
CURL_LOCK_DATA_DNS | DNS 缓存 | 共享主机名解析结果,减少重复 DNS 查询 |
CURL_LOCK_DATA_SSL_SESSION | TLS/SSL 会话缓存 | 复用 TLS 会话,跳过完整握手(需编译 SSL 支持) |
CURL_LOCK_DATA_CONNECT | 连接池(connection pool) | 在多个 easy handle 之间复用底层连接,是“连接共享”的关键(见下文源码分析) |
CURL_LOCK_DATA_PSL | Public Suffix List | 共享 PSL 数据(需编译USE_LIBPSL) |
CURL_LOCK_DATA_HSTS | HSTS 缓存 | 共享 HTTP Strict Transport Security 状态(需编译 HSTS 支持) |
CURL_LOCK_DATA_SHARE | 内部枚举 | 用于共享句柄自身的加锁,不通过CURLSHOPT_SHARE声明 |
对应实现见 lib/curl_share.c:curl_share_setopt处理CURLSHOPT_SHARE时,会根据类型初始化对应的内部数据结构(如Curl_cookie_init()创建 Cookie 存储、Curl_ssl_scache_create(25, 2, ...)创建默认容量为 25 条、每个对等体 2 个会话的 TLS 会话缓存、Curl_cpool_init(&share->cpool, share, 103)初始化容量 103 的连接池),并通过share->specifier位图记录哪些数据被共享。若编译时禁用了对应功能(如CURL_DISABLE_COOKIES、CURL_DISABLE_HSTS),返回CURLSHE_NOT_BUILT_IN。
与之对应的CURLSHOPT_UNSHARE则从共享中移除某类数据(lib/curl_share.c),例如关闭 Cookie 共享时会调用Curl_cookie_cleanup()释放存储。
官方示例:两个 easy handle 共享 Cookie
以下示例完整来自原文档,展示两个 easy handle 通过同一个共享句柄共享 Cookie 的典型流程:
int main(void) { CURL *curl = curl_easy_init(); CURL *curl2 = curl_easy_init(); /* a second handle */ if(curl) { CURLcode result; CURLSH *shobject = curl_share_init(); curl_share_setopt(shobject, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/"); curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); curl_easy_setopt(curl, CURLOPT_SHARE, shobject); result = curl_easy_perform(curl); curl_easy_cleanup(curl); /* the second handle shares cookies from the first */ curl_easy_setopt(curl2, CURLOPT_URL, "https://example.com/second"); curl_easy_setopt(curl2, CURLOPT_COOKIEFILE, ""); curl_easy_setopt(curl2, CURLOPT_SHARE, shobject); result = curl_easy_perform(curl2); curl_easy_cleanup(curl2); curl_share_cleanup(shobject); } }要点解读:
curl_share_init()创建共享句柄,CURLSHOPT_SHARE+CURL_LOCK_DATA_COOKIE声明共享 Cookie;CURLOPT_COOKIEFILE设为空字符串表示开启 Cookie 引擎(读取/写入会话 Cookie),而不是从文件加载;- 第一个请求在
https://example.com/收到的 Cookie 会被写入共享对象,第二个请求访问/second时自动携带; - 最后必须
curl_share_cleanup(shobject)释放共享句柄。
CURLOPT_SHARE可与 CURLOPT_COOKIE 等选项配合,具体共享配置(包括CURLSHOPT_SHARE/CURLSHOPT_UNSHARE)详见 docs/libcurl/opts/CURLSHOPT_SHARE.md。
源码实现:setopt_share 的绑定与解绑逻辑
CURLOPT_SHARE的底层处理函数是setopt_share()(lib/setopt.c):
static CURLcode setopt_share(struct Curl_easy *data, struct Curl_share *set) { CURLcode result; if(data->conn) { /* As this handle already has a connection attached, changing share now would be complicated and error-prone */ infof(data, "Cannot change share object while in use"); result = CURLE_BAD_FUNCTION_ARGUMENT; } else { /* disconnect from old share, if any and possible */ result = Curl_share_easy_unlink(data); if(!result && GOOD_SHARE_HANDLE(set)) /* use new share if it set */ result = Curl_share_easy_link(data, set); } return result; }从源码可以确认三个关键实现事实:
- 已连接句柄禁止换绑:如果 easy handle 当前已挂接连接(
data->conn非空),修改共享对象会返回CURLE_BAD_FUNCTION_ARGUMENT并打印 "Cannot change share object while in use" 日志——这与原文档“传输进行中置 NULL 可能导致未定义行为”的警告互为印证,最佳实践是在curl_easy_perform()之前完成绑定。 - 引用计数管理:
Curl_share_easy_link()/Curl_share_easy_unlink()通过share_ref_inc()/share_ref_dec()维护共享句柄的引用计数(lib/curl_share.c)。最后一个引用释放时(引用计数归零),share_destroy()会依次销毁连接池、DNS 缓存、Cookie、HSTS、SSL 会话缓存、PSL 及内部互斥锁,并关闭内部 admin easy handle(lib/curl_share.c)。 - 解绑时的数据剥离:
Curl_share_easy_unlink()在解绑时若共享了连接池,会调用Curl_detach_connection()分离已有连接并把lastconnect_id重置为 -1;若 Cookie/HSTS 与共享对象是同一实例,也会置回 NULL(lib/curl_share.c)。
进阶实践:共享连接池(官方示例)
仓库中的 docs/examples/shared-connection-cache.c 提供了一个完整可编译的进阶示例:共享连接缓存,使“每次新建 easy handle、传输完成后立即销毁”的循环仍然能复用 TCP/TLS 连接。
static void my_lock(CURL *curl, curl_lock_data data, curl_lock_access laccess, void *useptr) { (void)curl; (void)data; (void)laccess; (void)useptr; fprintf(stderr, "-> Mutex lock\n"); } static void my_unlock(CURL *curl, curl_lock_data data, void *useptr) { (void)curl; (void)data; (void)useptr; fprintf(stderr, "<- Mutex unlock\n"); } int main(void) { CURLSH *share; int i; CURLcode result = curl_global_init(CURL_GLOBAL_ALL); if(result != CURLE_OK) return (int)result; share = curl_share_init(); curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT); curl_share_setopt(share, CURLSHOPT_LOCKFUNC, my_lock); curl_share_setopt(share, CURLSHOPT_UNLOCKFUNC, my_unlock); for(i = 0; i < 3; i++) { CURL *curl = curl_easy_init(); if(curl) { curl_easy_setopt(curl, CURLOPT_URL, "https://curl.se/"); curl_easy_setopt(curl, CURLOPT_SHARE, share); result = curl_easy_perform(curl); if(result != CURLE_OK) fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(result)); curl_easy_cleanup(curl); } } curl_share_cleanup(share); curl_global_cleanup(); return (int)result; }这个示例演示了标准的共享句柄生命周期:curl_global_init→curl_share_init→ 声明共享类型 → 注册锁回调 → 循环绑定CURLOPT_SHARE执行传输 →curl_share_cleanup→curl_global_cleanup。
多线程使用与锁回调
原文档强调:如果多个 curl handle 在多线程中同时使用同一个共享句柄,必须使用共享句柄的锁方法。libcurl 本身不为共享数据加锁,锁由应用提供,通过以下三个CURLSHOPT_*选项注册:
| 选项 | 作用 |
|---|---|
CURLSHOPT_LOCKFUNC | 设置加锁回调,原型void (*curl_lock_function)(CURL *handle, curl_lock_data data, curl_lock_access access, void *userptr) |
CURLSHOPT_UNLOCKFUNC | 设置解锁回调,原型void (*curl_unlock_function)(CURL *handle, curl_lock_data data, void *userptr) |
CURLSHOPT_USERDATA | 传给回调的自定义指针(如互斥锁对象) |
curl_lock_data与curl_lock_access(CURL_LOCK_ACCESS_NONE/CURL_LOCK_ACCESS_SHARED/CURL_LOCK_ACCESS_SINGLE)同样定义在 include/curl/curl.h。
从源码看,锁回调只在“该类型确实被共享”且回调已注册时才被调用。以加锁为例(lib/curl_share.c):
CURLSHcode Curl_share_lock_share(struct Curl_share *share, struct Curl_easy *data, curl_lock_data type, curl_lock_access accesstype) { if(!share) return CURLSHE_INVALID; if(share->specifier & (unsigned int)(1 << type) && share->lockfunc) /* only call this if set! */ share->lockfunc(data, type, accesstype, share->clientdata); /* else if we do not share this, pretend successful lock */ return CURLSHE_OK; }即:未共享的类型会“假装加锁成功”直接放行,这也是CURL_LOCK_DATA_SHARE等内部枚举存在的原因。实践中的典型做法是在锁回调内调用pthread_mutex_lock/pthread_mutex_unlock或 Windows 的EnterCriticalSection/LeaveCriticalSection,把CURLSHOPT_USERDATA指向锁对象本身。
另外注意:curl_share_setopt()在共享对象已被任一 easy handle 使用时(引用计数大于 1)会返回CURLSHE_IN_USE拒绝修改(lib/curl_share.c),因此共享类型的配置应在绑定任何句柄之前一次性完成。
返回值与错误处理
curl_easy_setopt(handle, CURLOPT_SHARE, share)返回CURLcode:
CURLE_OK (0):设置成功;- 非零值表示出错,例如
CURLE_BAD_FUNCTION_ARGUMENT(在句柄已连接时尝试换绑),详细错误码见 libcurl-errors(3)。
共享句柄侧的错误码由CURLSHcode表示(CURLSHE_OK、CURLSHE_IN_USE、CURLSHE_BAD_OPTION、CURLSHE_NOMEM、CURLSHE_NOT_BUILT_IN、CURLSHE_INVALID等),同样定义于 include/curl/curl.h。
使用建议与注意事项汇总
- 绑定时机:始终在
curl_easy_perform()之前设置CURLOPT_SHARE;传输进行中修改或置 NULL 可能导致未定义行为。 - 共享类型的声明:通过
curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_*)显式声明,未声明者不共享;每个类型可多次设置,连接池类型尤其如此(源码注释明确“It is safe to set this option several times on a share”)。 - 多线程必配锁:只要多个 easy handle 可能同时访问共享数据,就必须实现
CURLSHOPT_LOCKFUNC/CURLSHOPT_UNLOCKFUNC回调。 - 生命周期:
curl_share_init()创建的句柄必须最终由curl_share_cleanup()释放;内部使用引用计数,最后一个使用它的 easy handle 被清理后,共享句柄才真正销毁,因此先清理所有 easy handle 再清理共享句柄是最稳妥的顺序。 - 能力依赖:
CURL_LOCK_DATA_COOKIE依赖 HTTP + Cookie 支持,CURL_LOCK_DATA_SSL_SESSION依赖 SSL 后端,CURL_LOCK_DATA_HSTS依赖 HSTS 支持,CURL_LOCK_DATA_PSL依赖 libpsl;未编译对应功能时会返回CURLSHE_NOT_BUILT_IN。
【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考