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 { listen 8080; 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 配置指令的參數也支持“變量插值”。不過,需要說明的是,並非所有的配置指令都支持“變量插值”。事實上,指令參數是否允許“變量插值”,取決於該指令的實現模塊。
如果我們想通過 echo 指令直接輸出含有“美元符”($)的字符串,那么有沒有辦法把特殊的$字符給轉義掉呢?答案是否定的。不過幸運的是,我們可以繞過這個限制,比如通過不支持“變量插值”的模塊配置指令專門構造出取值為$ 的 Nginx 變量,然后再在 echo 中使用這個變量。看下面這個例子:
geo $dollar { default "$"; } server { listen 8080; 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 { listen 8080; 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 { listen 8080; location /bad { echo $foo; } }
此時 Nginx 服務器會拒絕加載配置:
[emerg] unknown "foo" variable
是的,我們甚至都無法啟動服務!
有趣的是,Nginx 變量的創建和賦值操作發生在全然不同的時間階段。Nginx 變量的創建只能發生在 Nginx 配置加載的時候,或者說 Nginx 啟動的時候;而賦值操作則只會發生在請求實際處理的時候。這意味着不創建而直接使用變量會導致啟動失敗,同時也意味着我們無法在請求處理時動態地創建新的 Nginx 變量。
Nginx 變量一旦創建,其變量名的可見范圍就是整個 Nginx 配置,甚至可以跨越不同虛擬主機的 server
配置塊。我們來看一個例子:
server { listen 8080; 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 變量的生命期是不可能跨越請求邊界的。