Nginx變數詳解(學習筆記十九)

sktj發表於2018-05-17

Nginx 的配置檔案使用的就是一門微型的程式語言,許多真實世界裡的 Nginx 配置檔案其實就是一個一個的小程式。當然,是不是“圖靈完全的”暫且不論,至少據我觀察,它在設計上受 Perl 和 Bourne Shell 這兩種語言的影響很大。在這一點上,相比 Apache 和 Lighttpd 等其他 Web 伺服器的配置記法,不能不說算是 Nginx 的一大特色了。既然是程式語言,一般也就少不了“變數”這種東西(當然,Haskell 這樣奇怪的函式式語言除外了)。

熟悉 Perl、Bourne Shell、C/C++ 等指令式程式設計語言的朋友肯定知道,變數說白了就是存放“值”的容器。而所謂“值”,在許多程式語言裡,既可以是3.14這樣的數值,也可以是hello world這樣的字串,甚至可以是像陣列、雜湊表這樣的複雜資料結構。然而,在 Nginx 配置中,變數只能存放一種型別的值,因為也只存在一種型別的值,那就是字串。

比如我們的nginx.conf檔案中有下面這一行配置:

set$a “hello world”;

我們使用了標準ngx_rewrite模組的set配置指令對變數$a進行了賦值操作。特別地,我們把字串hello world賦給了它。

我們看到,Nginx 變數名前面有一個$符號,這是記法上的要求。所有的 Nginx 變數在 Nginx 配置檔案中引用時都須帶上$字首。這種表示方法和 Perl、PHP 這些語言是相似的。

雖然$這樣的變數字首修飾會讓正統的Java和C#程式設計師不舒服,但這種表示方法的好處也是顯而易見的,那就是可以直接把變數嵌入到字串常量中以構造出新的字串:

set$a hello;

set$b “$a, $a”;

這裡我們通過已有的 Nginx 變數$a的值,來構造變數$b的值,於是這兩條指令順序執行完之後,$a的值是hello,而$b的值則是hello, hello. 這種技術在 Perl 世界裡被稱為“變數插值”(variable interpolation),它讓專門的字串拼接運算子變得不再那麼必要。我們在這裡也不妨採用此術語。

    我們來看一個比較完整的配置示例:

server{

listen8080;

location/test {

set$foo hello;

echo “foo: $foo”;

}

}

這個例子省略了nginx.conf配置檔案中最外圍的http配置塊以及events配置塊。使用curl這個 HTTP 客戶端在命令列上請求這個/test介面,我們可以得到

$ curl `http://localhost:8080/test`

foo: hello

這裡我們使用第三方ngx_echo模組的echo配置指令將$foo變數的值作為當前請求的響應體輸出。

我們看到,echo配置指令的引數也支援“變數插值”。不過,需要說明的是,並非所有的配置指令都支援“變數插值”。事實上,指令引數是否允許“變數插值”,取決於該指令的實現模組。

如果我們想通過echo指令直接輸出含有“美元符”($)的字串,那麼有沒有辦法把特殊的$字元給轉義掉呢?答案是否定的(至少到目前最新的 Nginx 穩定版1.0.10)。不過幸運的是,我們可以繞過這個限制,比如通過不支援“變數插值”的模組配置指令專門構造出取值為$的 Nginx 變數,然後再在echo中使用這個變數。看下面這個例子:

geo$dollar {

default “$”;

}

server{

listen8080;

location/test {

echo “This is a dollar sign: $dollar”;

}

}

測試結果如下:

$ curl `http://localhost:8080/test`

This is a dollar sign: $

這裡用到了標準模組ngx_geo提供的配置指令geo來為變數$dollar賦予字串”$”,這樣我們在下面需要使用美元符的地方,就直接引用我們的$dollar變數就可以了。其實ngx_geo模組最常規的用法是根據客戶端的 IP 地址對指定的 Nginx 變數進行賦值,這裡只是借用它以便“無條件地”對我們的$dollar變數賦予“美元符”這個值。

    在“變數插值”的上下文中,還有一種特殊情況,即當引用的變數名之後緊跟著變數名的構成字元時(比如後跟字母、數字以及下劃線),我們就需要使用特別的記法來消除歧義,例如:

server{

listen8080;

location/test {

set$first “hello “;

echo “${first}world”;

}

}

這裡,我們在echo配置指令的引數值中引用變數$first的時候,後面緊跟著world這個單詞,所以如果直接寫作”$firstworld”則 Nginx “變數插值”計算引擎會將之識別為引用了變數$firstworld. 為了解決這個難題,Nginx 的字串記法支援使用花括號在$之後把變數名圍起來,比如這裡的${first}. 上面這個例子的輸出是:

$ curl `http://localhost:8080/test

hello world

set指令(以及前面提到的geo指令)不僅有賦值的功能,它還有建立 Nginx 變數的副作用,即當作為賦值物件的變數尚不存在時,它會自動建立該變數。比如在上面這個例子中,如果$a這個變數尚未建立,則set指令會自動建立$a這個使用者變數。如果我們不建立就直接使用它的值,則會報錯。例如

?server{

?listen8080;

?

?location/bad {

?         echo $foo;

?     }

? }

此時 Nginx 伺服器會拒絕載入配置:

    [emerg] unknown “foo” variable

是的,我們甚至都無法啟動服務!

    有趣的是,Nginx 變數的建立和賦值操作發生在全然不同的時間階段。Nginx 變數的建立只能發生在 Nginx 配置載入的時候,或者說 Nginx 啟動的時候;而賦值操作則只會發生在請求實際處理的時候。這意味著不建立而直接使用變數會導致啟動失敗,同時也意味著我們無法在請求處理時動態地建立新的 Nginx 變數。

Nginx 變數一旦建立,其變數名的可見範圍就是整個 Nginx 配置,甚至可以跨越不同虛擬主機的server配置塊。我們來看一個例子:

server{

listen8080;

location/foo {

echo “foo = [$foo]”;

}

location/bar {

set$foo 32;

echo “foo = [$foo]”;

}

}

這裡我們在location /bar中用set指令建立了變數$foo,於是在整個配置檔案中這個變數都是可見的,因此我們可以在location /foo中直接引用這個變數而不用擔心 Nginx 會報錯。

下面是在命令列上用curl工具訪問這兩個介面的結果:

$ curl `http://localhost:8080/foo`

foo = []

$ curl `http://localhost:8080/bar`

foo = [32]

$ curl `http://localhost:8080/foo`

foo = []

從這個例子我們可以看到,set指令因為是在location /bar中使用的,所以賦值操作只會在訪問/bar的請求中執行。而請求/foo介面時,我們總是得到空的$foo值,因為使用者變數未賦值就輸出的話,得到的便是空字串。

從這個例子我們可以窺見的另一個重要特性是,Nginx 變數名的可見範圍雖然是整個配置,但每個請求都有所有變數的獨立副本,或者說都有各變數用來存放值的容器的獨立副本,彼此互不干擾。比如前面我們請求了/bar介面後,$foo變數被賦予了值32,但它絲毫不會影響後續對/foo介面的請求所對應的$foo值(它仍然是空的!),因為各個請求都有自己獨立的$foo變數的副本。

    對於 Nginx 新手來說,最常見的錯誤之一,就是將 Nginx 變數理解成某種在請求之間全域性共享的東西,或者說“全域性變數”。而事實上,Nginx 變數的生命期是不可能跨越請求邊界的


關於 Nginx 變數的另一個常見誤區是認為變數容器的生命期,是與location配置塊繫結的。其實不然。我們來看一個涉及“內部跳轉”的例子:

server{

listen8080;

location/foo {

set$a hello;

echo_exec /bar;

}

location/bar {

echo “a = [$a]”;

}

}

這裡我們在location /foo中,使用第三方模組ngx_echo提供的echo_exec配置指令,發起到location /bar的“內部跳轉”。所謂“內部跳轉”,就是在處理請求的過程中,於伺服器內部,從一個location跳轉到另一個location的過程。這不同於利用 HTTP 狀態碼301和302所進行的“外部跳轉”,因為後者是由 HTTP 客戶端配合進行跳轉的,而且在客戶端,使用者可以通過瀏覽器位址列這樣的介面,看到請求的 URL 地址發生了變化。內部跳轉和Bourne Shell(或Bash)中的exec命令很像,都是“有去無回”。另一個相近的例子是C語言中的goto語句。

既然是內部跳轉,當前正在處理的請求就還是原來那個,只是當前的location發生了變化,所以還是原來的那一套 Nginx 變數的容器副本。對應到上例,如果我們請求的是/foo這個介面,那麼整個工作流程是這樣的:先在location /foo中通過set指令將$a變數的值賦為字串hello,然後通過echo_exec指令發起內部跳轉,又進入到location /bar中,再輸出$a變數的值。因為$a還是原來的$a,所以我們可以期望得到hello這行輸出。測試證實了這一點:

$ curl localhost:8080/foo

a = [hello]

但如果我們從客戶端直接訪問/bar介面,就會得到空的$a變數的值,因為它依賴於location /foo來對$a進行初始化。

從上面這個例子我們看到,一個請求在其處理過程中,即使經歷多個不同的location配置塊,它使用的還是同一套 Nginx 變數的副本。這裡,我們也首次涉及到了“內部跳轉”這個概念。值得一提的是,標準ngx_rewrite模組的rewrite配置指令其實也可以發起“內部跳轉”,例如上面那個例子用rewrite配置指令可以改寫成下面這樣的形式:

server{

listen8080;

location/foo {

set$a hello;

rewrite^ /bar;

}

location/bar {

echo “a = [$a]”;

}

}

其效果和使用echo_exec是完全相同的。後面我們還會專門介紹這個rewrite指令的更多用法,比如發起301和302這樣的“外部跳轉”。

從上面這個例子我們看到,Nginx 變數值容器的生命期是與當前正在處理的請求繫結的,而與location無關。

前面我們接觸到的都是通過set指令隱式建立的 Nginx 變數。這些變數我們一般稱為“使用者自定義變數”,或者更簡單一些,“使用者變數”。既然有“使用者自定義變數”,自然也就有由 Nginx 核心和各個 Nginx 模組提供的“預定義變數”,或者說“內建變數”(builtin variables)。

Nginx 內建變數最常見的用途就是獲取關於請求或響應的各種資訊。例如由ngx_http_core模組提供的內建變數$uri,可以用來獲取當前請求的 URI(經過解碼,並且不含請求引數),而$request_uri則用來獲取請求最原始的 URI (未經解碼,並且包含請求引數)。請看下面這個例子:

location/test {

echo “uri = $uri”;

echo “request_uri = $request_uri”;

}

這裡為了簡單起見,連server配置塊也省略了,和前面所有示例一樣,我們監聽的依然是8080埠。在這個例子裡,我們把$uri$request_uri的值輸出到響應體中去。下面我們用不同的請求來測試一下這個/test介面:

$ curl `http://localhost:8080/test`

uri = /test

request_uri = /test

$ curl `http://localhost:8080/test?a=3&b=4`

uri = /test

request_uri = /test?a=3&b=4

$ curl `http://localhost:8080/test/hello%20world?a=3&b=4`

uri = /test/hello world

request_uri = /test/hello%20world?a=3&b=4

另一個特別常用的內建變數其實並不是單獨一個變數,而是有無限多變種的一群變數,即名字以arg_開頭的所有變數,我們估且稱之為$arg_XXX變數群。一個例子是$arg_name,這個變數的值是當前請求名為name的 URI 引數的值,而且還是未解碼的原始形式的值。我們來看一個比較完整的示例:

location/test {

echo “name: $arg_name”;

echo “class: $arg_class”;

}

然後在命令列上使用各種引數組合去請求這個/test介面:

$ curl `http://localhost:8080/test`

name:

class:

$ curl `http://localhost:8080/test?name=Tom&class=3`

name: Tom

class: 3

$ curl `http://localhost:8080/test?name=hello%20world&class=9`

name: hello%20world

class: 9

其實$arg_name不僅可以匹配name引數,也可以匹配NAME引數,抑或是Name,等等:

$ curl `http://localhost:8080/test?NAME=Marry`

name: Marry

class:

$ curl `http://localhost:8080/test?Name=Jimmy`

name: Jimmy

class:

Nginx 會在匹配引數名之前,自動把原始請求中的引數名調整為全部小寫的形式。

如果你想對 URI 引數值中的%XX這樣的編碼序列進行解碼,可以使用第三方ngx_set_misc模組提供的set_unescape_uri配置指令:

location/test {

set_unescape_uri $name $arg_name;

set_unescape_uri $class $arg_class;

echo “name: $name”;

echo “class: $class”;

}

現在我們再看一下效果:

$ curl `http://localhost:8080/test?name=hello%20world&class=9`

name: hello world

class: 9

空格果然被解碼出來了!

從這個例子我們同時可以看到,這個set_unescape_uri指令也像set指令那樣,擁有自動建立 Nginx 變數的功能。後面我們還會專門介紹到ngx_set_misc模組。

$arg_XXX這種型別的變數擁有無窮無盡種可能的名字,所以它們並不對應任何存放值的容器。而且這種變數在 Nginx 核心中是經過特別處理的,第三方 Nginx 模組是不能提供這樣充滿魔法的內建變數的。

類似$arg_XXX的內建變數還有不少,比如用來取 cookie 值的$cookie_XXX變數群,用來取請求頭的$http_XXX變數群,以及用來取響應頭的$sent_http_XXX變數群。這裡就不一一介紹了,感興趣的讀者可以參考ngx_http_core模組的官方文件。

需要指出的是,許多內建變數都是隻讀的,比如我們剛才介紹的$uri$request_uri. 對只讀變數進行賦值是應當絕對避免的,因為會有意想不到的後果,比如:

?location/bad {

?set$uri /blah;

?     echo $uri;

? }

這個有問題的配置會讓 Nginx 在啟動的時候報出一條令人匪夷所思的錯誤:

    [emerg] the duplicate “uri” variable in …

如果你嘗試改寫另外一些只讀的內建變數,比如$arg_XXX變數,在某些 Nginx 的版本中甚至可能導致程式崩潰。



也有一些內建變數是支援改寫的,其中一個例子是$args. 這個變數在讀取時返回當前請求的 URL 引數串(即請求 URL 中問號後面的部分,如果有的話 ),而在賦值時可以直接修改引數串。我們來看一個例子:

location/test {

set$orig_args $args;

set$args “a=3&b=4”;

echo “original args: $orig_args”;

echo “args: $args”;

}

這裡我們把原始的 URL 引數串先儲存在$orig_args變數中,然後通過改寫$args變數來修改當前的 URL 引數串,最後我們用echo指令分別輸出$orig_args和$args變數的值。接下來我們這樣來測試這個/test介面:

$ curl `http://localhost:8080/test`

original args:

args: a=3&b=4

$ curl `http://localhost:8080/test?a=0&b=1&c=2`

original args: a=0&b=1&c=2

args: a=3&b=4

在第一次測試中,我們沒有設定任何 URL 引數串,所以輸出$orig_args變數的值時便得到空。而在第一次和第二次測試中,無論我們是否提供 URL 引數串,引數串都會在location /test中被強行改寫成a=3&b=4.

需要特別指出的是,這裡的$args變數和$arg_XXX一樣,也不再使用屬於自己的存放值的容器。當我們讀取$args時,Nginx 會執行一小段程式碼,從 Nginx 核心中專門存放當前 URL 引數串的位置去讀取資料;而當我們改寫$args時,Nginx 會執行另一小段程式碼,對相同位置進行改寫。Nginx 的其他部分在需要當前 URL 引數串的時候,都會從那個位置去讀資料,所以我們對$args的修改會影響到所有部分的功能。我們來看一個例子:

location/test {

set$orig_a $arg_a;

set$args “a=5”;

echo “original a: $orig_a”;

echo “a: $arg_a”;

}

這裡我們先把內建變數$arg_a的值,即原始請求的 URL 引數a的值,儲存在使用者變數$orig_a中,然後通過對內建變數$args進行賦值,把當前請求的引數串改寫為a=5,最後再用echo指令分別輸出$orig_a和$arg_a變數的值。因為對內建變數$args的修改會直接導致當前請求的 URL 引數串發生變化,因此內建變數$arg_XXX自然也會隨之變化。測試的結果證實了這一點:

$ curl `http://localhost:8080/test?a=3`

original a: 3

a: 5

我們看到,因為原始請求的 URL 引數串是a=3, 所以$arg_a最初的值為3, 但隨後通過改寫$args變數,將 URL 引數串又強行修改為a=5, 所以最終$arg_a的值又自動變為了5.

我們再來看一個通過修改$args變數影響標準的 HTTP 代理模組ngx_proxy的例子:

server{

listen8080;

location/test {

set$args “foo=1&bar=2”;

proxy_passhttp://127.0.0.1:8081/args;

}

}

server{

listen8081;

location/args {

echo “args: $args”;

}

}

這裡我們在http配置塊中定義了兩個虛擬主機。第一個虛擬主機監聽 8080 埠,其/test介面自己通過改寫$args變數,將當前請求的 URL 引數串無條件地修改為foo=1&bar=2. 然後/test介面再通過ngx_proxy模組的proxy_pass指令配置了一個反向代理,指向本機的 8081 埠上的 HTTP 服務/args. 預設情況下,ngx_proxy模組在轉發 HTTP 請求到遠方 HTTP 服務的時候,會自動把當前請求的 URL 引數串也轉發到遠方。

而本機的 8081 埠上的 HTTP 服務正是由我們定義的第二個虛擬主機來提供的。我們在第二個虛擬主機的location /args中利用echo指令輸出當前請求的 URL 引數串,以檢查/test介面通過ngx_proxy模組實際轉發過來的 URL 請求引數串。

我們來實際訪問一下第一個虛擬主機的/test介面:

$ curl `http://localhost:8080/test?blah=7`

args: foo=1&bar=2

我們看到,雖然請求自己提供了 URL 引數串blah=7,但在location /test中,引數串被強行改寫成了foo=1&bar=2. 接著經由proxy_pass指令將我們被改寫掉的引數串轉發給了第二個虛擬主機上配置的/args介面,然後再把/args介面的 URL 引數串輸出。事實證明,我們對$args變數的賦值操作,也成功影響到了ngx_proxy模組的行為。

    在讀取變數時執行的這段特殊程式碼,在 Nginx 中被稱為“取處理程式”(get handler);而改寫變數時執行的這段特殊程式碼,則被稱為“存處理程式”(set handler)。不同的 Nginx 模組一般會為它們的變數準備不同的“存取處理程式”,從而讓這些變數的行為充滿魔法。

    其實這種技巧在計算世界並不鮮見。比如在物件導向程式設計中,類的設計者一般不會把類的成員變數直接暴露給類的使用者,而是另行提供兩個方法(method),分別用於該成員變數的讀操作和寫操作,這兩個方法常常被稱為“存取器”(accessor)。下面是 C++ 語言中的一個例子:

#include 

using namespace std;

class Person {

public:

const string get_name() {

return m_name;

}

void set_name(const string name) {

m_name = name;

}

private:

string m_name;

};

在這個名叫Person的 C++ 類中,我們提供了get_name和set_name這兩個公共方法,以作為私有成員變數m_name的“存取器”。

    這樣設計的好處是顯而易見的。類的設計者可以在“存取器”中執行任意程式碼,以實現所需的業務邏輯以及“副作用”,比如自動更新與當前成員變數存在依賴關係的其他成員變數,抑或是直接修改某個與當前物件相關聯的資料庫表中的對應欄位。而對於後一種情況,也許“存取器”所對應的成員變數壓根就不存在,或者即使存在,也頂多扮演著資料快取的角色,以緩解被代理資料庫的訪問壓力。

    與物件導向程式設計中的“存取器”概念相對應,Nginx 變數也是支援繫結“存取處理程式”的。Nginx 模組在建立變數時,可以選擇是否為變數分配存放值的容器,以及是否自己提供與讀寫操作相對應的“存取處理程式”。

    不是所有的 Nginx 變數都擁有存放值的容器。擁有值容器的變數在 Nginx 核心中被稱為“被索引的”(indexed);反之,則被稱為“未索引的”(non-indexed)。

我們前面在(二)中已經知道,像$arg_XXX這樣具有無數變種的變數群,是“未索引的”。當讀取這樣的變數時,其實是它的“取處理程式”在起作用,即實時掃描當前請求的 URL 引數串,提取出變數名所指定的 URL 引數的值。很多新手都會對$arg_XXX的實現方式產生誤解,以為 Nginx 會事先解析好當前請求的所有 URL 引數,並且把相關的$arg_XXX變數的值都事先設定好。然而事實並非如此,Nginx 根本不會事先就解析好 URL 引數串,而是在使用者讀取某個$arg_XXX變數時,呼叫其“取處理程式”,即時去掃描 URL 引數串。類似地,內建變數$cookie_XXX也是通過它的“取處理程式”,即時去掃描Cookie請求頭中的相關定義的。


在設定了“取處理程式”的情況下,Nginx 變數也可以選擇將其值容器用作快取,這樣在多次讀取變數的時候,就只需要呼叫“取處理程式”計算一次。我們下面就來看一個這樣的例子:

map$args $foo {

default     0;

debug       1;

}

server{

listen8080;

location/test {

set$orig_foo $foo;

set$args debug;

echo “orginal foo: $orig_foo”;

echo “foo: $foo”;

}

}

這裡首次用到了標準ngx_map模組的map配置指令,我們有必要在此介紹一下。map在英文中除了“地圖”之外,也有“對映”的意思。比方說,中學數學裡講的“函式”就是一種“對映”。而 Nginx 的這個map指令就可以用於定義兩個 Nginx 變數之間的對映關係,或者說是函式關係。回到上面這個例子,我們用map指令定義了使用者變數$foo與$args內建變數之間的對映關係。特別地,用數學上的函式記法y = f(x)來說,我們的$args就是“自變數”x,而$foo則是“因變數”y,即$foo的值是由$args的值來決定的,或者按照書寫順序可以說,我們將$args變數的值對映到了$foo變數上。

現在我們再來看map指令定義的對映規則:

map$args $foo {

default     0;

debug       1;

}

花括號中第一行的default是一個特殊的匹配條件,即當其他條件都不匹配的時候,這個條件才匹配。當這個預設條件匹配時,就把“因變數”$foo對映到值0. 而花括號中第二行的意思是說,如果“自變數”$args精確匹配了debug這個字串,則把“因變數”$foo對映到值1. 將這兩行合起來,我們就得到如下完整的對映規則:當$args的值等於debug的時候,$foo變數的值就是1,否則$foo的值就為0.

明白了map指令的含義,再來看location /test. 在那裡,我們先把當前$foo變數的值儲存在另一個使用者變數$orig_foo中,然後再強行把$args的值改寫為debug,最後我們再用echo指令分別輸出$orig_foo和$foo的值。

從邏輯上看,似乎當我們強行改寫$args的值為debug之後,根據先前的map對映規則,$foo變數此時的值應當自動調整為字串1, 而不論$foo原先的值是怎樣的。然而測試結果並非如此:

$ curl `http://localhost:8080/test`

original foo: 0

foo: 0

第一行輸出指示$orig_foo的值為0,這正是我們期望的:上面這個請求並沒有提供 URL 引數串,於是$args最初的取值就是空,再根據我們先前定義的對映規則,$foo變數在第一次被讀取時的值就應當是0(即匹配預設的那個default條件)。

而第二行輸出顯示,在強行改寫$args變數的值為字串debug之後,$foo的條件仍然是0,這顯然不符合對映規則,因為當$args為debug時,$foo的值應當是1. 這究竟是為什麼呢?

其實原因很簡單,那就是$foo變數在第一次讀取時,根據對映規則計算出的值被快取住了。剛才我們說過,Nginx 模組可以為其建立的變數選擇使用值容器,作為其“取處理程式”計算結果的快取。顯然,ngx_map模組認為變數間的對映計算足夠昂貴,需要自動將因變數的計算結果快取下來,這樣在當前請求的處理過程中如果再次讀取這個因變數,Nginx 就可以直接返回快取住的結果,而不再呼叫該變數的“取處理程式”再行計算了。

為了進一步驗證這一點,我們不妨在請求中直接指定 URL 引數串為debug:

$ curl `http://localhost:8080/test?debug`

original foo: 1

foo: 1

我們看到,現在$orig_foo的值就成了1,因為變數$foo在第一次被讀取時,自變數$args的值就是debug,於是按照對映規則,“取處理程式”計算返回的值便是1. 而後續再讀取$foo的值時,就總是得到被快取住的1這個結果,而不論$args後來變成什麼樣了。

map指令其實是一個比較特殊的例子,因為它可以為使用者變數註冊“取處理程式”,而且使用者可以自己定義這個“取處理程式”的計算規則。當然,此規則在這裡被限定為與另一個變數的對映關係。同時,也並非所有使用了“取處理程式”的變數都會快取結果,例如我們前面在(三)中已經看到$arg_XXX並不會使用值容器進行快取。

類似ngx_map模組,標準的ngx_geo等模組也一樣使用了變數值的快取機制。

在上面的例子中,我們還應當注意到map指令是在server配置塊之外,也就是在最外圍的http配置塊中定義的。很多讀者可能會對此感到奇怪,畢竟我們只是在location /test中用到了它。這倒不是因為我們不想把map語句直接挪到location配置塊中,而是因為map指令只能在http塊中使用!

很多 Nginx 新手都會擔心如此“全域性”範圍的map設定會讓訪問所有虛擬主機的所有location介面的請求都執行一遍變數值的對映計算,然而事實並非如此。前面我們已經瞭解到map配置指令的工作原理是為使用者變數註冊 “取處理程式”,並且實際的對映計算是在“取處理程式”中完成的,而“取處理程式”只有在該使用者變數被實際讀取時才會執行(當然,因為快取的存在,只在請求生命期中的第一次讀取中才被執行),所以對於那些根本沒有用到相關變數的請求來說,就根本不會執行任何的無用計算。

這種只在實際使用物件時才計算物件值的技術,在計算領域被稱為“惰性求值”(lazy evaluation)。提供“惰性求值” 語義的程式語言並不多見,最經典的例子便是 Haskell. 與之相對的便是“主動求值” (eager evaluation)。我們有幸在 Nginx 中也看到了“惰性求值”的例子,但“主動求值”語義其實在 Nginx 裡面更為常見,例如下面這行再普通不過的set語句:

set$b “$a,$a”;

這裡會在執行set規定的賦值操作時,“主動”地計算出變數$b的值,而不會將該求值計算延緩到變數$b實際被讀取的時候。


前面在(二)中我們已經瞭解到變數值容器的生命期是與請求繫結的,但是我當時有意避開了“請求”的正式定義。大家應當一直預設這裡的“請求”都是指客戶端發起的 HTTP 請求。其實在 Nginx 世界裡有兩種型別的“請求”,一種叫做“主請求”(main request),而另一種則叫做“子請求”(subrequest)。我們先來介紹一下它們。

所謂“主請求”,就是由 HTTP 客戶端從 Nginx 外部發起的請求。我們前面見到的所有例子都只涉及到“主請求”,包括(二)中那兩個使用echo_execrewrite指令發起“內部跳轉”的例子。

而“子請求”則是由 Nginx 正在處理的請求在 Nginx 內部發起的一種級聯請求。“子請求”在外觀上很像 HTTP 請求,但實現上卻和 HTTP 協議乃至網路通訊一點兒關係都沒有。它是 Nginx 內部的一種抽象呼叫,目的是為了方便使用者把“主請求”的任務分解為多個較小粒度的“內部請求”,併發或序列地訪問多個location介面,然後由這些location介面通力協作,共同完成整個“主請求”。當然,“子請求”的概念是相對的,任何一個“子請求”也可以再發起更多的“子子請求”,甚至可以玩遞迴呼叫(即自己呼叫自己)。當一個請求發起一個“子請求”的時候,按照 Nginx 的術語,習慣把前者稱為後者的“父請求”(parent request)。值得一提的是,Apache 伺服器中其實也有“子請求”的概念,所以來自 Apache 世界的讀者對此應當不會感到陌生。

    下面就來看一個使用了“子請求”的例子:

location/main {

echo_location /foo;

echo_location /bar;

}

location/foo {

echo foo;

}

location/bar {

echo bar;

}

這裡在location /main中,通過第三方ngx_echo模組的echo_location指令分別發起到/foo和/bar這兩個介面的GET型別的“子請求”。由echo_location發起的“子請求”,其執行是按照配置書寫的順序序列處理的,即只有當/foo請求處理完畢之後,才會接著處理/bar請求。這兩個“子請求”的輸出會按執行順序拼接起來,作為/main介面的最終輸出:

$ curl `http://localhost:8080/main`

foo

bar

我們看到,“子請求”方式的通訊是在同一個虛擬主機內部進行的,所以 Nginx 核心在實現“子請求”的時候,就只呼叫了若干個 C 函式,完全不涉及任何網路或者 UNIX 套接字(socket)通訊。我們由此可以看出“子請求”的執行效率是極高的。

    回到先前對 Nginx 變數值容器的生命期的討論,我們現在依舊可以說,它們的生命期是與當前請求相關聯的。每個請求都有所有變數值容器的獨立副本,只不過當前請求既可以是“主請求”,也可以是“子請求”。即便是父子請求之間,同名變數一般也不會相互干擾。讓我們來通過一個小實驗證明一下這個說法:

location/main {

set$var main;

echo_location /foo;

echo_location /bar;

echo “main: $var”;

}

location/foo {

set$var foo;

echo “foo: $var”;

}

location/bar {

set$var bar;

echo “bar: $var”;

}

在這個例子中,我們分別在/main,/foo和/bar這三個location配置塊中為同一名字的變數,$var,分別設定了不同的值並予以輸出。特別地,我們在/main介面中,故意在呼叫過/foo和/bar這兩個“子請求”之後,再輸出它自己的$var變數的值。請求/main介面的結果是這樣的:

$ curl `http://localhost:8080/main`

foo: foo

bar: bar

main: main

顯然,/foo和/bar這兩個“子請求”在處理過程中對變數$var各自所做的修改都絲毫沒有影響到“主請求”/main. 於是這成功印證了“主請求”以及各個“子請求”都擁有不同的變數$var的值容器副本。

不幸的是,一些 Nginx 模組發起的“子請求”卻會自動共享其“父請求”的變數值容器,比如第三方模組ngx_auth_request. 下面是一個例子:

location/main {

set$var main;

auth_request /sub;

echo “main: $var”;

}

location/sub {

set$var sub;

echo “sub: $var”;

}

這裡我們在/main介面中先為$var變數賦初值main,然後使用ngx_auth_request模組提供的配置指令auth_request,發起一個到/sub介面的“子請求”,最後利用echo指令輸出變數$var的值。而我們在/sub介面中則故意把$var變數的值改寫成sub. 訪問/main介面的結果如下:

$ curl `http://localhost:8080/main`

main: sub

我們看到,/sub介面對$var變數值的修改影響到了主請求/main. 所以ngx_auth_request模組發起的“子請求”確實是與其“父請求”共享一套 Nginx 變數的值容器。

對於上面這個例子,相信有讀者會問:“為什麼‘子請求’/sub的輸出沒有出現在最終的輸出裡呢?”答案很簡單,那就是因為auth_request指令會自動忽略“子請求”的響應體,而只檢查“子請求”的響應狀態碼。當狀態碼是2XX的時候,auth_request指令會忽略“子請求”而讓 Nginx 繼續處理當前的請求,否則它就會立即中斷當前(主)請求的執行,返回相應的出錯頁。在我們的例子中,/sub“子請求”只是使用echo指令作了一些輸出,所以隱式地返回了指示正常的200狀態碼。

ngx_auth_request模組這樣父子請求共享一套 Nginx 變數的行為,雖然可以讓父子請求之間的資料雙向傳遞變得極為容易,但是對於足夠複雜的配置,卻也經常導致不少難於除錯的詭異 bug. 因為使用者時常不知道“父請求”的某個 Nginx 變數的值,其實已經在它的某個“子請求”中被意外修改了。諸如此類的因共享而導致的不好的“副作用”,讓包括ngx_echongx_lua,以及ngx_srcache在內的許多第三方模組都選擇了禁用父子請求間的變數共享。


Nginx 內建變數用在“子請求”的上下文中時,其行為也會變得有些微妙。

前面在(三)中我們已經知道,許多內建變數都不是簡單的“存放值的容器”,它們一般會通過註冊“存取處理程式”來表現得與眾不同,而它們即使有存放值的容器,也只是用於快取“存取處理程式”的計算結果。我們之前討論過的$args變數正是通過它的“取處理程式”來返回當前請求的 URL 引數串。因為當前請求也可以是“子請求”,所以在“子請求”中讀取$args,其“取處理程式”會很自然地返回當前“子請求”的引數串。我們來看這樣的一個例子:

location/main {

echo “main args: $args”;

echo_location /sub “a=1&b=2”;

}

location/sub {

echo “sub args: $args”;

}

這裡在/main介面中,先用echo指令輸出當前請求的$args變數的值,接著再用echo_location指令發起子請求/sub. 這裡值得注意的是,我們在echo_location語句中除了通過第一個引數指定“子請求”的 URI 之外,還提供了第二個引數,用以指定該“子請求”的 URL 引數串(即a=1&b=2)。最後我們定義了/sub介面,在裡面輸出了一下$args的值。請求/main介面的結果如下:

$ curl `http://localhost:8080/main?c=3`

main args: c=3

sub args: a=1&b=2

顯然,當$args用在“主請求”/main中時,輸出的就是“主請求”的 URL 引數串,c=3;而當用在“子請求”/sub中時,輸出的則是“子請求”的引數串,a=1&b=2。這種行為正符合我們的直覺。

$args類似,內建變數$uri用在“子請求”中時,其“取處理程式”也會正確返回當前“子請求”解析過的 URI:

location/main {

echo “main uri: $uri”;

echo_location /sub;

}

location/sub {

echo “sub uri: $uri”;

}

請求/main的結果是

$ curl `http://localhost:8080/main`

main uri: /main

sub uri: /sub

這依然是我們所期望的。

但不幸的是,並非所有的內建變數都作用於當前請求。少數內建變數只作用於“主請求”,比如由標準模組ngx_http_core提供的內建變數$request_method.

變數$request_method在讀取時,總是會得到“主請求”的請求方法,比如GET、POST之類。我們來測試一下:

location/main {

echo “main method: $request_method”;

echo_location /sub;

}

location/sub {

echo “sub method: $request_method”;

}

在這個例子裡,/main和/sub介面都會分別輸出$request_method的值。同時,我們在/main介面裡利用echo_location指令發起一個到/sub介面的GET“子請求”。我們現在利用curl命令列工具來發起一個到/main介面的POST請求:

$ curl –data hello `http://localhost:8080/main`

main method: POST

sub method: POST

這裡我們利用curl程式的–data選項,指定hello作為我們的請求體資料,同時–data選項會自動讓傳送的請求使用POST請求方法。測試結果證明了我們先前的預言,$request_method變數即使在GET“子請求”/sub中使用,得到的值依然是“主請求”/main的請求方法,POST.

有的讀者可能覺得我們在這裡下的結論有些草率,因為上例是先在“主請求”裡讀取(並輸出)$request_method變數,然後才發“子請求”的,所以這些讀者可能認為這並不能排除$request_method在進入子請求之前就已經把第一次讀到的值給快取住,從而影響到後續子請求中的輸出結果。不過,這樣的顧慮是多餘的,因為我們前面在(五)中也特別提到過,快取所依賴的變數的值容器,是與當前請求繫結的,而由ngx_echo模組發起的“子請求”都禁用了父子請求之間的變數共享,所以在上例中,$request_method內建變數即使真的使用了值容器作為快取(事實上它也沒有),它也不可能影響到/sub子請求。

為了進一步消除這部分讀者的疑慮,我們不妨稍微修改一下剛才那個例子,將/main介面輸出$request_method變數的時間推遲到“子請求”執行完畢之後:

location/main {

echo_location /sub;

echo “main method: $request_method”;

}

location/sub {

echo “sub method: $request_method”;

}

讓我們重新測試一下:

$ curl –data hello `http://localhost:8080/main`

sub method: POST

main method: POST

可以看到,再次以POST方法請求/main介面的結果與原先那個例子完全一致,除了父子請求的輸出順序顛倒了過來(因為我們在本例中交換了/main介面中那兩條輸出配置指令的先後次序)。

由此可見,我們並不能通過標準的$request_method變數取得“子請求”的請求方法。為了達到我們最初的目的,我們需要求助於第三方模組ngx_echo提供的內建變數$echo_request_method

location/main {

echo “main method: $echo_request_method”;

echo_location /sub;

}

location/sub {

echo “sub method: $echo_request_method”;

}

此時的輸出終於是我們想要的了:

$ curl –data hello `http://localhost:8080/main`

main method: POST

sub method: GET

我們看到,父子請求分別輸出了它們各自不同的請求方法,POST和GET.

類似$request_method,內建變數$request_uri一般也返回的是“主請求”未經解析過的 URL,畢竟“子請求”都是在 Nginx 內部發起的,並不存在所謂的“未解析的”原始形式。

如果真如前面那部分讀者所擔心的,內建變數的值快取在共享變數的父子請求之間起了作用,這無疑是災難性的。我們前面在(五)中已經看到ngx_auth_request模組發起的“子請求”是與其“父請求”共享一套變數的。下面是一個這樣的可怕例子:

map$uri $tag {

default     0;

/main       1;

/sub        2;

}

server{

listen8080;

location/main {

auth_request /sub;

echo “main tag: $tag”;

}

location/sub {

echo “sub tag: $tag”;

}

}

這裡我們使用久違了的map指令來把內建變數$uri的值對映到使用者變數$tag上。當$uri的值為/main時,則賦予$tag值 1,當$uri取值/sub時,則賦予$tag值 2,其他情況都賦0. 接著,我們在/main介面中先用ngx_auth_request模組的auth_request指令發起到/sub介面的子請求,然後再輸出變數$tag的值。而在/sub介面中,我們直接輸出變數$tag. 猜猜看,如果我們訪問介面/main,將會得到什麼樣的輸出呢?

$ curl `http://localhost:8080/main`

main tag: 2

咦?我們不是分明把/main這個值對映到1上的麼?為什麼實際輸出的是/sub對映的結果2呢?

其實道理很簡單,因為我們的$tag變數在“子請求”/sub中首先被讀取,於是在那裡計算出了值2(因為$uri在那裡取值/sub,而根據map對映規則,$tag應當取值2),從此就被$tag的值容器給快取住了。而auth_request發起的“子請求”又是與“父請求”共享一套變數的,於是當 Nginx 的執行流回到“父請求”輸出$tag變數的值時,Nginx 就直接返回快取住的結果2了。這樣的結果確實太意外了。

    從這個例子我們再次看到,父子請求間的變數共享,實在不是一個好主意。


(一)中我們提到過,Nginx 變數的值只有一種型別,那就是字串,但是變數也有可能壓根就不存在有意義的值。沒有值的變數也有兩種特殊的值:一種是“不合法”(invalid),另一種是“沒找到”(not found)。

舉例說來,當 Nginx 使用者變數$foo建立了卻未被賦值時,$foo的值便是“不合法”;而如果當前請求的 URL 引數串中並沒有提及XXX這個引數,則$arg_XXX內建變數的值便是“沒找到”。

無論是“不合法”也好,還是“沒找到”也罷,這兩種 Nginx 變數所擁有的特殊值,和空字串(””)這種取值是完全不同的,比如 JavaScript 語言中也有專門的undefined和null這兩種特殊值,而 Lua 語言中也有專門的nil值: 它們既不等同於空字串,也不等同於數字0,更不是布林值false. 其實 SQL 語言中的NULL也是類似的一種東西。

雖然前面在(一)中我們看到,由set指令建立的變數未初始化就用在“變數插值”中時,效果等同於空字串,但那是因為set指令為它建立的變數自動註冊了一個“取處理程式”,將“不合法”的變數值轉換為空字串。為了驗證這一點,我們再重新看一下(一)中討論過的那個例子:

location/foo {

echo “foo = [$foo]”;

}

location/bar {

set$foo 32;

echo “foo = [$foo]”;

}

這裡為了簡單起見,省略了原先寫出的外圍server配置塊。在這個例子裡,我們在/bar介面中用set指令隱式地建立了$foo變數這個名字,然後我們在/foo介面中不對$foo進行初始化就直接使用echo指令輸出。我們當時測試/foo介面的結果是

$ curl `http://localhost:8080/foo`

foo = []

從輸出上看,未初始化的$foo變數確實和空字串的效果等同。但細心的讀者當時應該就已經注意到,對於上面這個請求,Nginx 的錯誤日誌檔案(一般檔名叫做error.log)中多出一行類似下面這樣的警告:

    [warn] 5765#0: *1 using uninitialized “foo” variable, …

這一行警告是誰輸出的呢?答案是set指令為$foo註冊的“取處理程式”。當/foo介面中的echo指令實際執行的時候,它會對它的引數”foo = [$foo]”進行“變數插值”計算。於是,引數串中的$foo變數會被讀取,而 Nginx 會首先檢查其值容器裡的取值,結果它看到了“不合法”這個特殊值,於是它這才決定繼續呼叫$foo變數的“取處理程式”。於是$foo變數的“取處理程式”開始執行,它向 Nginx 的錯誤日誌列印出上面那條警告訊息,然後返回一個空字串作為$foo的值,並從此快取在$foo的值容器中。

細心的讀者會注意到剛剛描述的這個過程其實就是那些支援值快取的內建變數的工作原理,只不過set指令在這裡借用了這套機制來處理未正確初始化的 Nginx 變數。值得一提的是,只有“不合法”這個特殊值才會觸發 Nginx 呼叫變數的“取處理程式”,而特殊值“沒找到”卻不會。

上面這樣的警告一般會指示出我們的 Nginx 配置中存在變數名拼寫錯誤,抑或是在錯誤的場合使用了尚未初始化的變數。因為值快取的存在,這條警告在一個請求的生命期中也不會列印多次。當然,ngx_rewrite模組專門提供了一條uninitialized_variable_warn配置指令可用於禁止這條警告日誌。

剛才提到,內建變數$arg_XXX在請求 URL 引數XXX並不存在時會返回特殊值“找不到”,但遺憾的是在 Nginx 原生配置語言(我們估且這麼稱呼它)中是不能很方便地把它和空字串區分開來的,比如:

location/test {

echo “name: [$arg_name]”;

}

這裡我們輸出$arg_name變數的值同時故意在請求中不提供 URL 引數name:

$ curl `http://localhost:8080/test`

name: []

我們看到,輸出特殊值“找不到”的效果和空字串是相同的。因為這一回是 Nginx 的“變數插值”引擎自動把“找不到”給忽略了。

那麼我們究竟應當如何捕捉到“找不到”這種特殊值的蹤影呢?換句話說,我們應當如何把它和空字串給區分開來呢?顯然,下面這個請求中,URL 引數name是有值的,而且其值應當是空字串:

$ curl `http://localhost:8080/test?name=`

name: []

但我們卻無法將之和前面完全不提供name引數的情況給區分開。

幸運的是,通過第三方模組ngx_lua,我們可以輕鬆地在 Lua 程式碼中做到這一點。請看下面這個例子:

location/test {

content_by_lua `

if ngx.var.arg_name == nil then

ngx.say(“name: missing”)

else

ngx.say(“name: [“, ngx.var.arg_name, “]”)

end

`;

}

這個例子和前一個例子功能上非常接近,除了我們在/test介面中使用了ngx_lua模組的content_by_lua配置指令,嵌入了一小段我們自己的 Lua 程式碼來對 Nginx 變數$arg_name的特殊值進行判斷。在這個例子中,當$arg_name的值為“沒找到”(或者“不合法”)時,/foo介面會輸出name: missing這一行結果:

curl `http://localhost:8080/test`

name: missing

因為這是我們第一次接觸到ngx_lua模組,所以需要先簡單介紹一下。ngx_lua模組將 Lua 語言直譯器(或者LuaJIT即時編譯器)嵌入到了 Nginx 核心中,從而可以讓使用者在 Nginx 核心中直接執行 Lua 語言編寫的程式。我們可以選擇在 Nginx 不同的請求處理階段插入我們的 Lua 程式碼。這些 Lua 程式碼既可以直接內聯在 Nginx 配置檔案中,也可以單獨放置在外部.lua檔案裡,然後在 Nginx 配置檔案中引用.lua檔案的路徑。

回到上面這個例子,我們在 Lua 程式碼裡引用 Nginx 變數都是通過ngx.var這個由ngx_lua模組提供的 Lua 介面。比如引用 Nginx 變數$VARIABLE時,就在 Lua 程式碼裡寫作ngx.var.VARIABLE就可以了。當 Nginx 變數$arg_name為特殊值“沒找到”(或者“不合法”)時,ngx.var.arg_name在 Lua 世界中的值就是nil,即 Lua 語言裡的“空”(不同於 Lua 空字串)。我們在 Lua 裡輸出響應體內容的時候,則使用了ngx.say這個 Lua 函式,也是ngx_lua模組提供的,功能上等價於ngx_echo模組的echo配置指令。

現在,如果我們提供空字串取值的name引數,則輸出就和剛才不相同了:

$ curl `http://localhost:8080/test?name=`

name: []

在這種情況下,Nginx 變數$arg_name的取值便是空字串,這既不是“沒找到”,也不是“不合法”,因此在 Lua 裡,ngx.var.arg_name就返回 Lua 空字串(””),和剛才的 Luanil值就完全區分開了。

這種區分在有些應用場景下非常重要,比如有的 web service 介面會根據name這個 URL 引數是否存在來決定是否按name屬性對資料集合進行過濾,而顯然提供空字串作為name引數的值,也會導致對資料集中取值為空串的記錄進行篩選操作。

不過,標準的$arg_XXX變數還是有一些侷限,比如我們用下面這個請求來測試剛才那個/test介面:

$ curl `http://localhost:8080/test?name`

name: missing

此時,$arg_name變數仍然讀出“找不到”這個特殊值,這就明顯有些違反常識。此外,$arg_XXX變數在請求 URL 中有多個同名XXX引數時,就只會返回最先出現的那個XXX引數的值,而默默忽略掉其他例項:

$ curl `http://localhost:8080/test?name=Tom&name=Jim&name=Bob`

name: [Tom]

要解決這些侷限,可以直接在 Lua 程式碼中使用ngx_lua模組提供的ngx.req.get_uri_args函式。


$arg_XXX類似,我們在(二)中提到過的內建變數$cookie_XXX變數也會在名為XXX的 cookie 不存在時返回特殊值“沒找到”:

location/test {

content_by_lua `

if ngx.var.cookie_user == nil then

ngx.say(“cookie user: missing”)

else

ngx.say(“cookie user: [“, ngx.var.cookie_user, “]”)

end

`;

}

利用curl命令列工具的–cookie name=value選項可以指定name=value為當前請求攜帶的 cookie(通過新增相應的Cookie請求頭)。下面是若干次測試結果:

$ curl –cookie user=agentzh `http://localhost:8080/test`

cookie user: [agentzh]

$ curl –cookie user= `http://localhost:8080/test`

cookie user: []

$ curl `http://localhost:8080/test`

cookie user: missing

我們看到,cookieuser不存在以及取值為空字串這兩種情況被很好地區分開了:當 cookieuser不存在時,Lua 程式碼中的ngx.var.cookie_user返回了期望的 Luanil值。

在 Lua 裡訪問未建立的 Nginx 使用者變數時,在 Lua 裡也會得到nil值,而不會像先前的例子那樣直接讓 Nginx 拒絕載入配置:

location/test {

content_by_lua `

ngx.say(“$blah = “, ngx.var.blah)

`;

}

這裡假設我們並沒有在當前的nginx.conf配置檔案中建立過使用者變數$blah,然後我們在 Lua 程式碼中通過ngx.var.blah直接引用它。上面這個配置可以順利啟動,因為 Nginx 在載入配置時只會編譯content_by_lua配置指令指定的 Lua 程式碼而不會實際執行它,所以 Nginx 並不知道 Lua 程式碼裡面引用了$blah這個變數。於是我們在執行時也會得到nil值。而ngx_lua提供的ngx.say函式會自動把 Lua 的nil值格式化為字串”nil”輸出,於是訪問/test介面的結果是:

curl `http://localhost:8080/test`

$blah = nil

這正是我們所期望的。

上面這個例子中另一個值得注意的地方是,我們在content_by_lua配置指令的引數中提及了$bar符號,但卻並沒有觸發“變數插值”(否則 Nginx 會在啟動時抱怨$blah未建立)。這是因為content_by_lua配置指令並不支援引數的“變數插值”功能。我們前面在(一)中提到過,配置指令的引數是否允許“變數插值”,其實取決於該指令的實現模組。

設計返回“不合法”這一特殊值的例子是困難的,因為我們前面在(七)中已經看到,由set指令建立的變數在未初始化時確實是“不合法”,但一旦嘗試讀取它們時,Nginx 就會自動呼叫其“取處理程式”,而它們的“取處理程式”會自動返回空字串並將之快取住。於是我們最終得到的是完全合法的空字串。下面這個使用了 Lua 程式碼的例子證明了這一點:

location/foo {

content_by_lua `

if ngx.var.foo == nil then

ngx.say(“$foo is nil”)

else

ngx.say(“$foo = [“, ngx.var.foo, “]”)

end

`;

}

location/bar {

set$foo 32;

echo “foo = [$foo]”;

}

請求/foo介面的結果是:

$ curl `http://localhost:8080/foo`

$foo = []

我們看到在 Lua 裡面讀取未初始化的 Nginx 變數$foo時得到的是空字串。

最後值得一提的是,雖然前面反覆指出 Nginx 變數只有字串這一種資料型別,但這並不能阻止像ngx_array_var這樣的第三方模組讓 Nginx 變數也能存放陣列型別的值。下面就是這樣的一個例子:

location/test {

array_split “,” $arg_names to=$array;

array_map “[$array_it]” $array;

array_join ” ” $array to=$res;

echo $res;

}

這個例子中使用了ngx_array_var模組的array_split、array_map和array_join這三條配置指令,其含義很接近 Perl 語言中的內建函式split、map和join(當然,其他指令碼語言也有類似的等價物)。我們來看看訪問/test介面的結果:

$ curl `http://localhost:8080/test?names=Tom,Jim,Bob

[Tom] [Jim] [Bob]

我們看到,使用ngx_array_var模組可以很方便地處理這樣具有不定個數的組成元素的輸入資料,例如此例中的namesURL 引數值就是由不定個數的逗號分隔的名字所組成。不過,這種型別的複雜任務通過ngx_lua來做通常會更靈活而且更容易維護。

    至此,本系列教程對 Nginx 變數的介紹終於可以告一段落了。我們在這個過程中接觸到了許多標準的和第三方的 Nginx 模組,這些模組讓我們得以很輕鬆地構造出許多有趣的小例子,從而可以深入探究 Nginx 變數的各種行為和特性。在後續的教程中,我們還會有很多機會與這些模組打交道。

    通過前面討論過的眾多例子,我們應當已經感受到 Nginx 變數在 Nginx 配置語言中所扮演的重要角色:它是獲取 Nginx 中各種資訊(包括當前請求的資訊)的主要途徑和載體,同時也是各個模組之間傳遞資料的主要媒介之一。在後續的教程中,我們會經常看到 Nginx 變數的身影,所以現在很好地理解它們是非常重要的。

在下一個系列的教程,即Nginx 配置指令的執行順序系列中,我們將深入探討 Nginx 配置指令的執行順序以及請求的各個處理階段,因為很多 Nginx 使用者都搞不清楚他們書寫的眾多配置指令之間究竟是按照何種時間順序執行的,也搞不懂為什麼這些指令實際執行的順序經常和配置檔案裡的書寫順序大相徑庭。


相關文章