nginx 變量 獲取 ngx_http_script_run 詳解

ngx_http_script_run 的作用是從配置文件中獲取配置項的值。
這樣說可能太抽象,我們來舉個例子。

比如:
現在需要從 nginx.conf 中取這樣一個配置項:

tt_signpass “mypass$remote_addr”;

注意,這個配置項的值包含 2 個部分 mypass(靜態字符 / 常量) + $remote_addr(動態參數)。

那 ngx_http_script_run 是怎麼完成這 2 個部分的解析的呢?
先來跟蹤下這個函數:

u_char *
ngx_http_script_run(ngx_http_request_t *r, ngx_str_t *value,
    void *code_lengths, size_t len, void *code_values)
{
    ngx_uint_t                    i;
    ngx_http_script_code_pt       code;
    ngx_http_script_len_code_pt   lcode;
    ngx_http_script_engine_t      e;
    ngx_http_core_main_conf_t    *cmcf;

    // 獲取 core 模塊的配置項
    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

    for (i = 0; i < cmcf->variables.nelts; i++) {
       if (r->variables[i].no_cacheable) {
           r->variables[i].valid = 0;
            r->variables[i].not_found = 0;
       }
  }

    ngx_memzero(&e, sizeof(ngx_http_script_engine_t));

    e.ip = code_lengths;
    e.request = r;
    e.flushed = 1;

    // 找到已經設置好的函數,執行並獲得字符長度結果
    while (*(uintptr_t *) e.ip) {
        lcode = *(ngx_http_script_len_code_pt *) e.ip;
        len += lcode(&e);
    }

    value->len = len;
    value->data = ngx_pnalloc(r->pool, len);
    if (value->data == NULL) {
        return NULL;
    }

    e.ip = code_values;
    e.pos = value->data;

    // 找到已經設置好的函數,調用獲取返回值
    while (*(uintptr_t *) e.ip) {
        code = *(ngx_http_script_code_pt *) e.ip;
        code((ngx_http_script_engine_t *) &e);
    }

    return e.pos;
}

看到這裏,有個疑問:回調函數哪裏來的? 來,直接看
-> ngx_http_script_compile, 它的作用是根據配置項的字符,分離常量和變量,同時準備獲取實際內容的環境和條件。
可能有人會有疑問:
1,爲什麼不馬上獲取?
2,獲取出來的內容不是直接就是字符了麼,還分什麼實際內容。

這都是針對配置項的值可能包含了 $remote_addr 這種動態參數,並且這種動態參數對於每次請求來說,都可能是不一樣的值。所以有這個需求,在需要的地方及時獲取。ngx_http_script_compilengx_http_script_run 一起, 這 2 個函數的實現我們可以看成是 LazyLoad, 或者是延遲調用這樣的概念。

我們繼續看 ngx_http_script_compile

ngx_int_t
ngx_http_script_compile(ngx_http_script_compile_t *sc)
{
    u_char       ch;
    ngx_str_t    name;
    ngx_uint_t   i, bracket;

    // 初始化存放變量的數組
    if (ngx_http_script_init_arrays(sc) != NGX_OK) {
        return NGX_ERROR;
    }

    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) // Perl的正則庫支持,處理類似 $1 的變量模型
            ...
#endif
            // 處理 ${var}www 變量和字符相連的模型,以 { 開始
            if (sc->source->data[i] == '{') {
                bracket = 1;

                if (++i == sc->source->len) {
                    goto invalid_variable;
                }

                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];

                // 處理 ${var}www 變量和字符相連的模型,以 } 結束
                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;
        }

        // 根據 sc->compile_args 確定 '?' 是帶參數的形式解析,
        // 還是認定它本身是常量字符,這裏是針對 ${var}?*** 這種模型來解析
        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.data = &sc->source->data[i];

        // 在解析常量的時候遇到了 '$''?'
        while (i < sc->source->len) {

            if (sc->source->data[i] == '$') {
                break;
            }

            // 此處處理類似 ABC?abc 的模型
            if (sc->source->data[i] == '?') {

                sc->args = 1;

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

            i++;
            name.len++;
        }

        sc->size += name.len;

        // 對常量的長度和值的獲取,準備回調等環境
        if (ngx_http_script_add_copy_code(sc, &name, (i == sc->source->len))
            != NGX_OK)
        {
            return NGX_ERROR;
        }
    }

    return ngx_http_script_done(sc);

    ...
}

ngx_http_script_compile 函數是對配置項值解析的流程和框架的預加載,在實際環境中只需要調用 ngx_http_script_run 就可以獲取到配置項實際代表的內容了。
將複雜的邏輯提前,簡化實際運行的處理邏輯,這符合一個高性能服務器的要求。

看到這裏,可能有了一個粗略的瞭解,但是在 ngx_http_script_run 調用的回調到底是哪個呢?

再來看 ngx_http_script_compile 中調用的 2 個函數:

ngx_http_script_add_var_code
ngx_http_script_add_copy_code

來分別跟蹤下:

static ngx_int_t
ngx_http_script_add_var_code(ngx_http_script_compile_t *sc, ngx_str_t *name)
{
    ngx_int_t                    index, *p;
    ngx_http_script_var_code_t  *code;

    index = ngx_http_get_variable_index(sc->cf, name);

    if (index == NGX_ERROR) {
        return NGX_ERROR;
    }

    if (sc->flushes) {
        p = ngx_array_push(*sc->flushes);
        if (p == NULL) {
            return NGX_ERROR;
        }

        *p = index;
    }

    //爲將要解析的變量,申請空間,並添加到鏈尾
    code = ngx_http_script_add_code(*sc->lengths,
                                    sizeof(ngx_http_script_var_code_t), NULL);
    if (code == NULL) {
        return NGX_ERROR;
    }

    //添加獲取變量長度的回調函數
    code->code = (ngx_http_script_code_pt) ngx_http_script_copy_var_len_code;
    code->index = (uintptr_t) index;

    code = ngx_http_script_add_code(*sc->values,
                                    sizeof(ngx_http_script_var_code_t),
                                    &sc->main);
    if (code == NULL) {
        return NGX_ERROR;
    }

    // 添加獲取參數的內容的回調
    code->code = ngx_http_script_copy_var_code;
    code->index = (uintptr_t) index;

    return NGX_OK;
}

這個函數先爲要解析的變量申請了空間,並附加在全局鏈表尾部,然後爲這個變量長度的計算定義了回調函數 ngx_http_script_copy_var_len_code, 以及爲這個變量內容的獲取定義了回調函數 ngx_http_script_copy_var_code, 那麼其實在ngx_http_script_run 函數中就是通過調用這 2 個回調來獲取變量的值的。

對於常量的獲取其實也是相同的道理。來看下常量的處理函數:

static ngx_int_t
ngx_http_script_add_copy_code(ngx_http_script_compile_t *sc, ngx_str_t *value,
    ngx_uint_t last)
{
    u_char                       *p;
    size_t                        size, len, zero;
    ngx_http_script_copy_code_t  *code;

    zero = (sc->zero && last);
    len = value->len + zero;

    //申請內存,並添加至鏈尾
    code = ngx_http_script_add_code(*sc->lengths,
                                    sizeof(ngx_http_script_copy_code_t), NULL);
    if (code == NULL) {
        return NGX_ERROR;
    }

    //爲常量長度的獲取,添加回調函數
    code->code = (ngx_http_script_code_pt) ngx_http_script_copy_len_code;
    code->len = len;

    size = (sizeof(ngx_http_script_copy_code_t) + len + sizeof(uintptr_t) - 1)
            & ~(sizeof(uintptr_t) - 1);

    code = ngx_http_script_add_code(*sc->values, size, &sc->main);
    if (code == NULL) {
        return NGX_ERROR;
    }

    //爲常量內容的獲取,添加回調函數
    code->code = ngx_http_script_copy_code;
    code->len = len;

    p = ngx_cpymem((u_char *) code + sizeof(ngx_http_script_copy_code_t),
                   value->data, value->len);

    if (zero) {
        *p = '\0';
        sc->zero = 0;
    }

    return NGX_OK;
}

OK,相同的結構,偶們不在重複了。

接下來我們來看看獲取變量內容的回調函數的實現 ngx_http_script_copy_var_code 中都幹了些什麼。

void
ngx_http_script_copy_var_code(ngx_http_script_engine_t *e)
{
    u_char                      *p;
    ngx_http_variable_value_t   *value;
    ngx_http_script_var_code_t  *code;

    code = (ngx_http_script_var_code_t *) e->ip;

    e->ip += sizeof(ngx_http_script_var_code_t);

    if (!e->skip) {

        // e->flushed 標示是否去緩存還是重頭獲取值
        if (e->flushed) {
            value = ngx_http_get_indexed_variable(e->request, code->index);

        } else {
            value = ngx_http_get_flushed_variable(e->request, code->index);
        }

        if (value && !value->not_found) {
            p = e->pos;

            //嗯,終於找到獲取值的位置了。
            e->pos = ngx_copy(p, value->data, value->len);

        }
    }
}

對於以上代碼,有興趣的可以關注 ngx_http_get_indexed_variable 和 ngx_http_get_flushed_variable 這 2 個函數,不同點在於是否需要獲取事先設置好的 handler 去立即更新數據。 另外 nginx 提供了一些了功能性函數。比如 array, string 的操作,用 C 寫出了面向對象的概念,值得思考。 其中的一些功能函數,比如 ngx_array_push_n , 在數組擴充上是新結構大小的 2 倍(和 Vector 一致,這一點 C/C++ 和 Java都是一樣的, Java 中 ArrayList 是 (×3/2 + 1)),由此可見,編程的思想基本是一致的。

扯遠了,回來總結下,當我們要取一個配置項的值:

tt_signpass “mypass$remote_addr”;

對於常量和變量部分,其實是分開去獲取,然後拼接在一起的。在 ngx_http_script_compile 的實現中,先將常量和變量拆分,然後分別爲他們設置好回調處理函數(長度 + 內容)。比如變量內容的獲取就爲它設置好處理函數 ngx_http_script_copy_var_code。實際的運行環境中,在 ngx_http_script_run 中調用事先在 ngx_http_script_compile 中設置好的回調函數,來獲取該變量具體的值。

發佈了20 篇原創文章 · 獲贊 7 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章