nghttp2_select_alpn 详解:基于 nghttp2 库的 HTTP/2 服务端 ALPN 协议协商实现
【免费下载链接】fluent-bitFast and Lightweight Logs, Metrics and Traces processor for Linux, BSD, OSX and Windows项目地址: https://gitcode.com/GitHub_Trending/fl/fluent-bit
导读
nghttp2_select_alpn是 nghttp2 HTTP/2 C 库为服务端提供的 ALPN(Application-Layer Protocol Negotiation,应用层协议协商)辅助函数,用于在 TLS 握手阶段从客户端提交的协议列表中自动挑选 HTTP/2(h2)或 HTTP/1.1(http/1.1),从而让一个服务端口同时优雅地支持新旧两种协议。本文以 lib/nghttp2-1.65.0/doc/nghttp2_select_alpn.rst 文档为骨架,结合仓库内的头文件声明、核心实现、单元测试与官方示例,完整讲解该函数的输入输出格式、三步选择算法、与 OpenSSL 回调的集成方式及常见边界情况,帮助读者在自己基于 nghttp2 的服务端程序中正确接入 HTTP/2。
函数概览与声明位置
nghttp2_select_alpn的完整声明位于 nghttp2 公共头文件 lib/nghttp2-1.65.0/lib/includes/nghttp2/nghttp2.h 中,其原型为:
NGHTTP2_EXTERN int nghttp2_select_alpn(const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen);原型文档(nghttp2.h 中对应注释)明确说明这是一个服务端 ALPN 辅助函数(helper function for dealing with ALPN in server side):它本身不参与网络 I/O,也不解析 TLS 报文,而是把"从客户端协议列表中选出一个协议"这一纯逻辑步骤封装成可复用函数,供 TLS 层的 ALPN 选择回调调用。
该函数的实现位于 lib/nghttp2-1.65.0/lib/nghttp2_alpn.c,与其并存的还有一个被标记为Deprecated(已弃用)的旧版函数nghttp2_select_next_protocol。头文件中的警告注释(nghttp2.h#L5704-L5706)明确建议新代码改用nghttp2_select_alpn。两者选择算法完全一致,主要区别在于旧版out参数类型为unsigned char **(不带const),新版更符合 OpenSSL 回调对out指向只读协议数据的使用方式。
输入格式:length-prefixed、非 NUL 结尾的协议列表
函数的第三个参数in携带客户端(peer)按其偏好顺序排列的协议列表,该列表采用length-prefixed(长度前缀)编码:每个协议项由一个字节的长度 + 相应字节的协议标识符组成,列表整体不以 NUL 结尾,总字节数由inlen精确给出。
原文档给出了h2与http/1.1并存时的内存布局示例:
in[0] = 2 in[1..2] = "h2" in[3] = 8 in[4..11] = "http/1.1" inlen = 12逐字节解读:
| 位置 | 内容 | 含义 |
|---|---|---|
in[0] | 2 | 第一个协议标识符的长度为 2 字节 |
in[1..2] | "h2" | 第一个协议为 HTTP/2(h2) |
in[3] | 8 | 第二个协议标识符的长度为 8 字节 |
in[4..11] | "http/1.1" | 第二个协议为 HTTP/1.1 |
inlen | 12 | 整个列表共 12 字节(1+2+1+8) |
这种编码格式正是 RFC 7301(ALPN) 规定的ProtocolNameList在 TLS 扩展中的传输格式,因此in/inlen可以直接复用 TLS 栈(如 OpenSSL)ALPN 回调传入的客户端列表数据,无需二次转换。
仓库单元测试 lib/nghttp2-1.65.0/tests/nghttp2_alpn_test.c 中用字节数组构造了更复杂的列表来验证解析,例如包含http/1.1、h2、spdy/3三个候选的列表(nghttp2_alpn_test.c#L43-L45):
const unsigned char p[] = {8, 'h', 't', 't', 'p', '/', '1', '.', '1', 2, 'h', '2', 6, 's', 'p', 'd', 'y', '/', '3'};可以看到,h2并未排在客户端列表的第一位,这正好用于检验"按内容匹配而非按位置匹配"的选择逻辑。
三步选择算法与返回值语义
nghttp2_select_alpn的协议选择遵循严格的优先级顺序(nghttp2_alpn.c#L59-L70):
第 1 步:优先选择 HTTP/2。若客户端列表中包含当前库支持的 HTTP/2 协议标识符,则选中h2,函数返回1,后续步骤不再执行。nghttp2 内部通过宏NGHTTP2_PROTO_ALPN "\x2h2"表示带 2 字节长度前缀的h2(定义见 nghttp2.h#L109-L116),对应的明文标识符为NGHTTP2_PROTO_VERSION_ID "h2"(nghttp2.h#L92-L98)。
第 2 步:退而求其次选择 HTTP/1.1。若列表中不含h2但包含http/1.1,则选中http/1.1,返回0。这是保证"新老协议并存、平滑回退"的关键:无法协商出 HTTP/2 的连接仍可退化为 HTTP/1.1 继续服务,而不是直接握手失败。
第 3 步:无交集(non-overlap)。若列表既不包含h2也不包含http/1.1,则不做任何选择,返回-1。此时*out与*outlen保持原样不被修改(left untouched),由调用方(TLS 回调)决定如何处置——通常返回SSL_TLSEXT_ERR_NOACK表示不确认 ALPN 扩展,让连接按无 ALPN 的普通方式继续。
选中协议时的输出:当选中h2时,"h2"的指针被写入*out,其长度2被写入*outlen(即NGHTTP2_PROTO_VERSION_ID_LEN)。
返回值速查表:
| 返回值 | 含义 | *out/*outlen |
|---|---|---|
1 | 选中 HTTP/2(h2) | 指向"h2",长度 2 |
0 | 选中 HTTP/1.1(http/1.1) | 指向"http/1.1",长度 8 |
-1 | 无交集,未选择 | 保持原值不变 |
底层实现:线性扫描匹配
从源码看,实际匹配工作由内部静态函数select_alpn完成(nghttp2_alpn.c#L29-L41):
static int select_alpn(const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, const char *key, unsigned int keylen) { unsigned int i; for (i = 0; i + keylen <= inlen; i += (unsigned int)(in[i] + 1)) { if (memcmp(&in[i], key, keylen) == 0) { *out = (unsigned char *)&in[i + 1]; *outlen = in[i]; return 0; } } return -1; }它利用 length-prefixed 结构按步长in[i] + 1(长度字节 + 标识符本身)遍历整个列表,用memcmp将每个条目与目标协议(\x2h2或\x8http/1.1)逐字节比较。匹配成功后,*out指向长度字节之后的协议标识符起始位置,*outlen取长度字节的值。整个实现不依赖任何动态分配,是典型的嵌入式友好型纯函数设计。
与 OpenSSL 回调的集成:完整可运行示例
原文档给出了一段标准集成代码:定义一个签名符合 OpenSSL 要求的 ALPN 选择回调,在回调中调用nghttp2_select_alpn,并通过SSL_CTX_set_alpn_select_cb注册到 SSL 上下文中。以下代码完整保留并稍作注释:
static int alpn_select_proto_cb(SSL* ssl, const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg) { int rv; rv = nghttp2_select_alpn(out, outlen, in, inlen); if (rv == -1) { return SSL_TLSEXT_ERR_NOACK; } if (rv == 1) { ((MyType*)arg)->http2_selected = 1; } return SSL_TLSEXT_ERR_OK; } ... SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, my_obj);集成要点说明:
- 回调签名必须严格匹配 OpenSSL 的
alpn_select_cb原型:int (*)(SSL *ssl, const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg)。nghttp2_select_alpn的前四个参数与回调的后四个参数一一对应,因此可以近乎直接透传。 - 返回值
-1(无交集)对应SSL_TLSEXT_ERR_NOACK:表示不向客户端返回 ALPN 协议选择,连接继续,由应用层决定后续行为。 arg参数可用于状态记录:如示例中当rv == 1(协商出 HTTP/2)时,通过((MyType*)arg)->http2_selected = 1记录协商结果,便于握手完成后应用层据此走 HTTP/2 处理路径。SSL_TLSEXT_ERR_OK表示接受本次选择,OpenSSL 会依据*out/*outlen把选中的协议写入 TLS 握手报文返回给客户端。
官方示例中的真实用法
仓库自带的官方示例 lib/nghttp2-1.65.0/examples/libevent-server.c 给出了与本仓库 doc 文档几乎一致的落地实现。其回调(libevent-server.c#L110-L124)简化了状态记录,仅当rv != 1时返回SSL_TLSEXT_ERR_NOACK:
static int alpn_select_proto_cb(SSL *ssl, const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg) { int rv; (void)ssl; (void)arg; rv = nghttp2_select_alpn(out, outlen, in, inlen); if (rv != 1) { return SSL_TLSEXT_ERR_NOACK; } return SSL_TLSEXT_ERR_OK; }随后在创建 SSL 上下文的create_ssl_ctx中注册回调(libevent-server.c#L163):
SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, NULL);注意示例在此处传入的arg为NULL,同时整体策略是"只接受 h2,否则不确认"——这是 HTTP/2 专用服务器的常见做法;而 doc 文档中的示例则允许退化到http/1.1(返回0时同样返回SSL_TLSEXT_ERR_OK),适合需要同时服务 HTTP/1.1 与 HTTP/2 的混合场景。两种策略各有适用场景,读者可依据自身需求选择。
测试验证:三种分支均有覆盖
nghttp2 的单元测试 lib/nghttp2-1.65.0/tests/nghttp2_alpn_test.c 完整覆盖了该函数的三个返回分支,可作为理解行为的权威参照:
1. 命中h2(返回 1):构造包含http/1.1、h2、spdy/3的列表,断言返回值1,且输出长度等于NGHTTP2_PROTO_VERSION_ID_LEN(2)、输出内容等于NGHTTP2_PROTO_VERSION_ID("h2")(nghttp2_alpn_test.c#L43-L59)。注意h2并不在列表首位,验证了匹配与顺序无关,仅关注"是否包含"。
2. 未命中h2、命中http/1.1(返回 0):构造仅含spdy/4、spdy/2.1、http/1.1的列表,断言返回值0,输出长度为 8、内容为"http/1.1"(nghttp2_alpn_test.c#L62-L81)。
3. 完全无交集(返回 -1):构造仅含spdy/4、spdy/2.1、http/1.0的列表(注意是http/1.0而非http/1.1),断言返回值-1,且out保持NULL、outlen保持0——印证了文档中"*out与*outlen不被修改"的约定(nghttp2_alpn_test.c#L83-L102)。
测试还同时验证了已弃用的nghttp2_select_next_protocol具有相同行为,说明两者仅是接口形式差异,选择逻辑完全等价。
关键边界与注意事项
结合文档、源码与测试,使用nghttp2_select_alpn时需注意以下要点:
in必须严格符合 length-prefixed 格式,且inlen必须精确等于列表总字节数。实现按in[i] + 1步进遍历,若格式损坏(如长度字节超过剩余空间),遍历会提前安全退出并最终返回-1,但调用方仍应确保数据来自可信的 TLS 栈解析结果。- 匹配是"包含性"而非"顺序性":只要列表中任意位置出现
h2即返回 1,与客户端偏好排序无关;只有完全不含h2时才考虑http/1.1。 h2c(明文 HTTP/2)不在本函数处理范围内:本函数仅处理 TLS 场景下的 ALPN;h2c需要通过 HTTP/1.1 Upgrade 头或其他机制协商。*out指向的是in缓冲区内部:函数不复制协议数据,输出指针是输入列表的子区间。因此调用方不得在握手完成前释放或修改in所指缓冲区。- 返回值语义与 OpenSSL 回调返回值不要混淆:
nghttp2_select_alpn返回的是"选择了什么"(1/0/-1),而回调返回的是SSL_TLSEXT_ERR_*常量(告诉 TLS 栈"如何处置本次选择"),二者需要显式映射。 - 旧函数已弃用:新代码应直接使用
nghttp2_select_alpn,避免使用nghttp2_select_next_protocol。
总结
nghttp2_select_alpn用约 40 行源码实现了一个标准、无副作用、可嵌入任意 TLS 后端的 ALPN 选择器:输入为 RFC 7301 格式的客户端协议列表,输出为"优先h2、其次http/1.1、否则不选"的三态结果。它在 nghttp2 生态中的角色是连接 TLS 层与 HTTP/2 层的桥梁——通过SSL_CTX_set_alpn_select_cb注册后,即可让单个服务端口在 TLS 握手中自动完成 HTTP/2 与 HTTP/1.1 的协商与回退。对于基于 nghttp2 构建服务端(包括本仓库中 Fluent Bit 的 HTTP/2 相关能力)的开发者而言,理解该函数的选择优先级、输出语义与回调集成方式,是正确启用 HTTP/2 的第一步。
【免费下载链接】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),仅供参考