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

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

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

本文的关注点在于

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

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

1、HTTP请求的生命流程

上下游的流转,本文集中于第②步

1
2
3
4
5
6
7
8
              
┌────────┐ ① ┌────────────┐ ② ┌────────┐
Client │ ⇄ protocol_xx ⇄ │ NGINX │ ⇄ protocol_xx ⇄ │ App
└────────┘ ④ └────────────┘ ③ └────────┘


协议转换(protocol_xx ⇄ protocol_xx)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
┌────────────────────────────┐
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、实际的例子

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                
┌────────┐ ┌────────────┐ ┌────────┐
Client │ │ NGINX │ │ App
└────────┘ └────────────┘ └────────┘
│ │ │
│ ① 请求 (HTTP/2)
├─────────────────────────▶ │

│ ② 转发请求 (HTTP/1)
│ │ ──────────────────────▶ │

│ ③ 接收响应 (HTTP/1)
│ │ ◀─────────────────────── │

│ ④ 返回响应 (HTTP/2)
◀───────────────────────── │

nginx的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
# 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协议版本的不同,进行不同处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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 到私有协议 的转换,便需引入一些定制逻辑。

事实上,实现此目标的主要流程可分为以下四步:

  1. 根据http请求的uri/参数/头部确认是哪个upstream
  2. 将http的uri、参数、头部、body转换为私有协议规范
  3. 初始化upstream,接入到私有协议模块处理,进行交互
  4. 判断客户端协议,将数据转换为HTTP协议格式,然后交由http框架处理与客户端的交互
1
HTTP 请求 -> [ 路由规则 -> upstream 确定 -> 请求数据转私有协议 -> upstream 通信 -> 响应转 HTTP ] -> 返回客户端

3.1、根据 HTTP 请求确认目标 upstream

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

基本思路:

  • 定义一个内部变量 $upstream_name,其值由请求参数经过一定规则匹配后动态生成;
  • proxy_pass 中使用该变量完成请求转发:
1
proxy_pass $upstream_name;

执行时机:

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

1
static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r);

该函数又调用了:

1
2
3
static 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。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
http接入 - uri:
a/xxx -> http_xxx_001
b/xxx -> protocol_1_xxx_001
b/xxx -> protocol_2_xxx_001

upstream 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_inr->request_body 构造成特定的 buffer;
  • 传入私有协议模块处理。

从实际来看,设计一个高可用、高可靠的协议难度很大,又要满足各种场景,又要可靠,还要性能更好一些(很遗憾,我们的协议并没有HTTP2的性能好,场景也缺失一些)。

协议归根结底是一种信息交换的规则,对话双方遵守同一种规则,就可以交换信息,类似于英语和汉语等等,也类似于编程语言?本质上就是人与机器做交互。从实际的实现看,设计的难度远远大于实现。

3.3、与upstream交互

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

    核心流程如下:

    1. 选择合适的 server 节点
      通常upstream代表一个集群,其中会有大于等于1个节点,因此还需要根据负载算法,找到具体的server节点,支持轮询、hash、最小连接数等多种负载均衡算法
    2. 发送封装后的请求数据
      按协议定义的格式准确填充 buffer。
    3. 异步接收 upstream 的响应数据
      与 HTTP upstream 模块类似,需注册读事件处理响应内容

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

3.4、返回响应到客户端

正常HTTP协议的upstream事件处理函数是读取响应头、响应body,然后再发送到客户端,依次注册回调函数。这里也类似,与upstream 通信完成后,Nginx 需要将私有协议格式的响应内容转换为 HTTP 响应返回给客户端。

常见处理逻辑:

  • 设置 HTTP 状态码、响应头部(如 Content-TypeContent-Length);
  • 将私有协议响应体解码、转码成 JSON、HTML、纯文本等 HTTP 能识别的格式;
  • 调用 ngx_http_send_headerngx_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
2
3
4
5
6
7
typedef 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 回调中,调用如下方法注册变量:

1
2
ngx_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

1
2
3
4
5
6
7
8
9
10
11
static ngx_int_t
ngx_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 结构体:

1
2
3
4
5
6
ngx_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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static ngx_int_t
ngx_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 使用:

1
2
3
location / {
proxy_pass $upstream_name;
}

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


nginx之上下游不同协议的转换
https://zjfans.github.io/2025/05/10/nginx之上下游不同协议的转换/
作者
张三疯
发布于
2025年5月10日
许可协议