再谈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建立在变量上的这套处理机制,就像一个预定好的方程,类似的运算过程,以不同的变量值来运行,获得我们想要的不同结果。





评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值