首页  /  Nginx 进阶  /  正文

Nginx 反向代理的报文处理与真实 IP

Nginx 进阶 2026-10-02📖 14 分钟👁 —
🚀 Nginx 水平扩展与优化第 5 / 8 页12345678📖 完整导航 →

HTTP 报文结构

浏览器作为客户端向 nginx 发送 HTTP 请求:

HTTP 请求报文的结构,请求行、请求头、请求体

请求行、请求头、请求体不用多说,之前看多了 bp 和控制台都能看。

反向代理的内存与文件缓冲区流程

一次请求在 Server 里经过 keepalive 和缓冲区参数处理的流程

如上面的流程图:浏览器会先处理请求头,看协议、用不用 keepalive 之类的,再处理请求体。由于可能会遇到大文件,在读取之前有一个缓冲区,缓冲读到的客户端提交的文件(缓冲不是缓存),其中 client_body_buffer_size 参数决定了缓冲区的大小。

在此过程,如下面两个图,参数 proxy_request_buffering on/off 决定了是否向上游服务器直接发起请求,如果是 on 就是完全读取完文件之后再反向代理,off 就是边读 body 边请求上游服务器(并行)。

在选择上游服务器的过程,根据 upstream 配置选择(权重、轮询等),这个过程是异步的,默认是轮询去选择的。

Upstream 初始化过程和上游服务器连接过程总览
location 里选完服务器之后连接上游的过程

详细说明(图3):选出来之后 nginx 是群异步的形式处理请求。先向操作系统的 epoll 事件队列加入连接请求事件注册,同时在准备读取报文时注册回调函数(在 epoll 中所有的数据读取,网络连接建立/断开都是以事件形式触发,触发之后就能执行回调函数,这个过程就是先注册回调函数,以便建立连接之后可以处理一些事情,比如三次握手之后处理事情)。

(图3)连接建立完成后进入可写状态,可写状态还会触发一个事件,触发事件时没有阻塞、同步概念,只有在触发事件后,系统给 nginx 发信号,nginx 调取回调函数,再去执行。

执行过程中向上游服务器发送客户端带来的请求,此过程不仅用到了之前配置的 upstream 的 keepalive 参数(图4),还会配置一些其他的(图5)。

upstream 块里的 keepalive_request、keepalive_timeout、keepalive_time 参数
proxy_pass、proxy_set_header 和 proxy_connect/send/read_timeout 这些参数

其中 proxy_set_header 配置了 nginx 转发过程给 http 报文新加的请求头(header),也就是给上游服务器看的 header,然后打包 http 请求为二进制发给上游服务器(至此,可写事件完成)。

上游服务器返回 body 时 header 和 body 各自用哪块缓冲区

上游服务器处理后返回结果给 nginx,nginx 依旧通过 epoll 事件触发传递信号,处理上游返回的数据,此过程 nginx 的进程一直处于伺机待发状态(nginx 是多进程(worker)同时运行处理业务逻辑的)。

body 缓冲区放大看一遍各参数的位置

nginx 在处理返回数据时有下面的过程,其中 proxy_buffering on; 指的是上游服务器返回的数据要不要缓冲。面临一个问题:上下游服务器速率不一致,上游服务器在局域网的速度很容易跑满带宽(千/万兆网卡),而客户端用户跑不满,速率慢是常态。所以呢,如果上游服务器返回了大的文件,假设 20G 的电影,nginx 是一次性把它加载到内存然后慢慢发送到客户端吗?肯定不是。

首先 nginx 这种有 proxy_buffering off,完全不缓冲,读多少传多少,边读边发,此时上下游速度基本一致。坏处就是一直占用 nginx 的读线程,网络请求无法中断,假设 20G 需要发送好几个小时,那么消耗是很大的。常规做法是打开缓冲,复用网络连接。

此时 proxy_buffers 32 64k; 配置中,32 是个个数,64k 是大小,在内存中分配 32 个 64k 大小的缓冲块去缓冲数据,单位是字节。

20g 是夸张情况,通常情况上下游速率不一致影响不会太大,2~3MB 是常见的。

总结一下为什么配置两个参数:客户端(下游)速率慢导致 nginx 遇到了两个选择,是否缓冲数据和怎么分配缓冲内存的问题。

缓冲写满之后用的临时文件相关参数

如果内存装不下,写满了,就需要用到额外的两个配置(下图):

其中 proxy_max_temp_file_size 限制了向磁盘写入的最大值,默认 1G。

proxy_temp_path /spool/nginx/proxy_temp 1 2; 缓冲文件写在哪,后面是文件路径,1 2 代表层级,建立层级更深的文件目录,不限制层级创建很多文件查找很麻烦。

proxy_temp_file_write_size 8k 限制了每次写入临时文件的缓冲大小。

还要补充的一点,只要配置了 proxy_buffering on,那么默认缓冲不够就写入 /temp。

这里也印证了 linux 的 temp 交换分区的作用。

HTTP、Server、Location 三个块里针对客户端的缓冲区参数

如果配置了 proxy_buffering off 那么 nginx 不会缓冲上游服务器返回的 body 部分,但是 header 一直缓冲,此配置是 proxy_buffer_size 从配置上看不出来是针对 header 的,同样的 proxy_buffering on/off 也看不出是针对 header 的。而且关闭了 body 的缓冲区,header 会被用来缓冲部分数据,如果这个不够(一般都不够),不会写入磁盘。

上述都是理论部分,下面展开具体的配置。

Nginx 对客户端的缓冲和限制

由于 nginx 有多个虚拟主机,可以配置到 http、server(全局)、location(最小)三个块,http 可以被 server 继承,可以互相覆盖,具体下面来说。之前了解的都在 upstream 中、location 中。对客户端的限制可以整体去配置,可在 http 块下包含所有的 server,在 server 中也可以具体调整。

我的配置中没有对 http 独立配置,因为 http 在 /etc/nginx/nginx.conf 是 nginx 主配置文件自带的顶层容器。

root@VM-0-12-ubuntu:/etc/nginx# cat nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
}

http {

	sendfile on;
	tcp_nopush on;
	types_hash_max_size 2048;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;

	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	gzip on;
  
	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;
}

客户端的配置参数

缓冲和缓存的区别就是不能复用,临时和持续的区别。

client_body_buffer_size 客户端 body 缓冲区大小。

client_header_buffer_size 客户端请求头缓冲区大小,若请求头比缓冲区大,会用到临时文件,默认 32 位 8k,64 位平台 16k(操作系统的 cpu 架构不同)。

关于 32 位和 64 位的说明:根据业务请求做调整即可,如果是接口一类的,都是 GET 请求,如果是 POST 表单可能需要修改,问卷调查的那种 16k 是完全足够的(16k 字节),很多情况默认的都是字节而不是比特。

large_client_header_buffers 如果请求中 header 的 url 非常长,超出了缓存限制,就会写入这个参数设定的缓冲区,默认 8k,最好是别超出,自己去调整一下 header。

client_max_body_size 1000M; 默认 1M,限制客户端最大 body 数据(用户上传前/后都可以检查,最好是上传前),用户上传的 HTTP 请求包有 content-length 请求头标记大小。不能太小了,随便一张照片都好几 MB,如果写为 0 就不限制大小了。

client_body_temp_path 客户端 body 缓冲区在磁盘的位置。

client_body_timeout 指的是上传过程客户端突然不传了,然后开始计时,超时了断开连接,不能一直占用资源。

client_header_timeout 同上。

client_body_in_file_only on; 把 body 完整写入磁盘文件,请求结束不会删除,请求没有 body 也会写入一个 0 字节大小的文件出来。

这个也算是禁忌了,容易撑死服务器。

client_body_in_single_buffer 在 nginx 的 body 内部想读取变量 $request_body(Nginx 的一个系统变量),可以通过变量读到用户 body 的具体内容,如果内容分配到 buffer 中分为 16k 的几个块,好几个地方,内存有,磁盘也有,而且是好多区域分开的块,读取会慢,如果内存可以使用单一连续的缓冲区在读取 body 数据的时候很快。

这个主要是二次开发用,不做二次开发不配的。

GET 和表单的区别

GETPOST
数据位置URL 后面 ?key=value请求体里
明文可见地址栏直接看到地址栏看不到
长度限制浏览器限制约 2KB无限制
缓存会被浏览器缓存不会被缓存
刷新重复提交无害浏览器会提示"是否重新提交"
用途查数据、翻页、搜索登录、注册、上传文件

反向代理无法获取客户端 IP 地址

Java 里拿客户端地址的代码,引入 Controller 相关的包
MainController 里遍历请求头、打印 remoteAddr 那段代码

这段代码的目的是拿到客户端的 IP 地址,下面的 X-forwarded-for 也可以拿到 IP。

本地跑的效果如下:

本地访问 127.0.0.1:8080 时打印出来的请求头和 remoteAddr

虚拟机 tomcat 部署后访问:

改访问 192.168.44.104:8080 之后打印的结果
ipconfig 里 WLAN、VMnet1、VMnet8 三块网卡的地址

正常来浏览器显示的是 WLAN 的 IP 地址,但是却显示了虚拟机的,说明经历了代理中转。

宿主机 180 经虚拟网卡 44.1 直接到 tomcat 的链路

直接通过本机的 44.1 连接所以宿主机 IP 和虚拟网卡可以看作是一个机器,所以也可以认为不存在代理:也就是直接访问虚拟机不需要物理网卡转发,因此跳过了它。

如果不直接通过网卡访问 tomcat,加入了反向代理:

中间多了一跳反向代理 44.101 的链路

此时访问 tomcat,浏览器读到的就是反向代理服务器的 IP。

获取真 IP

原理说明

上面提到了,很多情况都获取不到真实的 IP,所以需要方法获取到真 IP。

通过 header 可以获取真 IP,X-Forwarded-For 是扩展 header 不在 http/1.1 的协议中,nginx 通过配置 proxy_set_header [Header字段名称] [值] 可以获取到这个 header。

因为用户上网必然经过一个公网出口(网关/路由器),只要在这个出口之后的每一层代理都配置传递 X-Forwarded-For,这个 IP 就能被带到最终服务器。

具体配置

配置位置作用范围优先级
http所有 server 块(全局默认值)最低
server当前虚拟主机覆盖 http 的同名字段
location当前匹配路径最高(覆盖 server 和 http)

假设我们想要给默认站点添加配置:

做法配置位置适用场景
全局统一server 块整个站点所有请求都经过反向代理(没有静态资源路径),最省事
按路径区分location 块站点有静态资源路径(不转发后端),只对 /api/ 等需要代理的路径配置

proxy_set_header X-Forwarded-For $remote_addr; 插入 server 块。

upstream httpget {
    server 172.16.0.9;
    server 10.1.4.16;
    keepalive 16;
}

server {
    listen 80;
    server_name 168kaguyachi.site;

    root /var/www/html;
    index index.html;

    #header头相关
    proxy_set_header X-Forwarded-For $remote_addr;
    # 这个变量取决于获取IP的功能中用的变量名
    # 我这里没有写功能,无法获取IP

    #向户端
    keepalive_requests 500;
    send_timeout 70s;
    keepalive_timeout 65 65;


    location / {
    proxy_pass http://httpget;
    add_header Content-Type "text/html; charset=utf-8";

    #向上游
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    }

}
请求路径是否经过 proxy_passX-Forwarded-For 是否被添加
http://168kaguyachi.site/(所有路径)✅ 是(因为 location / 匹配所有请求)✅ 是
http://168kaguyachi.site/xxx✅ 是(同上)✅ 是
未来添加的 /static/ 路径(如果单独配了 location 且不 proxy_pass)❌ 否⚠️ 仍然会被添加(但因为没有转发,这个头实际上不会被发送到任何地方

如果客户端修改了数据包的请求头 IP 如何?

客户端修改请求头里的 IP(比如伪造 X-Forwarded-For),在直连场景下确实可以伪造,但在经过反向代理的场景下,Nginx 会用 $remote_addr 覆盖掉客户端的伪造值

场景一:用户直连 Nginx(没有代理)

但如果 Nginx 配置的是:

proxy_set_header X-Forwarded-For $http_x_forwarded_for;

那就会直接使用用户伪造的值,这就有风险了。所以正确的配置是 $remote_addr 或 $proxy_add_x_forwarded_for,而不是 $http_x_forwarded_for。

场景二:用户 → 代理 → Nginx

所以:只要 Nginx 配置了正确的 proxy_set_header,客户端伪造的 IP 就会被 Nginx 的 $remote_addr 覆盖掉,无法生效

如果是一些大的系统,nginx 后面还套入了代理 nginx(LVS--Nginx),此时想要获取真实 IP 会出问题的。

两级反向代理都要取 IP 时,X-Forwarded-For 和 remote_addr 分别在哪一段生效

如图第一台 nginx 的 remote_addr 是客户机的真 IP,如果第二个 nginx 使用相同的方法获取 IP,获取的就是前面 nginx 的 IP 101,覆盖了真实的 IP。

还有很多方法,但是这两个也够了。

一些有用的 header 介绍

浏览器控制台的 header 是最原始的 header。

nginx 默认会覆盖获取的 upstream 的值。

控制台里看到的原始请求头,remoteAddr 是 192.168.44.101

最上面的 pragma 和 cache 等字段都是标识浏览器缓存的。

upgrade-insecure-requests 标识可以 http 可以升级为 https。

user-agent 标明了用户的一些信息,比如平台(win、mac)、浏览器类型等,之后根据不同的客户端展示不同的页面即可,这里面还可以自定义想要加入的信息,也可以伪造。

accept 表明了浏览器接收的数据类型。

encoding 是标识请求的数据可以被浏览器压缩/解压(提升传输效率),后面具体讲。

language 表明了文本类型。