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

java填坑路發表於2018-06-30

在計算機語言中,變數是用來儲存和表示資料的,但不同的語言表示變數的方式不同,像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”;

}

這裡需要簡單說一下“$arg_word”這個變數,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_”、“sent_http_”、“upstream_http_”、“upstream_cookie”、“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就可以了。

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

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

location /a {

   return 200 “test sent_http_”;

}

用帶-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中,一旦某個變數被認定為自定義或內建變數,後續就不會再被賦予動態變數的特性。

比如例子中的“$arg_a”,其實已經變成了一個自定義變數,相應的動態變數特徵也就不存在了,但其它以“$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_”、“sent_http_”、“upstream_http_”、“upstream_cookie”、“cookie_”,“arg_”。(在nginx的1.13.2版本中又多一個“$sent_trailer_”)

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

8.nginx中所有的變數都會關聯一個get_handler()方法,不可快取的變數每次獲取值時都會呼叫這個方法,可快取的變數只會呼叫一次

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

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

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

歡迎工作一到五年的Java程式設計師朋友們加入Java架構開發:744677563

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導


相關文章