概述
使用NGINX服務器進行HTTP報文的處理和轉發時,有一些容易混淆的概念。比如像正向代理和反向代理、root和alias、proxy和rewrite。
這篇博客主要想記錄一下自己對proxy和rewrite的一些理解。(這里不去考慮rewrite的最后一項參數和區分301、302,建議想看完整細節的看這篇博客,更多關於NGINX的知識可以看官方文檔)。
Little NGINX
我用C++實現一個閹割版的NGINX,以此來感受proxy和rewrite兩個過程中,HTTP報文變化的區別。具體的項目可以看這個代碼倉庫,我不是依據NGINX的源碼進行的代碼編寫,所以具體的實現上會有所差異,實際使用的功能也沒有NGINX那么豐富。還有就是代碼雖然套了C++的殼,但核心部分是基於Linux C編寫的,所以只支持在Linux系統或MacOS等類Unix系統上使用。
-
克隆項目
git clone https://gitee.com/xuanyusan/little_nginx.git cd little_nginx
-
編譯項目
g++ little_nginx.cpp -o little_nginx
-
開啟little_nginx
./little_nginx
打印出已配置的服務則運行成功,此時可以在瀏覽器輸入
localhost:8880
訪問服務。 -
開啟nodejs koa服務
cd servers/node_koa/ npm install node index.js
開啟nodejs服務后,在瀏覽器輸入
localhost:8880/koa/
可以訪問到nodejs的服務。在瀏覽器輸入
localhost:8880/koa/path1
則可以體驗proxy處理重定向的整個流程,little_nginx的stdout也會有對應輸出,幫助理解。
proxy和rewrite的區別
proxy和rewrite的區別可以通過下面的兩個圖進行簡單理解。對圖中WEB服務的理解應該包括NGINX提供的WEB服務。
rewrite
從圖中可以看出,rewrite必定會發生重定向,因為它會給客戶端返回一個302的響應報文,而其中的Location參數就是由一開始請求的path根據rewrite參數處理得到的。
假設nginx配置為:
rewrite ^/(.*) https://www.baidu.com/$1 redirect;
我們通過curl去看HTTP報文的信息則是這樣的:
$ curl -v http://localhost/test/
* Trying ::1:80...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 80 failed: Connection refused
* Trying 127.0.0.1:80...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /test/ HTTP/1.1
> Host: localhost
> User-Agent: curl/7.65.3
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.17.3
< Date: Wed, 15 Sep 2021 10:02:04 GMT
< Content-Type: text/html
< Content-Length: 145
< Connection: keep-alive
< Location: https://www.baidu.com/test/
<
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.17.3</center>
</body>
</html>
* Connection #0 to host localhost left intact
可以看到相應頭的Location參數就是由請求報文的/test/
保留了test/
部分並在前面接上https://www.baidu.com/
得到的。
而這就必須保證rewrite第二個參數的域名或者ip以及端口必須是客戶端可以訪問的,不行的話,就不能用rewrite。所以rewrite更常用於域名變更和網站搬遷。
proxy
而proxy是不一定會發生重定向的,即使發生重定向,也是由具體的服務決定的,而不是NGINX這個中間代理可以越俎代庖的。NGINX的proxy有兩個關鍵配置,一個是proxy_pass,一個是proxy_redirect,分別對應請求報文和響應報文的處理。
proxy_pass就是修改請求的協議、路徑和host,然后發給具體的服務端。proxy_redirect就是在具體服務端發回來一個302響應報文的時候,根據proxy_redirect參數的規則去修改Location。
假設我們現在的機器上,在本地回環的8881端口跑着一個nodejs的服務,然后我們通過機器某個局域網的ip地址去訪問80端口的/test
。
nodejs服務入口文件如下:
const Koa = require('koa');
const Router = require('koa-router');
const Koa_Logger = require("koa-logger");
const koaBody = require('koa-body')
const app = new Koa();
const router = new Router();
const logger = new Koa_Logger();
router.get("/baseurl", async (ctx)=>{
ctx.body = "<h1>Node Koa</h1>"
});
router.get("/baseurl/path1", async (ctx)=>{
ctx.redirect("/baseurl/path3");
});
router.get("/baseurl/path3", async (ctx)=>{
ctx.body = "<h1>Node Koa Path3</h1>"
});
app.use(logger);
app.use(koaBody());
app.use(router.routes());
app.listen(8881, () => {
console.log('應用已經啟動,http://localhost:8881');
});
如果我們只配置了proxy_pass:
proxy_pass http://localhost:8881/baseurl;
訪問非重定向的頁面是正常的:
$ curl -v http://localhost/test/
* Trying ::1:80...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 80 failed: Connection refused
* Trying 127.0.0.1:80...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /test/ HTTP/1.1
> Host: localhost
> User-Agent: curl/7.65.3
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.17.3
< Date: Wed, 15 Sep 2021 10:28:57 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 17
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
<h1>Node Koa</h1>
因為這個時候我們的HTTP請求的協議、ip和端口被修改為了proxy_pass參數里的http
、localhost
和8881
,並且發送給了localhost的8881端口。這時,監聽8881端口的服務就可以接收報文並返回結果。
所以proxy_pass的參數就不要求客戶端可以訪問,只要代理服務器可以訪問就可以了。客戶甚至不知道他們訪問的服務到底是誰提供的,這也是反向代理的一大特點。
但是,如果服務端發生了重定向,結果則如下:
$ curl -v http://localhost/test/path1
* Trying ::1:80...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 80 failed: Connection refused
* Trying 127.0.0.1:80...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /test/path1 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.65.3
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Server: nginx/1.17.3
< Date: Wed, 15 Sep 2021 10:29:54 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 59
< Connection: keep-alive
< Location: /baseurl/path3
<
* Connection #0 to host localhost left intact
Redirecting to <a href="/baseurl/path3">/baseurl/path3</a>.
可以看到它重定向的結果不是我們預期中的http://localhost/test/path3,而是http://localhost/baseurl/path3。而想要讓它重定向到http://localhost/test/path3,我們就不能直接把nodejs扔給我們的響應頭原封不動地扔給客戶端,而是要用proxy_redirect來指定相應頭Location參數的修改規則。比如:
proxy_redirect /baseurl /test;
$ curl -v http://localhost/test/path1
* Trying ::1:80...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 80 failed: Connection refused
* Trying 127.0.0.1:80...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /test/path1 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.65.3
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Server: nginx/1.17.3
< Date: Wed, 15 Sep 2021 10:37:30 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 59
< Location: http://localhost/test/path3
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
Redirecting to <a href="/baseurl/path3">/baseurl/path3</a>.
這里可以明顯看到Location的改變,當然proxy不會去修改報文的body部分,所以body部分還是/baseurl/path3
。這里萬一因為網絡原因,重定向沒有完成,顯示了這個頁面,用戶又去點擊,就會出現錯誤。NGINX也不是萬能的,還是需要我們自己去理解代理背后的邏輯,才能有效應對各種問題。