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

请求行、请求头、请求体不用多说,之前看多了 bp 和控制台都能看。
反向代理的内存与文件缓冲区流程

如上面的流程图:浏览器会先处理请求头,看协议、用不用 keepalive 之类的,再处理请求体。由于可能会遇到大文件,在读取之前有一个缓冲区,缓冲读到的客户端提交的文件(缓冲不是缓存),其中 client_body_buffer_size 参数决定了缓冲区的大小。
在此过程,如下面两个图,参数 proxy_request_buffering on/off 决定了是否向上游服务器直接发起请求,如果是 on 就是完全读取完文件之后再反向代理,off 就是边读 body 边请求上游服务器(并行)。
在选择上游服务器的过程,根据 upstream 配置选择(权重、轮询等),这个过程是异步的,默认是轮询去选择的。


详细说明(图3):选出来之后 nginx 是群异步的形式处理请求。先向操作系统的 epoll 事件队列加入连接请求事件注册,同时在准备读取报文时注册回调函数(在 epoll 中所有的数据读取,网络连接建立/断开都是以事件形式触发,触发之后就能执行回调函数,这个过程就是先注册回调函数,以便建立连接之后可以处理一些事情,比如三次握手之后处理事情)。
(图3)连接建立完成后进入可写状态,可写状态还会触发一个事件,触发事件时没有阻塞、同步概念,只有在触发事件后,系统给 nginx 发信号,nginx 调取回调函数,再去执行。
执行过程中向上游服务器发送客户端带来的请求,此过程不仅用到了之前配置的 upstream 的 keepalive 参数(图4),还会配置一些其他的(图5)。


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

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

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 交换分区的作用。

如果配置了 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 和表单的区别
| GET | POST | |
|---|---|---|
| 数据位置 | URL 后面 ?key=value | 请求体里 |
| 明文可见 | 地址栏直接看到 | 地址栏看不到 |
| 长度限制 | 浏览器限制约 2KB | 无限制 |
| 缓存 | 会被浏览器缓存 | 不会被缓存 |
| 刷新 | 重复提交无害 | 浏览器会提示"是否重新提交" |
| 用途 | 查数据、翻页、搜索 | 登录、注册、上传文件 |
反向代理无法获取客户端 IP 地址


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

虚拟机 tomcat 部署后访问:


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

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

此时访问 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(包括静态资源、代理路径等)都会带上X-Forwarded-For头。 - 配在
location块:只有这个路径的请求会带上X-Forwarded-For头,其他路径不会。
| 做法 | 配置位置 | 适用场景 |
|---|---|---|
| 全局统一 | 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_pass | X-Forwarded-For 是否被添加 |
|---|---|---|
http://168kaguyachi.site/(所有路径) | ✅ 是(因为 location / 匹配所有请求) | ✅ 是 |
http://168kaguyachi.site/xxx | ✅ 是(同上) | ✅ 是 |
未来添加的 /static/ 路径(如果单独配了 location 且不 proxy_pass) | ❌ 否 | ⚠️ 仍然会被添加(但因为没有转发,这个头实际上不会被发送到任何地方 |
如果客户端修改了数据包的请求头 IP 如何?
客户端修改请求头里的 IP(比如伪造 X-Forwarded-For),在直连场景下确实可以伪造,但在经过反向代理的场景下,Nginx 会用 $remote_addr 覆盖掉客户端的伪造值
场景一:用户直连 Nginx(没有代理)
- 用户自己在请求头里塞了一个假的
X-Forwarded-For: 1.2.3.4。 - 如果 Nginx 配置了
proxy_set_header X-Forwarded-For $remote_addr;,Nginx 会直接用$remote_addr(TCP 连接的真实 IP)覆盖掉用户伪造的值。 - 所以伪造无效。
但如果 Nginx 配置的是:
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
那就会直接使用用户伪造的值,这就有风险了。所以正确的配置是 $remote_addr 或 $proxy_add_x_forwarded_for,而不是 $http_x_forwarded_for。
场景二:用户 → 代理 → Nginx
- 用户在请求头里伪造
X-Forwarded-For: 1.2.3.4。 - 请求先到达代理(比如 CDN),代理会把用户的 IP 追加到
X-Forwarded-For链中,变成1.2.3.4, 用户真实IP。 - 代理把请求转发给 Nginx。
- Nginx 在转发给后端时,用
$remote_addr(代理的 IP)覆盖掉伪造的,最终后端拿到的是真实用户IP, 代理IP。
所以:只要 Nginx 配置了正确的 proxy_set_header,客户端伪造的 IP 就会被 Nginx 的 $remote_addr 覆盖掉,无法生效
如果是一些大的系统,nginx 后面还套入了代理 nginx(LVS--Nginx),此时想要获取真实 IP 会出问题的。

如图第一台 nginx 的 remote_addr 是客户机的真 IP,如果第二个 nginx 使用相同的方法获取 IP,获取的就是前面 nginx 的 IP 101,覆盖了真实的 IP。
- 法1:解决问题的通常做法是不配置 remote_addr,直接转发请求到上游服务器
- 法2:在一二级代理之间再加一个针对一级 nginx 的
X-Forwarded-For,这样最终就有两个X-Forwarded-For的 IP,最终转发到上游
还有很多方法,但是这两个也够了。
一些有用的 header 介绍
浏览器控制台的 header 是最原始的 header。
nginx 默认会覆盖获取的 upstream 的值。

最上面的 pragma 和 cache 等字段都是标识浏览器缓存的。
upgrade-insecure-requests 标识可以 http 可以升级为 https。
user-agent 标明了用户的一些信息,比如平台(win、mac)、浏览器类型等,之后根据不同的客户端展示不同的页面即可,这里面还可以自定义想要加入的信息,也可以伪造。
accept 表明了浏览器接收的数据类型。
encoding 是标识请求的数据可以被浏览器压缩/解压(提升传输效率),后面具体讲。
language 表明了文本类型。