nginx原始碼分析之變數
nginx中的變數在nginx中的使用非常的多,正因為變數的存在,使得nginx在配置上變得非常靈活。
我們知道,在nginx的配置檔案中,配合變數,我們可以動態的得到我們想要的值。最常見的使用是,我們在寫access_log
的格式時,需要用到多很多變數。
而這些變數是如何工作的呢?我們可以輸出哪些變數?我們又怎麼才能輸出自己想要的內容呢?當然,我們可能還想知道,如何在我們的模組裡面去使用變數,如何新增變數,獲取變數的值,以及設定變數的內容?如何使用,以及需要注意些什麼?
問題一大堆,那接下來,就讓我們一起去一探nginx原始碼的祕密。
我要講的內容
- 變數的分類
- 相關結構
- 模組中操作變數的函式
- 變數的實現原始碼及流程
1. 變數的分類
站在使用者的角度來看,我們在配置檔案中可以看到:
- set新增的變數(變數名由使用者設定)
- nginx功能模組中新增的變數,如geo模組(變數名由使用者設定)
- nginx內建的變數(變數名已由nginx設定好,可以看
ngx_http_core_variables
結構) - 有一定規則的變數,如”
http_host
”等(有相同字首,表示某一類變數),我們就稱為規則變數吧
從這裡,也解決我們的問題,在配置access_log
時,我們可以配置哪些變數,是否是使用者新增的變數,是否是內建變數在ngx_http_core_variables
中有,其次,是否是規則變數,另外,如果想輸出自己的內容,那隻能寫模組自己新增一個變量了,或者hack nginx在ngx_http_core_variables
從nginx內部實現上來看,變數可分為:
- hash過的變數
- 未hash過的變數,變數有設定
NGX_HTTP_VAR_NOHASH
- 未hash過的變數,但有一定規則的變數,如以這些串開頭的:”
http_
”,”sent_http_
”,”upstream_http_
”,”cookie_
”,”arg_
”
我們在模組裡面可以通過ngx_http_add_variable
來新增一個變數,在後面的介紹中我們可以看到。而我們新增的變數,最好不要是以這些規則開頭的變數,否則就有可能會覆蓋掉這些規則的變數。
從變數獲取者來看,可以分為索引變數與未索引的變數。
- 索引變數,我們通過
ngx_http_get_variable_index
ngx_http_get_indexed_variable
與ngx_http_get_flushed_variable
來獲取索引過變數的值。如果要索引某個變數,則只能在配置檔案初始化的時候來設定。ngx_http_get_variable_index
不會新增一個真正的變數,在配置檔案初始化結束時,會檢查該變數的合法性。索引過的變數,將會有快取等特性(快取在r->variables
中)。 - 未索引過的變數,則只能通過
ngx_http_get_variable
來獲取變數的值。
2. 相關結構
接下來,我們就要開始進入原始碼的世界了,先看看幾個關鍵結構:
// ngx_variable_value_t即變數的結果,變數的值
typedef struct {
unsigned len:28;
unsigned valid:1; // 當前變數是否合法
unsigned no_cacheable:1; // 當前變數是否可以快取,快取過的變數將只會呼叫一次get_handler函式
unsigned not_found:1;// 變數是否找到
unsigned escape:1;
u_char *data; // 變數的資料
} ngx_variable_value_t;
// 變數本身的資訊
struct ngx_http_variable_s {
ngx_str_t name; // 變數的名稱
ngx_http_set_variable_pt set_handler; // 變數的設定函式
ngx_http_get_variable_pt get_handler; // 變數的get函式
uintptr_t data; // 傳給get與set_handler的值
ngx_uint_t flags; // 變數的標誌
ngx_uint_t index; // 如果有索引,則是變數的索引號
};
// 在ngx_http_core_module的配置檔案中儲存了所使用的變數資訊
typedef struct {
ngx_hash_t variables_hash; // 變數的hash表
ngx_array_t variables; // 索引變數的陣列
ngx_hash_keys_arrays_t *variables_keys; // 變數的hash陣列
} ngx_http_core_main_conf_t;
// 變數在每個請求中的值是不一樣的,也就是說變數是請求相關的
// 所以在ngx_http_request_s中有一個變數陣列,主要用於快取當前請求的變數結果
// 從而可以避免一個變數的多次計數,計算過一次的變數就不用再計算了
// 但裡面儲存的一定是索引變數的值,是否快取,也要由變數的特性來決定
struct ngx_http_request_s {
ngx_http_variable_value_t *variables;
}
3. 模組中操作變數的函式
那麼,在模組中,我們要如何使用一個變數呢?在前面講分類的時候,我們也提到過了,這裡再總結並細說一下:
首先,如果要新增一個變數,我們需要呼叫ngx_http_add_variable
函式來新增一個變數。新增時需要指明變數的名稱就行了。
// name: 即變數的名字
// flags: 如果同一個變數要多次新增,則flags應該設定NGX_HTTP_VAR_CHANGEABLE
// 否則,多次新增將會提示重複
// flags表示可以是:NGX_HTTP_VAR_CHANGEABLE
// NGX_HTTP_VAR_NOCACHEABLE
// NGX_HTTP_VAR_INDEXED
// NGX_HTTP_VAR_NOHASH
ngx_http_variable_t *ngx_http_add_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags);
然後,要獲取變數,如果要高效一點,我們可以先將該變數放到索引數組裡面,通過ngx_http_get_variable_index
來新增一個變數的索引:
// name: 即nginx支援的任意變數名
// 返回該變數的索引
ngx_int_t ngx_http_get_variable_index(ngx_conf_t *cf, ngx_str_t *name);
不過,要注意的是,新增的變數必須是nginx支援的已存在的變數。即如果是hash過的變數,則一定是通過ngx_http_add_variable
新增的變數,否則,一定是規則變數,如”http_host
”。當然,在解析配置檔案的時候,變數不一定是要先通過ngx_http_add_variable
然後才能獲取索引,這個是不需要有順序保證的。nginx會將在最後配置檔案解析完成後,去驗證這些索引變數的合法性,在ngx_http_variables_init_vars
函式中可以看到,我們在後面具體再分析。
所以,可以看到,獲取索引的操作,一定是要在解析配置檔案的過程是進行的, 一旦配置檔案解析完成後,索引變數不能再新增。在獲取索引號後,我們需要儲存該索引號,以便在後面通過索引號來獲取變數。
那麼,索引變數的獲取,可以通過ngx_http_get_indexed_variable
與ngx_http_get_flushed_variable
來獲取,兩個函式間的區別,我們後面再介紹:
ngx_http_variable_value_t *ngx_http_get_indexed_variable(ngx_http_request_t *r, ngx_uint_t index);
ngx_http_variable_value_t *ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index);
而如果沒有索引過的變數,則只能通過ngx_http_get_variable
函式來獲取了。
// key 由ngx_hash_strlow來計算
ngx_http_variable_value_t *ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key);
可以看到,key是通過ngx_hash_strlow來計算的,所以變數名是沒有大小寫區分的。
最後,通過獲取變數的函式,我們可以看到,變數是與請求相關的,也就是獲取的變數都是與當前請求相關的。
4. 變數的實現原始碼及流程
那接下來,我們就來看看nginx在原始碼中的實現吧!
初始化:
首先,在資料結構中,我們知道ngx_http_core_main_conf_t
中儲存了變數相關的一些資訊,我們新增的變數key放在cmcf->variables_keys
中,而cmcf->variables儲存變數的索引結構,cmcf->variables_hash
則儲存著變數hash過的結構。
ngx_http_add_variable
新增變數的時候,會先放到cmcf->variables_keys
中,然後在解析完後,再生成hash結構體。
那麼,ngx_http_core_module
的preconfiguration階段,呼叫ngx_http_variables_add_core_vars
初始化變數的資料結構,然後再新增ngx_http_core_variables
結構中的變數。所以可以看出,nginx中內建的變數是在這個數組裡面的。
然後在解析其它模組的配置檔案時,會通過ngx_http_add_variable
函式來新增變數:
ngx_http_variable_t *
ngx_http_add_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags)
{
// 先檢查變數是否在已新增
key = cmcf->variables_keys->keys.elts;
for (i = 0; i < cmcf->variables_keys->keys.nelts; i++) {
if (name->len != key[i].key.len
|| ngx_strncasecmp(name->data, key[i].key.data, name->len) != 0)
{
continue;
}
v = key[i].value;
// 如果已新增,並且是不可變的變數,則提示變數的重複新增
// 其它NGX_HTTP_VAR_CHANGEABLE就是為了讓變數的重複新增時不出錯,都指向同一變數
if (!(v->flags & NGX_HTTP_VAR_CHANGEABLE)) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"the duplicate \"%V\" variable", name);
return NULL;
}
// 如果變數已新增,並且有NGX_HTTP_VAR_CHANGEABLE表志,則直接返回
return v;
}
// 新增這個變數
v = ngx_palloc(cf->pool, sizeof(ngx_http_variable_t));
v->name.len = name->len;
// 注意,變數名不區分大小寫
ngx_strlow(v->name.data, name->data, name->len);
rc = ngx_hash_add_key(cmcf->variables_keys, &v->name, v, 0);
if (rc == NGX_ERROR) {
return NULL;
}
return v;
}
在新增完變數後,我們需要設定變數的get_handler
與set_handler
。get_handler
是當我們在獲取變數的時候呼叫的函式,在該函式中,我們需要設定變數的值。而在set_handler
則是用於主動設定變數的值。get_handler
與set_handler
的區別是:get_handler
是在變數使用時獲取值,而set_handler
則是變數會主動先設定好,在使用的時候就不用再算了。目前,set
指令,設定一個變數的值是用的set_handler
。
在需要獲取變數的模組中,可以通過ngx_http_get_variable_index
來得到變數的索引,這個函式工作很簡單,就是在ngx_http_core_main_conf_t
的variables中新增一個變數,並返回該變數在陣列中的索引號。原始碼就不展示了。然後,在解析配置檔案之後,在ngx_http_block
中通過ngx_http_variables_init_vars
函式來初始化變數,在ngx_http_variables_init_vars
中,會做兩個事情,檢查索引變數,以及初始化變數的hash表。首先,對索引陣列中的每一個元素,會先檢查是否在ngx_http_core_main_conf_t
的variables_keys
中出現,即是否是新增過的,然後再檢查是否是有特定規則的變數,如”http_host
”,如果都不是,則說明該變數是不存在的,該索引會對應於一個不存在的變數,所以就會提示錯誤,程式無法啟動。然後,如果變數有設定NGX_HTTP_VAR_NOHASH
,則會跳過該變數,不進行hash,再對hash過的變數建立hash表。
在請求中: 當一個請求過來時,在ngx_http_init_request函式中,即請求初始化的時候,會建立一個與ngx_http_core_main_conf_t中的變數索引陣列variables大小一樣的陣列。r->variables有兩個作用,一是為了快取變數的值,二是可以在建立子請求時,父請求給子請求傳遞一些資訊。注意,變數的值是與當前請求相關的,所以每個請求裡面會不一樣。 然後在模組裡面ngx_http_get_indexed_variable和ngx_http_get_flushed_variable,這兩個函式的程式碼還是要小講一下:
ngx_http_variable_value_t *
ngx_http_get_indexed_variable(ngx_http_request_t *r, ngx_uint_t index)
{
ngx_http_variable_t *v;
ngx_http_core_main_conf_t *cmcf;
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
// 變數已經獲取過了,就不再計算變數的值,直接返回
if (r->variables[index].not_found || r->variables[index].valid) {
return &r->variables[index];
}
// 如果變數是初次獲取,則呼叫變數的get_handler來得到變數值,並快取到r->variables中去
v = cmcf->variables.elts;
if (v[index].get_handler(r, &r->variables[index], v[index].data)
== NGX_OK)
{
if (v[index].flags & NGX_HTTP_VAR_NOCACHEABLE) {
r->variables[index].no_cacheable = 1;
}
return &r->variables[index];
}
// 變數獲取失敗,設定為不合法,以及未找到
// 注意我們在呼叫完本函式後,需要檢查函式的返回值以及這兩個屬性
r->variables[index].valid = 0;
r->variables[index].not_found = 1;
return NULL;
}
ngx_http_variable_value_t *
ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_t index)
{
ngx_http_variable_value_t *v;
v = &r->variables[index];
if (v->valid) {
// 變數已經獲取過了,而且是合法的並且可快取的,則直接返回
if (!v->no_cacheable) {
return v;
}
// 否則,清除標誌,並再次獲取變數的值
v->valid = 0;
v->not_found = 0;
}
return ngx_http_get_indexed_variable(r, index);
}
注意:ngx_http_get_flushed_variable
會考慮到變數的cache標誌,如果變數是可快取的,則只有在變數是合法的時才返回變數的值,否則重新獲取變數的值。而ngx_http_get_indexed_variable
則不管變數是否可快取,只要獲取過一次了,不管是否成功,則都不會再獲取了。最後,如果是未索引的變數,我們可以通過ngx_http_get_variable
函式來得到變數的值。ngx_http_get_variable
做的工作:
- 變數是hash過的,而且變數有索引過,則呼叫
ngx_http_get_flushed_variable
來得到變數值。 - 變數hash過,未索引過,則呼叫變數的
get_handler
來獲取變數,注意,此時每次呼叫變數,都將會呼叫get_handler
來計算變數的值,然後返回該值。注意因為只有索引過的變數的值才會快取到ngx_http_request_t
的variables中去,所以變數的新增方要注意,如果當前變數是可快取的,要將該變數建立索引,即呼叫完ngx_http_add_variable
後,再呼叫ngx_http_get_variable_index
來將該變數建立索引。 - 特定規則的變數,”http_”開頭的會呼叫
ngx_http_variable_unknown_header_out
函式,”upstream_http_
”開頭的會呼叫ngx_http_upstream_header_variable
函式,”cookie_”開頭的會呼叫ngx_http_variable_cookie
函式,”arg_”開頭的會呼叫ngx_http_variable_argument
函式。 - 變數未找到,設定變數
至此,變數的整個流程差不多就完了,另外還有一個要注意的是,在建立子請求時候的變數。在ngx_http_subrequest
函式中,我們可以看到,子請求的variables是直接指向父請求的variables陣列的,所以子請求與父請求是共享variables陣列的,這樣父子請求就可以傳遞變數的值。但正因為如此,我們在使用父子請求的時候會產生一些問題,如果一個父請求建立多個子請求,他們之間獲取同一個變數時,會有很明顯的干擾,因為每個請求的環境是不一樣的,這樣獲取的值也是不一樣的。
好吧,變數也簡單的介紹了一下。