1. 程式人生 > >順風詳解Nginx系列—Ngx中的變量

順風詳解Nginx系列—Ngx中的變量

zhang 只有一個 前綴 oct not 包含 使用curl ali 配置文件

在計算機語言中,變量是用來存儲和表示數據的,但不同的語言表示變量的方式不同,像java語言會把變量抽象成各種類型,並且每種類型都會用一個特殊的符號表示,比如表示一個整數需要這樣:

 int age= 25;

用int去聲明age是一個變量,並且是一個表示整數的變量。

另外一種語言比如lua,在使用的時候並不需要預先聲明其類型,他可以在程序運行的時候確定變量的類型,甚至在變量前面都不需要任何關鍵字直接拿來就用,比如:

age = 25;

name = “張三”;

在沒有任何征兆的情況下就定義了兩個變量,而且該語言會動態的識別變量的數據類型。

可以看到,雖然都是變量,但不同的語言表示變量的方式且是不一樣的。既然nginx中也有變量的概念,自然也會有自己的一套變量的規則。比如nginx中可以使用set指令定義一個變量:

set $a “hello”;

可以在return指令中使用這個變量:

return 200 “$a world”;

那nginx中的變量跟其他編程語言有什麽不同?以及nginx中的變量又有那些規則?使用的時候應該註意些什麽?接下來我會用一些例子來做詳細說明。

變量表示和變量插入

nginx中變量的表示方法和真正語言的不同,它不像java語言那樣需要用一個修飾符,也不想lua語言那麽隨意。nginx使用“$”符號作為前綴來表示一個變量,並且它還有一個其它語言沒有的特性:變量可以直接插入到一個字符串中,插入後並不會改變變量的特性,並且對插入變量的個數沒有限制。比如這個例子:

location / {

set $a  “hello”;

set $b  “world”;

return 200 “$a $b”;   

}

在上面這個例子中,return這個指令可以識別出它後面字符串中的變量值,因此它的輸出結果會是這樣

curl http://127.0.0.1/

helloworld

除了直接在變量名字前加“$”符號表示一個變量外,nginx中還有另外一種形式來表示變量:在“$”符號的基礎上加上一對花括號,並把變量名放在花括號中,比如

set ${a} “hello”

set ${b} “world”

現在可能你會有一個疑問:用“$”表示變量已經很簡潔了,為什麽又要多出一對花括號?這樣豈不是更啰嗦了?而且其它語言中好像也沒什麽先例。

其實nginx引入花括號來表示變量正是為了滿足其它語言中沒有的一種變量特性-----變量插入,而設計的。

假設現在有這樣一個無聊的需求:當用戶輸入一個英語單詞後,我們會給出這個單詞的復數形式。為了使例子簡單這裏只考慮後綴是‘s’的復數單詞。下面的例子是一種實現方式:

location /{

 set $suffix “s”;

 set $word “$arg_word”;

 return 200 “The plural of theword $word is $word$suffix”;

}

這裏需要簡單說一下“$argword”這個變量,nginx以“arg”開頭的變量表示的是http請求中查詢參數中的入參,比如有一個如下的請求:

http://127.0.0.1/get?name=1&age=2

那麽在nginx就可以使用“$arg_name”獲取這個請求中入參name的值1,而用“$arg_age”獲取請求中入參age的值2。

現在我們用curl來測一下上面的例子:

curl http://127.0.0.1/?word=dog

打印結果如下:

The plural of the word dog is dogs

可以看到結果符合我們的預期。

回過頭來再仔細看一下需求我們發現需求中只有一個未知變量----一個英語單詞,而我們為了實現這個功能在nginx中用到了兩個變量,其中變量“$suffix”是一個固定值,也就是說這個變量並不是必須的,我們完全可以直接使用“s”這個字符。

在我剛接觸nginx的時候,我曾經的想法是直接在變量後邊加上字符“s”, 就像這樣:

location /{

return 200 “$arg_word plural is$arg_words”;

}

我寄希望於nginx可以自動分辨出$arg_word是個入參變量,因為在查詢參數中確實存在word這個入參,這樣在加上緊跟其後的字符‘s’這個功能就算完成了。現在想想,還真是錯的一塌糊塗。

我們用curl測試一下這個錯誤的例子,看看他會發生什麽:

curl http://127.0.0.1/?word=book

響應結果如下:

bookplural is

很明顯,nginx並沒有識別出變量“$arg_words”是“$arg_word”變量和“s”字符的組合,而是把他們當成了一個整體“words”,而請求中又沒有這個入參,因此nginx就用空字符代替了這個變量。

實際上在nginx內部對於這種查詢入參中沒有的變量值都會打一個特殊的標記:not_found,表示在查詢參數中沒有找到對應的入參,因此對應的變量值也就沒有。

簡單驗證一下是不是真的如我們所說的,這次我們使用兩個入參值來驗證一下效果:

curl “http://127.0.0.1/?word=book&words=books”

這次因為有兩個入參,所以我們需要用引號把curl後面的url引起來,然後來看一下結果:

book plural is books

好了,錯誤的例子示範完了現在看看正確的方式:使用變量的另一種表示形式-----花括號,它就是nginx專門用來處理變量和字符拼接而設計的。

location /{

return 200 “$arg_word plural is${arg_word}s”;

}

驗證一下:

curl http://127.0.0.1/?word=book

book plural is books

這樣看起來是不是簡潔了很多?

我們上面一直在說nginx是支持變量插入的,我們舉的例子也確實如此,但就此得出nginx支持變量插入的結論其實是不嚴謹的。因為nginx是一個高度模塊化的程序軟件,是不是支持這種變量插入的形式其實完全區取決於每個模塊具體實現,我們上面提到的set和return兩個指令都屬於同一個nginx模塊----ngx_http_rewrite,該模塊確實又賦予了這兩個指令支持變量插入的功能,所以我們就看到了上面的效果。其它模塊是不是支持這種特性其實是不確定的,等後續把nginx中變量是如何實現的闡述完畢後讀者就會有一個更清晰的認識,這裏就不再展開了。

表示變量的有效字符

在大部分語言中並不是所有的字符都可以用來表示變量名,一般會有一個範圍限制。nginx對表示變量名的字符也是有規定的,nginx中僅允許四種類型的字符或他們的組合做為變量名,分別是大寫字母(A-Z)、小寫字母(a-z)、數字(0-9)、下劃線(_),其它都是非法的。

我們用一個無聊的例子來驗證一下:

location / {

set $0101   “我是0101”;

set  $_0_1_4  “我是_0_1_4”;

set $A_a_0   “我是A_a_0”;

return200  “$0101  $_0_1_4 $A_a_0”;

}

用curl訪問一下這個資源看看:

curl http://127.0.0.1/

我是0101 我是_0_1_4 我是A_a_0

可以看到這些變量名看上去奇奇怪怪,但它們確實做到了正確的輸出。

那如果在配置文件中出現了不是上面提到的四種字符nginx是如何應對的呢?不妨用一個例子看驗證一下:

location /{

set  $變量  “我是變量”;

}

當我們試圖啟動nginx的時候發現是可以正常啟動的,此時你可能開始懷疑之前說的變量的四種字符限定類型是錯誤的,因為nginx似乎並沒有認為這是一個非法的變量名,但事實真的是這樣嗎?

現在我們對這個例子稍微改動一下,為它加上一個return指令再看看是什麽效果,這次我們在這個例子的左邊標上行號:

40: location/ {

41: set $變量 “我是變量”;

42: return 200 “$變量”;

43: }

此時當我們再次試圖啟動nginx的時候你會發現nginx根本無法啟動,並且會打印一條日誌:

nginx: [emerg] invalid variable name in /path/conf/nginx.conf:42

意思是說在nginx.conf配置文件中有一個無效的變量名,根據行號可以看到正是我們剛加上的return指令的位置。

從表面看我們似乎可以得出這樣一個結論:set指令在nginx的啟動階段不會校驗變量的有效性,只有return指令才會校驗其有效性。遺憾的是這樣的結論仍然是錯誤的,我們用一個例子來反駁一下這個錯誤結論:

40: location / {

41: set $變量 “我是變量”;

42: set $a “$變量”;

43: }

在這裏例子中我們去掉了return指令,用另一個set指令取而代之。此時我們再次試圖啟動ngnx的時候發現nginx仍然無法啟動成功,並且跟用return指令時一樣,後臺打印了一條同樣的日誌:

nginx: [emerg] invalid variable namein /path/conf/nginx.conf:42

同樣的行數,同樣的錯誤。同樣都是set指令,但只有42行的set指令被提示出錯誤。

把這兩個報錯的指令拿過來跟沒報錯的指令對比一下:

 set  $a     “$變量”;

 return 200  “$變量”;

 set  $變量  “我是變量”;

可以看到兩個報錯的指令都是在使用“$變量”這個變量,而不報錯的指令且是在定義這個變量,這其實就是nginx內部用來檢驗變量名是否合法的策略。只有某個變量在真正被使用的時候nginx才會檢查變量名的合法性,比如set指令中的為定義的變量賦值就是一種“使用”,而被定義的變量不能叫“使用”;再比如像return指令這樣的行為,它沒有發生任何變量定義行為,所以這種也叫“使用”。

你以為這樣就結束了嗎?咱們再看一個例子:

location / {

set  $arg_變量 “我是變量”;

return  200  “$arg_變量”;

}

這個例子使用了中英文混合字符作為變量,此時我們試圖啟動nginx的時候發現nginx不但可以正常啟動的,而且還可以正常訪問:

curl http://127.0.0.1/

變量

此時你可能懷疑我們上面剛剛結論又是錯誤,但是先別急。再仔細看看輸出結果我們會發現,這並不是一個我們想要的結果,我們想要的正確結果應該是輸出“我是變量”這個四個漢字,但是這個例子且少了兩個字。

出現這種情況其實是因為涉及到了nginx中的動態變量,動態變量和非變量字符混合到一起後的效果讓我們產生了一種變量名可以是中文字符的錯覺,我們的結論其實是沒有錯的。

關於動態變量會在後面的小節中詳細的講解,讀者可以先保留這個疑問繼續向下看,或者暫停一下自己去研究一下出現這種情況的原因。

內置變量和自定義變量

幾乎所有的編程語言在使用變量前都需要先定義,即使像前面介紹的lua那樣“隨便”的語言,在變量使用前都需要先定義並初始化以下,比如:

age = 25;

print(age);

25

那如果不定義它會發生什麽呢?直接打印看看是什麽效果:

>print(name);

nil

看,它沒有報錯,而是直接返回了一個字符串“nil”,該字符類似於其它語言中的空值,也就是說lua把未定義的變量設置成了空值。當然了,像java、c等這種編程語言對這種情況也會有自己的處理方式,比如當他們遇到了一個未定義的變量時候在編譯階段就會直接給你“懟”回去,直接告訴你編譯不通過。

那麽在nginx中是如何處理這種情況的呢?我們在nginx.conf中搞一個未定義的變量試試,看看nginx會做什麽反應:

location / {

return  200  “$a”;

}

當啟動nginx的時候會發現,nginx又是無法啟動,並且會打印一條日誌:

nginx: [emerg] unknown "a"variable

意思是說我nginx不認識變量a。仔細分析一下這句話會發現這裏有一個隱含信息,那就是起碼nginx承認這是一個變量,只不過它不認識這個變量。這個提示跟上面我們使用“$變量”這個中文字符定義變量時提示的信息是不一樣的,之前直接提示這是一個無效的變量,相同的地方是這兩種使用變量的方式都會導致nginx無法正常啟動。

因此我們得出結論nginx中的變量在使用之前也是需要預先定義的。在有些語言中當你使用了未定義的變量後可能是編譯無法通過,而在nginx則會導致nginx無法正常啟動。

在nginx中變量的定義又分了兩種:一種是自定義變量,就是上面用set指令設置的變量,它會在配置文件中明確指出這是一個被定義的變量。另外是內置變量,它在nginx啟動之前就已經被設置好了,不需要在配置文件中明確定義。

但是要註意,並不是說自定義變量就一定要使用set指令,nginx中可以自定義變量的模塊有很多,之所以一直在用set指令講解變量,是因為我希望讀者把更多的註意裏放到變量本身上來,盡量避免為了說明一個問題而又引入其它額外的問題,比如我們下面要用到的geo模塊。

ngx_geo模塊是nginx的自帶的一個標準模塊,該模塊只包含一個指令geo,作用是根據客戶端ip來定義一個變量,比如下面的例子:

http {

geo $a {

 default   “我是geo默認值”;

 127.0.0.1  “客戶端ip是127.0.0.1”;

}

location / {

 return 200 “$a”;   

}

}

我們用curl訪問以下這個資源看看效果:

curl http://127.0.0.1/

客戶端ip是127.0.0.1

可以看到變量$a的值變成了geo指令中設定的值。

同樣是定義變量,geo指令跟set指令且有很大的不同,比如指令的放置位置,set指令可以放在location塊中,而geo指令則只能放在http塊中。

另外一個顯著的不同是set指令定義的變量值是一個字符串形式,而geo定義的變量值則需要使用花括號括起來,並且該指令內部還隱含的做了邏輯判斷。比如如果客戶端ip地址是127.0.0.1則該變量值是“客戶端ip是127.0.0.1”,如果不是則就是默認值“我是geo默認值”。

默認情況下geo指令會自己獲取客戶端的ip,然後根據相應的配置去映射變量,但其實它也可以接收一個指定ip,比如下面的例子:

geo $arg_name $a {

  default      “我是geo默認值”;

  127.0.0.1    “我是張三”;

  192.168.1.1 “我是李四”;

}

location / {

return 200 “$a”;

}

驗證一下看看效果:

curl http://127.0.0.1/?name=127.0.0.1

我是張三

curl http://127.0.0.1/?name=192.168.1.1

我是李四

把入參name去掉再看看效果:

curl http://127.0.0.1/

我是geo默認值

這裏既然用到ngx_geo模塊,那我們就回過頭來在看看之前提到的變量插入的問題,之前說過並不是所有的模塊都支持變量插入的,ngx_geo就是這樣一個模塊。在geo指令中的花括號中是沒有變量這一說的,在geo的花括號中放入的變量只會原樣展示,比如下面的例子

geo $a {

  default      “我是geo默認值 $arg_name”;

  127.0.0.1    “我是張三 $arg_name”;

}

location / {

return 200 “$a”;

}

當你試圖用一個帶著name參數的請求訪問這個locaiton的時候,它會把花括號中對應的值原樣輸出:

curl http://127.0.0.1

我是張三$arg_name

除了自定義變量,nginx中的另一種變量就是內置變量了,內置變量在nginx啟動之前就已經被設置好了,不需要在配置文件中明確定義。

來看一個內置變量的例子:

location /{

return200 “$uri”

}

按照我們目前的知識,基於上面的配置nginx應該無法啟動才對,因為在配置文件中我們沒有對變量“$uri”做定義,但事實上它不但可以啟動成功,而且還可以很好的工作,用curl檢測一下:

curl http://127.0.0.1/abc

打印結果如下:

/abc

這其實就是因為變量“$uri”是一個內置變量,他在nginx內部已經提前定義好了。

另外內置變量也是分模塊的,每個模塊都可以有自己的內置變量,比如$uri這個內置變量就屬於ngx_http_core這個http核心模塊中的變量,關於這個模塊的其它內置變量讀者可以關註nginx的官方文檔:

http://nginx.org/en/docs/http/ngx_http_core_module.html#variables 

變量的可見性

nginx中變量的另一個比較奇特的地方是每一個變量都是全局可見的,但它又不是全局變量。所謂全局可見,是指不管變量定義在配置文件的哪個地方,它在整個配置文件中都是可見的,但這個並不表示他是全局變量。

上面這句話的描述可能還是比較抽象,舉個例子:

location/a {

return200 “I am $a”;

}

location/b {

set $a “b”;

return 200 “I am $a”;

}

在這個例子中第一個location中的變量“$a”既不是自定義變量也不是內置變量,按照目前了解到的知識,nginx應該是無法啟動的。

而第二個location中可以看到用set指令定義了一個變量“$a”,從語法上看這是一個合法的配置,所以它是可以正常啟動的。那如果把這兩個location放在同一個配置文件中,nginx是不是可以正常啟動呢?

答案是肯定的,原因就是nginx中的變量是全局可見的,第一個location中的變量“$a”看到了第二個location中對它的定義。那它又不是全局變量又是怎麽回事呢?我們用curl訪問以下第二個location:

curl http://127.0.0.1/b

打印結果是:

I am b

這個結果應該是毫無疑問的。

現在不確定的應該是訪問第一個location的時候應該出現什麽結果,如果變量“$a”是一個全局變量,那很顯然它的值應該也是“b”。但它不是全局變量,那應該是什麽值呢?用curl測試一下:

curl http://127.0.0.1/a

打印結果是:

 I am

從表面上看此時變量“$a”應該是空字符或者空格之類非可見性字符,但是因為在當前的例子中,變量“$a”的前後不存在可見的字符,導致沒辦法區分此時變量“$a”到底是個什麽內容。

現在我們把第一個locaiton例子稍微改動一下:

location /a {

return 200 “I am -->$a<--”;

}

在變量前後都加入了可視的字符,然後再用curl測驗一下:

curl http://127.0.0.1/a

結果如下:

I am --><--

通過結果可以推斷出變量“$a”變成了一個空字符,這個現象其實間接的說明了變量“$a”在nginx並不是一個全局變量,因為它沒有打印出b這個字符。

另外通過後臺日誌可以看到如下一條相關的日誌信息:

[warn] 1733#0: *3 using uninitialized "a" variable,

(這條日誌只是節選了跟當前變量相關的信息)

日誌說nginx正在使用一個未初始化的變量,該變量的名字是a。從這條日誌看nginx中的變量也有初始化這個概念。從變量“$a”的打印結果看nginx會把未初始化的變量設置為空字符。

關於空字符,我們這裏不妨再弄一個小插曲。變量變成空字符我們之前說過,nginx會把請求入參中不存在的變量也當成空字符對待,比如這樣一個配置:

location / {

return 200“$arg_words”;

}

如果我們請求這個locaiton的時候不帶words這個請求入參,那麽該locaiton就會打印出空字符。但它跟我們這裏提到的變量“$a”有所不同,他不會有相應的日誌打出,它只是在nginx內部打了一個標記----not found,這個標記用戶是看不到的。所以雖然同是空字符,但它們在nginx內部且有不同的含義,一個是未初始化,一個是未找到(not found)。

通過以上闡述,大多數讀者可能對變量的全局可見性有了一個較清晰的認識,但對全局可見的同時又不是“全局變量”這個概念可能還會有點模糊,其實這個又涉及到了變量的隔離性問題,變量隔離性這個概念我單獨抽出了一個小節來介紹,等後續看完這個小節後讀者應該就會對這個概念有一個更清晰的認識,本小節就不再贅述了。

動態內置變量

在之前的小節中有用到“$uri”這個變量來說明內置變量,但是並沒有提到內置變量的另外一種形式,即動態內置變量。這裏所謂“動態”指的是變量的名字是不確定的,這個不確定性發生在nginx的運行過程中。比如對一個http請求,同一個請求可以有不同的查詢參數,而查詢參數的不同又可以返回不同的結果,舉個例子,有如下一個查詢功能:

/query?name=xxx

/query?age=yyy

該查詢功能有兩個入參,一個是name,一個是age,當僅有name的時候返回所有名字是xxx的人;而當僅有age的時候返回所有年齡是yyy的人;當兩個參數都存在的時候返回的是名字是xxx且年齡是yyy的人。當請求實際發生的時候,在nginx內部肯定可以解析出所有的查詢入參和對應的值的,但是在配置文件中如何得個這個入參的值就比較費勁,有人可能會說可以直接把入參名字做成內置變量名,比如像如下這樣

location /query {

return 200 “$name and $age”;

}

看起來問題迎刃而解,可問題是nginx需要內置多少這種內置變量呢?

http中的查詢參數是一個自定義行為,每個使用者都可以隨意決定自己請求中的查詢參數,即便同一個功能,有著同樣意義的查詢參數,查詢參數的實際值也可以不一樣。比如上面的例子,完全可以把兩個查詢參數name和age替換為n和a,按照這種變化程度,nginx根本不可能完全猜測出用戶對查詢參數的定義,所以這種方案是行不通的。

nginx的解決方案是使用前綴的方式來表示http模塊中各種動態內置變量,比如上面例子中的兩個查詢入參name和age,可以分別用arg_name和arg_age來表示其對應的變量,而arg_就是查詢參數中某個入參的變量前綴。如此一來nginx只需要在內部內置一個以arg_開頭的規則就可以方便的表示這類數據了。

目前在nginx的http模塊中有六種內置動態變量,分別是“http_”、“senthttp”、“upstreamhttp”、“upstreamcookie”、“cookie”,“arg_”。其中以“upstream”開頭的動態變量需要涉及到額外的知識,為了不分散讀者的註意力這裏就不再介紹了,本小節主要介紹一下其它四種內置動態變量。

以“http”開頭的動態內置變量可以表示http請求過程中的任意請求頭,使用的過程中不區分大小寫,並且請求頭中如果有“-”字符需要用“”字符替代。

我們先用curl去訪問以下nginx的官方文檔頁,來看看請求過程中都發送了哪些請求頭:

curl http://nginx.org/en/dosc/ -v

去掉其它部分,只保留請求頭部分,打印結果如下:

GET /en/docs HTTP/1.1

User-Agent: curl/7.19.7(x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18libssh2/1.4.2

Host: nginx.org

Accept: /

可以看到有三個請求頭,根據nignx的規則,在配置文件中獲取這三個請求頭值只需要在對應的請求頭名字前加上“http_”前綴就可以了,示例如下:

location / {

return 200 “User-Agent: $http_user_agent ”;

}

用curl測是結果如下:

curl http://127.0.0.1/

 User-Agent: curl/7.19.7(x86_64-redhat-linux-gnu) libcurl/7.19.7    NSS/3.14.0.0 zlib/1.2.3 libidn/1.18libssh2/1.4.2

同樣如果想獲取其它兩個請求頭,使用$http_host和$http_accept就可以了。

以“senthttp”開頭的動態內置變量可以表示http響應過程中的任意響應頭,規則跟“http_”動態內置變量一樣。

使用如下的配置來看一下響應過程中包括哪些響應頭:

location /a {

return 200 “test senthttp”;

}

用帶-v參數的curl訪問該資源

curl http://127.0.0.1/a -v

僅保留響應頭部分:

< Server: nginx/1.9.4

< Date: Sat, 21 Apr 2018 09:04:36 GMT

< Connection: keep-alive

現在來看一個在配置文件中使用connection這個響應頭的一個例子:

location /a {

return 200 “I am $sent_http_connection”;

}

用curl測試一下,結果如下:

curl http://127.0.0.1/a

I amkeep-alive

看起來一切都很順暢,貌似不管哪個響應頭,加上對應的前綴就可以輕而易舉的獲取。那我們再換一個響應頭驗證一下,看看能不能獲取content_length這個頭的值:

location/a {

return 200 “I am $sent_http_content_length”;

}

用curl驗證一下:

curl http://127.0.0.1/a

I am

結果並沒有像我們預想的那樣,content_length這個響應頭的內容不見了,翻閱nginx文檔好像也沒有不妥的地方,白紙黑字寫上的規則怎麽說不行就不行了呢?此時你可能懷疑是nginx的bug,遺憾的是並不是這樣的。

出現這種現象是因為涉及到了nginx中http模塊階段執行的模式。實際運行的時候nginx把整個請求過程分成了多個階段,各個階段對應完成不同的功能,我們這裏出現的情況是因為return這個指令對應的階段運行時,用來設置content_length這個響應頭內容的階段還沒有執行,所以出現了該響應頭內容“不見了”的情況。

關於http模塊的階段執行會在後續的文章中做詳細的介紹,此時讀者有這麽一個概念就可以。目前讀者只需要知道,雖然nginx提供了這種動態獲取變量值得功能,但並不是在任何時候都能取到這個值的就行了。

以“cookie_”開頭的動態內置變量可以表示http請求過程中的某個cookie值。需要和“$http_cookie”這個內置變量(非動態內置變量)區分一下,它代表請求中整個cookie值,比如:

location/a {

return 200 “cookie:$http_cookie”;

}

使用curl模擬一下帶cookie的請求:

curl http://127.0.0.1/a -H “cookie:a=b;b=c”

cookie:a=b;b=c

可以看到它把cookie名字是a和cookie名字是b的值都打印出來了。

而以“cookie_”開頭的變量則代表某個實際cookie值,比如“$cookie_a”代表本次請求中cookie名字是a的對應值,一個獲取某個cookie值的例子:

location/a {

return200  “ cookie b 的值是 [$cookie_b]”

}

使用curl模擬帶cookie的請求:

curl http://127.0.0.1/a -H “cookie:a=b;b=哈哈哈”

輸出結果如下:

cookie b 的值是 [哈哈哈]

最後一個是以“arg”開頭的動態內置變量,用法跟以“cookie”開頭的變量類似,就不再贅述了。這裏需要說的是在“變量的有效字符”小節中用到的“$arg_變量”這個變量,之前的配置例子是這樣的:

location / {

set  $arg_變量 “我是變量”;

return  200  “$arg_變量”;

}

這個配置輸出的結果是“變量”而非我們認為的“我是變量”,這是因為“$arg變量”並不是一個變量,而是變量“$arg”和文本字符“變量”的一個拼接。在nginx中變量“$arg_”不代表任何入參值,它會被nginx轉換成空字符,所以最終結果就是一個文本“變量”。

可變變量和不可變變量

大部分編程語言在聲明變量時一般會有特定的修飾符來標識變量是否可變,比如java中的“final”修飾符和C中的“const”修飾符。如果某個變量在聲明時加上了這些修飾符,那麽它在後續是無法再被修改的,這體現了變量的不可變性。

nginx中的變量也存在可變和不可變之分,但是它並沒有顯著的修飾符,所以從表面上你根本看不出來該變量是否可變。不過nginx在啟動過程中提供了一個自檢查機制,當在配置文件中試圖修改一個不可變變量時,nginx是不會順利啟動的。通過這種機制可以間接的判斷某個變量是否可變,這種機制我們在前面已經體驗過好多次,其實也算是nginx的一種自我保護機制,盡早發現錯誤盡早制止錯誤。

前面講自定義變量的時候涉及到了兩個指令set和geo,現在我們來看看用set指令定義的變量是否可改變,一個例子如下:

location/a {

set $a“old a”;

set $a “new a”;

return 200 “$a”;

}

用curl測驗一下:

curl http://127.0.0.1/a

new a

再看一個geo指令的例子:

http {

geo $a {default “old a”}

geo $a {default “new a”}

location /a {

 return 200 “$a”;

}

}

用curl測驗一下:

curl http://127.0.0.1/a

new a

兩個例子都可以成功啟動並打印數據,因此我們判定這兩個指令定義的變量可以被改變。

記住上面的這個結論,然後咱們再看一個例子:

location/a {

set $host “I am host”;

return 200 “$host”;

}

nginx啟動失敗,並且打印了一條錯誤日誌:

nginx: [emerg] the duplicate "host"variable in /path/conf/nginx.conf:49

看到這種結果你可能開始懷疑剛剛得出的結論似乎又是錯誤的。查閱nginx文檔會發現“$host”這個變量是http核心模塊中的一個內置變量,此時你可能會猜測nginx中的內置變量是不可以改變的。為了驗證這個結論我們再找一個內置變量驗證一下:

location /a {

set $args “I am querystring”;

return 200 “$args”;

}

例子中的“$args”是一個內置變量,表示請求中的查詢參數。當我們試圖啟動nginx的時候發現完全沒有問題,而且用curl也可以正確訪問,這時候你可能已經懵了,感覺nginx變量的這些行為毫無章法。

實際上這個問題的答案僅從做實驗和文檔上是找不到的,只能從代碼上一窺究竟,不過我不打算帶著讀者讀代碼,後面會有專門的文章來介紹變量在代碼層的實現,這裏簡單說一下原理:

nginx中每個變量在被定義的時候都會打上一個是否可以被改變的標記,然後把放到一個容器中,當後續有人試圖再次定義用一個變量的時候,nginx會首先從這個容器中查找這個變量,如果找到相同的變量則需要判斷容器中的變量是否存在可改變的標記,如果有則定義的變量會把容器中的變量覆蓋掉,如果沒有則返回錯誤並終止nginx啟動。

另一個要註意的是http模塊中的內置變量放入該容器中的時機,內置變量要先於“set”或“geo”指令,如果某個內置變量被打上了不可改變的標記,後續其它指令就無法再定義相同名字的變量了。

目前nginx的核心http模塊中幾乎所有內置變量都是不可改變的,只有“$args”和“$limit_rate”這兩個內置變量可以被改變。

另外由於http模塊的動態內置變量並不會把自己放入到容器中,所以它看起來是可以被改變的,比如:

location/a {

set $arg_a “I am a”;

return 200 “$arg_a”;

}

用curl驗證下:

curl http://127.0.0.1/a?a=b

I am a

可以看到這個包含了一個內置變量的例子可以正常啟動,並且輸出了數據,

所以關於大部分內置變量不可改變這個結論,似乎需要再加上一條:除動態內置變量外。

實際上是因為動態變量被重新定義後它就不再是動態變量了,它之所以不再是動態變量,那是因為動態變量的“定義”發生在所有內置變量和自定義變量之後。在nginx中,一旦某個變量被認定為自定義或內置變量,後續就不會再被賦予動態變量的特性。

比如例子中的“$arga”,其實已經變成了一個自定義變量,相應的動態變量特征也就不存在了,但其它以“$arg”開頭的變量仍然是動態變量。

可緩存變量和不可緩存變量

nginx中所有的變量在定義的時候都會被關聯上一個get_handler()方法,所有變量在第一次獲取值的時候,都是通過這個handler方法獲取的,後續再次獲取變量值的時候,是否仍然調用該handler方法則取決於該變量是否可以被緩存。

不可緩存的變量在獲取值的時候都是實時計算的,比如“$arg_”開頭的動態變量,每次獲取值的時候都會從查詢參數中重新解析對應的值;而可以緩存的變量並不會每次都調用這個handler方法,在它的整個生命周期中,如果這個變量沒有被刷新過,那麽自始至終只會調用一次。

nginx中用set指令定義的變量都是可以緩存的,但set指令不會改變已有變量的緩存特性(比如內置變量,但動態變量除外),而所有以“arg_”開頭的動態變量都是不可緩存的,這兩種變量結合在一起的時候會產生一種有意思的現象,來看一個簡單的例子:

location/a {

set $a “$arg_name”;

return 200 “$a = $arg_name”;

}

用curl測試一下:

curl http://127.0.0.1/a?name=zhangsan

zhangsan =zhangsan

這個結果看起來並沒有超出我們的預期,跟變量是不是可以緩存好像也沒啥關系。

下面我們把這個例子稍微改造一下,改成如下形式:

location /a {

set $a “$arg_name”;

set $args “name=lisi”;

return 200 “$a = $arg_name”;

}

再次用curl測一下:

curl http://127.0.0.1/a?name=zhangsan

zhangsan = lisi

這時候我們可以看到,“$a”和“$arg_name”這兩個變量雖然都是在表示入參name的值,但是且輸出了不同的結果。

這其實就是變量是否可緩存的特性引起的,因為變量“$a”是一個可緩存的變量,當被設置後變量值就被保存下來了;而“$arg_name”是一個不可被緩存的變量,每次獲取該值的時候都會調用其對應的handler方法。

我們看到第一次調用的時候查詢參數值是“name=zhangsan”,這個值被賦值給了變量“$a”,在第二次獲取該變量值之前,我們把查詢參數改成了“name=lisi”,當它再次調用對應的handler方法的時候獲取到的值就變成了“lisi”。

動態內置變量此時仍然是一個特殊的存在,我們之前說過,動態變量被重新定義後它就不再是動態變量了,所以它也就不再保有不可緩存的特性,看個例子就知道了:

location /a {

set $arg_name “$arg_name”;

set $a “$arg_name”;

set $args “name=lisi”;

return 200 “$a = $arg_name”;

}

用跟上面同樣的入參訪問以下該location:

curl http://127.0.0.1/a?name=zhangsan

zhangsan =zhangsan

可以看到這兩個變量的值又一樣了。其實原因很簡單,用set指令重新定義“$arg_name”後它就不再是動態變量了,它原本的不可緩存特性也就不存在了,所以此時查詢參數的更改對他也就不起任何作用。

變量的隔離性

nginx中變量的隔離性類似於其它編程語言中變量的作用域,但它又不像其它語言那樣有全局和局部變量之分。nginx中的變量隔離是基於請求的,同一個變量在不同的請求中毫無關系,即A請求不會讀到(或改變)B請求中的變量值,B也不會讀到(改變)A的,比如下面一個例子:

server {

set $a “$uri”;

location /a {

 return 200 “I am $a”;

}

location /b {

 return 200 “I am $a”;

}

}

我們在server塊定義了一個看似是“全局變量”的“$a”,如果它有全局性,那麽訪問上面的兩個location的時候肯定會得到相同的值,但nginx中不是這樣的。

在nginx中兩個location都可以看到這個變量“$a”,這體現了nginx變量的全局可見性;但兩個location看到的變量值確實是不一樣的,這體現了隔離性。用curl驗證一下結論是否正確:

curl http://127.0.0.1/a

/a

curl http://127.0.0./b

/b

可以看到結果跟預期一致。

在同一個請求中nginx的變量是有全局性的,但僅限於當前請求中。不管變量的更改發生在配置文件的哪個位置,在同一個請求中都可以被看到,看下面一個例子:

server {

set $a “server”;

location / {

     set $a “location”;

     if ($uri) {

        set $a “if”;

     }

     return 200 “$a”;

}

}

從上面的例子可以看到,變量“$a”被更改了三次。因為“$uri”總會有值,所以if塊中的set指令也會執行。這種情況如果在其它語言中一般是輸出字符串“location”的,因為每塊作用域都會關聯一塊內存空間來存放本作用域內的變量值。但是nginx在整個請求過程中只會為某個變量保留一份存儲空間,所以變量值也會只保留最後一次修改的值,因此上面的例子一定是輸出字符串“if”。

子請求中的變量

子請求這個概念並不屬於http協議,在nginx中它不像http協議中的301、302那樣會重新發起一個新的請求,而是一個簡單的方法調用,而且nginx在發起子請求的時候不需要再次解析http請求頭協議,直接共享父請求的,所以它比瀏覽器直接發起的請求要節省資源。

當nginx在內部發起一個子請求的時候,父請求會把自己的變量共享給子請求,但是這個共享並不是共享變量的值。我們之前說過每個變量都會對應一個handler方法,只有當這個變量允許被緩存的時候,我們才可以認為主子請求共享同一個變量值,否則他們都會在各自的環境中執行相同的handler方法,最終計算的值也會因為環境的不同而不同。

根據當前了解到的知識以及nginx中自帶的模塊,很難把變量在子請求中的特性詳盡的描述出來,為了不引入過多新的知識,這裏僅引用nginx自帶的一個ngx_http_addition模塊來闡述這個知識。這個模塊默認沒有安裝,需要讀者根據文檔自行安裝一下。

先來看一個子請求共享父請求變量的例子,首先需要在nginx的安裝目錄下找到一個名字叫html的目錄,然後在該目錄下創建一個f.html,在我這裏該文件的絕對路徑如下:

/path/html/f.html

然後在這個文件中輸入一行字,內容如下:

-->I am f.html<--

然後在nginx.conf配置文件中做如下配置:

location /f.html {

set $a “father”;

add_before_body  /sub;

}

location /sub {

return 200 “ -->I am sub [$a]<-- ”;

}

其中指令add_before_body的作用是發起一個子請求,並且把獲取到的子請求的內容放置到父請求內容的最前面。現在我們要關註的是當訪問“/f.html”時,變量“$a”的傳遞性。根據之前對變量規則的介紹我們知道變量“$a”是可以被緩存的,所以它在主請求中的值會被共享到子請求中,所以子請求“/sub”中的變量“$a”會被替換成父請求中的“father”,下面用curl驗證一下:

curl http://127.0.0.1/f.html

可以看到輸出結果如下

-->I am sub [father]<-- -->Iam f.html<--

跟預測結果一致。

既然主子請求中的變量可以共享,那就表示在其中一個子請求中改變變量的值時,該值也會反應到當前主請求和當前主請求發起的其它子請求中,但是就目前掌握的知識,我們還無法用nginx自帶的模塊模擬第一種情況(該值也會反應到當前主請求)。我們把上面的例子稍作改造,來模擬一下第二種情況:

location /f.html {

set $a “father”;

add_before_body  /sub;

add_after_body   /sub2;

}

location /sub {

set $a “sub”;

return 200 “ -->I am sub [$a]<-- ”;

}

location /sub2 {

return 200 “ -->I am sub2 [$a]<-- ”;

}

這個例子中引入了一個新的指令add_after_body,它的作用是把子請求“/sub2”中獲取的內容放到主請求的最後。根據我們已知的規則,當訪問主請求“/f.html”的時候,會發生如下的過程:

主請求中會存在一個變量“$a”值是“father”

然後主請求對“/sub”發起子請求,在該子請求中變量“$a”的值被改變成了“sub”,由於變量“$a”是主子請求共享的,所以此時主請求看到的值和其它之請求看到的值都是“sub”

然後繼續向下走,當前子請求獲取的輸出內容為“-->I am sub [sub]<-- ”

然後繼續回到主請求,此時主請求的輸出內容是“-->I am f.html<--”

接著繼續往下走,在主請求中又發起了另一個子請求“/sub2”,在該請求中又用到了變量“$a”,我們知道這個變量已經在第一個子請求中被設置成了“sub”,而這個變量又是可共享的,所以此時該子請求獲得的內容是“ -->I am sub2 [sub]<--”

最後生成的內容就是上面3到5步生成的字符串順序相加

用curl驗證一下:

curl http://127.0.0.1/f.html

-->I am sub [sub]<-- -->I am f.html<--

-->I am sub2 [sub]<--

對於不可緩存的變量而言,在主子請求中變量是不存在共享的,因為在任何時候,這些變量值都是調用其對應的handler方法實時計算出來的,來看一個例子:

locaiotn /f.html {

add_before_body  /sub;

add_after_body   /sub2;

}

location /sub {

return 200 “ -->I am sub [$uri]<-- ”;

}

location /sub2 {

return 200 “ -->I am sub2 [$uri]<--”;

}

用curl訪問以下主請求“/f.html”

curl http://127.0.0.1/f.html

-->I am sub [/sub]<-- -->I am f.html<--

-->I am sub2 [/sub2]<--

因為內置變量“$uri”是不可緩存變量,所以每次獲取變量值時都會調用它對應的handler方法來重新計算,這樣就得到了不同的值。也正是因為它是不可緩存的才獲取到了我們期望的值。

nginx中還有另外一種變量,不管存在於哪個請求中,它始終只表示父請求中的值。比如核心http模塊中的“$request_method”變量,不過目前在nginx自帶的標準模塊中好像也就這麽一個“奇怪”的存在。感興趣的同學可以找幾個例子去驗證一下,本小節就不再贅述了。

其它

nginx中變量類型比較單調,不像其它真正編程語言那樣有各種類型。nginx中的變量不管是內置變量還是自定義變量,幾乎都是字符型的。這裏既然用了“幾乎”倆字,那說明一定有例外,來個例子看一下:

location /a {

return200 “-->${binary_remote_addr}<--”;

}

location /b {

return200 “--><--”;

}

分別訪問兩個location,看看是什麽結果

curl http://127.0.0.1/a

--><--

curl http://127.0.0.1/b

--><--

從表面上看,兩個請求輸出了同樣的結果。似乎可以推斷出這個變量的作用是輸出空字符,但是想想又覺得不可能。nginx怎麽可能這個大費周章的用一個這麽長的變量來表示一個空字符。既然不可能是空字符,那應該是什麽呢?咱們換一種訪問方式,這次使用curl訪問的時候帶上-v,然後我們只看響應頭:

curl http://127.0.0.1/a -v

< HTTP/1.1 200 OK

< Server: nginx/1.9.4

< Date: Mon, 23 Apr 2018 13:48:34 GMT

< Content-Type: application/octet-stream

< Content-Length: 12

< Connection: keep-alive

curl http://127.0.0.1/b -v

< HTTP/1.1 200 OK

< Connection: keep-alive

從結果上可以看到,這兩個請求的響應頭中除了日期就只有“Content-Length”值是不一樣的。很明顯變量“${binary_remote_addr}”的內容長度是4個字節,但是從輸出結果上看不出這4個字節是什麽。nginx的官方文檔對這個變量的解釋是,這是一個二進制的IP地址,如果是IPv4則長度是4字節,如果是IPv6則長度是6字節。因此我們知道了變量“${binary_remote_addr}”並不是一個字符型的,而是一個4字節的ip地址,並且是一個IPv4形式的二進制數據。它之所以顯示成了空字符,是因為我的終端無法把這個二進制數據解釋成可視的字符。

在整篇文章舉例說明問題的時候,關於變量的使用,我都是用雙引號括起來的,這並不表示必須使用雙引號,單引號或不用引號都是可以的,只有在不加引號就無法表示某個字符串是一個整體的時候加引號才是必須的,比如字符串

I am a uri

在不加引號的情況下,nginx根本無法判斷它是一個整體,比如這樣

return 200 Iam a uri;

完全是一個不正確的使用方式,nginx是無法啟動成功的。

實際上如果你願意,nginx配置文件中幾乎任何字符串都可以用雙引號括起來,比如下面的例子:

“location” “/a” {

“return” “200” “Iam a”;

}

雖然這種形式看起來乖乖的,但在nginx中它仍然是一個合法且正確的配置形式。

總結

以上內容從宏觀上介紹了nginx中變量的一些特性,啰裏啰嗦說了一大堆,其實主要說了以下內容:

1.nginx中使用“$”或“${}”符號來表示一個變量

2.nginx中的變量支持變量插入,比如“I am a $uri”

3.可以表示變量的有效字符只有四種:“a-z”、“A-Z”、“0-9”、“_”

4.nginx中變量可分為內置變量(比如$uri)和自定義變量(比如用set定義的變量)

5.nginx 中所有的變量都是全局可見的,但它又不是全局變量

6.nginx中有六種動態內置變量,分別是“http_”、“senthttp”、“upstreamhttp”、“upstreamcookie”、“cookie”,“arg_”。(在nginx的1.13.2版本中又多一個“$senttrailer”)

7.nginx中幾乎所有的內置變量都是不可變的,除了“args”和“$limt_rate”

8.nginx中所有的變量都會關聯一個get_handler()方法,不可緩存的變量每次獲取值時都會調用這個方法,可緩存的變量只會調用一次

9.nginx中的變量在各個請求之前是相互隔離的(主子請求除外)

10.變量在主子請求之間是共享的,但最終值是否相同則取決於該變量是否可緩存

11.nginx中的變量值都是字符型的(除了“${binary_remote_addr}”變量)

順風詳解Nginx系列—Ngx中的變量