nginx之上下游不同协议的转换

nginx之上下游不同协议的转换

Nginx 主要用于实现路由和反向代理功能。在实际应用中,客户端与后端服务可能使用不同的协议,例如客户端使用 HTTP/2,而后端仍采用 HTTP/1.1。作为应用层(L7)代理,Nginx 会分别与客户端和后端建立独立的连接,解析、处理请求和响应,并根据目标协议格式重新构造数据后转发。

需要注意的是:无论前后端协议是否一致,Nginx 始终执行这一套“应用层重构”流程。当协议不一致时,Nginx 会根据各自的协议要求,分别构造并发送符合协议格式的请求和响应(例如,将 HTTP/2 帧格式转换为 HTTP/1.1 文本响应)。因此,协议格式的转换是 Nginx 转发过程中的附加处理,而不是特殊情况。

本文的关注点在于

Nginx 是如何根据upstream使用的协议,构造并发送不同格式的请求转发给后端

以及如何实现HTTP协议到私有协议的转换

1、HTTP请求的生命流程上下游的流转,本文集中于第②步

12345678 ┌────────┐ ① ┌────────────┐ ② ┌────────┐│ Client │ ⇄ protocol_xx ⇄ │ NGINX │ ⇄ protocol_xx ⇄ │ App │└────────┘ ④ └────────────┘ ③ └────────┘ ▲ │ 协议转换(protocol_xx ⇄ protocol_xx)

对于一个http请求,worker的处理流程如下

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162┌────────────────────────────┐│ Worker Process Loop ││ ngx_worker_process_cycle │└────────────┬───────────────┘ ↓ ngx_process_events_and_timers() ↓ ngx_event_process_events() ↓ 🔔 epoll_wait() ↓ 🔌 socket 可读 (新连接) ↓ ngx_event_accept() ↓ //此处tcp连接建立成功 ✅ ngx_http_init_connection() ↓ 设置连接回调为 ngx_http_wait_request_handler ↓ 🔁 等待并读取请求行(GET /...) ↓ ngx_http_process_request_line() ↓ 读取并解析请求头 ↓ ngx_http_process_request_headers() ↓ 创建 ngx_http_request_t 完整结构 ↓ //初步解析HTTP行与头部,创建核心结构体r 进入阶段引擎 ngx_http_core_run_phases() ↓ 🔍 NGX_HTTP_FIND_CONFIG_PHASE → 匹配 location,找到 proxy_pass → 设置 r->content_handler = ngx_http_proxy_handler ↓ ▶ NGX_HTTP_REWRITE_PHASE 等(可选) → 执行重写指令,如 rewrite/set/return 等 ↓ //rewrite阶段,处理请求的相关逻辑 ▶ NGX_HTTP_CONTENT_PHASE → 调用 ngx_http_proxy_handler(r) ↓ //确认 upstream,初始化结构体,并建立连接 ngx_http_proxy_handler() ↓ ngx_http_upstream_init(r) → ngx_http_upstream_connect(r, u) → ngx_event_connect_peer() //发起与后端的连接 → 设置 u->write_event_handler = ngx_http_upstream_send_request_handler ↓ //构造并发送请求到后端(构造 Host、URI) → ngx_http_upstream_send_request() ↓ //异步读取后端响应(头 + body) → ngx_http_upstream_process_header() ↓ → ngx_http_upstream_send_response() //开始转发响应体 ↓ //通过 output filter 输出给客户端 → ngx_http_output_filter(r, out) ↓ ngx_http_finalize_request() ↓ //可保持 keepalive 或关闭连接

2、实际的例子一个实际的例子,链路如下:

1234567891011121314151617 ┌────────┐ ┌────────────┐ ┌────────┐│ Client │ │ NGINX │ │ App │└────────┘ └────────────┘ └────────┘ │ │ │ │ ① 请求 (HTTP/2) ├─────────────────────────▶ │ │ │ ② 转发请求 (HTTP/1) │ │ ──────────────────────▶ │ │ │ ③ 接收响应 (HTTP/1) │ │ ◀─────────────────────── │ │ │ ④ 返回响应 (HTTP/2) ◀───────────────────────── │

nginx的配置:

1234567891011121314151617181920server { # HTTP2 listen 443 ssl http2; server_name ZJfans.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://backend_app; # HTTP1.1 proxy_http_version 1.1; proxy_set_header Connection ""; }}

对于客户端,根据第1节的处理流程来看,nginx在ngx_http_wait_request_handler函数中,会根据HTTP协议版本的不同,进行不同处理

12345678910111213141516171819202122#if (NGX_HTTP_V2) h2scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v2_module); if (!hc->ssl && (h2scf->enable || hc->addr_conf->http2)) { //h2scf->enable端口开启HTTP2的标识 size = ngx_min(sizeof(NGX_HTTP_V2_PREFACE) - 1, (size_t) (b->last - b->pos)); if (ngx_memcmp(b->pos, NGX_HTTP_V2_PREFACE, size) == 0) { if (size == sizeof(NGX_HTTP_V2_PREFACE) - 1) { ngx_http_v2_init(rev); return; } ngx_post_event(rev, &ngx_posted_events); return; } }#endif

这其实是处理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框架处理与客户端的交互

1HTTP 请求 -> [ 路由规则 -> upstream 确定 -> 请求数据转私有协议 -> upstream 通信 -> 响应转 HTTP ] -> 返回客户端

3.1、根据 HTTP 请求确认目标 upstream当一个 HTTP 请求进入 Nginx,我们需要依据请求内容(如 URI、参数、Header 等)确定应该路由到哪个 upstream。这一步实际上是一个“请求路由判定”过程。

基本思路:

定义一个内部变量 $upstream_name,其值由请求参数经过一定规则匹配后动态生成;

在 proxy_pass 中使用该变量完成请求转发:

1proxy_pass $upstream_name;

执行时机:

在 Nginx 中,proxy_pass 的执行发生在 content phase 阶段,因此 $upstream_name 变量也会在此阶段被求值。Nginx 在处理 HTTP 请求时,会依次执行注册到 NGX_HTTP_CONTENT_PHASE 阶段的模块,ngx_http_proxy_handler 是其中之一:

1static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r);

该函数又调用了:

123static ngx_int_t ngx_http_proxy_eval(ngx_http_request_t *r, ngx_http_proxy_ctx_t *ctx, ngx_http_proxy_loc_conf_t *plcf);

核心逻辑是:在这里解析 $upstream_name 的值,也就是实际为当前请求分配 upstream 地址的地方。

自定义逻辑:

可以在 $upstream_name 的计算逻辑中嵌入业务自定义的路由规则,根据 URI、参数等信息选择合适的 upstream。

举个例子:

123456789101112131415161718192021http接入 - uri:a/xxx -> http_xxx_001b/xxx -> protocol_1_xxx_001b/xxx -> protocol_2_xxx_001upstream http_xxx_001 { server 10.0.0.1:7000; server 10.0.0.2:7000;}upstream protocol_1_xxx_001 { protocol_1; server 10.0.1.1:8000; server 10.0.1.2:8000;}upstream protocol_2_xxx_001 { protocol_2; server 10.0.1.1:9000; server 10.0.1.2:9000;}

实际上只要确认upstream,就可以确认是什么协议。

3.2、HTTP 到私有协议的转换确定了 upstream + 协议后,Nginx 需要将标准的 HTTP 请求数据转换为私有协议格式,这一步称为协议转换。

内容包括:

将 URI、Query 参数、Headers、Body 数据按协议格式封装;

可能涉及字段编码、数据压缩等;

考虑不同类型请求(GET、POST 等)如何转换;

保证转换结果符合目标私有协议规范。

实现:

自定义一个模块挂在 content phase,作为 proxy 前置处理器;

使用模块内部逻辑,将 r->headers_in 和 r->request_body 构造成特定的 buffer;

传入私有协议模块处理。

ngx_http_upstream_init_request是请求初始化的入口,通常可以做相关的处理。

3.3、与upstream交互

数据封装完毕,下一步是将构造好的私有协议请求通过 Nginx upstream 机制发送到后端服务,并接收响应。

核心流程如下:

选择合适的 server 节点通常upstream代表一个集群,其中会有大于等于1个节点,因此还需要根据负载算法,找到具体的server节点,支持轮询、hash、最小连接数等多种负载均衡算法

发送封装后的请求数据 按协议定义的格式准确填充 buffer。

异步接收 upstream 的响应数据 与 HTTP upstream 模块类似,需注册读事件处理响应内容

事实上这一部分只借用已有模块即可

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 结构体注册,其结构定义如下:

1234567typedef struct { ngx_str_t name; ngx_http_set_variable_pt set_handler; ngx_http_get_variable_pt get_handler; uintptr_t data; ngx_uint_t flags;} ngx_http_variable_t;

name: 变量名,例如 upstream_name;

get_handler: 获取变量值时调用的函数;

set_handler: 设置变量值时调用的函数;

data: 附加数据,通常为索引或上下文参数;

flags: 控制是否缓存、是否可设置等属性。

注册变量的方式:

在模块的 postconfiguration 回调中,调用如下方法注册变量:

12ngx_http_variable_t *var = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE);var->get_handler = my_get_variable;

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:

1234567891011static ngx_int_tngx_http_xxx_module_postconfiguration(ngx_conf_t *cf) { ngx_str_t name = ngx_string("upstream_name"); ngx_http_variable_t *var = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE); if (var == NULL) { return NGX_ERROR; } var->get_handler = ngx_http_my_variable_get; var->data = 0; return NGX_OK;}

NGX_HTTP_VAR_CHANGEABLE:表示变量值可被动态设置;

get_handler:指定变量的获取逻辑函数;

data:传递给 handler 的附加信息。

二、自定义变量的取值函数(get_handler)

变量的值由 get_handler 实现,它会在变量首次被引用时调用,并返回一个 ngx_http_variable_value_t 结构体:

123456ngx_http_proxy_handler ->ngx_http_proxy_eval ->ngx_http_script_run -> ngx_http_script_copy_var_len_code (code((ngx_http_script_engine_t *) &e);) -> ngx_http_get_indexed_variable ->ngx_http_my_variable_get

自定义的变量赋值函数实现

12345678910111213141516171819static ngx_int_tngx_http_my_variable_get(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data){ ngx_str_t upstream = ngx_string("http://backend_default"); if (ngx_strncmp(r->uri.data, "/api", 4) == 0) { upstream = ngx_string("http://backend_api"); } v->len = upstream.len; v->valid = 1; v->no_cacheable = 0; v->not_found = 0; v->data = upstream.data; return NGX_OK;}

说明:

r->uri.data 是当前请求的 URI;

根据 URI 判断要转发到哪个 upstream;

返回的 v->data 即 $upstream_name 的值。

三、在配置中使用自定义变量

一旦注册成功,就可以在 nginx.conf 中通过 $upstream_name 使用:

123location / { proxy_pass $upstream_name;}

Nginx 在执行 proxy_pass 时,会自动调用自定义的 get_handler 获取实际的 upstream 地址,从而完成 基于请求内容的动态转发。

相关推荐

暖的成语
下载旧版本彩票365软件

暖的成语

01-04 👁️ 5049
「轻音乐·安静」舒缓放松,缓解压力
下载旧版本彩票365软件

「轻音乐·安静」舒缓放松,缓解压力

08-18 👁️ 4681
微信明确:有此类交易,封号!
BET体育365投注官网

微信明确:有此类交易,封号!

09-18 👁️ 8933