Nginx的变量机制


在Nginx中,当同一个请求需要在不同模块之间进行数据传递或者说在配置文件里面使用模块的动态数据时,一般来说都是使用变量,正因为有了变量的存在,使nginx在配置上变得非常灵活。


我们知道,在nginx的配置文件中,配合变量,我们可以动态的得到我们想要的值。最常见的使用是,我们在写access_log的格式时,需要用到多很多变量。 我们可能想知道,如何在我们的模块里面去使用变量,如何添加变量,获取变量的值,以及设置变量的内容?如何使用,以及需要注意些什么?那么,接下来就让我们一窥Nginx变量的秘密。

    

关于nginx变量,我要讲的内容


一.变量的分类

二.变量的创建

三.变量的使用


一、变量的分类


首先,看一下与nginx变量相关的容器:


1. cmcf->variables: 这是一个array数组,收集了用户已经使用的所有变量,这些变量里可能有的是非法的变量,即未“声明”过就使用,这种合法性判断会在解析完配置文件后进行。


2. cmcf->variables_hash:这是一个hash表,保存了cmcf->variables_keys中所有变量(除去那些带有NGX_HTTP_VAR_NOHASH标志的变量)。


3. cmcf->variables_keys:这是一个临时的容器,保存了所有“声明”过的变量,配置文件解析完成后将会释放,而该容器中所有变量会被hash的变量和未被hash的变量分类保存,被hash过的变量里面,又细分为索引的变量和未进行索引的变量,而在索引过的变量里面,又细分为需要缓存的变量和未不需要缓存的变量。


4. r->variables: 这是一个ngx_http_variable_value_t的数组,保存了所有缓存的变量值。注意,r->variables和cmcf->variables是一一对应的关系,有点像key-value的那种对应关系,即cmcf->variables上的所有变量,其变量值都对应缓存在了r->variables上,v->no_cacheable标志位决定了是调用变量的get_handler获取变量的值还是直接从缓存数组r->variables上获取变量值。


站在使用者的角度来看,我们在配置文件中可以看到:


1.通过set指令添加的变量(变量名由用户设定)

2.nginx功能模块中添加的变量,如geo模块和map模块(变量名由用户设定)

3.nginx内建的变量(变量名已由nginx设定好,可以看ngx_http_core_variables结构)

4.有一定规则的变量,如arg_xxx, cookie_xxx, sent_cookie_xxx, http_xxx, sent_http_xxx, upstream_http_xxx这六大类的变量(有相同前缀,表示某一类变量),我们就称为规则变量吧。


从nginx内部实现上来看,变量可分为:

1.hash过的变量
2.未hash过的变量,变量有设置NGX_HTTP_VAR_NOHASH
3.未hash过的变量,但有一定规则的变量


在我们模块里面可以通过ngx_http_add_variable来添加一个变量,而我们添加的变量,最好不要是以这些规则开头的变量,否则就有可能会覆盖掉这些规则的变量。


从变量获取者来看,可以分为索引变量与未索引的变量。


1.索引变量,我们通过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_indexed_variable与ngx_http_get_flushed_variable的区别:前者不会判断缓存的特性,而后者则会判断v->no_cacheable标志位是否有效。

2.未索引过的变量,则只能通过ngx_http_get_variable来获取变量的值。


二、变量的创建


1. 相关结构


接下来,我们就要开始进入源码的世界了,先看看几个关键结构:

2. 模块中操作变量的函数


那么,在模块中,我们要如何添加一个变量呢?如果要添加一个变量,我们需要调用ngx_http_add_variable函数来添加一个变量。添加时需要指明变量的名称就行了。


// 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;
 }
// 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来计算的,所以变量名是没有大小写区分的。


最后,通过获取变量的函数,我们可以看到,变量是与请求相关的,也就是获取的变量都是与当前请求相关的。


3. 变量的实现源码及流程


那接下来,我们就来看看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数组的,这样父子请求就可以传递变量的值。但正因为如此,我们在使用父子请求的时候会产生一些问题,如果一个父请求创建多个子请求,他们之间获取同一个变量时,会有很明显的干扰,因为每个请求的环境是不一样的,这样获取的值也是不一样的。


三.变量的使用


Nginx的内部变量指的就是Nginx的官方模块中所导出的变量,在Nginx中,大部分常用的变量都是CORE HTTP模块导出的。而在Nginx中,不仅可以在模块代码中使用变量,而且还可以在配置文件中使用。


假设我们需要在配置文件中使用http模块的host变量,那么只需要这样在变量名前加一个$符号就可以了($host).而如果需要在模块中使用host变量,那么就比较麻烦,Nginx提供了下面几个接口来取得变量:

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_variable_value_t *ngx_http_get_variable(ngx_http_request_t *r,
     ngx_str_t *name, ngx_uint_t key); 他们的区别是这样子的,ngx_http_get_indexe

d_variable和ngx_http_get_flushed_variable都是用来取得有索引的变量,不过他们的区别是后一个会处理 NGX_HTTP_VAR_NOCACHEABLE这个标记,也就是说如果你想要cache你的变量值,那么你的变量属性就不能设置NGX_HTTP_VAR_NOCACHEABLE,并且通过ngx_http_get_flushed_variable来获取变量值.而ngx_http_get_variable和上面的区别就是它能够得到没有索引的变量值。


通过上面我们知道可以通过索引来得到变量值,可是这个索引该如何取得呢,Nginx也提供了对应的接口:


ngx_int_t ngx_http_get_variable_index(ngx_conf_t *cf, ngx_str_t *name);

通过这个接口,就可以取得对应变量名的索引值。


接下来来看对应的例子,比如在http_log模块中,如果在log_format中配置了对应的变量,那么它会调用ngx_http_get_variable_index来保存索引:


static ngx_int_t
 ngx_http_log_variable_compile(ngx_conf_t *cf, ngx_http_log_op_t *op,
     ngx_str_t *value)
 {
     ngx_int_t  index;
     //得到变量的索引
     index = ngx_http_get_variable_index(cf, value);
     if (index == NGX_ERROR) {
         return NGX_ERROR;
     }


     op->len = 0;
     op->getlen = ngx_http_log_variable_getlen;
     op->run = ngx_http_log_variable;
     //保存索引值
     op->data = index;


     return NGX_OK;
  }

然后http_log模块会使用ngx_http_get_indexed_variable来得到对应的变量值,这里要注意,就是使用这个接口的时候,判断返回值,不仅要判断是否为空,也需要判断value->not_found,这是因为只有第一次调用才会返回空,后续返回就不是空,因此需要判断value->not_found:


static u_char *
 ngx_http_log_variable(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op)
 {
     ngx_http_variable_value_t  *value;
     //获取变量值
     value = ngx_http_get_indexed_variable(r, op->data);


     if (value == NULL || value->not_found) {
             *buf = '-';
             return buf + 1;
     }


     if (value->escape == 0) {
             return ngx_cpymem(buf, value->data, value->len);


     } else {
             return (u_char *) ngx_http_log_escape(buf, value->data, value->len);
     }
  }