天天看點

NGINX 腳本語言原理及源碼分析(一)

概述

NGINX本身提供了簡單的腳本解析功能來提高配置使用的靈活性。與常用的語言一樣,可以通過在NGINX配置檔案中使用變量和指令來完成對NGINX的程式設計,來實作特定的功能。

簡單變量使用例子:

location /test {

     if ($args_name = “test”) {

         return 200 “welcome”;

    }

}

上面的配置完成的功能是,如果客戶通路的伺服器的/test URI并且攜帶的name參數值是test, 則給client傳回狀态碼200并且輸出“welcome”。

上述簡單的例子中,if對應的是指令,args_name對應的是變量。變量和指令也是任何程式設計語言必備的要素。通過使用腳本語言和變量,可以大大提高NGINX的靈活性,避免了通過大量添加新代碼來支援新功能。

NGINX提供的變量機制,使得NGINX在處理每一個具體的HTTP請求時,可以在不同處理階段直接傳遞資料。比如在POST_READ階段在讀取了client請求以後,我們就可以擷取本次請求的相關host等資訊。通過變量機制,後續的子產品就可以通過變量$host來擷取本次請求的host資料資訊。

下面我們通過一系列的文章,來分析NGINX腳本語言是如何實作的。本篇文章,我們先介紹NGINX變量的基本特性。下一篇,我們繼續分析變量的實作原理。最後我們将分析NGINX指令的實作原理以及變量是如何被使用的。

變量的定義

任何語言的變量都是用來存儲和表示資料的。在定義變量時,我們首先需要給變量命名。比如在C語言中,我們通過int abc = 10;來定義一個整數類型的變量。其中int表示變量的類型,abc表示變量的名字。

關于定義變量的類型,在NGINX中所有的變量除了内置變量“${binary_remote_addr}”以外,剩餘的變量都是字元串類型。

每種語言對于可以表示變量的字元要求不盡相同。在NGINX腳本語言中,變量的名字是使用“$”或“${}”符号來表示一個變量。可以用來定義變量的有效字元隻有四種:“a-z”、“A-Z”、“0-9”、“_”。 比如 set $abc ‘abc’或者set ${abc} ‘abc’。 符号{}的意義在于,如果一個變量名需要和它相鄰的字元進行區分時,需要顯式地添加{}來實作。比如 變量${a}s 與$as和${as}兩個變量表示的意義是不一樣的,前者表示在變量a的值後面再添加一個s字元。 $as和${as}都表示變量as本身。

NGINX的變量支援變量插入,比如我們通過set $def “this is a test $abc”定義變量$def。在進行指派時,需要首先計算出變量abc的值然後再和另外的字元串連接配接,最後把連接配接後的字元串指派給變量def。如果變量abc的值是“yeah!”,那麼變量def的值就是 “this is a test yeah!”。

變量的分類

NGINX變量可以分為内置變量和自定義變量兩種。自定義變量是通過NGINX不同子產品的進行顯示定義。比如通過rewrite子產品中的set指令可以如下定義: set $test “abc”;這個指令完成定義一個名為test的變量,并且在變量第一次被使用時把字元串abc指派給test變量。還有比如geo子產品也可以如下根據用戶端的IP來定義一個新的變量。

geo $a  {

    default    “我是geo預設值”;

    127.0.0.1  “用戶端ip是127.0.0.1”;

}

除了自定義變量以外,NGINX還支援大量的内置變量。這些内置變量根據系統的層次模型,可以分為系統相關的變量比如$pid、網絡三層相關的變量,比如$remote_addr、四層相關的變量,比如$remote_port、表示層(SSL,TLS)相關的變量,比如$http_ssl_vars以及七層HTTP,比如$url相關的變量。

内置變量又可以分為靜态内置變量和動态内置變量。靜态内置變量是在不同的子產品中通過代碼預先定義的。比如在NGX_HTTP_CORE_MODULAR裡面就定義了大量的系統變量如$HOST,  $URI等。

所謂“動态”指的是變量的名字是不确定的,這個不确定性發生在NGINX的運作過程中。比如對于一個HTTP請求,同一個請求可以有不同的查詢參數,而查詢參數的不同又可以傳回不同的結果,比如這個查詢功能:

/query?name=jikui

/query?occurpation=coder

該查詢功能有兩個輸入參數,一個是name,一個是occupation。當請求發生的時候,在NGINX内部肯定可以解析出所有的查詢入參和對應的值,但是在配置檔案中如何得到和使用呢? NGINX通過使用字首的方式來表示HTTP子產品中各種動态内置變量。分别使用用arg_name和arg_occurpation來表示其對應的變量。而arg_就是查詢參數中某個入參的變量字首。如此一來NGINX隻需要在内部内置一個以arg_開頭的規則就可以友善的表示這類參數相關資料了。

目前在NGINX的http子產品中有如下幾種内置動态變量,分别是“http_”、“sent_http_”、“sent_trailer_”、“upstream_http_”、 “upstream_cookie_”、“cookie_”,“arg_”, “upstream_trailer_”。

以“http_”開頭的動态内置變量可以表示http請求過程中的任意請求頭,使用的過程中不區分大小寫,并且請求頭中如果有“-”字元需要用“_”字元替代。另外别的種類的動态内置變量也有相應的對應HTTP處理階段。

變量的作用域

NGINX在啟動時,會對變量進行靜态檢查,如果有指令使用未經定義的變量,NGINX啟動會出錯。并且列印如下的出錯資訊:“NGINX: [emerg] unknown "**" variable”。

NGINX變量在配置檔案中是全局可見的,基于此,在如下的配置中,雖然我們是在location /test2中定義的變量name,在locaiton /test1中也進行了使用,但是這樣在NGINX啟動時是也不會出錯。

        location /test1 {

               return 200 “I am $name”;

        }

        location /test2 {

             set $name“jikui”;

             return 200 “I am $name”;

         }

在實際的通路中,如果通路location test1傳回的結果是 “I am”而通路location test2傳回的結果是 “I am jikui”。這是因為,雖然靜态的變量定義是全局可見的,但是對應每一個請求,都會有自己的一份變量的定義和數值。雖然對于不同的請求,都有各自不同的變量定義,但是在父子請求模型中,所有的子請求自動繼承父請求所有的變量值。     

變量的可變性

根據變量是否可以被再次指派,NGINX中的變量分為可變變量和不可變變量。在C語言中有特定的修飾符const用來描述這個變量的可變性。但是在NGINX中,并沒有顯著的修飾符來區分變量的可變性。隻能從變量的實作代碼實作中來判斷某個變量是否可變。

具體來說,在定義NGINX的變量時都會打上一個是否可以被改變的标記,然後把這個變量放到一個容器中。當後續試圖再次定義同一個變量時,NGINX會首先從這個容器中查找這個變量,如果找到相同的變量,再判斷這個變量的可改變的标記。如果變量的可變标志是True,則會把容器中的變量覆寫掉,反之則傳回錯誤并終止NGINX啟動。具體的實作我們下篇變量實作中繼續分析。

NGINX的内置變量要先于“set”或“geo”指令存放到系統中,如果某個内置變量,比如$host,被打上了不可改變的标記,後續其它指令就無法再定義相同名字的變量了。如果試圖再次定義$host變量,則會出現如下錯誤:

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

目前NGINX的核心http子產品中幾乎所有靜态内置變量都是不可改變的。隻有“$args”和“$limit_rate”這兩個内置變量可以被改變。另外由于http子產品的動态内置變量并不會把自己放入到容器中,是以它是可以被改變。

比如:

location /name {

    set $arg_name “jikui”;

    return 200 “$arg_name”;

}

在通路location /name時,輸出的是通過set指令設定的數值“jikui”而不是url參數中的“test”。

curl http://127.0.0.1/name?name=test

jikui

這是因為在NGINX中,一自定義或内置變量不會被賦予動态變量的特性。比如例子中的“$arg_name”,通過set指令指派後,其實已經變成了一個自定義變量,相應的動态變量特征也就不存在了。但在這個請求中,其它以“$arg_”開頭的變量仍然是動态變量。  

變量的可緩存性

變量的可緩存性是指,在擷取變量值時,是否需要每次都實時計算變量的值。對于不可緩存變量,擷取數值時都是實時計算的。對于可緩存的變量,不需要每次都實時計算。

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

比如“$arg_”開頭的動态變量,每次擷取值時都會從查詢參數中重新解析對應的值;而可以緩存的變量并不會每次都調用這個handler方法,在它的整個生命周期中,如果這個變量沒有被重新整理過,那麼自始至終隻會調用一次。

NGINX中用set指令定義的變量都是可以緩存的,但set指令不會改變已有變量的緩存特性,而所有以“arg_”開頭的動态變量都是不可緩存的。這兩種變量結合在一起的時候會産生一種有意思的現象,來看一個簡單的例子:

比如:

location /url {

    set   $name   “$arg_name”;

    set   $args   “name=jikui”;

    return 200   “$name = $arg_name”;

}

通路url輸出結果是:

curl http://127.0.0.1/url?name=test

test = jikui

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

這其實就是變量是否可緩存的特性引起的,因為變量“$name”是一個可緩存的變量,當被設定後變量值就被儲存下來了;而“$arg_name”是一個不可被緩存的變量,每次擷取該值的時候都會調用其對應的handler方法。我們看到第一次調用的時候查詢參數值是“name=test”,這個值被指派給了變量“$name”,在第二次擷取該變量值之前,我們把查詢參數改成了“name=jikui”,當它再次調用對應的handler方法的時候擷取到的值就變成了“jikui”。

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

location /a {

    set     $arg_name     “$arg_name”;

    set     $name     “$arg_name”;

    set     $args     “name=jikui”;

    return   200    “$name = $arg_name”;

}

通路location:

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

test = test

可以看到這兩個變量的值又一樣了。原因是,在用set指令重新定義“$arg_name”後,它就不再是動态變量了,它原本的不可緩存特性也就不存在了,是以此時查詢參數的更改對他也就不起任何作用。具體原因我們可以通過下篇代碼分析來看實際的實作原理。

變量的隔離性

NGINX中變量的隔離性類似于其它程式設計語言中變量的作用域。比如C語言中的局部變量和全局變量。而NGINX中的變量的作用域是基于請求的。同一個變量在不同的請求中毫無關系(子請求例外,子請求繼承了父請求所有的變量),即A請求不會讀到(或改變)B請求中的變量值,B也不會讀到(改變)A的,比如下面一個例子:

server {

    set $name “$uri”;

    location /test1 {

        return 200 “I am $name”;

    }

    location /test2 {

        return 200 “I am $name”;

    }

}

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

在NGINX中兩個location都可以看到這個變量“$name”,這展現了NGINX變量的全局可見性;但兩個location看到的變量值确實是不一樣的,這展現了隔離性。上述配置的運作結果是:

curl http://127.0.0.1/test1

I am test1

curl http://127.0.0./test2

I am test2

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

server {

    set $name “server”;

    location / {

        set $name “location”;

        if ($uri) {

           set $name “if”;

        }

        return 200 “$name”;

    }

}

從上面的例子可以看到,變量“$name”被更改了三次。上面的例子一定是輸出字元串“if”。從上面這個例子我們看到,NGINX 變量值容器的生命期是與目前正在處理的請求綁定的,而與 location 無關。

結語

本篇簡單介紹了NGINX變量的一些特性。下篇我們将繼續分析變量是如何實作的。然後我們還将通過分析NGINX的腳本語言的實作,來分析NGINX的變量是如何在系統中被使用的。

繼續閱讀