再談nginx變量(一)

這裏以ngx_http_script_compile爲線索,看一下nginx的變量原理中還有哪些值得挖掘的地方。
ngx_http_script_compile函數被調用,一般都是用來處理變量的,特別是在配置處理階段,出現變量的時候(即"$"開頭的配置),一般都會使用這個函數來做處理,生成所謂的“運行時處理機“。在函數的開始,有個ngx_http_script_init_arrays函數,從字面來看,我們也能大體知道它的作用,這裏先暫時放一下,後面再討論。

核心的處理就在一個for循環裏。其中sc包含了我們需要的絕大部分信息,那麼這個所謂的sc到底是個什麼來歷呢?我們以proxy_pass爲例子:
ngx_http_proxy_pass:
...
/* n是對proxy_pass中出現的變量進行計數,看有幾個變量要處理 */
if (n) {

        ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

        sc.cf = cf;
        sc.source = url; // url就是proxy_pass後面配置的字符串,含有變量
        /* 
         * 這裏需要交代的是,nginx這套”運行時處理機“在處理結果的處理上是分長度和內容
         * 兩部分的,也就是說,獲得變量實際值對應長度和內容的處理子(也就是處理函數),分別
         * 保存在lengths和values中。
         */
        sc.lengths = &plcf->proxy_lengths;
        sc.values = &plcf->proxy_values;
        sc.variables = n;

        /* 這兩個值是作爲一次compile的結束標記,在lengths和values的最後添加一個空處理子,即NULL指針。
         * 後面會講到:在運行時處理時,即處理lengths和values的時候,碰到NULL,這次處理過程就宣告結束 */
        sc.complete_lengths = 1;
        sc.complete_values = 1;

        if (ngx_http_script_compile(&sc) != NGX_OK) {
            return NGX_CONF_ERROR;
        }

        return NGX_CONF_OK;
    }
接下來看ngx_http_script_compile的核心處理部分:
for (i = 0; i < sc->source->len; /* void */ ) {

        name.len = 0;

        if (sc->source->data[i] == '$') {
            // 以'$'結尾,是有錯誤的,因爲這裏處理的都是變量,而不是正則(正則裏面末尾帶$是有意思的)
            if (++i == sc->source->len) {
                goto invalid_variable;
            }

#if (NGX_PCRE)
            {
            ngx_uint_t  n;

            /*
             * 注意,在這裏所謂的變量有兩種,一種是$後面跟字符串的,一種是跟數字的。
             * 這裏判斷是否是數字形式的變量。
             */
            if (sc->source->data[i] >= '1' && sc->source->data[i] <= '9') {

                n = sc->source->data[i] - '0';

                if (sc->captures_mask & (1 << n)) {
                    sc->dup_capture = 1;
                }
                /*
                 * 在sc->captures_mask中將數字對應的位置1,那麼captures_mask的作用是什麼?
                 * 在後面對sc結構體分析時會提到。
                 */
                sc->captures_mask |= 1 << n;
                if (ngx_http_script_add_capture_code(sc, n) != NGX_OK) {
                    return NGX_ERROR;
                }

                i++;

                continue;
            }
            }
#endif
            
            /*
             * 這裏是個有意思的地方,舉個例子,假設有個這樣一個配置proxy_pass $host$uritest,
             * 我們這裏其實是想用nginx的兩個內置變量,host和uri,但是對於$uritest來說,如果我們
             * 不加處理,那麼在函數裏很明顯會將uritest這個整體作爲一個變量,這顯然不是我們想要的。
             * 那怎麼辦呢?nginx裏面使用"{}"來把一些變量包裹起來,避免跟其他的字符串混在一起,在此處
             * 我們可以這樣用${uri}test,當然變量之後是數字,字母或者下劃線之類的字符纔有必要這樣處理
             * 代碼中體現的很明顯。
             */
            if (sc->source->data[i] == '{') {
                bracket = 1;

                if (++i == sc->source->len) {
                    goto invalid_variable;
                }
                // name用來保存一個分離出的變量
                name.data = &sc->source->data[i];

            } else {
                bracket = 0;
                name.data = &sc->source->data[i];
            }

            for ( /* void */ ; i < sc->source->len; i++, name.len++) {
                ch = sc->source->data[i];
                
                // 在"{}"中的字符串會被分離出來(即break語句),避免跟後面的字符串混在一起
                if (ch == '}' && bracket) {
                    i++;
                    bracket = 0;
                    break;
                }
                
                /*
                 * 變量中允許出現的字符,其他字符都不是變量的字符,所以空格是可以區分變量的。
                 * 這個我們在配置裏經常可以感覺到,而它的原理就是這裏所顯示的了
                 */
                if ((ch >= 'A' && ch <= 'Z')
                    || (ch >= 'a' && ch <= 'z')
                    || (ch >= '0' && ch <= '9')
                    || ch == '_')
                {
                    continue;
                }

                break;
            }

            if (bracket) {
                ngx_conf_log_error(NGX_LOG_EMERG, sc->cf, 0,
                                   "the closing bracket in \"%V\" "
                                   "variable is missing", &name);
                return NGX_ERROR;
            }

            if (name.len == 0) {
                goto invalid_variable;
            }
            
            // 變量計數
            sc->variables++;
            // 得到一個變量,做處理
            if (ngx_http_script_add_var_code(sc, &name) != NGX_OK) {
                return NGX_ERROR;
            }

            continue;
        }
        
        /* 
         * 程序到這裏意味着一個變量分離出來,或者還沒有碰到變量,一些非變量的字符串,這裏不妨稱爲”常量字符串“
         * 這裏涉及到請求參數部分的處理,比較簡單。這個地方一般是在一次分離變量或者常量結束後,後面緊跟'?'的情況
         * 相關的處理子在ngx_http_script_add_args_code會設置。
         */
        if (sc->source->data[i] == '?' && sc->compile_args) {
            sc->args = 1;
            sc->compile_args = 0;

            if (ngx_http_script_add_args_code(sc) != NGX_OK) {
                return NGX_ERROR;
            }

            i++;

            continue;
        }
        
        // 這裏name保存一段所謂的”常量字符串“
        name.data = &sc->source->data[i];

        // 分離該常量字符串
        while (i < sc->source->len) {

            // 碰到'$'意味着碰到了下一個變量
            if (sc->source->data[i] == '$') {
                break;
            }
            /*
             * 此處意味着我們在一個常量字符串分離過程中遇到了'?',如果我們不需要對請求參數做特殊處理的話,
             * 即sc->compile_args = 0,那麼我們就將其作爲常量字符串的一部分來處理。否則,當前的常量字符串會
             * 從'?'處,截斷,分成兩部分。*/
            if (sc->source->data[i] == '?') {

                sc->args = 1;

                if (sc->compile_args) {
                    break;
                }
            }

            i++;
            name.len++;
        }
        
        // 一個常量字符串分離完畢,sc->size統計整個字符串(即sc->source)中,常量字符串的總長度
        sc->size += name.len;

        // 常量字符串的處理子由這個函數來設置
        if (ngx_http_script_add_copy_code(sc, &name, (i == sc->source->len))
            != NGX_OK)
        {
            return NGX_ERROR;
        }
    }
    
    // 本次compile結束,做一些收尾善後工作。
    return ngx_http_script_done(sc);
上面我們分析了一個compile過程的主要工作,很顯然,還有細節沒有討論到。在compile過程中,共需要處理4類:$1這樣的capture變量,普通的變量($uri),args變量,常量(即常量字符串)。其實這些變量的處理過程總體來說並不算多麻煩,而有些細節確實難點。我們這裏總結下,之前的博客有對變量和腳本引擎機制做過探討了,這裏把一下沒有談論到的難點和細節,或者之前不是太清楚的分析再來探討一下,希望你我都能有所收穫。

對於流程,一般gdb跟一下,配合debug日誌,基本上可以理清,難點就在於一些結構中的成員,特別是有些標記位的使用,卻是貫穿整個系統,在理解上有不少難度,這是我們這裏討論的重點,對於這些地方搞懂了,流程就不是什麼大問題了。

首先看ngx_http_script_compile_t結構,這個結構在compile的時候被使用過。
typedef struct {
    ngx_conf_t                 *cf;             // 配置信息
    ngx_str_t                  *source;         // 需要compile的字符串
    
    /*
     * 保存普通變量在變量表中的index,關於什麼是變量表,後面會討論
     */
    ngx_array_t               **flushes;
    ngx_array_t               **lengths;         // 處理變量長度的處理子數組
    ngx_array_t               **values;          // 處理變量內容的處理子數組

    ngx_uint_t                  variables;       // 普通變量的個數,而非其他三種(args變量,$n變量以及常量字符串)

    /* 
     * 下面三個變量放在一起討論,他們都跟pcre的正則處理相關,這三個用到的地方比較少
     */
    ngx_uint_t                  ncaptures;      // 當前處理時,出現的$n變量的最大值,如配置的最大爲$3,那麼ncaptures就等於3

    /*
     * 以位移的形式保存$1,$2...$9等變量,即響應位置上置1來表示,主要的作用是爲dup_capture準備,
     * 正是由於這個mask的存在,才比較容易得到是否有重複的$n出現。
     */
    ngx_uint_t                  captures_mask;  

    /*
     * 這個標記位主要在rewrite模塊裏使用,在ngx_http_rewrite中,
     * if (sc.variables == 0 && !sc.dup_capture) {
     *     regex->lengths = NULL;
     * }
     * 沒有重複的$n,那麼regex->lengths被置爲NULL,這個設置很關鍵,在函數
     * ngx_http_script_regex_start_code中就是通過對regex->lengths的判斷,來做不同的處理,
     * 因爲在沒有重複的$n的時候,可以通過正則自身的captures機制來獲取$n,一旦出現重複的,
     * 那麼pcre正則自身的captures並不能滿足我們的要求,我們需要用自己handler來處理。
     */
    unsigned                    dup_capture:1;

    ngx_uint_t                  size;           // 待compile的字符串中,”常量字符串“的總長度
    
    /* 
     * 對於main這個成員,有許多要挖掘的東西。main一般用來指向一個
     * ngx_http_script_regex_code_t的結構,那麼這個main到底起到了什麼作用呢?
     * 這裏有對它進行分析。
     */
    void                       *main;

    unsigned                    compile_args:1;       // 是否需要處理請求參數
    unsigned                    complete_lengths:1;   // 是否設置lengths數組的終止符,即NULL
    unsigned                    complete_values:1;    // 是否設置values數組的終止符
    unsigned                    zero:1;               // values數組運行時,得到的字符串是否追加'\0'結尾
    unsigned                    conf_prefix:1;        // 是否在生成的文件名前,追加路徑前綴
    unsigned                    root_prefix:1;        // 同conf_prefix

    unsigned                    args:1;               // 待compile的字符串中是否發現了'?'
} ngx_http_script_compile_t;
在函數ngx_http_script_add_var_code中,用到了ngx_http_core_main_conf_t(後面以cmcf代替)中的variables,即所謂的全局變量數組,
由於是cmcf,以爲着在所有的server塊,location塊,包括upstream塊裏面,都是可見的,即一方修改,便會在其他地方呈現出變化的道理。
在各個module中的preconfiguration函數裏,都會將該module預設的一些全局變量,放到cmcf->variables_keys中。另外一個重要的成員就是
cmcf->variables,前面提到cmcf->variables_keys是所有預設的變量(和通過set指令設置的),而cmcf->variables則是配置中實際用到的變量。放到cmcf->variables中的變量實際上是先佔個位置,這些變量的更多信息,來源於cmcf->variables_keys,所以在配置解析結束之後,通過ngx_http_variables_init_vars函數來填充這個變量的各個重要信息。

在r中,也有一個variables成員,它是個數組,而且數組成員的個數跟cmcf->variabels是一樣的,區別在於cmcf->variabels的成員類型是:
struct ngx_http_variable_s {
    ngx_str_t                     name;        /* 變量的字符串值 */
   
    ngx_http_set_variable_pt      set_handler; /* 使用變量中的值設置request的某個成員的值 */
    ngx_http_get_variable_pt      get_handler; /* 根據request中成員(如uri,args等)的值來設置,r->variables中對應變量的內容 */
    uintptr_t                     data;        /* 在set和get操作中使用,一般是r中某個成員在request結構中的offset */
    ngx_uint_t                    flags;       /* 一些在set和get中控制特定動作的標誌,後面會講到 */
    ngx_uint_t                    index;       /* 某個變量在r->variabels或者cmcf->variabels中數組中的下標 */
};
而r->variables的成員類型是:
typedef struct {
    unsigned    len:28;               /* 變量值的長度 */

    unsigned    valid:1;              /* 變量是否有效 */
    unsigned    no_cacheable:1;       /* 變量是否是可緩存的,一般來說,某些變量在第一次得到變量值後,後面再次用到時,可以直接使用上            
                                       * 次的值,而對於一些所謂的no_cacheable的變量,則需要在每次使用的時候,都要通過get_handler之 
                                       * 類操作,再次獲取 
                                       */
    unsigned    not_found:1;          /* 變量沒有找到,一般是指某個變量沒用能夠通過get獲取到其變量值 */
    unsigned    escape:1;             /* 變量值是否需要作轉義處理*/

    u_char     *data;                 /* 變量值 */
} ngx_variable_value_t;
這兩個結構的關係很密切,一個所謂變量,一個所謂變量值,所以nginx建立在變量上的這套處理機制,就像一個預定好的方程,類似的運算過程,以不同的變量值來運行,獲得我們想要的不同結果。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章