Nginx 的 HPACK 实现完全自研,位于 src/http/v2/ 目录,核心函数为 ngx_http_v2_encode_header 和 ngx_http_v2_decode_header,动态表采用紧凑缓冲区与引用计数管理,严格遵循 RFC 7541。

nginx 对 HTTP/2 首部压缩(HPACK)的实现不依赖外部库,而是完全自主实现了一套轻量、高效、符合 RFC 7541 的编码/解码逻辑,核心集中在 src/http/v2/ 目录下,与连接状态、流管理深度耦合,而非独立模块。
HPACK 实现位于 ngx_http_v2.c 及配套头文件中
nginx 没有引入 nghttp2 或其他第三方 HPACK 实现,所有逻辑均内置于 HTTP/2 协议栈。关键文件包括:
-
src/http/v2/ngx_http_v2.h:定义 HPACK 动态表结构(ngx_http_v2_table_t)、索引类型、最大大小限制(默认 4096 字节)及静态表常量 -
src/http/v2/ngx_http_v2.c:包含ngx_http_v2_encode_header(编码)和ngx_http_v2_decode_header(解码)两个主函数,以及ngx_http_v2_huff_decode/ngx_http_v2_huff_encode(霍夫曼编解码) -
src/http/v2/ngx_http_v2_table.c:实现动态表的插入(ngx_http_v2_add_header)、查找(ngx_http_v2_find_header)、淘汰(LRU 风格,基于引用计数+时间戳)
动态表管理采用紧凑内存布局与引用计数
nginx 的动态表不是链表或哈希表,而是一维连续缓冲区(pool->dtable),每个条目以变长整数(prefix-encoded)存储 name/value 长度 + 原始字节。插入新首部时:
- 先计算新条目所需空间(含 name、value、长度编码开销),若超出当前
settings.header_table_size,则从表尾向前逐个淘汰,直到腾出足够空间 - 淘汰不立即释放内存,而是移动
pool->dtable_used指针,保留已分配缓冲区供复用,避免频繁 malloc/free - 查找 header 时遍历动态表(逆序,优先匹配最新插入项),并比对 name 和 value 的完整字节 —— 不做哈希,但通过短路比较(先比长度再比内容)优化性能
编码策略严格遵循 RFC,但简化了部分边界处理
nginx 在编码时始终优先尝试索引模式(indexed)、然后是带增量更新的索引模式(incremental indexing),最后才用字面量(literal with substitution)。关键细节包括:
- 静态表索引从 1 开始(RFC 规定),对应常见首部如
:method: GET(索引 2)、content-type(索引 31)等,硬编码在ngx_http_v2_static_table数组中 - 动态表索引从
0x80(128)起算,编码时用 prefix 编码(如 6-bit prefix 表示 0–63 范围内的索引) - 霍夫曼编码使用固定码表(
ngx_http_v2_huff_syms),仅支持 RFC 附录 B 定义的 257 符号;解码时用查表+位移方式快速还原,不建树 - 不支持“动态表大小更新指令”以外的任何 table size 更新机制 —— 即只响应对端发来的
SETTINGS_HEADER_TABLE_SIZE,自身不主动发送该 setting
调试与验证建议:结合日志与 wireshark
要确认实际 HPACK 行为,可开启 nginx 调试日志并抓包交叉分析:
- 编译时加
--with-debug,配置中启用error_log /path/debug.log debug;,HTTP/2 相关日志会输出 header 编码类型、索引值、动态表大小变化等 - 用 Wireshark 抓取 TLS 层解密后的 HTTP/2 流(需配置 SSLKEYLOGFILE),过滤
http2.header,观察Header Block Fragment的原始字节,对照 RFC 7541 解析 prefix、huffman、index 等字段 - 修改
src/http/v2/ngx_http_v2_table.c中的ngx_http_v2_add_header,临时添加ngx_log_debug打印每次插入的 name/value 及最终索引,可直观看到动态表演化过程

















