libcurl HTTP/2 Server Push 开发实战:CURLMOPT_PUSHFUNCTION 回调完全指南
【免费下载链接】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
导读
CURLMOPT_PUSHFUNCTION 是 libcurl 多接口(multi interface)中用于**审批或拒绝 HTTP/2 服务端推送(Server Push,PUSH_PROMISE 帧)**的核心选项。本文将围绕该选项展开:从回调签名、参数语义、返回码到 PUSH_PROMISE 头访问器,完整讲解如何在自己的 C 程序中接收、筛选并保存服务端主动推送的资源;同时结合本仓库中 lib/http2.c 的底层实现与 docs/examples/http2-serverpush.c、docs/examples/http2-pushinmemory.c 两个官方示例,揭示 libcurl 内部克隆句柄、同源校验、头信息释放等关键机制。读完本文,你将能够编写一个安全、可控的 HTTP/2 服务端推送接收程序。
一、选项概览:何时需要它
HTTP/2 协议(RFC 7540)允许服务器在响应客户端请求的同时,主动向客户端推送额外的资源(例如 HTML 页面引用的 CSS/JS/图片),这一机制称为Server Push,在协议层面体现为PUSH_PROMISE帧。
libcurl 在收到PUSH_PROMISE帧时,会为每一个被推送的流创建一个新的 easy handle,并通过多接口的回调机制询问应用程序是否接受该推送。这就是CURLMOPT_PUSHFUNCTION的职责:
该回调在服务器通过 PUSH_PROMISE 帧推送新的 HTTP/2 流时被调用。如果没有设置推送回调,所有被推送的流都会被自动拒绝。
选项的基本信息(来自 CURLMOPT_PUSHFUNCTION.md 的元数据):
- 所属协议:HTTP(HTTP/2)
- 引入版本:7.44.0
- 默认值:NULL(未设置回调,全部推送被拒绝)
- 配套选项:CURLMOPT_PUSHDATA(向回调传递用户指针)、CURLMOPT_PIPELINING、CURLOPT_PIPEWAIT
二、回调签名与设置方式
2.1 原型
#include <curl/curl.h> int curl_push_callback(CURL *parent, CURL *easy, size_t num_headers, struct curl_pushheaders *headers, void *clientp); CURLMcode curl_multi_setopt(CURLM *handle, CURLMOPT_PUSHFUNCTION, curl_push_callback func);在 include/curl/multi.h 中,该回调类型与三个返回码常量一同定义:
#define CURL_PUSH_OK 0 #define CURL_PUSH_DENY 1 #define CURL_PUSH_ERROROUT 2 /* added in 7.72.0 */ typedef int (*curl_push_callback)(CURL *parent, CURL *easy, size_t num_headers, struct curl_pushheaders *headers, void *clientp);2.2 参数语义
| 参数 | 含义 |
|---|---|
parent | 推送到达时所在的父流的 easy handle。新句柄是从 parent 复制(duplicate)而来的,因此继承了它的全部选项;应用可以在回调里按需修改这些选项 |
easy | 新建的、代表即将开始的这次传输的 easy handle |
num_headers | 已收到的 PUSH_PROMISE 头部的 name+value 对数 |
headers | 用于通过访问器函数读取推送头部的不透明句柄(见下文第四节) |
clientp | 通过 CURLMOPT_PUSHDATA 设置的指针,libcurl 本身不触碰、不解释它,仅原样转交给回调 |
2.3 回调体内的注意事项
- 如果回调返回
CURL_PUSH_OK,新的 easy handle 将由 libcurl 添加到 multi handle 中,回调自身绝不能执行添加操作; - PUSH_PROMISE 头只能通过
curl_pushheader_byname(3)与curl_pushheader_bynum(3)这两个访问器在回调内部读取;推送流到达后正常的响应头与普通流一样,仍通过常规的 header callback(CURLOPT_HEADERFUNCTION)提供; - 更新版本的 libcurl 还允许在回调内通过
curl_easy_header(3)访问头部字段。
2.4 底层设置路径
从源码看,该选项的解析位于 lib/multi.c:
case CURLMOPT_PUSHFUNCTION: multi->push_cb = va_arg(param, curl_push_callback);它把函数指针保存到 multi handle 的push_cb字段;CURLMOPT_PUSHDATA则把指针保存到push_userp字段,作为回调的clientp参数。两个选项在 include/curl/multi.h 中分别对应选项编号 14(函数指针类型)与 15(对象指针类型)。
三、回调返回值:接受、拒绝还是报错
CURL_PUSH_OK(0)
应用接受该推送流,可以开始接收数据;该 curl 句柄的所有权已移交给应用。libcurl 会将其加入 multi 句柄继续执行。
CURL_PUSH_DENY(1)
回调拒绝该推送流,不会有任何数据到达应用;该 easy handle 由 libcurl 销毁。
CURL_PUSH_ERROROUT(2)
拒绝被推送的流,并在父流(parent)上返回错误,导致父流以错误状态关闭(7.72.0 版本新增)。
其他返回值
所有其他返回值保留给未来使用,应用不应返回。
四、PUSH_PROMISE 头访问器
两个访问器只能在推送回调内部使用,在回调之外调用它们没有意义、也不会有功能。
4.1 curl_pushheader_byname —— 按名称取值
char *curl_pushheader_byname(struct curl_pushheaders *h, const char *name);返回给定头部字段名的值(找不到返回 NULL)。这是为应用提供的快捷方式,省去遍历全部头部的开销;该函数返回的数据在回调返回时即被释放,且不得修改。若多个同名字段,只返回第一个。
典型用法是读取伪头部:path:
char *headp = curl_pushheader_byname(headers, ":path"); if(headp && !strncmp(headp, "/push-", 6)) { /* 接受以 /push- 开头的推送 */ ... }参考文档:curl_pushheader_byname.md。
4.2 curl_pushheader_bynum —— 按下标取值
char *curl_pushheader_bynum(struct curl_pushheaders *h, size_t num);返回第num个头部字段的"name:value" 字符串(越界返回 NULL)。该字符串同样在回调返回时被释放,不可修改。可用于遍历输出全部推送头:
size_t i = 0; char *field; do { field = curl_pushheader_bynum(headers, i); if(field) fprintf(stderr, "Push header: %s\n", field); i++; } while(field);参考文档:curl_pushheader_bynum.md。
4.3 底层实现细节
在 lib/http2.c 中,struct curl_pushheaders对用户完全隐藏内部结构(只以不完整类型暴露):
struct curl_pushheaders { struct Curl_easy *data; struct h2_stream_ctx *stream; const nghttp2_push_promise *frame; };curl_pushheader_byname的实现采用前缀匹配 + 冒号边界校验:先校验句柄与头部名称合法性(拒绝空名称、单独的:或名称中再含:的情况),再逐项前缀比较,并确认name之后紧跟冒号才返回冒号后的值。这保证了:path这类伪头部能被正确命中。
五、官方示例一:按路径过滤并落盘
文档中的完整示例演示了最核心的用法——只接受路径以/push-开头的推送:
#include <string.h> /* only allow pushes for filenames starting with "push-" */ static int push_callback(CURL *parent, CURL *easy, size_t num_headers, struct curl_pushheaders *headers, void *clientp) { char *headp; int *transfers = (int *)clientp; FILE *out; headp = curl_pushheader_byname(headers, ":path"); if(headp && !strncmp(headp, "/push-", 6)) { fprintf(stderr, "The PATH is %s\n", headp); /* save the push here */ out = fopen("pushed-stream", "wb"); /* write to this file */ curl_easy_setopt(easy, CURLOPT_WRITEDATA, out); (*transfers)++; /* one more */ return CURL_PUSH_OK; } return CURL_PUSH_DENY; } int main(void) { int counter; CURLM *multi = curl_multi_init(); curl_multi_setopt(multi, CURLMOPT_PUSHFUNCTION, push_callback); curl_multi_setopt(multi, CURLMOPT_PUSHDATA, &counter); }要点拆解:
- 通过
curl_pushheader_byname(headers, ":path")读取推送资源的路径; - 满足条件时:为
easy句柄设置CURLOPT_WRITEDATA指向目标文件、递增传输计数、返回CURL_PUSH_OK; - 不满足时返回
CURL_PUSH_DENY,libcurl 负责销毁该句柄; - 应用自己的状态(
counter)通过 CURLMOPT_PUSHDATA 传入,这正是clientp参数的典型用法——libcurl 不会碰这个指针。
六、官方示例二:完整的多接口推送接收程序
仓库中的 docs/examples/http2-serverpush.c 是一个可编译运行的完整程序(约 290 行),演示了推送接收的全流程,值得逐段对照学习。
6.1 主传输的建立
static int setup(CURL *curl, const char *url) { ... curl_easy_setopt(curl, CURLOPT_URL, url); /* HTTP/2 please */ curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); /* we use a self-signed test server, skip verification during debugging */ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_setopt(curl, CURLOPT_WRITEDATA, out_download); curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, my_trace); #if CURLPIPE_MULTIPLEX > 0 /* wait for pipe connection to confirm */ curl_easy_setopt(curl, CURLOPT_PIPEWAIT, 1L); #endif return 0; }注意两个关键选项:
CURLOPT_HTTP_VERSION设为CURL_HTTP_VERSION_2_0,明确要求 HTTP/2(Server Push 只在 HTTP/2 上存在);CURLOPT_PIPEWAIT让传输等待连接复用/多路复用就绪,与CURLMOPT_PIPELINING的CURLPIPE_MULTIPLEX配合使用,是接收推送的环境前提。
6.2 推送回调:为每个推送流单独开文件
static int server_push_callback(CURL *parent, CURL *curl, size_t num_headers, struct curl_pushheaders *headers, void *userp) { ... snprintf(filename, sizeof(filename), "push%u", count++); out_push = fopen(filename, "wb"); if(!out_push) { fprintf(stderr, "Failed to create output file for push\n"); return CURL_PUSH_DENY; /* 打不开文件就拒绝 */ } curl_easy_setopt(curl, CURLOPT_WRITEDATA, out_push); fprintf(stderr, "**** push callback approves stream %u, got %lu headers!\n", count, (unsigned long)num_headers); for(i = 0; i < num_headers; i++) { /* 用 bynum 遍历全部头 */ headp = curl_pushheader_bynum(headers, i); fprintf(stderr, "**** header %lu: %s\n", (unsigned long)i, headp); } headp = curl_pushheader_byname(headers, ":path"); /* 用 byname 取路径 */ if(headp) fprintf(stderr, "**** The PATH is %s\n", headp); (*transfers)++; /* 追踪活动传输数 */ return CURL_PUSH_OK; }该回调同时示范了bynum(遍历全部头)与byname(精准取:path)两种访问方式,并演示了"资源不可用即拒绝"的防御式写法。
6.3 事件循环与句柄清理陷阱
curl_multi_setopt(multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX); curl_multi_setopt(multi, CURLMOPT_PUSHFUNCTION, server_push_callback); curl_multi_setopt(multi, CURLMOPT_PUSHDATA, &transfers); curl_multi_add_handle(multi, curl); do { struct CURLMsg *m; int still_running; CURLMcode mresult = curl_multi_perform(multi, &still_running); if(still_running) mresult = curl_multi_poll(multi, NULL, 0, 1000, NULL); if(mresult) break; /* 关键:libcurl 为推送创建并加入了 easy handle, 但清理工作必须由应用自己完成 */ do { int msgq = 0; m = curl_multi_info_read(multi, &msgq); if(m && (m->msg == CURLMSG_DONE)) { curl = m->easy_handle; transfers--; curl_multi_remove_handle(multi, curl); curl_easy_cleanup(curl); } } while(m); } while(transfers); /* 所有传输结束(含所有推送)才退出 */源码注释明确提醒:做 Server Push 时要格外小心——libcurl 自己创建并添加了一个或多个 easy handle,但传输结束后的清理(curl_multi_remove_handle+curl_easy_cleanup)必须由应用完成。循环以transfers计数为退出条件,主传输加所有被接受的推送流全部结束后程序才退出。
七、进阶变体:推送到内存
仓库中的 docs/examples/http2-pushinmemory.c 展示了把推送内容接收进内存缓冲、而不是写入文件的写法:
- 定义
struct Memory { char *memory; size_t size; }配合CURLOPT_WRITEFUNCTION动态扩容收集数据; - 用固定大小的数组
files[MAX_FILES](MAX_FILES为 10)保存多个推送流; - 回调中一旦超出容量立即
return CURL_PUSH_DENY,防止内存无限增长:
if(pushindex == MAX_FILES) /* cannot fit anymore */ return CURL_PUSH_DENY; /* write to this buffer */ init_memory(&files[pushindex]); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &files[pushindex]); pushindex++; headp = curl_pushheader_byname(headers, ":path"); if(headp) fprintf(stderr, "* Pushed :path '%s'\n", headp); (*transfers)++; /* one more */ return CURL_PUSH_OK;程序结束时统一free(files[i].memory)释放内存。这是"按资源配额决定是否接受推送"的实用模式。
八、底层原理:libcurl 如何处理一次推送
结合 lib/http2.c 的push_promise()函数,可以还原 libcurl 收到PUSH_PROMISE帧后的完整处理链:
- 检查回调:只有
multi->push_cb已设置(即应用调用了CURLMOPT_PUSHFUNCTION)才进入推送审批流程;否则直接忽略该帧(对应"未设置回调则全部拒绝"的文档语义); - 克隆父句柄:调用
h2_duphandle()(lib/http2.c)用curl_easy_init()创建新句柄,并继承父流的权重、share 等状态——这就是文档所说"新句柄从 parent 复制而来、继承全部选项"的实现; - 组装 URL:
set_transfer_url()从 PUSH_PROMISE 头的:scheme、:authority、:path伪头部拼接出推送资源的 URL; - 同源校验:通过
Curl_url_same_origin()强制要求推送资源与父流同源(scheme + hostname + port 一致),非同源直接拒绝,这是协议层面的安全约束; - 调用回调:构造隐藏的
struct curl_pushheaders,把(parent, newhandle, num_headers, &heads, push_userp)传给应用的push_cb,随后立即free_push_headers()释放头部数据——这正是访问器返回的数据"回调返回即失效"的原因; - 按返回值分流:返回值非 0(拒绝/报错)时调用
discard_newhandle()销毁新句柄;返回CURL_PUSH_OK时调用Curl_multi_add_perform()把新句柄加入 multi handle 开始执行——印证了"回调绝不能自行添加句柄"的规定。
九、版本、错误处理与配套选项
9.1 版本兼容
CURLMOPT_PUSHFUNCTION/CURLMOPT_PUSHDATA:7.44.0 引入;CURL_PUSH_ERROROUT返回值:7.72.0 引入;- 头访问器
curl_pushheader_byname/curl_pushheader_bynum:7.44.0 引入。
程序若需兼容旧版本,可在编译期用版本宏(如LIBCURL_VERSION_NUM)或检查头文件中常量是否定义(如#ifdef CURL_PUSH_ERROROUT)来降级处理。
9.2 返回值与错误码
curl_multi_setopt()返回CURLMcode:CURLM_OK(0)表示成功,非零表示出错,错误码含义参见 libcurl-errors.md。另可参考 curl_multi_setopt.md 了解 multi 选项设置的通用约定。
9.3 相关选项一览
| 选项/函数 | 作用 | 文档 |
|---|---|---|
| CURLMOPT_PUSHDATA | 传给推送回调的clientp指针,默认 NULL | docs/libcurl/opts/CURLMOPT_PUSHDATA.md |
CURLMOPT_PIPELINING(CURLPIPE_MULTIPLEX) | 启用 HTTP/2 多路复用,推送接收的前提 | docs/libcurl/opts/CURLMOPT_PIPELINING.md |
CURLOPT_PIPEWAIT | 等待连接多路复用就绪 | docs/libcurl/opts/CURLOPT_PIPEWAIT.md |
curl_pushheader_byname | 按名读取推送头 | docs/libcurl/curl_pushheader_byname.md |
curl_pushheader_bynum | 按下标读取推送头 | docs/libcurl/curl_pushheader_bynum.md |
十、最佳实践小结
- 务必设置推送回调:不设置
CURLMOPT_PUSHFUNCTION时 libcurl 会拒绝全部推送,资源白白浪费在 PUSH_PROMISE 帧上; - 回调内只做轻量决策:头数据在回调返回后即被释放,若需保留须自行拷贝;重活(写盘、解析)放在后续的 write/header 回调中;
- 显式管理句柄生命周期:接受推送后,传输结束时的
curl_multi_remove_handle与curl_easy_cleanup由应用负责; - 按资源策略拒绝:结合路径匹配(
:path)、配额上限(如内存示例的MAX_FILES)、文件打开失败等条件返回CURL_PUSH_DENY,需要终止整个父流时返回CURL_PUSH_ERROROUT; - 利用同源安全约束:libcurl 内置的同源校验已挡住跨源推送,应用侧可再叠加自己的过滤规则;
- 联动 HTTP/2 环境:记得设置
CURL_HTTP_VERSION_2_0与多路复用选项,否则推送根本不会发生。
仓库中 tests/libtest/cli_h2_serverpush.c 还提供了该特性的命令行测试程序,可作为进一步研读回调与事件循环交互的补充材料。
【免费下载链接】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),仅供参考