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

Nginx 的配置文件使用的就是一門微型的編程語言财破,許多真實世界里的 Nginx 配置文件其實就是一個一個的小程序笛厦。當然铆铆,是不是“圖靈完全的”暫且不論,至少據(jù)我觀察傀顾,它在設計上受 Perl 和 Bourne Shell 這兩種語言的影響很大襟铭。在這一點上,相比 Apache 和 Lighttpd 等其他 Web 服務器的配置記法,不能不說算是 Nginx 的一大特色了寒砖。既然是編程語言赐劣,一般也就少不了“變量”這種東西(當然,Haskell 這樣奇怪的函數(shù)式語言除外了)入撒。


熟悉 Perl隆豹、Bourne Shell、C/C++ 等命令式編程語言的朋友肯定知道茅逮,變量說白了就是存放“值”的容器璃赡。而所謂“值”,在許多編程語言里献雅,既可以是3.14這樣的數(shù)值碉考,也可以是hello world這樣的字符串,甚至可以是像數(shù)組挺身、哈希表這樣的復雜數(shù)據(jù)結(jié)構(gòu)侯谁。然而,在 Nginx 配置中章钾,變量只能存放一種類型的值墙贱,因為也只存在一種類型的值,那就是字符串贱傀。


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

set$a?"hello?world";

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


我們看到魁衙,Nginx 變量名前面有一個$符號,這是記法上的要求株搔。所有的 Nginx 變量在 Nginx 配置文件中引用時都須帶上$前綴剖淀。這種表示方法和 Perl、PHP 這些語言是相似的纤房。


雖然$這樣的變量前綴修飾會讓正統(tǒng)的Java和C#程序員不舒服纵隔,但這種表示方法的好處也是顯而易見的,那就是可以直接把變量嵌入到字符串常量中以構(gòu)造出新的字符串:

set$a?hello;

set$b?"$a,?$a";

這里我們通過已有的 Nginx 變量$a的值炮姨,來構(gòu)造變量$b的值捌刮,于是這兩條指令順序執(zhí)行完之后,$a的值是hello剑令,而$b的值則是hello, hello. 這種技術(shù)在 Perl 世界里被稱為“變量插值”(variable interpolation)糊啡,它讓專門的字符串拼接運算符變得不再那么必要拄查。我們在這里也不妨采用此術(shù)語吁津。


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

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配置指令的參數(shù)也支持“變量插值”。不過典尾,需要說明的是役拴,并非所有的配置指令都支持“變量插值”。事實上钾埂,指令參數(shù)是否允許“變量插值”河闰,取決于該指令的實現(xiàn)模塊。


如果我們想通過echo指令直接輸出含有“美元符”($)的字符串褥紫,那么有沒有辦法把特殊的$字符給轉(zhuǎn)義掉呢姜性?答案是否定的(至少到目前最新的 Nginx 穩(wěn)定版1.0.10)。不過幸運的是髓考,我們可以繞過這個限制部念,比如通過不支持“變量插值”的模塊配置指令專門構(gòu)造出取值為$的 Nginx 變量,然后再在echo中使用這個變量氨菇±芰叮看下面這個例子:

geo$dollar?{

default?"$";

}

server{

listen8080;

location/test?{

echo?"This?is?a?dollar?sign:?$dollar";

}

}

測試結(jié)果如下:

$?curl?'http://localhost:8080/test'

This?is?a?dollar?sign:?$

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


????在“變量插值”的上下文中楣责,還有一種特殊情況,即當引用的變量名之后緊跟著變量名的構(gòu)成字符時(比如后跟字母聂沙、數(shù)字以及下劃線)秆麸,我們就需要使用特別的記法來消除歧義,例如:

server{

listen8080;

location/test?{

set$first?"hello?";

echo?"${first}world";

}

}

這里及汉,我們在echo配置指令的參數(shù)值中引用變量$first的時候沮趣,后面緊跟著world這個單詞,所以如果直接寫作"$firstworld"則 Nginx “變量插值”計算引擎會將之識別為引用了變量$firstworld. 為了解決這個難題坷随,Nginx 的字符串記法支持使用花括號在$之后把變量名圍起來房铭,比如這里的${first}. 上面這個例子的輸出是:

$?curl?'http://localhost:8080/test

hello?world

set指令(以及前面提到的geo指令)不僅有賦值的功能,它還有創(chuàng)建 Nginx 變量的副作用温眉,即當作為賦值對象的變量尚不存在時缸匪,它會自動創(chuàng)建該變量。比如在上面這個例子中类溢,如果$a這個變量尚未創(chuàng)建凌蔬,則set指令會自動創(chuàng)建$a這個用戶變量露懒。如果我們不創(chuàng)建就直接使用它的值,則會報錯砂心。例如

?server{

?listen8080;

?

?location/bad?{

??????????echo?$foo;

??????}

??}

此時 Nginx 服務器會拒絕加載配置:

????[emerg]?unknown?"foo"?variable

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


????有趣的是辩诞,Nginx 變量的創(chuàng)建和賦值操作發(fā)生在全然不同的時間階段坎弯。Nginx 變量的創(chuàng)建只能發(fā)生在 Nginx 配置加載的時候,或者說 Nginx 啟動的時候译暂;而賦值操作則只會發(fā)生在請求實際處理的時候抠忘。這意味著不創(chuàng)建而直接使用變量會導致啟動失敗,同時也意味著我們無法在請求處理時動態(tài)地創(chuàng)建新的 Nginx 變量外永。


Nginx 變量一旦創(chuàng)建褐桌,其變量名的可見范圍就是整個 Nginx 配置,甚至可以跨越不同虛擬主機的server配置塊象迎。我們來看一個例子:

server{

listen8080;

location/foo?{

echo?"foo?=?[$foo]";

}

location/bar?{

set$foo?32;

echo?"foo?=?[$foo]";

}

}

這里我們在location /bar中用set指令創(chuàng)建了變量$foo荧嵌,于是在整個配置文件中這個變量都是可見的,因此我們可以在location /foo中直接引用這個變量而不用擔心 Nginx 會報錯砾淌。


下面是在命令行上用curl工具訪問這兩個接口的結(jié)果:

$?curl?'http://localhost:8080/foo'

foo?=?[]

$?curl?'http://localhost:8080/bar'

foo?=?[32]

$?curl?'http://localhost:8080/foo'

foo?=?[]

從這個例子我們可以看到啦撮,set指令因為是在location /bar中使用的,所以賦值操作只會在訪問/bar的請求中執(zhí)行汪厨。而請求/foo接口時赃春,我們總是得到空的$foo值,因為用戶變量未賦值就輸出的話劫乱,得到的便是空字符串织中。


從這個例子我們可以窺見的另一個重要特性是,Nginx 變量名的可見范圍雖然是整個配置衷戈,但每個請求都有所有變量的獨立副本狭吼,或者說都有各變量用來存放值的容器的獨立副本,彼此互不干擾殖妇。比如前面我們請求了/bar接口后刁笙,$foo變量被賦予了值32,但它絲毫不會影響后續(xù)對/foo接口的請求所對應的$foo值(它仍然是空的Gぁ)疲吸,因為各個請求都有自己獨立的$foo變量的副本。


????對于 Nginx 新手來說前鹅,最常見的錯誤之一摘悴,就是將 Nginx 變量理解成某種在請求之間全局共享的東西,或者說“全局變量”舰绘。而事實上蹂喻,Nginx 變量的生命期是不可能跨越請求邊界的



關(guān)于 Nginx 變量的另一個常見誤區(qū)是認為變量容器的生命期延赌,是與location配置塊綁定的。其實不然叉橱。我們來看一個涉及“內(nèi)部跳轉(zhuǎn)”的例子:

server{

listen8080;

location/foo?{

set$a?hello;

echo_exec?/bar;

}

location/bar?{

echo?"a?=?[$a]";

}

}

這里我們在location /foo中,使用第三方模塊ngx_echo提供的echo_exec配置指令者蠕,發(fā)起到location /bar的“內(nèi)部跳轉(zhuǎn)”窃祝。所謂“內(nèi)部跳轉(zhuǎn)”,就是在處理請求的過程中踱侣,于服務器內(nèi)部粪小,從一個location跳轉(zhuǎn)到另一個location的過程。這不同于利用 HTTP 狀態(tài)碼301和302所進行的“外部跳轉(zhuǎn)”抡句,因為后者是由 HTTP 客戶端配合進行跳轉(zhuǎn)的探膊,而且在客戶端,用戶可以通過瀏覽器地址欄這樣的界面待榔,看到請求的 URL 地址發(fā)生了變化逞壁。內(nèi)部跳轉(zhuǎn)和Bourne Shell(或Bash)中的exec命令很像,都是“有去無回”锐锣。另一個相近的例子是C語言中的goto語句腌闯。


既然是內(nèi)部跳轉(zhuǎn),當前正在處理的請求就還是原來那個雕憔,只是當前的location發(fā)生了變化姿骏,所以還是原來的那一套 Nginx 變量的容器副本。對應到上例斤彼,如果我們請求的是/foo這個接口分瘦,那么整個工作流程是這樣的:先在location /foo中通過set指令將$a變量的值賦為字符串hello,然后通過echo_exec指令發(fā)起內(nèi)部跳轉(zhuǎn)琉苇,又進入到location /bar中嘲玫,再輸出$a變量的值。因為$a還是原來的$a并扇,所以我們可以期望得到hello這行輸出趁冈。測試證實了這一點:

$?curl?localhost:8080/foo

a?=?[hello]

但如果我們從客戶端直接訪問/bar接口,就會得到空的$a變量的值拜马,因為它依賴于location /foo來對$a進行初始化渗勘。


從上面這個例子我們看到,一個請求在其處理過程中俩莽,即使經(jīng)歷多個不同的location配置塊旺坠,它使用的還是同一套 Nginx 變量的副本。這里扮超,我們也首次涉及到了“內(nèi)部跳轉(zhuǎn)”這個概念取刃。值得一提的是蹋肮,標準ngx_rewrite模塊的rewrite配置指令其實也可以發(fā)起“內(nèi)部跳轉(zhuǎn)”,例如上面那個例子用rewrite配置指令可以改寫成下面這樣的形式:

server{

listen8080;

location/foo?{

set$a?hello;

rewrite^?/bar;

}

location/bar?{

echo?"a?=?[$a]";

}

}

其效果和使用echo_exec是完全相同的璧疗。后面我們還會專門介紹這個rewrite指令的更多用法坯辩,比如發(fā)起301和302這樣的“外部跳轉(zhuǎn)”。


從上面這個例子我們看到崩侠,Nginx 變量值容器的生命期是與當前正在處理的請求綁定的漆魔,而與location無關(guān)。


前面我們接觸到的都是通過set指令隱式創(chuàng)建的 Nginx 變量却音。這些變量我們一般稱為“用戶自定義變量”改抡,或者更簡單一些,“用戶變量”系瓢。既然有“用戶自定義變量”阿纤,自然也就有由 Nginx 核心和各個 Nginx 模塊提供的“預定義變量”,或者說“內(nèi)建變量”(builtin variables)夷陋。


Nginx 內(nèi)建變量最常見的用途就是獲取關(guān)于請求或響應的各種信息欠拾。例如由ngx_http_core模塊提供的內(nèi)建變量$uri,可以用來獲取當前請求的 URI(經(jīng)過解碼骗绕,并且不含請求參數(shù))清蚀,而$request_uri則用來獲取請求最原始的 URI (未經(jīng)解碼,并且包含請求參數(shù))爹谭。請看下面這個例子:

location/test?{

echo?"uri?=?$uri";

echo?"request_uri?=?$request_uri";

}

這里為了簡單起見枷邪,連server配置塊也省略了,和前面所有示例一樣诺凡,我們監(jiān)聽的依然是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

另一個特別常用的內(nèi)建變量其實并不是單獨一個變量嘶卧,而是有無限多變種的一群變量,即名字以arg_開頭的所有變量凉袱,我們估且稱之為$arg_XXX變量群芥吟。一個例子是$arg_name,這個變量的值是當前請求名為name的 URI 參數(shù)的值专甩,而且還是未解碼的原始形式的值钟鸵。我們來看一個比較完整的示例:

location/test?{

echo?"name:?$arg_name";

echo?"class:?$arg_class";

}

然后在命令行上使用各種參數(shù)組合去請求這個/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參數(shù),也可以匹配NAME參數(shù)涤躲,抑或是Name棺耍,等等:

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

name:?Marry

class:

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

name:?Jimmy

class:

Nginx 會在匹配參數(shù)名之前,自動把原始請求中的參數(shù)名調(diào)整為全部小寫的形式种樱。


如果你想對 URI 參數(shù)值中的%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";

}

現(xiàn)在我們再看一下效果:

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

name:?hello?world

class:?9

空格果然被解碼出來了俊卤!


從這個例子我們同時可以看到,這個set_unescape_uri指令也像set指令那樣害幅,擁有自動創(chuàng)建 Nginx 變量的功能消恍。后面我們還會專門介紹到ngx_set_misc模塊。


$arg_XXX這種類型的變量擁有無窮無盡種可能的名字以现,所以它們并不對應任何存放值的容器狠怨。而且這種變量在 Nginx 核心中是經(jīng)過特別處理的,第三方 Nginx 模塊是不能提供這樣充滿魔法的內(nèi)建變量的叼风。


類似$arg_XXX的內(nèi)建變量還有不少,比如用來取 cookie 值的$cookie_XXX變量群棍苹,用來取請求頭的$http_XXX變量群无宿,以及用來取響應頭的$sent_http_XXX變量群。這里就不一一介紹了枢里,感興趣的讀者可以參考ngx_http_core模塊的官方文檔孽鸡。


需要指出的是,許多內(nèi)建變量都是只讀的栏豺,比如我們剛才介紹的$uri$request_uri. 對只讀變量進行賦值是應當絕對避免的彬碱,因為會有意想不到的后果犁河,比如:

?location/bad?{

?set$uri?/blah;

??????echo?$uri;

??}

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

????[emerg]?the?duplicate?"uri"?variable?in?...

如果你嘗試改寫另外一些只讀的內(nèi)建變量讳癌,比如$arg_XXX變量,在某些 Nginx 的版本中甚至可能導致進程崩潰珊膜。




也有一些內(nèi)建變量是支持改寫的灵奖,其中一個例子是$args. 這個變量在讀取時返回當前請求的 URL 參數(shù)串(即請求 URL 中問號后面的部分嚼沿,如果有的話 ),而在賦值時可以直接修改參數(shù)串瓷患。我們來看一個例子:

location/test?{

set$orig_args?$args;

set$args?"a=3&b=4";

echo?"original?args:?$orig_args";

echo?"args:?$args";

}

這里我們把原始的 URL 參數(shù)串先保存在$orig_args變量中骡尽,然后通過改寫$args變量來修改當前的 URL 參數(shù)串,最后我們用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 參數(shù)串,所以輸出$orig_args變量的值時便得到空爱态。而在第一次和第二次測試中谭贪,無論我們是否提供 URL 參數(shù)串,參數(shù)串都會在location /test中被強行改寫成a=3&b=4.


需要特別指出的是锦担,這里的$args變量和$arg_XXX一樣故河,也不再使用屬于自己的存放值的容器。當我們讀取$args時吆豹,Nginx 會執(zhí)行一小段代碼鱼的,從 Nginx 核心中專門存放當前 URL 參數(shù)串的位置去讀取數(shù)據(jù)理盆;而當我們改寫$args時,Nginx 會執(zhí)行另一小段代碼凑阶,對相同位置進行改寫猿规。Nginx 的其他部分在需要當前 URL 參數(shù)串的時候,都會從那個位置去讀數(shù)據(jù)宙橱,所以我們對$args的修改會影響到所有部分的功能姨俩。我們來看一個例子:

location/test?{

set$orig_a?$arg_a;

set$args?"a=5";

echo?"original?a:?$orig_a";

echo?"a:?$arg_a";

}

這里我們先把內(nèi)建變量$arg_a的值,即原始請求的 URL 參數(shù)a的值师郑,保存在用戶變量$orig_a中环葵,然后通過對內(nèi)建變量$args進行賦值,把當前請求的參數(shù)串改寫為a=5宝冕,最后再用echo指令分別輸出$orig_a和$arg_a變量的值张遭。因為對內(nèi)建變量$args的修改會直接導致當前請求的 URL 參數(shù)串發(fā)生變化,因此內(nèi)建變量$arg_XXX自然也會隨之變化地梨。測試的結(jié)果證實了這一點:

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

original?a:?3

a:?5

我們看到菊卷,因為原始請求的 URL 參數(shù)串是a=3, 所以$arg_a最初的值為3, 但隨后通過改寫$args變量,將 URL 參數(shù)串又強行修改為a=5, 所以最終$arg_a的值又自動變?yōu)榱?.


我們再來看一個通過修改$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配置塊中定義了兩個虛擬主機宝剖。第一個虛擬主機監(jiān)聽 8080 端口洁闰,其/test接口自己通過改寫$args變量,將當前請求的 URL 參數(shù)串無條件地修改為foo=1&bar=2. 然后/test接口再通過ngx_proxy模塊的proxy_pass指令配置了一個反向代理万细,指向本機的 8081 端口上的 HTTP 服務/args. 默認情況下扑眉,ngx_proxy模塊在轉(zhuǎn)發(fā) HTTP 請求到遠方 HTTP 服務的時候,會自動把當前請求的 URL 參數(shù)串也轉(zhuǎn)發(fā)到遠方赖钞。


而本機的 8081 端口上的 HTTP 服務正是由我們定義的第二個虛擬主機來提供的襟雷。我們在第二個虛擬主機的location /args中利用echo指令輸出當前請求的 URL 參數(shù)串,以檢查/test接口通過ngx_proxy模塊實際轉(zhuǎn)發(fā)過來的 URL 請求參數(shù)串仁烹。


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

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

args:?foo=1&bar=2

我們看到耸弄,雖然請求自己提供了 URL 參數(shù)串blah=7,但在location /test中卓缰,參數(shù)串被強行改寫成了foo=1&bar=2. 接著經(jīng)由proxy_pass指令將我們被改寫掉的參數(shù)串轉(zhuǎn)發(fā)給了第二個虛擬主機上配置的/args接口计呈,然后再把/args接口的 URL 參數(shù)串輸出。事實證明征唬,我們對$args變量的賦值操作捌显,也成功影響到了ngx_proxy模塊的行為。


????在讀取變量時執(zhí)行的這段特殊代碼总寒,在 Nginx 中被稱為“取處理程序”(get handler)扶歪;而改寫變量時執(zhí)行的這段特殊代碼,則被稱為“存處理程序”(set handler)。不同的 Nginx 模塊一般會為它們的變量準備不同的“存取處理程序”善镰,從而讓這些變量的行為充滿魔法妹萨。


????其實這種技巧在計算世界并不鮮見。比如在面向?qū)ο缶幊讨徐牌郏惖脑O計者一般不會把類的成員變量直接暴露給類的用戶乎完,而是另行提供兩個方法(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的“存取器”帽揪。


????這樣設計的好處是顯而易見的。類的設計者可以在“存取器”中執(zhí)行任意代碼辅斟,以實現(xiàn)所需的業(yè)務邏輯以及“副作用”转晰,比如自動更新與當前成員變量存在依賴關(guān)系的其他成員變量,抑或是直接修改某個與當前對象相關(guān)聯(lián)的數(shù)據(jù)庫表中的對應字段砾肺。而對于后一種情況挽霉,也許“存取器”所對應的成員變量壓根就不存在防嗡,或者即使存在变汪,也頂多扮演著數(shù)據(jù)緩存的角色,以緩解被代理數(shù)據(jù)庫的訪問壓力蚁趁。


????與面向?qū)ο缶幊讨械摹按嫒∑鳌备拍钕鄬苟埽琋ginx 變量也是支持綁定“存取處理程序”的。Nginx 模塊在創(chuàng)建變量時他嫡,可以選擇是否為變量分配存放值的容器番官,以及是否自己提供與讀寫操作相對應的“存取處理程序”。


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


我們前面在(二)中已經(jīng)知道酷师,像$arg_XXX這樣具有無數(shù)變種的變量群,是“未索引的”染乌。當讀取這樣的變量時山孔,其實是它的“取處理程序”在起作用,即實時掃描當前請求的 URL 參數(shù)串荷憋,提取出變量名所指定的 URL 參數(shù)的值台颠。很多新手都會對$arg_XXX的實現(xiàn)方式產(chǎn)生誤解,以為 Nginx 會事先解析好當前請求的所有 URL 參數(shù)勒庄,并且把相關(guān)的$arg_XXX變量的值都事先設置好串前。然而事實并非如此瘫里,Nginx 根本不會事先就解析好 URL 參數(shù)串,而是在用戶讀取某個$arg_XXX變量時酪呻,調(diào)用其“取處理程序”减宣,即時去掃描 URL 參數(shù)串。類似地玩荠,內(nèi)建變量$cookie_XXX也是通過它的“取處理程序”漆腌,即時去掃描Cookie請求頭中的相關(guān)定義的。


在設置了“取處理程序”的情況下阶冈,Nginx 變量也可以選擇將其值容器用作緩存闷尿,這樣在多次讀取變量的時候,就只需要調(diào)用“取處理程序”計算一次女坑。我們下面就來看一個這樣的例子:

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在英文中除了“地圖”之外匆骗,也有“映射”的意思劳景。比方說,中學數(shù)學里講的“函數(shù)”就是一種“映射”碉就。而 Nginx 的這個map指令就可以用于定義兩個 Nginx 變量之間的映射關(guān)系盟广,或者說是函數(shù)關(guān)系∥驮浚回到上面這個例子筋量,我們用map指令定義了用戶變量$foo與$args內(nèi)建變量之間的映射關(guān)系。特別地碉熄,用數(shù)學上的函數(shù)記法y = f(x)來說桨武,我們的$args就是“自變量”x,而$foo則是“因變量”y锈津,即$foo的值是由$args的值來決定的呀酸,或者按照書寫順序可以說,我們將$args變量的值映射到了$foo變量上琼梆。


現(xiàn)在我們再來看map指令定義的映射規(guī)則:

map$args?$foo?{

default?????0;

debug???????1;

}

花括號中第一行的default是一個特殊的匹配條件性誉,即當其他條件都不匹配的時候,這個條件才匹配叮叹。當這個默認條件匹配時艾栋,就把“因變量”$foo映射到值0. 而花括號中第二行的意思是說,如果“自變量”$args精確匹配了debug這個字符串蛉顽,則把“因變量”$foo映射到值1. 將這兩行合起來蝗砾,我們就得到如下完整的映射規(guī)則:當$args的值等于debug的時候,$foo變量的值就是1,否則$foo的值就為0.


明白了map指令的含義悼粮,再來看location /test. 在那里闲勺,我們先把當前$foo變量的值保存在另一個用戶變量$orig_foo中,然后再強行把$args的值改寫為debug扣猫,最后我們再用echo指令分別輸出$orig_foo和$foo的值菜循。


從邏輯上看,似乎當我們強行改寫$args的值為debug之后申尤,根據(jù)先前的map映射規(guī)則癌幕,$foo變量此時的值應當自動調(diào)整為字符串1, 而不論$foo原先的值是怎樣的。然而測試結(jié)果并非如此:

$?curl?'http://localhost:8080/test'

original?foo:?0

foo:?0

第一行輸出指示$orig_foo的值為0昧穿,這正是我們期望的:上面這個請求并沒有提供 URL 參數(shù)串勺远,于是$args最初的取值就是空,再根據(jù)我們先前定義的映射規(guī)則时鸵,$foo變量在第一次被讀取時的值就應當是0(即匹配默認的那個default條件)胶逢。


而第二行輸出顯示,在強行改寫$args變量的值為字符串debug之后饰潜,$foo的條件仍然是0初坠,這顯然不符合映射規(guī)則,因為當$args為debug時彭雾,$foo的值應當是1. 這究竟是為什么呢碟刺?


其實原因很簡單,那就是$foo變量在第一次讀取時冠跷,根據(jù)映射規(guī)則計算出的值被緩存住了南誊。剛才我們說過身诺,Nginx 模塊可以為其創(chuàng)建的變量選擇使用值容器蜜托,作為其“取處理程序”計算結(jié)果的緩存。顯然霉赡,ngx_map模塊認為變量間的映射計算足夠昂貴橄务,需要自動將因變量的計算結(jié)果緩存下來,這樣在當前請求的處理過程中如果再次讀取這個因變量穴亏,Nginx 就可以直接返回緩存住的結(jié)果蜂挪,而不再調(diào)用該變量的“取處理程序”再行計算了。


為了進一步驗證這一點嗓化,我們不妨在請求中直接指定 URL 參數(shù)串為debug:

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

original?foo:?1

foo:?1

我們看到棠涮,現(xiàn)在$orig_foo的值就成了1,因為變量$foo在第一次被讀取時刺覆,自變量$args的值就是debug严肪,于是按照映射規(guī)則,“取處理程序”計算返回的值便是1. 而后續(xù)再讀取$foo的值時,就總是得到被緩存住的1這個結(jié)果驳糯,而不論$args后來變成什么樣了篇梭。


map指令其實是一個比較特殊的例子,因為它可以為用戶變量注冊“取處理程序”酝枢,而且用戶可以自己定義這個“取處理程序”的計算規(guī)則恬偷。當然,此規(guī)則在這里被限定為與另一個變量的映射關(guān)系帘睦。同時袍患,也并非所有使用了“取處理程序”的變量都會緩存結(jié)果,例如我們前面在(三)中已經(jīng)看到$arg_XXX并不會使用值容器進行緩存竣付。


類似ngx_map模塊协怒,標準的ngx_geo等模塊也一樣使用了變量值的緩存機制。


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


很多 Nginx 新手都會擔心如此“全局”范圍的map設置會讓訪問所有虛擬主機的所有l(wèi)ocation接口的請求都執(zhí)行一遍變量值的映射計算座舍,然而事實并非如此。前面我們已經(jīng)了解到map配置指令的工作原理是為用戶變量注冊 “取處理程序”陨帆,并且實際的映射計算是在“取處理程序”中完成的曲秉,而“取處理程序”只有在該用戶變量被實際讀取時才會執(zhí)行(當然,因為緩存的存在疲牵,只在請求生命期中的第一次讀取中才被執(zhí)行)承二,所以對于那些根本沒有用到相關(guān)變量的請求來說,就根本不會執(zhí)行任何的無用計算纲爸。


這種只在實際使用對象時才計算對象值的技術(shù)亥鸠,在計算領域被稱為“惰性求值”(lazy evaluation)。提供“惰性求值” 語義的編程語言并不多見识啦,最經(jīng)典的例子便是 Haskell. 與之相對的便是“主動求值” (eager evaluation)负蚊。我們有幸在 Nginx 中也看到了“惰性求值”的例子,但“主動求值”語義其實在 Nginx 里面更為常見颓哮,例如下面這行再普通不過的set語句:

set$b?"$a,$a";

這里會在執(zhí)行set規(guī)定的賦值操作時家妆,“主動”地計算出變量$b的值,而不會將該求值計算延緩到變量$b實際被讀取的時候冕茅。


前面在(二)中我們已經(jīng)了解到變量值容器的生命期是與請求綁定的伤极,但是我當時有意避開了“請求”的正式定義腰鬼。大家應當一直默認這里的“請求”都是指客戶端發(fā)起的 HTTP 請求。其實在 Nginx 世界里有兩種類型的“請求”塑荒,一種叫做“主請求”(main request)熄赡,而另一種則叫做“子請求”(subrequest)。我們先來介紹一下它們齿税。


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


而“子請求”則是由 Nginx 正在處理的請求在 Nginx 內(nèi)部發(fā)起的一種級聯(lián)請求∏2眨“子請求”在外觀上很像 HTTP 請求串绩,但實現(xiàn)上卻和 HTTP 協(xié)議乃至網(wǎng)絡通信一點兒關(guān)系都沒有。它是 Nginx 內(nèi)部的一種抽象調(diào)用芜壁,目的是為了方便用戶把“主請求”的任務分解為多個較小粒度的“內(nèi)部請求”礁凡,并發(fā)或串行地訪問多個location接口,然后由這些location接口通力協(xié)作慧妄,共同完成整個“主請求”顷牌。當然,“子請求”的概念是相對的塞淹,任何一個“子請求”也可以再發(fā)起更多的“子子請求”窟蓝,甚至可以玩遞歸調(diào)用(即自己調(diào)用自己)。當一個請求發(fā)起一個“子請求”的時候饱普,按照 Nginx 的術(shù)語运挫,習慣把前者稱為后者的“父請求”(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指令分別發(fā)起到/foo和/bar這兩個接口的GET類型的“子請求”雇卷。由echo_location發(fā)起的“子請求”鬓椭,其執(zhí)行是按照配置書寫的順序串行處理的颠猴,即只有當/foo請求處理完畢之后,才會接著處理/bar請求小染。這兩個“子請求”的輸出會按執(zhí)行順序拼接起來翘瓮,作為/main接口的最終輸出:

$?curl?'http://localhost:8080/main'

foo

bar

我們看到,“子請求”方式的通信是在同一個虛擬主機內(nèi)部進行的裤翩,所以 Nginx 核心在實現(xiàn)“子請求”的時候资盅,就只調(diào)用了若干個 C 函數(shù)调榄,完全不涉及任何網(wǎng)絡或者 UNIX 套接字(socket)通信。我們由此可以看出“子請求”的執(zhí)行效率是極高的呵扛。


????回到先前對 Nginx 變量值容器的生命期的討論每庆,我們現(xiàn)在依舊可以說,它們的生命期是與當前請求相關(guān)聯(lián)的今穿。每個請求都有所有變量值容器的獨立副本缤灵,只不過當前請求既可以是“主請求”,也可以是“子請求”蓝晒。即便是父子請求之間腮出,同名變量一般也不會相互干擾。讓我們來通過一個小實驗證明一下這個說法:

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接口中晾嘶,故意在調(diào)用過/foo和/bar這兩個“子請求”之后侣滩,再輸出它自己的$var變量的值。請求/main接口的結(jié)果是這樣的:

$?curl?'http://localhost:8080/main'

foo:?foo

bar:?bar

main:?main

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


不幸的是,一些 Nginx 模塊發(fā)起的“子請求”卻會自動共享其“父請求”的變量值容器娇斑,比如第三方模塊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,發(fā)起一個到/sub接口的“子請求”毫缆,最后利用echo指令輸出變量$var的值唯竹。而我們在/sub接口中則故意把$var變量的值改寫成sub. 訪問/main接口的結(jié)果如下:

$?curl?'http://localhost:8080/main'

main:?sub

我們看到,/sub接口對$var變量值的修改影響到了主請求/main. 所以ngx_auth_request模塊發(fā)起的“子請求”確實是與其“父請求”共享一套 Nginx 變量的值容器苦丁。


對于上面這個例子浸颓,相信有讀者會問:“為什么‘子請求’/sub的輸出沒有出現(xiàn)在最終的輸出里呢?”答案很簡單旺拉,那就是因為auth_request指令會自動忽略“子請求”的響應體产上,而只檢查“子請求”的響應狀態(tài)碼。當狀態(tài)碼是2XX的時候蛾狗,auth_request指令會忽略“子請求”而讓 Nginx 繼續(xù)處理當前的請求晋涣,否則它就會立即中斷當前(主)請求的執(zhí)行,返回相應的出錯頁沉桌。在我們的例子中谢鹊,/sub“子請求”只是使用echo指令作了一些輸出算吩,所以隱式地返回了指示正常的200狀態(tài)碼。


ngx_auth_request模塊這樣父子請求共享一套 Nginx 變量的行為佃扼,雖然可以讓父子請求之間的數(shù)據(jù)雙向傳遞變得極為容易偎巢,但是對于足夠復雜的配置吃既,卻也經(jīng)常導致不少難于調(diào)試的詭異 bug. 因為用戶時常不知道“父請求”的某個 Nginx 變量的值政基,其實已經(jīng)在它的某個“子請求”中被意外修改了穗酥。諸如此類的因共享而導致的不好的“副作用”访锻,讓包括ngx_echo昨忆,ngx_lua峰伙,以及ngx_srcache在內(nèi)的許多第三方模塊都選擇了禁用父子請求間的變量共享累澡。


Nginx 內(nèi)建變量用在“子請求”的上下文中時拓型,其行為也會變得有些微妙尽超。


前面在(三)中我們已經(jīng)知道官撼,許多內(nèi)建變量都不是簡單的“存放值的容器”,它們一般會通過注冊“存取處理程序”來表現(xiàn)得與眾不同似谁,而它們即使有存放值的容器傲绣,也只是用于緩存“存取處理程序”的計算結(jié)果。我們之前討論過的$args變量正是通過它的“取處理程序”來返回當前請求的 URL 參數(shù)串巩踏。因為當前請求也可以是“子請求”秃诵,所以在“子請求”中讀取$args,其“取處理程序”會很自然地返回當前“子請求”的參數(shù)串塞琼。我們來看這樣的一個例子:

location/main?{

echo?"main?args:?$args";

echo_location?/sub?"a=1&b=2";

}

location/sub?{

echo?"sub?args:?$args";

}

這里在/main接口中菠净,先用echo指令輸出當前請求的$args變量的值彪杉,接著再用echo_location指令發(fā)起子請求/sub. 這里值得注意的是毅往,我們在echo_location語句中除了通過第一個參數(shù)指定“子請求”的 URI 之外,還提供了第二個參數(shù)派近,用以指定該“子請求”的 URL 參數(shù)串(即a=1&b=2)攀唯。最后我們定義了/sub接口,在里面輸出了一下$args的值渴丸。請求/main接口的結(jié)果如下:

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

main?args:?c=3

sub?args:?a=1&b=2

顯然侯嘀,當$args用在“主請求”/main中時,輸出的就是“主請求”的 URL 參數(shù)串谱轨,c=3戒幔;而當用在“子請求”/sub中時,輸出的則是“子請求”的參數(shù)串碟嘴,a=1&b=2溪食。這種行為正符合我們的直覺。


$args類似娜扇,內(nèi)建變量$uri用在“子請求”中時错沃,其“取處理程序”也會正確返回當前“子請求”解析過的 URI:

location/main?{

echo?"main?uri:?$uri";

echo_location?/sub;

}

location/sub?{

echo?"sub?uri:?$uri";

}

請求/main的結(jié)果是

$?curl?'http://localhost:8080/main'

main?uri:?/main

sub?uri:?/sub

這依然是我們所期望的。


但不幸的是雀瓢,并非所有的內(nèi)建變量都作用于當前請求枢析。少數(shù)內(nèi)建變量只作用于“主請求”,比如由標準模塊ngx_http_core提供的內(nèi)建變量$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指令發(fā)起一個到/sub接口的GET“子請求”饮睬。我們現(xiàn)在利用curl命令行工具來發(fā)起一個到/main接口的POST請求:

$?curl?--data?hello?'http://localhost:8080/main'

main?method:?POST

sub?method:?POST

這里我們利用curl程序的--data選項,指定hello作為我們的請求體數(shù)據(jù)篮奄,同時--data選項會自動讓發(fā)送的請求使用POST請求方法捆愁。測試結(jié)果證明了我們先前的預言,$request_method變量即使在GET“子請求”/sub中使用窟却,得到的值依然是“主請求”/main的請求方法昼丑,POST.


有的讀者可能覺得我們在這里下的結(jié)論有些草率,因為上例是先在“主請求”里讀瓤浜铡(并輸出)$request_method變量菩帝,然后才發(fā)“子請求”的,所以這些讀者可能認為這并不能排除$request_method在進入子請求之前就已經(jīng)把第一次讀到的值給緩存住茬腿,從而影響到后續(xù)子請求中的輸出結(jié)果胁附。不過,這樣的顧慮是多余的滓彰,因為我們前面在(五)中也特別提到過控妻,緩存所依賴的變量的值容器,是與當前請求綁定的揭绑,而由ngx_echo模塊發(fā)起的“子請求”都禁用了父子請求之間的變量共享弓候,所以在上例中,$request_method內(nèi)建變量即使真的使用了值容器作為緩存(事實上它也沒有)他匪,它也不可能影響到/sub子請求菇存。


為了進一步消除這部分讀者的疑慮,我們不妨稍微修改一下剛才那個例子邦蜜,將/main接口輸出$request_method變量的時間推遲到“子請求”執(zhí)行完畢之后:

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接口的結(jié)果與原先那個例子完全一致,除了父子請求的輸出順序顛倒了過來(因為我們在本例中交換了/main接口中那兩條輸出配置指令的先后次序)悼沈。


由此可見贱迟,我們并不能通過標準的$request_method變量取得“子請求”的請求方法姐扮。為了達到我們最初的目的,我們需要求助于第三方模塊ngx_echo提供的內(nèi)建變量$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,內(nèi)建變量$request_uri一般也返回的是“主請求”未經(jīng)解析過的 URL缚俏,畢竟“子請求”都是在 Nginx 內(nèi)部發(fā)起的惊搏,并不存在所謂的“未解析的”原始形式。


如果真如前面那部分讀者所擔心的忧换,內(nèi)建變量的值緩存在共享變量的父子請求之間起了作用恬惯,這無疑是災難性的。我們前面在(五)中已經(jīng)看到ngx_auth_request模塊發(fā)起的“子請求”是與其“父請求”共享一套變量的亚茬。下面是一個這樣的可怕例子:

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指令來把內(nèi)建變量$uri的值映射到用戶變量$tag上酪耳。當$uri的值為/main時,則賦予$tag值 1才写,當$uri取值/sub時葡兑,則賦予$tag值 2,其他情況都賦0. 接著赞草,我們在/main接口中先用ngx_auth_request模塊的auth_request指令發(fā)起到/sub接口的子請求讹堤,然后再輸出變量$tag的值。而在/sub接口中厨疙,我們直接輸出變量$tag. 猜猜看洲守,如果我們訪問接口/main,將會得到什么樣的輸出呢沾凄?

$?curl?'http://localhost:8080/main'

main?tag:?2

咦梗醇?我們不是分明把/main這個值映射到1上的么?為什么實際輸出的是/sub映射的結(jié)果2呢撒蟀?


其實道理很簡單叙谨,因為我們的$tag變量在“子請求”/sub中首先被讀取,于是在那里計算出了值2(因為$uri在那里取值/sub保屯,而根據(jù)map映射規(guī)則手负,$tag應當取值2),從此就被$tag的值容器給緩存住了姑尺。而auth_request發(fā)起的“子請求”又是與“父請求”共享一套變量的竟终,于是當 Nginx 的執(zhí)行流回到“父請求”輸出$tag變量的值時,Nginx 就直接返回緩存住的結(jié)果2了切蟋。這樣的結(jié)果確實太意外了统捶。


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


(一)中我們提到過匆绣,Nginx 變量的值只有一種類型,那就是字符串迷守,但是變量也有可能壓根就不存在有意義的值犬绒。沒有值的變量也有兩種特殊的值:一種是“不合法”(invalid)旺入,另一種是“沒找到”(not found)兑凿。


舉例說來,當 Nginx 用戶變量$foo創(chuàng)建了卻未被賦值時茵瘾,$foo的值便是“不合法”礼华;而如果當前請求的 URL 參數(shù)串中并沒有提及XXX這個參數(shù),則$arg_XXX內(nèi)建變量的值便是“沒找到”拗秘。


無論是“不合法”也好圣絮,還是“沒找到”也罷,這兩種 Nginx 變量所擁有的特殊值雕旨,和空字符串("")這種取值是完全不同的扮匠,比如 JavaScript 語言中也有專門的undefined和null這兩種特殊值,而 Lua 語言中也有專門的nil值: 它們既不等同于空字符串凡涩,也不等同于數(shù)字0棒搜,更不是布爾值false. 其實 SQL 語言中的NULL也是類似的一種東西。


雖然前面在(一)中我們看到活箕,由set指令創(chuàng)建的變量未初始化就用在“變量插值”中時力麸,效果等同于空字符串,但那是因為set指令為它創(chuàng)建的變量自動注冊了一個“取處理程序”育韩,將“不合法”的變量值轉(zhuǎn)換為空字符串克蚂。為了驗證這一點,我們再重新看一下(一)中討論過的那個例子:

location/foo?{

echo?"foo?=?[$foo]";

}

location/bar?{

set$foo?32;

echo?"foo?=?[$foo]";

}

這里為了簡單起見筋讨,省略了原先寫出的外圍server配置塊埃叭。在這個例子里,我們在/bar接口中用set指令隱式地創(chuàng)建了$foo變量這個名字悉罕,然后我們在/foo接口中不對$foo進行初始化就直接使用echo指令輸出赤屋。我們當時測試/foo接口的結(jié)果是

$?curl?'http://localhost:8080/foo'

foo?=?[]

從輸出上看,未初始化的$foo變量確實和空字符串的效果等同蛮粮。但細心的讀者當時應該就已經(jīng)注意到益缎,對于上面這個請求,Nginx 的錯誤日志文件(一般文件名叫做error.log)中多出一行類似下面這樣的警告:

????[warn]?5765#0:?*1?using?uninitialized?"foo"?variable,?...

這一行警告是誰輸出的呢然想?答案是set指令為$foo注冊的“取處理程序”莺奔。當/foo接口中的echo指令實際執(zhí)行的時候,它會對它的參數(shù)"foo = [$foo]"進行“變量插值”計算。于是令哟,參數(shù)串中的$foo變量會被讀取恼琼,而 Nginx 會首先檢查其值容器里的取值,結(jié)果它看到了“不合法”這個特殊值屏富,于是它這才決定繼續(xù)調(diào)用$foo變量的“取處理程序”晴竞。于是$foo變量的“取處理程序”開始運行,它向 Nginx 的錯誤日志打印出上面那條警告消息狠半,然后返回一個空字符串作為$foo的值噩死,并從此緩存在$foo的值容器中。


細心的讀者會注意到剛剛描述的這個過程其實就是那些支持值緩存的內(nèi)建變量的工作原理神年,只不過set指令在這里借用了這套機制來處理未正確初始化的 Nginx 變量已维。值得一提的是,只有“不合法”這個特殊值才會觸發(fā) Nginx 調(diào)用變量的“取處理程序”已日,而特殊值“沒找到”卻不會垛耳。


上面這樣的警告一般會指示出我們的 Nginx 配置中存在變量名拼寫錯誤,抑或是在錯誤的場合使用了尚未初始化的變量飘千。因為值緩存的存在堂鲜,這條警告在一個請求的生命期中也不會打印多次。當然护奈,ngx_rewrite模塊專門提供了一條uninitialized_variable_warn配置指令可用于禁止這條警告日志缔莲。


剛才提到,內(nèi)建變量$arg_XXX在請求 URL 參數(shù)XXX并不存在時會返回特殊值“找不到”逆济,但遺憾的是在 Nginx 原生配置語言(我們估且這么稱呼它)中是不能很方便地把它和空字符串區(qū)分開來的酌予,比如:

location/test?{

echo?"name:?[$arg_name]";

}

這里我們輸出$arg_name變量的值同時故意在請求中不提供 URL 參數(shù)name:

$?curl?'http://localhost:8080/test'

name:?[]

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


那么我們究竟應當如何捕捉到“找不到”這種特殊值的蹤影呢?換句話說简僧,我們應當如何把它和空字符串給區(qū)分開來呢建椰?顯然,下面這個請求中岛马,URL 參數(shù)name是有值的棉姐,而且其值應當是空字符串:

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

name:?[]

但我們卻無法將之和前面完全不提供name參數(shù)的情況給區(qū)分開。


幸運的是啦逆,通過第三方模塊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這一行結(jié)果:

curl?'http://localhost:8080/test'

name:?missing

因為這是我們第一次接觸到ngx_lua模塊狱杰,所以需要先簡單介紹一下。ngx_lua模塊將 Lua 語言解釋器(或者LuaJIT即時編譯器)嵌入到了 Nginx 核心中厅须,從而可以讓用戶在 Nginx 核心中直接運行 Lua 語言編寫的程序仿畸。我們可以選擇在 Nginx 不同的請求處理階段插入我們的 Lua 代碼。這些 Lua 代碼既可以直接內(nèi)聯(lián)在 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 里輸出響應體內(nèi)容的時候,則使用了ngx.say這個 Lua 函數(shù)屋休,也是ngx_lua模塊提供的坞古,功能上等價于ngx_echo模塊的echo配置指令。


現(xiàn)在劫樟,如果我們提供空字符串取值的name參數(shù)痪枫,則輸出就和剛才不相同了:

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

name:?[]

在這種情況下,Nginx 變量$arg_name的取值便是空字符串叠艳,這既不是“沒找到”奶陈,也不是“不合法”,因此在 Lua 里附较,ngx.var.arg_name就返回 Lua 空字符串("")吃粒,和剛才的 Luanil值就完全區(qū)分開了。


這種區(qū)分在有些應用場景下非常重要拒课,比如有的 web service 接口會根據(jù)name這個 URL 參數(shù)是否存在來決定是否按name屬性對數(shù)據(jù)集合進行過濾徐勃,而顯然提供空字符串作為name參數(shù)的值,也會導致對數(shù)據(jù)集中取值為空串的記錄進行篩選操作早像。


不過僻肖,標準的$arg_XXX變量還是有一些局限,比如我們用下面這個請求來測試剛才那個/test接口:

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

name:?missing

此時卢鹦,$arg_name變量仍然讀出“找不到”這個特殊值臀脏,這就明顯有些違反常識。此外,$arg_XXX變量在請求 URL 中有多個同名XXX參數(shù)時谁榜,就只會返回最先出現(xiàn)的那個XXX參數(shù)的值幅聘,而默默忽略掉其他實例:

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

name:?[Tom]

要解決這些局限,可以直接在 Lua 代碼中使用ngx_lua模塊提供的ngx.req.get_uri_args函數(shù)窃植。


$arg_XXX類似帝蒿,我們在(二)中提到過的內(nèi)建變量$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請求頭)。下面是若干次測試結(jié)果:

$?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不存在以及取值為空字符串這兩種情況被很好地區(qū)分開了:當 cookieuser不存在時葛超,Lua 代碼中的ngx.var.cookie_user返回了期望的 Luanil值。


在 Lua 里訪問未創(chuàng)建的 Nginx 用戶變量時延塑,在 Lua 里也會得到nil值绣张,而不會像先前的例子那樣直接讓 Nginx 拒絕加載配置:

location/test?{

content_by_lua?'

ngx.say("$blah?=?",?ngx.var.blah)

';

}

這里假設我們并沒有在當前的nginx.conf配置文件中創(chuàng)建過用戶變量$blah,然后我們在 Lua 代碼中通過ngx.var.blah直接引用它关带。上面這個配置可以順利啟動侥涵,因為 Nginx 在加載配置時只會編譯content_by_lua配置指令指定的 Lua 代碼而不會實際執(zhí)行它,所以 Nginx 并不知道 Lua 代碼里面引用了$blah這個變量宋雏。于是我們在運行時也會得到nil值芜飘。而ngx_lua提供的ngx.say函數(shù)會自動把 Lua 的nil值格式化為字符串"nil"輸出,于是訪問/test接口的結(jié)果是:

curl?'http://localhost:8080/test'

$blah?=?nil

這正是我們所期望的磨总。


上面這個例子中另一個值得注意的地方是嗦明,我們在content_by_lua配置指令的參數(shù)中提及了$bar符號,但卻并沒有觸發(fā)“變量插值”(否則 Nginx 會在啟動時抱怨$blah未創(chuàng)建)蚪燕。這是因為content_by_lua配置指令并不支持參數(shù)的“變量插值”功能娶牌。我們前面在(一)中提到過,配置指令的參數(shù)是否允許“變量插值”馆纳,其實取決于該指令的實現(xiàn)模塊诗良。


設計返回“不合法”這一特殊值的例子是困難的,因為我們前面在(七)中已經(jīng)看到厕诡,由set指令創(chuàng)建的變量在未初始化時確實是“不合法”累榜,但一旦嘗試讀取它們時,Nginx 就會自動調(diào)用其“取處理程序”灵嫌,而它們的“取處理程序”會自動返回空字符串并將之緩存住壹罚。于是我們最終得到的是完全合法的空字符串。下面這個使用了 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接口的結(jié)果是:

$?curl?'http://localhost:8080/foo'

$foo?=?[]

我們看到在 Lua 里面讀取未初始化的 Nginx 變量$foo時得到的是空字符串寿羞。


最后值得一提的是猖凛,雖然前面反復指出 Nginx 變量只有字符串這一種數(shù)據(jù)類型,但這并不能阻止像ngx_array_var這樣的第三方模塊讓 Nginx 變量也能存放數(shù)組類型的值绪穆。下面就是這樣的一個例子:

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 語言中的內(nèi)建函數(shù)split、map和join(當然菠红,其他腳本語言也有類似的等價物)第岖。我們來看看訪問/test接口的結(jié)果:

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

[Tom]?[Jim]?[Bob]

我們看到,使用ngx_array_var模塊可以很方便地處理這樣具有不定個數(shù)的組成元素的輸入數(shù)據(jù)试溯,例如此例中的namesURL 參數(shù)值就是由不定個數(shù)的逗號分隔的名字所組成蔑滓。不過,這種類型的復雜任務通過ngx_lua來做通常會更靈活而且更容易維護遇绞。


????至此键袱,本系列教程對 Nginx 變量的介紹終于可以告一段落了。我們在這個過程中接觸到了許多標準的和第三方的 Nginx 模塊摹闽,這些模塊讓我們得以很輕松地構(gòu)造出許多有趣的小例子蹄咖,從而可以深入探究 Nginx 變量的各種行為和特性。在后續(xù)的教程中付鹿,我們還會有很多機會與這些模塊打交道澜汤。


????通過前面討論過的眾多例子,我們應當已經(jīng)感受到 Nginx 變量在 Nginx 配置語言中所扮演的重要角色:它是獲取 Nginx 中各種信息(包括當前請求的信息)的主要途徑和載體倘屹,同時也是各個模塊之間傳遞數(shù)據(jù)的主要媒介之一银亲。在后續(xù)的教程中,我們會經(jīng)撑Τ祝看到 Nginx 變量的身影,所以現(xiàn)在很好地理解它們是非常重要的拍谐。


在下一個系列的教程烛缔,即Nginx 配置指令的執(zhí)行順序系列中,我們將深入探討 Nginx 配置指令的執(zhí)行順序以及請求的各個處理階段轩拨,因為很多 Nginx 用戶都搞不清楚他們書寫的眾多配置指令之間究竟是按照何種時間順序執(zhí)行的践瓷,也搞不懂為什么這些指令實際執(zhí)行的順序經(jīng)常和配置文件里的書寫順序大相徑庭。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末亡蓉,一起剝皮案震驚了整個濱河市晕翠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌砍濒,老刑警劉巖淋肾,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異爸邢,居然都是意外死亡樊卓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門杠河,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碌尔,“玉大人浇辜,你說我怎么就攤上這事⊥倨荩” “怎么了柳洋?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長叹坦。 經(jīng)常有香客問我膳灶,道長,這世上最難降的妖魔是什么立由? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任轧钓,我火速辦了婚禮,結(jié)果婚禮上锐膜,老公的妹妹穿的比我還像新娘毕箍。我一直安慰自己,他們只是感情好道盏,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布而柑。 她就那樣靜靜地躺著,像睡著了一般荷逞。 火紅的嫁衣襯著肌膚如雪媒咳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天种远,我揣著相機與錄音涩澡,去河邊找鬼。 笑死坠敷,一個胖子當著我的面吹牛妙同,可吹牛的內(nèi)容都是我干的想帅。 我是一名探鬼主播懂拾,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼祟剔!你這毒婦竟也來了限次?” 一聲冷哼從身側(cè)響起芒涡,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卖漫,沒想到半個月后费尽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡懊亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年依啰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片店枣。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡速警,死狀恐怖叹誉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闷旧,我是刑警寧澤长豁,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站忙灼,受9級特大地震影響匠襟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜该园,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一酸舍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧里初,春花似錦啃勉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至刁品,卻和暖如春泣特,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挑随。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工状您, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人镀裤。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓竞阐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親暑劝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容