1. 程式人生 > >nginx原始碼分析之變數

nginx原始碼分析之變數

nginx中的變數在nginx中的使用非常的多,正因為變數的存在,使得nginx在配置上變得非常靈活。

我們知道,在nginx的配置檔案中,配合變數,我們可以動態的得到我們想要的值。最常見的使用是,我們在寫access_log的格式時,需要用到多很多變數。 而這些變數是如何工作的呢?我們可以輸出哪些變數?我們又怎麼才能輸出自己想要的內容呢?當然,我們可能還想知道,如何在我們的模組裡面去使用變數,如何新增變數,獲取變數的值,以及設定變數的內容?如何使用,以及需要注意些什麼?

問題一大堆,那接下來,就讓我們一起去一探nginx原始碼的祕密。

我要講的內容

  1. 變數的分類
  2. 相關結構
  3. 模組中操作變數的函式
  4. 變數的實現原始碼及流程

1. 變數的分類

站在使用者的角度來看,我們在配置檔案中可以看到:

  1. set新增的變數(變數名由使用者設定)
  2. nginx功能模組中新增的變數,如geo模組(變數名由使用者設定)
  3. nginx內建的變數(變數名已由nginx設定好,可以看ngx_http_core_variables結構)
  4. 有一定規則的變數,如”http_host”等(有相同字首,表示某一類變數),我們就稱為規則變數吧

從這裡,也解決我們的問題,在配置access_log時,我們可以配置哪些變數,是否是使用者新增的變數,是否是內建變數在ngx_http_core_variables中有,其次,是否是規則變數,另外,如果想輸出自己的內容,那隻能寫模組自己新增一個變量了,或者hack nginx在ngx_http_core_variables

中新增一個變數。

從nginx內部實現上來看,變數可分為:

  1. hash過的變數
  2. 未hash過的變數,變數有設定NGX_HTTP_VAR_NOHASH
  3. 未hash過的變數,但有一定規則的變數,如以這些串開頭的:”http_”,”sent_http_”,”upstream_http_”,”cookie_”,”arg_

我們在模組裡面可以通過ngx_http_add_variable來新增一個變數,在後面的介紹中我們可以看到。而我們新增的變數,最好不要是以這些規則開頭的變數,否則就有可能會覆蓋掉這些規則的變數。

從變數獲取者來看,可以分為索引變數與未索引的變數。

  1. 索引變數,我們通過ngx_http_get_variable_index
    來獲得一個索引變數的索引號。然後可以通過ngx_http_get_indexed_variablengx_http_get_flushed_variable來獲取索引過變數的值。如果要索引某個變數,則只能在配置檔案初始化的時候來設定。ngx_http_get_variable_index不會新增一個真正的變數,在配置檔案初始化結束時,會檢查該變數的合法性。索引過的變數,將會有快取等特性(快取在r->variables中)。
  2. 未索引過的變數,則只能通過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_variablengx_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_handlerset_handlerget_handler是當我們在獲取變數的時候呼叫的函式,在該函式中,我們需要設定變數的值。而在set_handler則是用於主動設定變數的值。get_handlerset_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_tvariables_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做的工作:

  1. 變數是hash過的,而且變數有索引過,則呼叫ngx_http_get_flushed_variable來得到變數值。
  2. 變數hash過,未索引過,則呼叫變數的get_handler來獲取變數,注意,此時每次呼叫變數,都將會呼叫get_handler來計算變數的值,然後返回該值。注意因為只有索引過的變數的值才會快取到ngx_http_request_t的variables中去,所以變數的新增方要注意,如果當前變數是可快取的,要將該變數建立索引,即呼叫完ngx_http_add_variable後,再呼叫ngx_http_get_variable_index來將該變數建立索引。
  3. 特定規則的變數,”http_”開頭的會呼叫ngx_http_variable_unknown_header_out函式,”upstream_http_”開頭的會呼叫ngx_http_upstream_header_variable函式,”cookie_”開頭的會呼叫ngx_http_variable_cookie函式,”arg_”開頭的會呼叫ngx_http_variable_argument函式。
  4. 變數未找到,設定變數

至此,變數的整個流程差不多就完了,另外還有一個要注意的是,在建立子請求時候的變數。在ngx_http_subrequest函式中,我們可以看到,子請求的variables是直接指向父請求的variables陣列的,所以子請求與父請求是共享variables陣列的,這樣父子請求就可以傳遞變數的值。但正因為如此,我們在使用父子請求的時候會產生一些問題,如果一個父請求建立多個子請求,他們之間獲取同一個變數時,會有很明顯的干擾,因為每個請求的環境是不一樣的,這樣獲取的值也是不一樣的。

好吧,變數也簡單的介紹了一下。