1. 程式人生 > >Java程式設計師——大牛詳解Nginx系列—nginx變數實現原理(上)

Java程式設計師——大牛詳解Nginx系列—nginx變數實現原理(上)

上一篇(Java程式設計師——大牛詳解Nginx系列—Ngx中的變數)主要描述的是變數的使用,所以沒涉及任何程式碼,而這一篇主要描述變數的實現原理,避免不了會涉及到一些底層程式碼,對於不瞭解c語言的同學讀起來可能會有點吃力,這部分同學可以嘗試一下兩篇結合著讀,比如先讀一個知識點的用法,然後再回到這篇來看一下實現原理,以此來加深理解。如果在讀的過程中你有發現任何問題,還請反饋給我,我會非常感激。

 

1ngx內部如何表示變數

 

nginx內部用了兩種結構體來表示變數,一個用來表示變數的名字,另一個用來表示變數的值,分別是:

 

ngx_http_variables.h/ngx_http_variable_value_t;

ngx_http_variables.h/ngx_http_variable_t;

 

nginx內部在定義變數的時候,實際上就是在建立ngx_http_variable_t結構體。而代表變數值的結構體ngx_http_variable_value_t,則是通過繫結在ngx_http_variable_t結構體上的get_handler()方法建立或獲取的。比如set指令:

 

set $a "I am var";

 

該指令用來定義一個變數“$a”,並且賦值為“I am var”。

 

按照上面的描述,當解析到這個set指令的時候,在nginx內部會建立一個ngx_http_variable_t結構體表示變數名“a”,而當需要獲取這個變數值的時候,會呼叫這個變數上繫結的get_handler()方法,該方法會建立或者從快取中獲取一個ngx_http_variable_value_t結構體,對應的變數值“I am var”就存在於這個值結構體中。

 

 

把這兩個結構體的程式碼貼過來看看它更詳細的表示。

 

表示變數名的結構體有如下欄位:

typedef struct ngx_http_variable_s  ngx_http_variable_t;

struct ngx_http_variable_s { 

   ngx_str_t                name; 

   ngx_http_set_variable_pt set_handler;

   ngx_http_get_variable_pt get_handler;

   uintptr_t                 data;   

   ngx_uint_t               flags;   

   ngx_uint_t               index;       

 }

 

表示變數值的結構體有如下欄位:

 

 

typedef ngx_variable_value_t  ngx_http_variable_value_t;

typedef struct {

   unsigned    len:28;

   unsigned    valid:1;

   unsigned    no_cacheable:1;

   unsigned    not_found:1;

   unsigned    escape:1; 

   u_char     *data;

} ngx_variable_value_t; 

 

 

 

其中ngx_http_variable_s#name欄位用來存放變數的名字“a”,而變數值則用到了ngx_http_variable_value_t結構體中的兩個欄位len和data,data用來指向變數值字串的首地址,len則表示字元的長度。

 

1.1 nginx中的變數值都是字元型的嗎?

從上面兩個結構體中可以看到,用來存放變數值的欄位是一個無符號char型別的指標(c中一般用它來表示字串),該型別的指標類似於java語言中的String型別,例如:

 

 

 

u_char  *data = “hahaha”;

String   data = “hahaha”;

 

 

 

這兩句的作用都是定義並初始化一個字串,表面上看我們會認為nginx中的變數“應該”只有字元變數這一種型別,但是上一篇我們也提到了一個非字元型變數“${binary_remote_addr}”的例子,能夠出現這種情況其實來自於C語言的靈活性。下面通過一個片段程式碼來說明它是怎麼做到的:

 

   

 

ngx_http_variables.c/ngx_http_variable_binary_remote_addr()

v->len = sizeof(in_addr_t); 

v->data = (u_char *) &sin->sin_addr;

 

 

其中v是ngx_http_variable_value_t物件,可以看到nginx把代表網路地址(sin->sin_addr)的這個變數的本身的地址轉換成了一個u_char型別的指標,並賦值給v->data欄位,這種轉換在c語言中是合法的,然後v->len欄位表示了這個網路地址所佔的位元組個數(sin->sin_addr其實就是in_addr_t型別的),最終結果就是用一個char型別的指標指向了記憶體中的一塊空間,而這個空間裡面存放的且不是我們認知上的字元資料。如果讀者對c語言不太瞭解,可能仍然沒辦法理解為什麼一個字元型別的變數可以表示非字元型別的資料,這個其實涉及到了c語言中的型別強轉,下面簡單介紹一下。

 

 

熟悉java語法的讀者應該都知道,在java中資料型別相互轉換是有嚴格約束的,即使看上去相同的數字,如果型別不同,也沒辦法相互轉換,比如

 

 

Integer aa = 23;

Short bb = 23; 

 

 

這兩個變數都是在表示23這個數字,但是在java中試圖把aa強轉成Short和試圖把bb強轉成Integer是行不通的,例如:

 

 

bb = (Short)aa;

aa = (Integer)bb;

 

 

這兩種強轉都會報錯,正確的做法是使用這兩種資料型別自帶的xxxValue()方法,比如:

 

 

 

 

bb = aa.shortValue();

aa = bb.intValue();

 

 

 

但是對於兩個毫不相干的資料型別是不可能有類似的xxxValue()方法的,比如資料型別Integer和HashMap,在java現有的框架內,這兩種型別根本沒有相互強轉的可能性,所以java在編譯階段就會杜絕這種情況發生。

 

 

但是在c語言中這些所謂的強轉約束幾乎是不存在的,你可以把任意型別轉換成其它型別,比如:

 

 

 

unsigned int a = 255; 

unsigned char b =12; 

b = (unsigned char)a;

printf("a=%d b=%d\n",a,b); 

 

 

 

看,我們把一個int型別資料強轉成了一個char型別資料,並且編譯沒有報錯。你甚至可以把一個指標型別轉換成int型別,比如:

 

 

unsigned int a = 255; 

char *aa = "abc";  

a = (int)aa;  

printf("a=%d aa=%s\n",a,aa);

 

 

這些在c中都是合法的,並且也可以輸出結果。

 

 

上面的兩個例子雖然可以強轉成功,但最終結果且不一定如你所料。對於第一個例子我們可看到最終a和b都打印出了正確的結果255,但是當我們把變數a設定為256的時會發現這次的輸出結果b變成了0。另一個例子中a顯然不會輸出“abc”這個字串,而是一個很大的數字。我們暫且稱其為強轉的“副作用”,這是c語言中型別相互強轉的“代價”。出現這種情況是因為在c語言的型別強轉中,對應的資料並沒有發生任何變動,改變的只是對原有資料的解釋。這個其實有點指鹿為馬的意思,比方說A和B兩個人同時看到一個動物,A說這是一匹馬,而B說這是一隻鹿,最終不管A和B怎麼解釋這個動物,動物本身是不會發生任何改變的,改變的只有A和B兩個人各自的認知。

 

 

我們再舉一個更實際的例子來說明c語言中型別強轉的特性,在c中一個無符號的int型別資料可以如下表示:

 

unsigned int a = 255;

 

而在記憶體中他實際上是使用二進位制表示的,並且佔用了4個位元組記憶體,表示如下:

 

 

1111 1111 0000 0000 0000 0000 0000 0000

(從左到右,左邊是低地址,右邊是高地址,後續都是這個順序)

 

 

而一個無符號的char型別的資料在記憶體中只佔用了1個位元組記憶體,表示如下:

 

 

 

unsigned int a = 255;

1111 1111

 

 

根據轉換規則可以知道,當int型別轉換成char型別的時候,int型別資料本身並沒有變,只是char型別變數在解釋int型別的原始資料時候看不到其後面三個位元組的資料,因為char型別的眼裡只能看到一個位元組的長度。所以我們可以得出結論,當int型別的值在char型別的可表示範圍內是可以正確轉換的,一旦超出char型別的表示範圍則會發生意想不到的事。比如256這個數字在記憶體中的二級製表示如下:

 

 

0000 0000 1000 0000 0000 0000 0000 0000

 

可以看到它的前八位都是0,而char型別資料只能看到一個位元組八位的資料,所以這時候256轉成char的時候就變成了0。

 

通過上面的描述,讀者對c中的型別強轉應該會有一個大致的瞭解,專門把這個知識點拿出來解釋一遍其實是想告訴讀者,雖然nginx中變數值使用字元型別來定義的,但你仍然可以利用c語言的特性,用它來表示任何型別的變數值,比方說你可以用它來存放一個結構體:

 

 

 

ngx_http_variable_t  my_struct;

v->len = sizeof(ngx_http_variable_t); 

v->data = (u_char *) &my_struct;

 

 

如此我們就用data表示了一個結構體型別。但是大部分情況下是不建議這麼做的,因為從nginx整個框架來看,其實它無意把變數分成多種型別,大部分情況下字元型別就夠了,所以我們在開發的時候最好也遵守這個規則。

 

 

 

 

2如何定義一個變數

 

在nginx中使用一個變數之前需要先定義(建立),否則nginx會無法啟動。而定義變數的方式又有兩種:一種是nginx中的內建變數,這些變數都是“隱性”建立的,不需要使用者明確去定義,各個模組會各自建立自己的內建變數,並在合適的時機將其註冊到nginx中。另一種是通過指令的方式“顯示”的在nginx的配置檔案中定義某個變數,比如ngx_http_rewrite模組中的set指令、ngx_http_geo模組中的geo指令,他們在內部一般會通過nginx提供的公共api來建立和註冊變數。

 

不管使用哪種方式,最終都是建立變數對應的結構體,並且把變數對應的結構體註冊到相應的容器(cmcf->variables_keys ,後續會用它來代表該容器)中,然後由該容器統一管理。

 

在nginx中每定義一個變數都要建立一個ngx_http_variable_t結構體,比如:

  

 

set $a “I am a”;

set $b “I am b”;

 

當nginx解析到這兩條指令的時候就會為這兩個變數分別建立一個對應的ngx_http_variable_t結構體,然後將其註冊到對應的容器中。但是對應的變數值結構體ngx_http_variable_value_t並不會在此時建立,我們之前也說過,變數值是通過變數對應的get_handler()方法動態生成的,這個在後面的小節會講到。

 

2.1 ngx_http_add_variable()方法

nginx為各個模組提供了一個公共方法ngx_http_add_variable()來建立並註冊變數,這個方法一般用在需要在配置檔案建立自定義變數的模組中,是其它模組將自己的變數放入到nginx框架中的一個入口,比如ngx_http_rewrite模組的set指令、ngx_http_geo的geo指令等。該方法的原型如下:

 

 

ngx_http_variable_t * ngx_http_add_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags)  

 

其中name是要建立的變數的名字,會被設定到ngx_http_variables_t#flags欄位中;而flags則用來為將要建立的變數打標記,會被設定到ngx_http_variables_t#flags欄位中。

 

目前在nginx中可用的標記(ngx_http_variables_t#flags)有四種:

 

  NGX_HTTP_VAR_CHANGEABLE:該標記表示變數是可變的,也就是說一旦某個變數在建立的時候打上了這個標記,隨後再次試圖呼叫該方法建立同名變數時,該變數會被覆蓋。反之,如果沒有這個標記,那麼在隨後試圖建立同名變數時nginx都會報錯,並列印一條錯誤日誌:

 

 

nginx: [emerg] the duplicate "xx" variable in /path /conf/nginx.conf:48

 

這條日誌會提示你這是一個重複的變數,這就是該標記的意義所在。Nginx中大部分內建變數會被打上這個標記,比如“$uri”這個內建變數。

 

  NGX_HTTP_VAR_NOCACHEABLE:該標記用來表示變數是否可快取,如果有則表示該變數不可快取,反之則表示可以快取。對於不可快取的變數,每次獲取變數值都會呼叫該變數對應的handler方法(比如以“arg_”開頭的動態變數),也就是ngx_http_variables_t結構體中get_handler欄位對應的方法。如果變數在建立的時候沒有打這個標記,則表示這個變數是可以快取的,比如rewirte模組的set指令定義的變數就沒有打這個標記。

 

還有另外兩個標記分別是NGX_HTTP_VAR_INDEXED和NGX_HTTP_VAR_NOHASH,一個表示變數是可索引的,另一個表示變數不需要放到hash結構體中。這兩個標記主要在ssi模組中會用到,等後續講解ssi模組時再做詳細介紹,這裡就不再贅述了。

 

nginx中大部分內建變數的建立和註冊並沒有使用ngx_http_add_variable()方法,比如http核心模組中的變數,它在src/http/ngx_http_variables.c檔案中定義了一個ngx_http_variable_t型別的陣列,每個變數需要的必要資訊都直接硬編碼到了程式碼裡,然後會在某個合適的時機把陣列中所有的變數全部註冊到存放變數的容器中,這個容器跟ngx_http_add_variable()方法中用到的是同一個。

 

nginx在建立內建變數的時候沒有使用ngx_http_add_variable()方法也確實因為沒有那個必要,因為定義一個變數的最關鍵一個未知因素----變數名字,在內建變數中是已經被確認了的,它不像自定義變數那樣,需要在配置檔案解析時才能確認變數的名字。

 

 

3使用變數

    

在nginx配置中如何使用一個變數在上一篇文章中已經介紹過了,我們這裡就不再重複介紹了,這裡主要介紹在nginx內部是如何做的。比如像這樣一個指令:

 

 

return 200 “$uri”;

 

 

通過其配置可以知道,當用戶請求到該指令所在的範圍內的時候,它會打印出這個變數所代表的當前uri值。本小節會重點介紹一下在列印之前,這個變數值在nginx內部是如何獲取到的。

 

3.1註冊(索引)變數

在前面介紹如何定義變數的時候提到了ngx_http_add_variable()方法,我們說該方法的作用是建立變數,並將變數註冊到一個容器中。這裡的“註冊”聽起來似乎沒有什麼不妥之處,畢竟這是該變數第一次出現在系統內。但是如果要讓這個變數能夠被使用,還需要一個“二次註冊”,也可以稱之為“索引”變數。這個“二次註冊”也是要把變數註冊到一個容器中,但是這個容器(其實就是一個數組cmcf->variables)跟建立變數時註冊的容器(cmcf->variables_keys)是不一樣的,他是為了專門索引變數(為變數建立索引)而存在的,後續在使用變數的都會通過這個索引值。 

 

nginx專門為索引變數提供了一個ngx_http_get_variable_index()方法,該方法的宣告如下:

 

 

ngx_int_t ngx_http_get_variable_index(ngx_conf_t *cf, ngx_str_t *name)

 

這個方法的邏輯比較簡單,下面我們簡單描述一下它的基本邏輯:

 

  1. 先判斷這個容器是否存在,如果不存在則建立該容器

  2. 如果該容器存在,則檢查當前要索引的變數name是否已經存在該容器中,如果變數name已經存在該容器中,則直接返回該變數在容器中的索引值

  3. 如果變數name不在該容器中,則建立一個代表該變數的ngx_http_variable_t結構體,然後將其放入到該容器中

  4. 返回新建變數結構體在容器中的索引值

 

3.2通過ngx_http_get_indexed_variable()方法獲取變數值

當變數被索引完後會返回一個索引值,ngx_http_get_indexed_variable()方法就是根據這個索引值來獲取變數值的。

 

來看一下這個方法的原型:

 

 

ngx_http_variable_value_t * ngx_http_get_indexed_variable(ngx_http_request_t *r, ngx_uint_t index)

 

這個方法有兩個入參:一個是index,也就是要獲取的變數的索引值;另外一個是nginx內部用來表示一個請求的結構體物件(ngx_http_request_t),它裡面儲存了當前請求的所有資訊,而這個方法中用到了裡面的一個r->variables欄位,variables實際上就是一個數組,陣列大小和用來索引變數用的容器大小是一樣的,不同的是一個存放的是變數值(ngx_http_variable_value_t物件),一個存放的是變數名(ngx_http_variable_t物件)。另一個需要注意的是,對於同一個變數,它們的名字和變數值在各自的容器中的索引值是相等的,如此一來,nginx就可以通過索引變數時獲取的索引值,從r->variables陣列中獲取對應的變數值。 

 

簡單描述一下這個方法的大概邏輯:

 

  1. 該方法首先通過索引值去r->variables陣列中獲取變數值(ngx_http_variable_value_t結構體),然後通過相應的標記來檢查這個變數值是否可用,如果可用則直接返回,如果不可用則走下面的邏輯。

  2. 變數值不可用,所以需要呼叫該變數對應的get_handler()方法來動態生成變數值,而該方法是繫結在變數名結構體物件中的,所以這裡需要用這個索引值去變數名(ngx_http_variable_t)所在的容器(後續用cmcf->variables表示這個容器)中獲取這個方法。

  3. 取到變數名對應的get_handler()方法並呼叫,如果呼叫成功,那麼生成的變數值就會被設定到r->variables容器的對應位置,否則就返回空了。

  4. 在返回生成的變數值之前還需要做一個flags的判斷,這個flags就是我們在2.1中提到的ngx_http_variables_t#flags標記,它會檢查這個變數在定義的時候是否設定了NGX_HTTP_VAR_NOCACHEABLE標記,如果有則表示該變數值不可快取,那麼該變數值對應的v.no_cacheable就會被設定為1,後續再獲取變數值的時候,nginx可以利用該欄位值來確定是否要再次呼叫對應的get_handler()方法。

  5. 返回生成的變數。

 

3.3通過ngx_http_get_flushed_variable()方法獲取變數值

這個方法跟3.2中的方法一樣都是在獲取變數值,不同的是該方法有一個flush的概念,所謂flush顧名思義就是重新整理快取,但並不意味著通過該方法獲取的變數都沒有走快取,該方法會用到我們上面提到的v.no_cacheable欄位值來判定是否可以使用快取值,而v.no_cacheable欄位是否被設定則取決於變數定義時是否打上了NGX_HTTP_VAR_NOCACHEABLE標記(ngx_http_variable_t#flags)。目前只要某個變數被打上這個標記,並且用ngx_http_get_flushed_variable()方法獲取變數值,那麼獲取到的變數值就都是實時生成的,比如動態變數“arg_xxx”就設定了這個標記。

 

該方法原型跟3.2基本類似,具體定義如下:

 

 

ngx_http_variable_value_t *  ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index)  

 

 

該方法的大致邏輯如下:

  1. 首先通過索引值從r->variables陣列中取出該索引對應的變數值v

  2. 判斷變數是否合法(利用v->valid和v->not_found判斷),不合法則直接呼叫ngx_http_get_indexed_variable()方法獲取變數值。

  3. 如果變數合法,則判斷v->no_cacheable值,值為0表示可以走快取則直接返回變數值v;值為1表示不可以走快取,為變數值v打上不合法標記。

  4. 直接呼叫ngx_http_get_indexed_variable()方法獲取變數值,該方法會中會用到變數值是否合法標記,如果合法則走快取,不合法則回撥對應的get_handler()方法來獲取變數值。

 

3.4通過ngx_http_get_variable()方法獲取變數值

目前只在ngx_http_ssi_filter_module模組中用到該方法來獲取變數值,不過該方法並不像前面其它兩個方法那樣通過索引值獲取變數值,該方法通過變數名字從一個hash表(cmcf->variables_hash)中獲取該變數值,這個hash表的資料來源於cmcf->variables_keys容器,所有定義好(配置檔案中的和內建的)的變數都會被註冊到這個容器中。

 

來看一下方法定義:

 

 

ngx_http_variable_value_t * ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key)

 

 

入參name就是要查詢的變數名字,入參key是name的存放在hash結構中的hash值。

 

該方法的大致邏輯如下:

  1. 呼叫ngx_hash_find () 方法從cmcf->variables_hash容器中查詢name的變數值。

  2. 找到變數v後接著判斷該變數有沒有標記NGX_HTTP_VAR_INDEXED,如果有表示可以根據變數索引值獲取變數,則直接呼叫ngx_http_get_flushed_variable()方法獲取變數值並返回;如果沒有則呼叫該變數對應的get_handler()方法獲取變數值並返回。

  3. 如果沒有在容器中找個對應的變數則去判斷該變數是否是動態變數,如果是則執行對應的方法,比如變數是以“http_”開頭的動態變數,則使用ngx_http_variable_unknown_header_in()獲取變數值;如果不是動態變數則表示根不能不存在這個變數。

 

 

4兩個重要的容器

        

在nginx的變數設計中有兩個至關重要的容器,他們的作用前面我們也多多少少介紹了一點,目前為止,我們上面介紹的方法基本都在圍繞這兩個容器在工作,這一節我們簡單總結一下nginx這麼做的目的。

 

4.1容器cmcf->variables_keys

該容器存在的目的是為了收集nginx中定義的變數,包括自定義變數(比如set指令定義的變數)和內建變數(不包括動態內建變數),只有存在於該容器中的變數才能被使用。類似於一個放滿工具的倉庫,你只能使用倉庫中已經存在的工具,對於倉庫中沒有的工具你是無能為力的。

 

該容器另一個作用是變數排重,它會保證整個容器中只有一個同名變數,當你試圖向該容器中放一個已經存在的變數時它會返回錯誤,並且後臺會列印一條錯誤日誌“conflicting variable name xxx”。從這裡可以間接知道,整個nginx中變數名字都是唯一的,但這並不表示我們不能多次定義變數(比如set指令),這個其實涉及到了變數的另一個標記NGX_HTTP_VAR_CHANGEABLE,該標記的意思在“如何定義一個變數”已有介紹,這裡就不再贅述。

 

向容器中放變數基本有兩種方式:一種是像內建變數那樣,直接呼叫nginx提供的ngx_hash_add_key()方法,該方法不做任何變數業務處理(比如檢查NGX_HTTP_VAR_CHANGEABLE標記),在該方法的眼裡只有資料;另一種就像set指令那樣使用nginx專門為變數提供的ngx_http_add_variable()方法。

 

還有一個比較特殊的地方是該容器的生命週期,它只存在於配置解析階段,當nginx啟動成功該容器就不存在了,相當於把倉庫毀掉了。

 

4.2容器cmcf->variables

該容器存在的目的是為了收集在nginx中使用到的變數。如果一個變數被定義了,但是並沒有被使用,那麼他一般不會存在於該容器中,比如nginx中的內建變數。該容器基本上算是cmcf->variables_keys容器的一個子集,它更像你旅行時候帶著的旅行箱,一旦你已經上路了(比如nginx已經啟動了),你也就只能使用旅行箱的東西了。

 

目前向該容器中新增變數的方式只有一種:使用nginx提供的ngx_http_get_variable_index()方法。你當然可以自行寫程式碼向該容器中放入變數,但我們強烈建議你不要這麼做,因為這個方法中封裝了nginx設計變數的必要邏輯。

 

該容器本身就是一個數組,而所謂的索引值其實就是變數在該陣列中的位置下標。另外一個就是容器的大小,它雖然是cmcf->variables_keys容器的一個子集,但它能存放的元素個數跟cmcf->variables_keys是一樣的。

 

 

好了,變數實現原理上半部分就到這裡了。上半部分主要介紹了nginx為實現變數搞得一些“基礎材料”。下半部分內容主要描述nginx是如何使用這些“基礎材料”來炒出變數這道菜的。

程式設計師學習交流學習群:878249276,群裡有分享的視訊,面試指導,架構資料,還有思維導圖、群裡有視訊,都是乾貨的,你可以下載來看。主要分享分散式架構、高可擴充套件、高效能、高併發、效能優化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分散式專案實戰學習架構師視訊。合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!