nghttp2 扩展帧接收注册函数 nghttp2_option_set_user_recv_extension_type 全面解析
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
本指南以 nghttp2-1.65.0(内嵌于当前 fluent-bit 仓库lib/nghttp2-1.65.0/)中的 nghttp2_option_set_user_recv_extension_type.rst 为骨架,围绕nghttp2_option_set_user_recv_extension_type()这一个 API 展开,讲解它如何向 nghttp2 会话注册"应用程序愿意通过自定义回调接收的 HTTP/2 扩展帧类型",并结合库内实现(nghttp2_option.c、nghttp2_session.c)、头文件声明、程序员指南示例与单元测试,带你掌握扩展帧接收的完整调用链与底层原理。读完本文,你将能够在自己的 nghttp2 客户端/服务端程序中正确注册、接收并解包任意自定义 HTTP/2 扩展帧。
函数签名与核心语义
该 API 的原型定义在公共头文件 nghttp2.h 中:
#include <nghttp2/nghttp2.h> void nghttp2_option_set_user_recv_extension_type(nghttp2_option *option, uint8_t type);其作用(原文语义)是:设置应用程序愿意通过用户自定义回调处理的扩展帧类型。这里的"用户自定义回调"特指两个回调:
nghttp2_on_extension_chunk_recv_callback——负责逐段缓冲(buffering)到达的扩展帧载荷;nghttp2_unpack_extension_callback——负责把缓冲好的 wire format 载荷解包(unpack)为应用程序可用的结构。
二者在 nghttp2.h 中定义,完整语义见 types.rst。
核心语义要点如下:
type是扩展帧类型号,取值范围必须严格大于0x9(十进制 9);- 若传入的
type不满足上述条件,该函数什么都不做(静默忽略,不报错); - 可多次调用,为同一个
nghttp2_option注册多个扩展帧类型,全部生效; - 纯发送场景无需调用——如果应用只是通过
nghttp2_submit_extension()发送扩展帧、并不打算接收对端发来的扩展帧,则完全不需要调用此函数。
为什么必须严格大于 0x9:HTTP/2 帧类型空间
HTTP/2 规范(RFC 9113)在帧头(9 字节固定头部)的第 3 字节编码帧类型(8 位),并预定义了 0x0~0x9 共 10 种标准帧:
| 类型值 | 帧名 |
|---|---|
| 0x0 | DATA |
| 0x1 | HEADERS |
| 0x2 | PRIORITY |
| 0x3 | RST_STREAM |
| 0x4 | SETTINGS |
| 0x5 | PUSH_PROMISE |
| 0x6 | PING |
| 0x7 | GOAWAY |
| 0x8 | WINDOW_UPDATE |
| 0x9 | CONTINUATION |
因此0xa(10)及以上的类型号才属于扩展帧类型(如NGHTTP2_ALTSVC即为0xa)。nghttp2_option_set_user_recv_extension_type()的参数检查正是基于这一规范约定:type若落在标准帧区间内,注册无意义,函数直接返回。
底层实现剖析:位图式类型集合
该函数的实现在 nghttp2_option.c:
static void set_ext_type(uint8_t *ext_types, uint8_t type) { ext_types[type / 8] = (uint8_t)(ext_types[type / 8] | (1 << (type & 0x7))); } void nghttp2_option_set_user_recv_extension_type(nghttp2_option *option, uint8_t type) { if (type < 10) { return; } option->opt_set_mask |= NGHTTP2_OPT_USER_RECV_EXT_TYPES; set_ext_type(option->user_recv_ext_types, type); }从源码可以看到三个关键设计:
- 越界静默返回:
if (type < 10) return;与文档中 "must be strictly greater than 0x9. Otherwise, this function does nothing" 完全对应。 - 位图存储:
nghttp2_option结构体内定义了一个uint8_t user_recv_ext_types[32]数组(见 nghttp2_option.h),共 32×8 = 256 位,恰好覆盖 8 位帧类型号 0~255 的全部取值空间。set_ext_type通过type / 8定位字节、1 << (type & 0x7)定位位,把对应帧类型标记进位图。由于是"按位或"写入,多次调用天然合并,无需担心覆盖。 - 掩码标记:设置
opt_set_mask |= NGHTTP2_OPT_USER_RECV_EXT_TYPES,用于让会话创建逻辑知道该选项已被显式设置,从而把位图拷贝进会话。
从 option 到 session:类型集合如何生效
nghttp2_option只是配置载体,真正在收帧时起作用的是会话内部拷贝的位图。在 nghttp2_session.c 中,创建会话(如nghttp2_session_client_new2)时会把选项位图整体拷贝到会话:
memcpy((*session_ptr)->user_recv_ext_types, option->user_recv_ext_types, sizeof((*session_ptr)->user_recv_ext_types));随后,库在帧释放与分发路径上通过check_ext_type_set(session->user_recv_ext_types, frame->hd.type)判断某帧是否属于应用注册的自定义扩展帧类型(见 nghttp2_session.c 及 L5722 附近的收帧路径)。这解释了为什么必须在创建会话之前就把扩展帧类型注册进 option,并用该 option 创建会话——例如:
nghttp2_session_client_new2(&session, callbacks, user_data, option);接收扩展帧的完整调用链与示例
注册与回调装配
按 programmers-guide.rst 的完整示例,接收扩展帧需要四步:
- 实现
on_extension_chunk_recv_callback:把到达的载荷逐段缓冲到应用缓冲区; - 实现
unpack_extension_callback:把缓冲内容按自定义格式解包,可把结果对象存入*payload(库不持有该指针,应用需自行管理内存); - 通过
nghttp2_session_callbacks_set_on_extension_chunk_recv_callback()与nghttp2_session_callbacks_set_unpack_extension_callback()装配两个回调; - 调用
nghttp2_option_set_user_recv_extension_type()注册帧类型,并用该 option 创建会话。
以 ALTSVC(帧类型0xa)为例,两个回调的典型实现如下(来自程序员指南,略作整理):
typedef struct { const uint8_t *origin; size_t originlen; const uint8_t *field; size_t fieldlen; } alt_svc; /* buffers incoming ALTSVC payload */ uint8_t altsvc_buffer[4096]; size_t altsvc_bufferlen = 0; int on_extension_chunk_recv_callback(nghttp2_session *session, const nghttp2_frame_hd *hd, const uint8_t *data, size_t len, void *user_data) { if (sizeof(altsvc_buffer) < altsvc_bufferlen + len) { altsvc_bufferlen = 0; return NGHTTP2_ERR_CANCEL; } memcpy(altsvc_buffer + altsvc_bufferlen, data, len); altsvc_bufferlen += len; return 0; } int unpack_extension_callback(nghttp2_session *session, void **payload, const nghttp2_frame_hd *hd, void *user_data) { /* ALTSVC wire format: 2-byte origin length + origin + field-value */ uint8_t *p = altsvc_buffer, *end = altsvc_buffer + altsvc_bufferlen; size_t originlen = ((*p) << 8) + *(p + 1); p += 2; if (p + originlen > end) { altsvc_bufferlen = 0; return NGHTTP2_ERR_CANCEL; } alt_svc *altsvc = (alt_svc *)malloc(sizeof(alt_svc)); altsvc->origin = p; altsvc->originlen = originlen; altsvc->field = p + originlen; altsvc->fieldlen = end - (p + originlen); *payload = altsvc; /* 解包结果存入 *payload */ altsvc_bufferlen = 0; return 0; }装配回调并注册帧类型:
nghttp2_session_callbacks_set_on_extension_chunk_recv_callback( callbacks, on_extension_chunk_recv_callback); nghttp2_session_callbacks_set_unpack_extension_callback( callbacks, unpack_extension_callback); nghttp2_option_set_user_recv_extension_type(option, 0xa); nghttp2_session_client_new2(&session, callbacks, user_data, option);在 on_frame_recv_callback 中消费
解包完成后,库会照常调用nghttp2_on_frame_recv_callback,此时*payload可通过frame->ext.payload取得,应用可在该回调中读取数据并释放内存:
int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) { switch (frame->hd.type) { case 0xa: { alt_svc *altsvc = (alt_svc *)frame->ext.payload; fprintf(stderr, "ALTSVC frame received\n"); fprintf(stderr, " origin: %.*s\n", (int)altsvc->originlen, altsvc->origin); fprintf(stderr, " field : %.*s\n", (int)altsvc->fieldlen, altsvc->field); free(altsvc); break; } } return 0; }错误处理约定
两个回调的返回值约定一致(见 nghttp2.h):
- 返回
0表示成功; - 返回
NGHTTP2_ERR_CANCEL表示中止处理该扩展帧(nghttp2_on_frame_recv_callback将不再被调用); - 返回
NGHTTP2_ERR_CALLBACK_FAILURE表示致命错误,nghttp2_session_recv()与nghttp2_session_mem_recv2()会立即返回该错误码;其他非零返回值目前一律按NGHTTP2_ERR_CALLBACK_FAILURE处理。
与内置接收(builtin)类型的区别与优先级
nghttp2 另提供nghttp2_option_set_builtin_recv_extension_type(),用于接收库内置处理器支持的扩展帧,目前包括 ALTSVC、ORIGIN 与 PRIORITY_UPDATE(见 nghttp2_option.c 的 switch 实现)。两者的差异见 nghttp2_option_set_builtin_recv_extension_type.rst:
| 对比维度 | user_recv_extension_type | builtin_recv_extension_type |
|---|---|---|
| 处理方式 | 用户自定义回调(chunk + unpack) | 库内置 handler |
| 支持的帧类型 | 任意> 0x9的类型号(256 位位图) | 仅 ALTSVC、ORIGIN、PRIORITY_UPDATE |
| 适用场景 | 自定义私有扩展帧 | 标准扩展帧 |
| 优先级 | 同一类型同时注册时,user 版本优先 | 被 user 版本覆盖 |
关键点:如果同一帧类型同时通过两个函数注册,后者(user 版本)优先生效——这是文档与 programmers-guide.rst 中明确声明、并在会话内部check_ext_type_set优先于builtin_recv_ext_types判断中体现的设计(见 nghttp2_session.c)。这意味着即使 nghttp2 内置了某扩展帧的处理,你仍可"接管"该帧类型实现自己的 handler。
单元测试验证
仓库的会话测试 nghttp2_session_test.c 中的test_nghttp2_session_recv_extension直接覆盖了本 API 的行为:
nghttp2_option_new(&option); nghttp2_option_set_user_recv_extension_type(option, 111); ... nghttp2_session_client_new2(&session, &callbacks, &ud, option);测试构造了一个帧类型为111(0x6f)、带任意 flags 与 stream_id 的扩展帧,通过nghttp2_session_mem_recv2()送入会话,随后断言:接收字节数正确、on_frame_recv_callback收到的hd.type == 111、flags 与 stream_id 原样透传、载荷 "Hello World!" 完整进入缓冲。测试还分别验证了在on_extension_chunk_recv_callback和unpack_extension_callback中返回NGHTTP2_ERR_CANCEL时,on_frame_recv_callback不再被调用(frame_recv_cb_called == 0)。这三条路径共同印证了本文描述的完整调用链。
使用要点与注意事项
- 必须在创建会话前注册:位图在
nghttp2_session_client_new2()/nghttp2_session_server_new2()时从 option 拷贝,事后修改 option 不影响已创建的会话;会话生命周期内如需变更接收类型,需重建会话。 - 同一 option 可复用:一个
nghttp2_option可注册多个类型(多次调用),也可同时传给多个会话。 - type 参数是
uint8_t:帧类型号为 8 位,合法范围是 10~255(0xa~0xff);传入 0~9 会被静默忽略,建议在调用前自行校验避免误解。 - 发送不受影响:仅发送扩展帧时无需任何注册;只有接收路径才需要调用本函数。
- 内存管理责任在应用:
unpack_extension_callback写入*payload的指针由库透传给on_frame_recv_callback的frame->ext.payload,库不负责释放,应用需在消费后自行free;也可完全不使用*payload,采用自己的机制处理。 - 与内置类型的覆盖关系:若希望覆盖内置类型(如 ALTSVC)的处理,用本函数注册相同类型号即可,user 回调优先。
- 缓冲区需防御性检查:
on_extension_chunk_recv_callback可能被多次调用以传输完整载荷,需自行累计长度并做越界保护(参考示例中的sizeof(altsvc_buffer) < altsvc_bufferlen + len检查),越界时返回NGHTTP2_ERR_CANCEL中止处理。
参考文档索引
- 本函数 API 文档:nghttp2_option_set_user_recv_extension_type.rst
- 内置接收对照文档:nghttp2_option_set_builtin_recv_extension_type.rst
- 头文件声明与回调定义:nghttp2.h
- 完整收发示例(程序员指南):programmers-guide.rst
- 回调类型语义说明:types.rst
- 实现源码:nghttp2_option.c、nghttp2_session.c
- 单元测试:nghttp2_session_test.c
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考