Nginx-2 詳解處理 Http 請求
Nginx 作為當今最流行的開源 Web 服務器之一,以其高性能、高穩定性和豐富的功能而聞名。在處理 HTTP請求 的過程中,Nginx 采用了模塊化
的設計,將整個請求處理流程劃分為若干個階段,每個階段都可以由特定的模塊來處理。這種設計不僅使得 Nginx 具有極高的靈活性和可擴展性,而且也方便了開發者對 Nginx 進行定制和優化。
Nginx 在與客戶端建立連接(三次握手)后通過事件驅動模塊獲取 Http 請求,解析 header 頭部并進行處理,接下來的處理可以大致分為以下 11 階段,每個階段都會有一些模塊和配置項進行處理,需要關注的是每個模塊可以發揮什么作用,有哪些配置項。
而每一個階段中的也可能有多個模塊可以對其發揮作用,這些模塊之間也是有順序的
具體的處理順序可以去 nginx-1.28.0/objs/ngx_modules.c
中查看,該文件中 ngx_module_names 字段標識了模塊的處理順序,正常為由下向上執行,某些特殊指令可以跳轉到其他模塊
char *ngx_module_names[] = {"ngx_core_module","ngx_errlog_module","ngx_conf_module","ngx_regex_module","ngx_events_module","ngx_event_core_module","ngx_epoll_module","ngx_http_module","ngx_http_core_module","ngx_http_log_module","ngx_http_upstream_module","ngx_http_static_module","ngx_http_autoindex_module","ngx_http_index_module","ngx_http_mirror_module","ngx_http_try_files_module","ngx_http_auth_basic_module","ngx_http_access_module","ngx_http_limit_conn_module","ngx_http_limit_req_module","ngx_http_realip_module","ngx_http_geo_module","ngx_http_map_module","ngx_http_split_clients_module","ngx_http_referer_module","ngx_http_rewrite_module","ngx_http_proxy_module","ngx_http_fastcgi_module","ngx_http_uwsgi_module",......
}
1、POST_READ
該階段為接受到完整的 HTTP 頭部后,讀取請求內容階段,nginx 讀取并解析完請求頭之后就立即開始執行
該階段經常用到 realip 模塊,可以獲取到一些原始的值,用于做日志分析、限流等使用
我們知道一個請求從發出到 nginx 接收,中間可能經過了多臺服務器,為了獲取到這個原始 IP,通常會使用兩個特定的HTTP頭部字段:X-Forwarded-For
和 X-Real-IP
- X-Forwarded-For:用于傳遞客戶端請求經過的所有代理服務器的 IP 地址。這個頭部通常包含一個或多個 IP地址,它們按照請求經過代理的順序排列。如果請求直接發送到服務器,則此頭部可能不存在或只包含客戶端的 IP 地址。
- X-Real-IP:記錄客戶端的真實 IP 地址,它只包含一個 IP 地址。這個頭部字段由第一個代理服務器設置,并且在請求穿越后續代理時不會被更改,因此它代表了客戶端的原始 IP 地址。
realip 模塊
- 默認不會編譯,需要編譯時使用 --with-http_realip_module 加入
- 配置項
- set_real_ip_from:此指令用于定義信任的代理服務器。只有來自這些服務器的
X-Real-IP
和X-Forwarded-For
頭部字段才會被 Nginx 接受和處理。 - real_ip_header:通過這個指令,可以指定 Nginx 應該使用
X-Real-IP
還是X-Forwarded-For
頭部來確定客戶端的真實 IP 地址。 - realip_recursive:默認關閉,指示
realip
模塊是否已經遞歸地處理了X-Forwarded-For
頭部字段。
- set_real_ip_from:此指令用于定義信任的代理服務器。只有來自這些服務器的
- 重要變量
- http_x_real_ip:包含
X-Real-IP
頭部的值,即客戶端的真實IP地址。如果該頭部不存在,則變量為空 - remote_addr:默認情況下,這個變量包含服務器接收到的客戶端 IP 地址。當
realip
模塊啟用并正確配置后,它會被設置為客戶端的真實 IP 地址。 - http_x_forwarded_for:包含
X-Forwarded-For
頭部的值,這是一個 IP 地址列表,記錄了客戶端以及所有中間代理服務器的IP。
- http_x_real_ip:包含
實戰:
server {listen 9090;location / {proxy_pass http://127.0.0.1:9091; # 將請求轉發到 server 2proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 本機 IP 加入proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-Proto $scheme;}
}server {listen 9091;# 在 server 2 中配置 realip 模塊set_real_ip_from 127.0.0.1; # 允許來自本地的 IP 地址作為反向代理real_ip_header X-Forwarded-For; # 使用 X-Forwarded-For 頭部獲取真實 IPreal_ip_recursive on; # 如果有多個代理,遞歸獲取真實 IPlocation / {access_log /home/nginx/nginx/logs/access_realip.log;# 測試輸出真實 IPreturn 200 "\nClient real ip: $remote_addr, X-Forwarded-For: $http_x_forwarded_for\n";}
}
簡單解釋一下:
在本機 nginx 啟動兩個端口 9090 和 9091,9090 在收到請求后會模擬代理服務器轉發給 9091,9091 認為本機 ip 是可信的(可以這么理解:如果這個 ip 是可信的,它給我說它收到了一個來自 ** 的請求我才相信,否則我就認為和我三次握手的那個 ip 才是真正的請求 ip )
測試:
[root@VM-16-11-centos ~]# curl -H 'X-Forwarded-For: 1.1.1.1, 2.2.2.2' 123.207.214.107:9090Client real ip: 123.207.214.107, X-Forwarded-For: 1.1.1.1, 2.2.2.2, 123.207.214.107
修改 set_real_ip_from 的值為 127.0.0.2 再測試
[root@VM-16-11-centos ~]# curl -H 'X-Forwarded-For: 1.1.1.1, 2.2.2.2' 123.207.214.107:9090Client real ip: 127.0.0.1, X-Forwarded-For: 1.1.1.1, 2.2.2.2, 123.207.214.107
原因是: 127.0.0.1 被認為不可信,所以雖然 X-Forwarded-For 中最后一個 ip 是 123.207.214.107,但 9091 發現與自己通信的其實是本機的 9090,認為真實 ip 就是 127.0.0.1
2、SERVER_REWRITE
在 uri 與 location 匹配之前修改請求的 URI(重定向),在 server 塊中的請求地址重寫階段
由于該階段在 rewrite 階段前,所以如果這里出現 return 指令,就不會返回 location 中的 return 了
【1】測試 server 下的重寫
server {listen 9092;root html/;# 測試 server 下的重寫 404 但是返回 403 界面error_page 404 /403.html;}
[root@VM-16-11-centos nginx]# cat html/403.html
This is 403!
[root@VM-16-11-centos ~]# curl 123.207.214.107:9092/test.txt
This is 403!
【2】server 下的重寫與 location 中的 return 相比,return 生效
server {listen 9092;root html/;# 測試 server 下的 error_page 和 location 下的 return 生效情況:location 生效error_page 404 /403.html;location / {return 404 "find nothing location!\n";}}
[root@VM-16-11-centos ~]# curl 123.207.214.107:9092/test.txt
find nothing location!
【2】server 下的 return 先于 location 中的 return 返回
server {listen 9092;root html/;# 測試 server 下的 error_page 和 location 下的 return 生效情況:location 生效error_page 404 /403.html;location / {return 404 "find nothing location!\n";}# 測試 server 下的 return 和 location 下的 return 生效: server 中生效return 404 "find nothing server!\n";}
[root@VM-16-11-centos ~]# curl 123.207.214.107:9092/test.txt
find nothing server!
3、FIND_CONFIG
配置查找階段,根據請求 uri 匹配 location 表達式,這個階段不支持 nginx 模塊注冊處理程序,而是由ngx_http_core_module 模塊來完成當前請求與 location 配置快之間的配對工作
location 的匹配規則是僅匹配 URI,忽略參數,有下面三種大的情況:
- 前綴字符串
- 常規匹配
- =:精確匹配
- ^~:匹配上后則不再進行正則表達式匹配
- 正則表達式
- ~:大小寫敏感的正則匹配
- ~*:大小寫不敏感
- 用戶內部跳轉的命名 location ,可以通過
error_page
或try_files
等指令進行內部跳轉。- @
建議不同業務使用不同的前綴,原因是 location 支持嚴格匹配、正則、前綴匹配等多種匹配方式,需要考慮匹配的優先級,相比與熟知匹配優先級,各個業務使用不同前綴更加易讀
4、REWRITE
location 塊中的請求地址重寫階段,當 rewrite 指令用于 location 中時即運行
語法為:rewrite regex replacement [flag]
作用為將 regex 對應指定的 URL 替換成 replacement
其中 flag
字段有以下可選項:
- last:用
replacement
這個 URL 進行新的 location 匹配 - break:break 指令停止當前腳本指令的執行,等價于獨立的 break 指令
- redirect:返回 302 臨時重定向
- permanent:返回 301 永久重定向(永久重定向會被瀏覽器緩存)
新建一個這樣的目錄結構
[root@VM-16-11-centos nginx]# tree html/test/
html/test/
|-- first
| `-- 1.txt
|-- second
| `-- 2.txt
|-- third`-- 3.txt
配置文件:
server {listen 9093;rewrite_log on;root html/test;location /first {rewrite /first(.*) /second$1 last;return 200 'first!\n';}location /second {rewrite /second(.*) /third$1;return 200 'second!\n';}location /third {return 200 'third!\n';}}
[root@VM-16-11-centos ~]# curl 123.207.214.107:9093/first/1.txt
second!
[root@VM-16-11-centos ~]# curl 123.207.214.107:9093/first/2.txt
second![root@VM-16-11-centos ~]# curl 123.207.214.107:9093/second/3.txt
second!
[root@VM-16-11-centos ~]# curl 123.207.214.107:9093/second/2.txt
second!
出現上述響應的原因是 flag
標志的設置,訪問 /first/ 時被重寫為了 /second 并按照 last
這個 flag 的規則進行了重新匹配,而 /second 雖然進行了重寫,但未配置標志位,所以直接返回了 second 請求下的 200 響應
server {listen 9093;rewrite_log on;root html/test;location /first {rewrite /first(.*) /second$1 last;return 200 'first!\n';}location /second {rewrite /second(.*) /third$1 break;return 200 'second!\n';}location /third {return 200 'third!\n';}}
[root@VM-16-11-centos ~]# curl 123.207.214.107:9093/first/3.txt
3.txt
[root@VM-16-11-centos ~]# curl 123.207.214.107:9093/third/3.txt
third![root@VM-16-11-centos ~]# curl 123.207.214.107:9093/first/3
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.28.0</center>
</body>
</html>
[root@VM-16-11-centos ~]# curl 123.207.214.107:9093/third/3
third!
當 /second 對應的 flag 被設置為 break 后,url 重寫后不再進行 location 匹配,而是真正的去找對應的 /third/3
5、POST_REWRITE
請求地址重寫提交階段,防止遞歸修改 uri 造成死循環,(一個請求執行10次就會被 nginx 認定為死循環)該階段只由 ngx_http_core_module 模塊實現,無外部模塊
配置文件
server {listen 9094;# 開啟日志,便于調試access_log logs/access_post_rewrite.log;error_log logs/post_rewrite_error.log debug; # 啟用調試日志# 重寫規則location /loop {rewrite ^/loop(.*)$ /loop_rewritten\$1 last;return 200 "No loop detected\n";}# 默認返回location / {return 200 "Request processed successfully\n";}}
測試
[root@localhost nginx]# curl 123.207.214.107:9094/loop/1
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.28.0</center>
</body>
</html>
查看日志:
2025/06/13 15:55:34 [notice] 32345#0: *4474 rewritten data: "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", args: "", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
2025/06/13 15:55:34 [notice] 32345#0: *4474 "^/loop(.*)$" matches "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
2025/06/13 15:55:34 [notice] 32345#0: *4474 rewritten data: "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", args: "", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
2025/06/13 15:55:34 [notice] 32345#0: *4474 "^/loop(.*)$" matches "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
2025/06/13 15:55:34 [notice] 32345#0: *4474 rewritten data: "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", args: "", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
2025/06/13 15:55:34 [notice] 32345#0: *4474 "^/loop(.*)$" matches "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
2025/06/13 15:55:34 [notice] 32345#0: *4474 rewritten data: "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", args: "", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
2025/06/13 15:55:34 [error] 32345#0: *4474 rewrite or internal redirection cycle while processing "/loop_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\_rewritten\/1", client: 218.58.62.117, server: , request: "GET /loop/1 HTTP/1.1", host: "123.207.214.107:9094"
最后會出現: rewrite or internal redirection cycle while processing 的錯誤,即一直在循環
6、PREACCESS
訪問權限檢查準備階段,http 模塊介入處理階段,模塊 ngx_limit_conn 和 ngx_limit_req 就運行在此階段
ngx_limit_conn 顧名思義是限制連接個數的,而 ngx_limit_req 是限制請求數的,conn 即 tcp 連接,在一個 keepalive 周期內,所有的req 可以共用一個 conn
ngx_limit_conn
# 定義一個空間 名為 addr,存放二進制 ip 地址,大小為 10m
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {listen 9097;root html/;location / {limit_conn_status 500; # 如果有多的連接返回 500 錯誤limit_conn_log_level warn;limit_rate 50; # 每秒返回 50Blimit_conn addr 1; # 同時只能有 1 個連接}
}
開兩個終端測試
[root@VM-16-11-centos zwj]# curl localhost:9097/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p><p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p><p><em>Thank you for using nginx.</em></p>
</body>
</html>
[root@VM-16-11-centos zwj]# curl localhost:9097/
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.28.0</center>
</body>
</html>
也可以查看 access.log
127.0.0.1 - - [15/Jun/2025:13:59:40 +0800] "GET / HTTP/1.1" 500 177 "-" "curl/7.29.0"
127.0.0.1 - - [15/Jun/2025:13:59:49 +0800] "GET / HTTP/1.1" 200 615 "-" "curl/7.29.0"
ngx_limit_req
# 定義一個空間 名為 addr_req,存放二進制 ip 地址,大小為 10m,每分鐘最多處理 3 個請求
limit_req_zone $binary_remote_addr zone=addr_req:10m rate=3r/m;# limit_req 這里注意 burst 和 nodelay 選項
# burst 桶大小 緩存請求
# nodelay 超出桶大小后是否直接返回,nodelay 就是不延時,直接返回定義的錯誤信息
limit_req zone=addr_req burst=3 nodelay;
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_req_zone $binary_remote_addr zone=addr_req:10m rate=3r/m;server {listen 9097;root html/;location / {limit_conn_status 500;limit_conn_log_level warn;limit_rate 50;limit_conn addr 1;#limit_req zone=one burst=3 nodelay;limit_req zone=addr_req;limit_req_status 503;}
}
也可以查看 error.log
2025/06/15 14:13:33 [error] 26915#0: *5530 limiting requests, excess: 0.882 by zone "addr_req", client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:9097"
"logs/error.log" 175L, 21036C
7、ACCESS
訪問權限檢查階段,標準模塊 ngx_access,第三方模塊 nginx_auth_request 以及第三方模塊 ngx_lua 的 access_by_lua 指令運行在此階段,配置指令多是執行訪問控制性質的任務,比如檢查用戶的訪問權限,檢查用戶的來源 IP 地址是否合法
最常用的其實是 allow deny 指令,比如在系統開發完成給遠程客戶使用階段,可以指定允許訪問的 IP 塊
location / { deny 192.168.1.1; allow 192.168.1.0/24; allow 10.1.1.0/16; allow 2001:0db8::/32; deny all;
}
這些指令是順序匹配,滿足其中一條后就不會繼續了
除此之外常用的還有 auth_basic 和 auth_request ,但這些模塊需要生成密碼文件來配置使用,有點麻煩這里就不再驗證了~
8、POST_ACCESS
在請求通過訪問控制之后,Nginx 執行這個階段的處理。這可以用于執行一些在訪問控制之后需要進行的操作。
9、TRY_FILES
如果 http 請求訪問靜態文件資源,try_files 配置項可以使這個請求順序地訪問多個靜態文件資源,直到某個靜態文件資源符合選取條件,重要部分為:try files 處理 和 mirrors
try files
允許我們在請求訪問靜態文件時,順序地嘗試訪問多個文件,直到找到一個符合條件的文件。如果沒有符合條件的文件,Nginx 會執行指定的備用操作。
目錄結構:
html/
|-- 403.html
|-- 50x.html
|-- index.html
|-- file
| |-- assets
| | |-- 1.css
| | |-- 2.css
| | `-- default.css
| `-- images
| |-- 1.txt
| |-- 2.txt
| `-- default.txt
配置
server {listen 9096;root html/file; # 網站根目錄# 處理靜態文件location / {try_files $uri $uri/ /index.html; # 依次嘗試 $uri、$uri/、index.html}# 針對特定路徑處理,比如 /images 目錄下的靜態文件location /images/ {try_files $uri $uri/ /images/default.txt; # 嘗試訪問圖片目錄下的文件,如果文件不存在,使用默認}# 其他配置location /assets/ {try_files $uri /assets/default.css; # 訪問 assets 目錄下的 CSS 文件,若不存在,使用默認樣式}}
[root@VM-16-11-centos nginx]# curl localhost:9096/
index
[root@VM-16-11-centos nginx]# curl localhost:9096/3.html
index[root@VM-16-11-centos nginx]# curl localhost:9096/images/111.txt
default
[root@VM-16-11-centos nginx]# curl localhost:9096/images/1.txt
1111.txt1111.txt1111.txt1111.txt1111.txt1111.txt1111.txt1111.txt1111.txt1111.txt1111.txt
mirror
處理請求時,生成子請求訪問其他服務,對子請求的返回值不做處理
10、CONTENT
內容產生階段,大部分HTTP模塊會介入該階段,是所有請求處理階段中最重要的階段,因為這個階段的指令通常是用來生成HTTP響應內容的
這里比較重要的部分包括:root/alias 的使用,index 和 autoindex 的使用
root 和 alias 的區別主要在于匹配到 location 后,最終的那個文件到底是加上 location 對應的前綴還是不加
[root@VM-16-11-centos nginx]# tree html/
html/
|-- 403.html
|-- 50x.html
|-- index.html
|-- root
| `-- index.html
配置文件
server {listen 9095;location /root {root html;}location /alias {alias html;}}
[root@VM-16-11-centos nginx]# curl localhost:9095/alias/index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p><p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p><p><em>Thank you for using nginx.</em></p>
</body>
</html>[root@VM-16-11-centos nginx]# curl localhost:9095/root/index.html
root/html[root@VM-16-11-centos nginx]# curl localhost:9095/alias/root/index.html
root/html
root 會將請求 url 完整的追加在其值的后面,而 alias 則會從請求 url 中先去除 location 匹配的部分再追加
index 指定 /
結尾的目錄訪問時,返回 index 文件內容
配置
server {listen 9095;charset utf-8; # 編碼格式,不配置的話中文可能亂碼#access_log logs/host.access.log main;location / {root html;index index.html index.htm;}error_page 500 502 503 504 /50x.html;location = /50x.html {root html;}
}
autoindex
server {listen 9095;charset utf-8; # 編碼格式,不配置的話中文可能亂碼#access_log logs/host.access.log main;location / {root myHtml;autoindex on; # 啟用目錄列表顯示autoindex_exact_size on; # 可選:禁用文件大小顯示(以字節為單位)autoindex_localtime on; # 可選:顯示本地時間}error_page 500 502 503 504 /50x.html;location = /50x.html {root html;}}
注意:index 模塊在 autoindex 之前,所以使用 autoindex 請務必關閉 index 或指定一個不存在的文件
11、LOG
日志模塊處理階段,記錄日志,正常會在 logs/access.log 中記錄 http 請求的日志信息
默認格式:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent ' '"$http_referer"
"$http_user_agent"';
可以根據需要自行修改,同時也會有日志壓縮和日志緩存的可配置項
日志格式
access_log path [format=name [buffer=size] [gzip[=level]] [flush=time] [if=condition]];access_log off;
path
:日志文件的路徑,可以包含變量。format
:指定日志格式的名稱。buffer
:設置日志寫入的緩沖區大小。gzip
:啟用日志文件的壓縮,并可選地設置壓縮級別。flush
:設置日志刷新的頻率。if
:通過條件判斷來控制是否記錄日志。
日志緩存 日志緩存功能可以減少磁盤 I/O 操作,通過批量寫入日志來提高性能。
寫入磁盤的條件
- 待寫入磁盤的日志大小超出緩存大小。
- 達到
flush
指定的時間。 - worker 進程執行 reopen 命令,或者正在關閉。
我只能說學到這里我大概知道 nginx 的每個階段可以做什么,具體的指令如果不是經常配置應該還是會忘記~