nginx之上下游不同协议的转换
Nginx 主要用于实现路由和反向代理功能。在实际应用中,客户端与后端服务可能使用不同的协议,例如客户端使用 HTTP/2,而后端仍采用 HTTP/1.1。作为应用层(L7)代理,Nginx 会分别与客户端和后端建立独立的连接,解析、处理请求和响应,并根据目标协议格式重新构造数据后转发。
需要注意的是:无论前后端协议是否一致,Nginx 始终执行这一套“应用层重构”流程。当协议不一致时,Nginx 会根据各自的协议要求,分别构造并发送符合协议格式的请求和响应(例如,将 HTTP/2 帧格式转换为 HTTP/1.1 文本响应)。因此,协议格式的转换是 Nginx 转发过程中的附加处理,而不是特殊情况。
本文的关注点在于
Nginx 是如何根据upstream使用的协议,构造并发送不同格式的请求转发给后端
以及如何实现HTTP协议到私有协议的转换
1、HTTP请求的生命流程
上下游的流转,本文集中于第②步
1 |
|
对于一个http请求,work的处理流程如下
1 |
|
2、实际的例子
一个实际的例子,链路如下:
1 |
|
nginx的配置:
1 |
|
对于客户端,根据第1节的处理流程来看,nginx在ngx_http_wait_request_handler函数中,会根据HTTP协议版本的不同,进行不同处理
1 |
|
这其实是处理HTTP的入口,之后会根据协议的不同,设置一系列的回调。
对于upstream,在NGX_HTTP_CONTENT_PHASE确定相关协议,其实proxy_pass这个指令只是做HTTP1.0/1.1的转发,使用proxy_http_version可以指定具体版本
协议 | 指令 | 处理函数 |
---|---|---|
HTTP1.0/1.1 | proxy_pass | ngx_http_proxy_handler |
HTTP2 | grpc_pass | ngx_http_grpc_pass |
websocket | proxy_pass (proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection “Upgrade”;) |
ngx_http_upstream_upgrade |
所以客户端和upstream是隔离的,但是都可以在r中取到相关的数据,r包含了一个请求生命周期的所有数据,对于写代码很友好。当然上下游数据格式是不一样的,nginx会将读取到的二进制数据封装为对应协议的格式,然后进行转发。
但是实际websocket又会不一样,类似TCP层面的转发,按照websocket的协议规范,客户端发送的数据必须使用掩码(masking),所以如果代理节点进行解码貌似也是一个破坏性的行为。
3、实现HTTP<->私有协议的路由转换
在微服务架构中,HTTP 是广泛采用的通用协议,但为了满足特定业务需求或提升性能,会采用自定义的私有协议。对于我们的早期业务而言,只存在2种私有协议,随着微服务体系的接入,需要支持HTTP协议。
Nginx 原生支持同协议之间的代理(如 HTTP 到 HTTP、TCP 到 TCP),但如果需要实现 HTTP 到私有协议 的转换,便需引入一些定制逻辑。
事实上,实现此目标的主要流程可分为以下四步:
- 根据http请求的uri/参数/头部确认是哪个upstream
- 将http的uri、参数、头部、body转换为私有协议规范
- 初始化upstream,接入到私有协议模块处理,进行交互
- 判断客户端协议,将数据转换为HTTP协议格式,然后交由http框架处理与客户端的交互
1 |
|
3.1、根据 HTTP 请求确认目标 upstream
当一个 HTTP 请求进入 Nginx,我们需要依据请求内容(如 URI、参数、Header 等)确定应该路由到哪个 upstream。这一步实际上是一个“请求路由判定”过程。
基本思路:
- 定义一个内部变量
$upstream_name
,其值由请求参数经过一定规则匹配后动态生成; - 在
proxy_pass
中使用该变量完成请求转发:
1 |
|
执行时机:
在 Nginx 中,proxy_pass
的执行发生在 content phase 阶段,因此 $upstream_name
变量也会在此阶段被求值。Nginx 在处理 HTTP 请求时,会依次执行注册到 NGX_HTTP_CONTENT_PHASE
阶段的模块,ngx_http_proxy_handler
是其中之一:
1 |
|
该函数又调用了:
1 |
|
核心逻辑是:在这里解析 $upstream_name
的值,也就是实际为当前请求分配 upstream 地址的地方。
自定义逻辑:
可以在 $upstream_name
的计算逻辑中嵌入业务自定义的路由规则,根据 URI、参数等信息选择合适的 upstream。
举个例子:
1 |
|
实际上只要确认upstream,就可以确认是什么协议。
3.2、HTTP 到私有协议的转换
确定了 upstream + 协议后,Nginx 需要将标准的 HTTP 请求数据转换为私有协议格式,这一步称为协议转换。
内容包括:
- 将 URI、Query 参数、Headers、Body 数据按协议格式封装;
- 可能涉及字段编码、数据压缩等;
- 考虑不同类型请求(GET、POST 等)如何转换;
- 保证转换结果符合目标私有协议规范。
实现:
- 自定义一个模块挂在 content phase,作为 proxy 前置处理器;
- 使用模块内部逻辑,将
r->headers_in
和r->request_body
构造成特定的 buffer; - 传入私有协议模块处理。
从实际来看,设计一个高可用、高可靠的协议难度很大,又要满足各种场景,又要可靠,还要性能更好一些(很遗憾,我们的协议并没有HTTP2的性能好,场景也缺失一些)。
协议归根结底是一种信息交换的规则,对话双方遵守同一种规则,就可以交换信息,类似于英语和汉语等等,也类似于编程语言?本质上就是人与机器做交互。从实际的实现看,设计的难度远远大于实现。
3.3、与upstream交互
数据封装完毕,下一步是将构造好的私有协议请求通过 Nginx upstream 机制发送到后端服务,并接收响应。
核心流程如下:
- 选择合适的 server 节点
通常upstream代表一个集群,其中会有大于等于1个节点,因此还需要根据负载算法,找到具体的server节点,支持轮询、hash、最小连接数等多种负载均衡算法 - 发送封装后的请求数据
按协议定义的格式准确填充 buffer。 - 异步接收 upstream 的响应数据
与 HTTP upstream 模块类似,需注册读事件处理响应内容
事实上这一部分只借用已有模块即可
- 选择合适的 server 节点
3.4、返回响应到客户端
正常HTTP协议的upstream事件处理函数是读取响应头、响应body,然后再发送到客户端,依次注册回调函数。这里也类似,与upstream 通信完成后,Nginx 需要将私有协议格式的响应内容转换为 HTTP 响应返回给客户端。
常见处理逻辑:
- 设置 HTTP 状态码、响应头部(如
Content-Type
、Content-Length
); - 将私有协议响应体解码、转码成 JSON、HTML、纯文本等 HTTP 能识别的格式;
- 调用
ngx_http_send_header
、ngx_http_output_filter
等函数将内容写回客户端。
这里需要读取响应后,做对应协议的格式转换,即转换为HTTP协议的数据格式(本质就是提取包含信息的数据,例如body、header、args这些,私有协议也有对应的规则)。然后交由http的框架处理与客户端交互,实际上nginx采用了过滤器的机制,根据客户端的协议,触发对应的过滤器模块,然后返回给客户端。
到此,这就是一个完整的由http接入的请求,经过nginx转发到私有协议的处理过程。
4、nginx的内部变量原理
在 Nginx 中,内部变量(如 $host
、$uri
、$args
、自定义变量等)是模块之间进行数据传递和动态配置的重要机制。理解其原理,有助于灵活实现如动态 proxy_pass
、请求路由、协议转换等高级功能。
4.1、内部变量的作用
内部变量是 Nginx 配置文件中通过 $
前缀引用的动态值。这些变量:
- 可在配置中被引用(如
proxy_pass $upstream_name
); - 可在模块中被赋值或获取;
- 可被多个模块共享、传递;
- 支持动态求值(如基于请求上下文的信息实时生成值)。
4.2、变量的定义与注册
Nginx 的变量机制由 ngx_http_variables_module
提供支持。每一个变量都通过一个 ngx_http_variable_t
结构体注册,其结构定义如下:
1 |
|
name
: 变量名,例如upstream_name
;get_handler
: 获取变量值时调用的函数;set_handler
: 设置变量值时调用的函数;data
: 附加数据,通常为索引或上下文参数;flags
: 控制是否缓存、是否可设置等属性。
注册变量的方式:
在模块的 postconfiguration
回调中,调用如下方法注册变量:
1 |
|
4.3、变量值的获取时机
Nginx 在处理请求时,会根据执行阶段和上下文动态求值变量:
- 在某些阶段(如 rewrite、access、content),变量会被第一次引用时动态求值;
- 如果设置了缓存标志,变量的值会被计算一次后缓存;
- 某些变量可能依赖于请求内容(如 body),需要确保值可用时再引用。
比如在 proxy_pass $upstream_name;
中,变量 $upstream_name
会在 content phase 阶段由 ngx_http_proxy_eval
调用变量系统求值。
4.4、自定义变量
在 Nginx 中通过 C 模块定义和使用内部变量:
一、自定义变量的注册
在模块的 postconfiguration
函数中注册变量,使用 ngx_http_add_variable
:
1 |
|
NGX_HTTP_VAR_CHANGEABLE
:表示变量值可被动态设置;get_handler
:指定变量的获取逻辑函数;data
:传递给 handler 的附加信息。
二、自定义变量的取值函数(get_handler)
变量的值由 get_handler
实现,它会在变量首次被引用时调用,并返回一个 ngx_http_variable_value_t
结构体:
1 |
|
自定义的变量赋值函数实现
1 |
|
说明:
r->uri.data
是当前请求的 URI;- 根据 URI 判断要转发到哪个 upstream;
- 返回的
v->data
即$upstream_name
的值。
三、在配置中使用自定义变量
一旦注册成功,就可以在 nginx.conf
中通过 $upstream_name
使用:
1 |
|
Nginx 在执行 proxy_pass
时,会自动调用自定义的 get_handler
获取实际的 upstream 地址,从而完成 基于请求内容的动态转发。