ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Fluent Bit 内嵌 nghttp2 编程指南:从会话架构到 HTTP/2 扩展帧与优先级调度

Fluent Bit 内嵌 nghttp2 编程指南:从会话架构到 HTTP/2 扩展帧与优先级调度 Fluent Bit 内嵌 nghttp2 编程指南从会话架构到 HTTP/2 扩展帧与优先级调度【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bitnghttp2 是 Fluent Bit 中实现 HTTP/2 协议处理的核心依赖库以静态库形式内嵌见 cmake/nghttp2.cmake本文以 nghttp2 官方编程指南 lib/nghttp2-1.65.0/doc/programmers-guide.rst 为骨架完整讲解其无 I/O 的会话架构、HTTP 消息校验规则、帧传输调度、用户自定义扩展帧框架、内置 ALTSVC 处理器以及面向 RFC 9218 的流优先级迁移同时结合 Fluent Bit 中 src/flb_http_client_http2.c 的真实调用链与 nghttp2 公共头文件给出可直接落地的 C 语言编程实践。读完本文你将掌握在任意事件循环中嵌入 nghttp2、正确收发 HTTP/2 帧、编写自定义扩展帧处理器以及迁移流优先级方案的能力。架构一个不做任何 I/O 的 HTTP/2 协议栈nghttp2 库架构中最值得注意的一点是它自身不执行任何 I/O。nghttp2 只基于输入的字节串完成 HTTP/2 协议处理处理过程中会调用由应用注册的回调函数它的输出同样只是一段字节串由应用负责将这段输出发送给对端。需要注意的是回调函数可能在“产生输出”的过程中被调用。不做 I/O 带来两个直接收益嵌入成本低现有应用通常都有自己的 I/O 事件循环。如果 nghttp2 自带 I/O在这种场景下几乎无法使用反之它可以被无缝嵌入任何事件驱动架构。语言绑定简单同样的原因使得为 nghttp2 编写轻量级语言包装如 Lua、Python 绑定变得容易。代价是应用作者需要多写一些代码才能拼出完整应用——尤其是简单的 toy 程序。但对于真实应用而言并非如此真实应用通常还要支持 nghttp2 并不提供的 HTTP/1为此你必须自研 HTTP/1 栈或引入第三方库并把它们与 nghttp2、I/O 事件循环绑定在一起。从这个角度看nghttp2 不做 I/O 的取舍是利大于弊的。Fluent Bit 正是这一设计哲学的受益者在 src/flb_http_client_http2.c 中flb_http2_client_session_init()创建会话与回调后把发送回调、帧接收回调、流关闭回调、头部开始/数据块/头部字段回调全部注册进nghttp2_session_callbacks随后完全由 Fluent Bit 自身的网络事件循环驱动会话收发nghttp2 只负责协议状态机与帧编解码。会话对象nghttp2_session应用使用的核心对象是nghttp2_session。它是一个不透明opaque结构体内部细节被隐藏以保证库在升级内部架构时不会破坏向后兼容性。应用通过专门的 setter 函数为会话设置回调并通过大量 API 调用与之交互。一个进程内可以创建任意多个nghttp2_session对象但单个会话对象在同一时刻只能被单个线程使用。这个约束不难满足——大多数基于事件的应用架构本身就是一个核心一个线程且一个连接的 I/O 由单线程处理。喂入输入recv 系 API向会话喂入输入有两种方式API行为适用建议nghttp2_session_recv()通过nghttp2_read_callback回调获取输入需要回调配合灵活但代码更多nghttp2_session_mem_recv2(nghttp2_session *session, const uint8_t *in, size_t inlen)直接以参数形式接收输入字节串更简单且避免了回调调用开销可能更快拿不准时优先使用nghttp2_session_mem_recv2()。其语义是处理in中来自对端的inlen字节数据除非发生错误或某个回调返回NGHTTP2_ERR_PAUSE否则它会尽量消费全部输入返回值为实际处理的字节数。可能的负错误码包括NGHTTP2_ERR_NOMEM内存不足、NGHTTP2_ERR_CALLBACK_FAILURE回调失败、NGHTTP2_ERR_BAD_CLIENT_MAGIC服务端会话检测到非法客户端 magic以及NGHTTP2_ERR_FLOODED检测到对端行为异常导致的泛洪此时必须关闭会话。详细签名见 nghttp2_session_mem_recv2.rst。取走输出send 系 API从会话取输出同样有两种方式API行为适用建议nghttp2_session_send()通过nghttp2_send_callback回调把输出交给应用当应用持有固定大小的输出缓冲时可能更顺手nghttp2_session_mem_send2(nghttp2_session *session, const uint8_t **data_ptr)把序列化输出直接返回给调用者更简单拿不准就用它nghttp2_session_mem_send2()把指针赋给*data_ptr并返回其长度无数据可发时返回 0。它一次调用未必返回全部待发数据需要循环调用直到返回 0 或负错误码且*data_ptr指向的缓冲只在下次调用nghttp2_session_mem_send2()/nghttp2_session_send()之前有效调用方必须在取下一块数据前把当前数据发完。它还可能产生非常小的字节串——如果应用禁用了 Nagle 算法TCP_NODELAY逐个发送小分片会产生大量小包应用应自行把小分片攒起来批量发送。详细签名见 nghttp2_session_mem_send2.rst。推荐的调用节奏与安全关闭一般的做法是从底层连接拿到输入后先调nghttp2_session_mem_send2()再调nghttp2_session_mem_recv2()。因为nghttp2_session_mem_send2()的调用过程中极有可能往传输队列里压入新内容例如发送 SETTINGS 后对端立即响应 ACK紧接着调用nghttp2_session_mem_recv2()能顺势消费这些新输入。Fluent Bit 的 flb_http2_client_session_ingest() 采用的正是先 recv 后 send的变体nghttp2_session_mem_recv()消化对端数据随后立即nghttp2_session_send()把应答帧刷出。两种顺序的核心原则一致——不要让可发送的输出滞留在会话内部。那么什么时候可以安全地关闭 HTTP/2 会话而不必等待底层连接关闭nghttp2 提供两个探测 APInghttp2_session_want_read()—— 是否还期望从对端读取nghttp2_session_want_write()—— 是否还有数据需要写出当两者都返回 0 时应用即可销毁nghttp2_session并关闭底层连接。但有一个重要前提如果用的是nghttp2_session_mem_send2()关闭连接前必须确认缓冲的输出已真正发送到对端因为nghttp2_session_want_write()并不统计会话外部尚未发送的缓冲数据。头文件与编译接入使用公共 API 只需包含一个头文件#include nghttp2/nghttp2.h头文件对应仓库中的 lib/nghttp2-1.65.0/lib/includes/nghttp2/nghttp2.h其在线渲染版见 nghttp2.h.rst.in版本定义位于 nghttp2ver.h.rst.in。以 Fluent Bit 为例cmake/nghttp2.cmake 通过以下方式把 nghttp2 编译为静态库并接入构建FLB_OPTION(ENABLE_LIB_ONLY ON) FLB_OPTION(ENABLE_STATIC_LIB ON) FLB_OPTION(ENABLE_SHARED_LIB OFF) FLB_DEFINITION(NGHTTP2_STATICLIB) include_directories( ${FLB_PATH_ROOT_SOURCE}/${FLB_PATH_LIB_NGHTTP2}/lib/includes/ ... ) add_subdirectory(${FLB_PATH_LIB_NGHTTP2} EXCLUDE_FROM_ALL) set(NGHTTP2_LIBRARIES nghttp2_static)要点是以NGHTTP2_STATICLIB宏定义启用静态编译把lib/includes/加入头文件搜索路径最终链接产物名为nghttp2_static。使用注意事项Remarks禁止在 nghttp2 回调函数内直接或间接调用nghttp2_session_send()、nghttp2_session_mem_send2()、nghttp2_session_recv()或nghttp2_session_mem_recv2()否则会导致崩溃。正确的模式是在回调里提交请求或帧nghttp2_submit_*系列等回调返回后在回调之外再调用上述 send/recv 函数。关于连接前奏connection preface的两点约定客户端配置下nghttp2_session_send()与nghttp2_session_mem_send2()会自动发送客户端 magic 字符串的前 24 个字节。该 magic 定义为NGHTTP2_CLIENT_MAGIC即PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n长度常量NGHTTP2_CLIENT_MAGIC_LEN为 24见 nghttp2.h。应用仍需自己负责调用nghttp2_submit_settings()提交 SETTINGS 帧以完成连接前奏的剩余部分。服务端配置下nghttp2_session_recv()与nghttp2_session_mem_recv2()默认会消费对端的 magic 字符串若不需要可用nghttp2_option_set_no_recv_client_magic()传入非零值关闭这一行为此时再收到非法 magic 会返回NGHTTP2_ERR_BAD_CLIENT_MAGIC。Fluent Bit 客户端初始化即遵循该流程flb_http2_client_session_init() 中先nghttp2_session_callbacks_new()创建回调集合再nghttp2_session_client_new()创建客户端会话随后立刻提交三个 SETTINGS 条目NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS1、NGHTTP2_SETTINGS_MAX_FRAME_SIZE、NGHTTP2_SETTINGS_ENABLE_PUSH0并调用nghttp2_session_send()把 magic 与 SETTINGS 一并刷出。HTTP 消息规则校验HTTP Messaging默认情况下nghttp2 会校验 HTTP/2 规范第 8 节RFC 7540描述的 HTTP 消息规则——但并非全部校验下面简述库实际执行的部分。以下叙述中统一省略 CONTINUATION 帧它必须紧跟 HEADERS 帧且原子化处理本质上就是一个大的 HEADERS 帧。如需关闭这些校验调用nghttp2_option_set_no_http_messaging()。注意禁用校验不会改变 HTTP 的基本客户端/服务端模型——即使校验关闭依然只有客户端能发送请求。请求与响应结构HTTP 请求含 PUSH_PROMISE 携带的请求的消息结构为一个包含请求头部的 HEADERS 帧起始零个或多个承载请求体的 DATA 帧零个或一个承载 trailer 头部的 HEADERS 帧收尾。请求头部必须包含:scheme、:method、:path伪头字段除非:method是CONNECT。:authority可选但 nghttp2 要求:authority与Host头字段至少出现其一。若:method为CONNECT则请求头部必须包含:method与:authority且不得出现:scheme与:path。HTTP 响应的消息结构为零个或多个承载非最终响应状态码 1xx的 HEADERS 帧一个承载最终响应头非 1xx的 HEADERS 帧零个或多个承载响应体的 DATA 帧零个或一个承载 trailer 头部的 HEADERS 帧。非最终与最终响应头部都必须包含:status伪头字段且其值只能是 3 位数字。所有请求与响应头部中每个伪头字段必须恰好有一个合法值此外 nghttp2 要求所有请求头部中的Host头字段不得超过一个。连接特定头字段与字段名/值合法性HTTP/2 禁止连接特定connection-specific头字段以下字段不得出现ConnectionKeep-AliveProxy-ConnectionTransfer-EncodingUpgrade另外TE头字段的值只能是trailers不得携带其他值。每个头字段名与值都必须遵守 RFC 7230 第 3.2 节的 field-name 与 field-value 产生式规则此外所有字段名必须小写。非法头字段按流错误stream error处理对应流会被重置reset。如果应用想用自己的方式处理这类头部可以通过nghttp2_on_invalid_header_callback回调接管对应注册函数为nghttp2_session_callbacks_set_on_invalid_header_callback*。对于http或httpsURI:path伪头字段必须以/开头唯一例外是 OPTIONS 请求其:path允许为*用于表示系统级 OPTIONS 请求。经过上述校验nghttp2 可以保证传入nghttp2_on_header_callback()的字段名非空且必需的伪头字段都存在且非空。Content-Length 校验nghttp2 同时强制执行Content-Length校验任何请求或响应头部的Content-Length头字段不得超过一个若存在必须能被解析为 64 位有符号整数后续所有 DATA 帧的数据长度之和不含填充字节必须与该值匹配。RFC 7230 还规定服务端不得在任何 1xx、204 响应中发送Content-Length且对 CONNECT 请求返回 200 时也不得携带Content-Length——nghttp2 一并强制执行。任何偏离都会导致类型为PROTOCOL_ERROR的流错误若错误出现在 PUSH_PROMISE 帧中则对 promised 流被推送的流提起流错误。帧传输顺序内部调度机制本节描述 libnghttp2 内部对 HTTP/2 帧传输的调度实现属于内部细节未来版本可能变化。libnghttp2 将 HTTP/2 帧划分为4 个类别按优先级从高到低类别包含的帧类别内排序urgent紧急PING、SETTINGSFIFOregular常规除 PING、SETTINGS、DATA 以及不创建计入并发流上限的流的 HEADERS之外的所有帧FIFOsyn_stream创建流的 HEADERS 帧计入并发流上限—dataDATA 帧由 HTTP/2 依赖树dependency tree决定调度其中regular类别对HEADERS 帧是否计入并发流上限做了区分——只有不创建流不消耗并发流配额的 HEADERS 才算 regular。如果应用希望以特定顺序发送帧而默认顺序不满足需求就必须借助回调如nghttp2_on_frame_send_callback自行调度。RST_STREAM 的特殊副作用nghttp2_submit_rst_stream()提交 RST_STREAM 时有一个特殊副作用它会取消所有 stream ID 与之匹配的、尚未发送的 HEADERS 与 DATA 帧。这在某些场景下会导致出乎意料的行为。例如假设应用想先发送响应 HEADERS 和 DATA再发送 RST_STREAM下面这段代码不会按预期工作nghttp2_submit_response2(...) nghttp2_submit_rst_stream(...)因为 RST_STREAM 把 HEADERS以及 DATA都取消了最终只会发出一个 RST_STREAM。正确做法是使用nghttp2_on_frame_send_callback等 HEADERS 和 DATA 帧确实发送出去之后再调用nghttp2_submit_rst_stream()。另外nghttp2_on_frame_not_send_callback会告诉你帧未被发送的原因。实现用户自定义的 HTTP/2 非关键扩展帧自 nghttp2 v1.8.0 起库提供了HTTP/2 非关键扩展non-critical extension框架允许应用发送和接收自定义扩展帧。此外 nghttp2 还内置了官方 HTTP/2 扩展帧如 ALTSVC的收发能力见下一节。发送自定义扩展帧发送扩展帧使用nghttp2_submit_extension(nghttp2_session *session, uint8_t type, uint8_t flags, int32_t stream_id, void *payload)同时实现nghttp2_pack_extension_callback回调负责把数据编码为线上wire格式并通过nghttp2_session_callbacks_set_pack_extension_callback()注册到nghttp2_session_callbacks。注意两个约束见 nghttp2_submit_extension.rsttype必须严格大于0x9标准帧类型范围[0x0, 0x9]不可用此函数发送否则返回NGHTTP2_ERR_INVALID_ARGUMENTpayload是库不持有的不透明指针应用须保证其内存有效直至该帧发送完成nghttp2_on_frame_send_callback或发送失败nghttp2_on_frame_not_send_callback若未设置 pack 回调调用返回NGHTTP2_ERR_INVALID_STATE。下面以发送 ALTSVC 帧RFC 7838 定义帧类型0xa为例给出完整代码。首先是自定义负载结构与打包回调typedef struct { const char *origin; const char *field; } alt_svc; nghttp2_ssize pack_extension_callback(nghttp2_session *session, uint8_t *buf, size_t len, const nghttp2_frame *frame, void *user_data) { const alt_svc *altsvc (const alt_svc *)frame-ext.payload; size_t originlen strlen(altsvc-origin); size_t fieldlen strlen(altsvc-field); uint8_t *p; if (len 2 originlen fieldlen || originlen 0xffff) { return NGHTTP2_ERR_CANCEL; } p buf; *p originlen 8; *p originlen 0xff; memcpy(p, altsvc-origin, originlen); p originlen; memcpy(p, altsvc-field, fieldlen); p fieldlen; return p - buf; }ALTSVC 的线上格式为2 字节大端 origin 长度 origin 字节 field 字节。把回调注册到会话回调集合nghttp2_session_callbacks_set_pack_extension_callback( callbacks, pack_extension_callback);然后提交扩展帧。注意frame-ext.payload就是nghttp2_submit_extension()传入的payloadstatic const alt_svc altsvc {example.com, h2\:8000\}; nghttp2_submit_extension(session, 0xa, NGHTTP2_FLAG_NONE, 0, (void *)altsvc);接收自定义扩展帧接收扩展帧需要实现两个回调nghttp2_unpack_extension_callback—— 负责把线上格式解码为应用对象nghttp2_on_extension_chunk_recv_callback—— 负责缓冲陆续到达的扩展帧负载。分别通过nghttp2_session_callbacks_set_unpack_extension_callback()与nghttp2_session_callbacks_set_on_extension_chunk_recv_callback()注册。此外应用还必须用nghttp2_option_set_user_recv_extension_type()告知库自己愿意接收哪种扩展帧类型——这要求应用先创建nghttp2_option对象并在创建会话时将其传入。仍以 ALTSVC 为例此处使用与发送侧不同的alt_svc结构。先把收到的负载缓冲到全局变量示例仅为演示生产代码中这样写不安全因为非线程安全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]; /* The length of byte written to altsvc_buffer */ 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) { uint8_t *origin, *field; size_t originlen, fieldlen; uint8_t *p, *end; alt_svc *altsvc; if (altsvc_bufferlen 2) { altsvc_bufferlen 0; return NGHTTP2_ERR_CANCEL; } p altsvc_buffer; end altsvc_buffer altsvc_bufferlen; originlen ((*p) 8) *(p 1); p 2; if (p originlen end) { altsvc_bufferlen 0; return NGHTTP2_ERR_CANCEL; } origin p; field p originlen; fieldlen end - field; altsvc (alt_svc *)malloc(sizeof(alt_svc)); altsvc-origin origin; altsvc-originlen originlen; altsvc-field field; altsvc-fieldlen fieldlen; *payload altsvc; 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);在unpack_extension_callback中我们把解码出的alt_svc对象赋给*payload。nghttp2 随后会调用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_option_set_user_recv_extension_type(option, 0xa);该nghttp2_option必须在会话创建时传入nghttp2_session_client_new2(session, callbacks, user_data, option);使用内置的 HTTP/2 扩展帧处理器除用户自定义扩展帧外nghttp2 还内置了官方扩展帧的收发支持。截至目前内置支持的是ALTSVC 扩展帧。发送 ALTSVC 使用nghttp2_submit_altsvc(nghttp2_session *session, uint8_t flags, int32_t stream_id, const uint8_t *origin, size_t origin_len, const uint8_t *field_value, size_t field_value_len)详细签名见 nghttp2_submit_altsvc.rst。使用规则flags当前被忽略应传NGHTTP2_FLAG_NONE若stream_id为 0则必须指定originorigin_len 非 0若stream_id非 0则origin必须为空origin_len 为 0ALTSVC 帧仅服务端会话可用客户端会话调用会返回NGHTTP2_ERR_INVALID_STATEorigin_len field_value_len超过 16382 时返回NGHTTP2_ERR_INVALID_ARGUMENT。接收 ALTSVC 则需要用nghttp2_option_set_builtin_recv_extension_type()声明接收意愿nghttp2_option_set_builtin_recv_extension_type(option, NGHTTP2_ALTSVC);这与自定义扩展帧的接收流程非常相似——option同样必须在创建会话时传入nghttp2_session_client_new2(session, callbacks, user_data, option);收到 ALTSVC 帧时nghttp2_on_frame_recv_callback会照常被调用。优先级规则如果同一帧类型同时通过nghttp2_option_set_builtin_recv_extension_type()和nghttp2_option_set_user_recv_extension_type()注册后者用户自定义处理优先——应用可以用自己的帧处理器覆盖内置处理器。流优先级从 RFC 7540 迁移到 RFC 9218RFC 7540 描述的流优先级方案已被 RFC 9113 正式弃用因此 nghttp2 中相应实现已被移除。应用被建议通过nghttp2_submit_settings()提交NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES设置 ID 为0x09见 nghttp2.h设置值为 1并向 RFC 9218 迁移。发送该设置值的含义是禁用 RFC 7540 优先级启用 RFC 9218 可扩展优先级方案Extensible Prioritization Scheme。RFC 9218 提供两种向对端传达流优先级的方式Priority 头字段客户端可通过nghttp2_submit_request2()在请求头中携带PRIORITY_UPDATE 帧帧类型0x10见 nghttp2.h客户端可通过nghttp2_submit_priority_update()发送。nghttp2 对两种方式都提供支持。服务端若要接收并处理 PRIORITY_UPDATE 帧必须先用nghttp2_option_set_builtin_recv_extension_type()以NGHTTP2_PRIORITY_UPDATE为类型参数注册参见前述内置扩展帧一节再把该 option 传入nghttp2_session_server_new2()或nghttp2_session_server_new3()创建服务端会话。服务端处理请求头中的 Priority 头字段并据此更新流优先级——除非 HTTP 消息校验被禁用nghttp2_option_set_no_http_messaging()。小结在 Fluent Bit 场景下的落地要点回到 Fluent Bit 项目本身这套编程模型已经在生产代码中得到验证构建接入通过 cmake/nghttp2.cmake 以nghttp2_static静态库方式内嵌头文件来自 lib/nghttp2-1.65.0/lib/includes/nghttp2/nghttp2.h会话生命周期src/flb_http_client_http2.c 展示了完整的callbacks_new → 注册回调 → client_new → submit_settings → session_send初始化链以及基于nghttp2_session_mem_recv/nghttp2_session_send的收发放大循环编程铁律回调内禁止调用 send/recv 系函数客户端的 magic 与 SETTINGS 帧职责分离关闭会话前用want_read/want_write判断并确保外部缓冲已发完扩展能力无论自定义扩展帧还是内置 ALTSVC/PRIORITY_UPDATE核心都是注册回调 提交帧 option 声明接收类型三件套同一套模式可以举一反三。如需进一步对照 API 细节可继续阅读本仓库 lib/nghttp2-1.65.0/doc/ 目录下的nghttp2_*.rst逐个函数的参考文档以及 programmers-guide.rst 原文。【免费下载链接】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),仅供参考
返回列表