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 地址,从而完成 基于请求内容的动态转发。