Nginx中变量的实现(下)

  • Post author:
  • Post category:其他



这是Nginx中变量的实现下篇,上篇可以点

这里



1.初始化变量



尽管是同一个变量,但在定义和索引的时候nginx会创建两个ngx_http_variable_t结构体,然后分别存在于两个不同的容器中。一般情况下定义变量的时候该变量携带的信息更全,而索引变量时则相对少一些。

初始化变量的过程其实就是两个容器融合的过程,这个过程在nginx中对应ngx_http_variables_init_vars()方法。因为最后cmcf->variables_keys容器是要被销毁的,所以融合的一个主要目的是把变量定义时是携带的信息(比如get_handler方法)迁移到cmcf->variables容器中的变量上。另外一个目的是检查cmcf->variables容器的变量是否被定义过,如果存在未定义的变量,并且该变量也不是动态变量,则直接返回错误,并且后台打印一条错误日志:“unknown xxx variable”。

动态变量的检查和设置发生在某个变量不存在于cmcf->variables_keys容器中时,检测的方式非常简单,就是匹配前缀。下面来看一个变量检测的例子:

location / {
    return 200 “cookie is:$http_cookie  name is:$arg_name”;
}

首先在配置文件解析阶段,当解析到return指令后,该指令对应的指令方法会把变量“http_cookie”和“arg_name”方法到cmcf->variables容器中,以便获取这两个变量的索引值。此后在初始化变量阶段,nginx先从cmcf->variables_keys容器中查找是否存在“http_cookie”变量,因为这个变量正好是一个内置变量,所以它肯定是存在于cmcf->variables_keys容器中的,找到后把配置信息迁移一下,那么该变量的融合工作就算完成了。变量“arg_name”的融合过程跟“http_cookie”基本相似,不一样的是“arg_name”是一个动态变量,只要你没有为它做过定义(比如 set $arg_name “name”),那么它肯定是不存在于cmcf->variables_keys容器中的,所以它会触发动态变量的融合逻辑。关于动态变量的融合逻辑,感兴趣的同学可以读一下ngx_http_variables_init_vars()方法,很简单,这里就不再叙述了。

看完上面的例子后我们需要对在4.2说过的一句话“该容器基本上算是cmcf->variables_keys容器的一个子集”做一个修正,从例子中可以看到,动态变量是不存在与cmcf->variables_keys容器中的,但被使用后的动态变量确是存在于cmcf->variables容器中的,所以这句话后面再加上一个动态变量的限制就完整了。

该方法的最后一块逻辑是使用cmcf->variables_keys容器中的变量生成一个hash容器,我们前面提到的ngx_http_get_variable()方法就是从这个hash容器中查找变量的,所有这些事请做完之后cmcf->variables_keys容器就会被销毁,所以最终变量就只会存在于两个容器中:hash容器和cmcf->variables容器。



2.如何开发变量



在nginx内部创建和使用变量所需要的基本功能主要就是我们上面提到的知识,但就目前来说这些还只是一个一个的单独的知识点,对于刚接触nginx开发或者对nginx开发不太熟悉的同学来说仍然无法在脑中形成一个比较完整的变量开发轮廓,所以我们本小节会尽可能的把现有的知识点串起来,以便让读者在脑中有一个基本的轮廓。



2.1如何开发自定义变量



在nginx中开发一个支持自定变量的功能大概需要做如下准备:

1.我们需要定义一个指令,就像“set”、“geo”等指令那样,比如我们定义自己的指令为“myset”。

2.这条指令应该是率属于某个模块的,就像“set”属于http_rewirte模块那样,所有我们还需要创建一个模块。

3.需要设计一个结构体,用来存放解析到的指令语句信息,比如下面的指令的信息我们需要把他们解析出来并放到一个地方(比如结构体):

  myset $a “a”;
  myset $b “b”;
  myset $c “$a+$b”;

4.如果要支持变量插入,那么我们还需要准备一个能够识别字符串中变量的方法,比如指令

  myset $c “$a+$b”;

如果支持变量插入,那么变量c的最终结果应该是“a+b”,如果不支持则应该是其字面意思“$a+$b”。

以上是编写自定义变量指令时需要的一些基本数据,但是就目前介绍的知识还无法优雅的支撑我们写一个完整的变量指令功能,为了避免读者陷入毫无准备的细节中,我们举一个简单例子来描述一下其中的脉络,假设我们有如下配置文件:

20  location /myset {
21       myset $a “a”;
22       myset $b “$a+b”;
23       return 200 “$a”;
24  }

那么我们开发的功能应该是这样工作的:

1.nginx解析到21行的“myset”指令后会调用其对应的指令方法(这个指令要做的动作),我们用my_handler()代替。

2.在my_handler()方法中我们需要调用ngx_http_add_variable()方法,把变量“$a”放入到cmcf->variables_key容器中,然后把对应的值“a”存放到我们事先准备好的结构体中。

3.然后设置变量“$a”对应结构体(ngx_http_variable_t)的一些信息,比如get_handler方法、flags标记等。

4.解析到22行的“myset”指令后跟21行的指令解析情况基本相似,不同的是22行的指令值中有一个变量“$a”,这时候就不是在定义变量了,而是在使用变量,所以我们先把这个变量解析出来。

5.然后调用ngx_http_get_variable_index()方法来获取变量“$a”的索引值。

6.最后把这个索引值和后面解析到的字符“+b”放到我们准备好的结构体中,到此我们的自定义指令就算解析完毕了。

7.剩下nginx自带的return指令做的工作跟第5步差不多,只不过它在解析过程中用到了脚本的概念,关于脚本我们在后续的文章中会做详细介绍,这里记住有这么个概念就行了。

以上是一个自定义变量指令的大概解析过程。

最后执行的时候会从return指令解析好的信息中获取变量“$a”的索引值,然后通过ngx_http_get_flushed_variable()方法获取变量对应的值,最终输出到客户端,至此我们上面介绍的知识点基本就都串起来了。



2.2开发内置变量



笔者曾经在使用nginx的lua模块的时候,为了监控性能需要拿到当前系统的毫秒级时间,而nginx和lua本身携带的方式都无法满足,它们虽然都可以拿到一个毫秒时间,但是精度都不能保证是1毫秒。当时的解决办法是用c扩展一个lua模块,但是因为需要重新编译,所以后来又引入了luajit中的ffi,通过ffi把获取毫秒时间的代码嵌入到了lua代码中,这样就可以利用luajit,避免我们手动编译c代码,这次我们使用另外一种方式:使用nginx内置变量来实时获取当前系统时间。

在nginx中目前有这样一个内置变量“$msec”,通过阅读官方文档可以看到它其实就一个可以表示毫秒时间的变量,但是nginx为了减少系统调用,把nginx中的时间做了一个缓存,如此一来在某些情况下它就没办法把时间精确到一毫秒。 现在我们就仿效这个变量来编写一个不会缓存的时间变量,取名为“$mymsec”并且我们把它的时间精度设置成微秒。

nginx核心内置变量都放在下面的数组中:

 /src/http/ngx_http_variables.c#ngx_http_core_variables[]

这是一个ngx_http_variable_t类型的数组,在前面我们提到创建变量就是要创建该结构体,并且有两种方式:一种是自定义,一种是内置,我们这里就是使用内置方式。现在我们再次把表示变量名的结构体贴出来,看看都需要设置哪些字段:

typedef struct ngx_http_variable_s  ngx_http_variable_t;
struct ngx_http_variable_s {
        ngx_str_t                   name;
        ngx_http_set_variable_ptset_handler;
        ngx_http_get_variable_ptget_handler;
        uintptr_t                   data;
        ngx_uint_t                  flags;
        ngx_uint_t                  index;
};

首先分析以下我们要创建的内置变量“$mymsec”的特性:

1.变量需要一个名字,就是“mymsec”。

2.要求不允许缓存,所以要打上NGX_HTTP_VAR_NOCACHEABLE标记

3.一个调用系统函数获取时间的get_handler()方法,我们取名为“ngx_http_variable_mytime”。

4.其它目前不需要,我们给一个默认值就可以

最后我们创建的结构体应该是这样:

{  ngx_string("mytime"),
    NULL,
    ngx_http_variable_mytime,
    0,
    NGX_HTTP_VAR_NOCACHEABLE,
    0 }

我们把他放到ngx_http_core_variables[]数组中,然后再把对应的方法做一个原型声明:

static ngx_int_t ngx_http_variable_mytime(ngx_http_request_t *r,ngx_http_variable_value_t *v, uintptr_t data);

并放到ngx_http_variables.c文件中,这样前期准备就差不多了,剩下就是如何实现这个方法了,我们把具体代码贴一下:

static ngx_int_t ngx_http_variable_mytime(ngx_http_request_t * ngx_http_variable_value_t *v, uintptr_t data)
{
     /* 用来存放生成的时间数据 */
     u_char      *p;
     /* 一个时间结构体,其中tv.tv_sec表示秒,tv.tv_usec表示微秒 */
     struct timeval tv;
     /* 用来存放生成的秒级时间 */
     time_t           sec;
     /* 用来存放生成的微秒时间 */
     ngx_uint_t       msec;

     /* 分配内存空间 */
     p = ngx_pnalloc(r->pool, NGX_TIME_T_LEN + 6);
     if (p == NULL) {
          return NGX_ERROR;
     }

     /* 调用系统函数获取当前系统时间,结果会放到tv中 */
     ngx_gettimeofday(&tv);
     /* 秒乘以1000*1000变微秒 */
     sec = tv.tv_sec * 1000 * 1000;
     msec = sec + tv.tv_usec; // 微秒

     /* 获取的时间数据转换成字符后的长度 */
     v->len = ngx_sprintf(p, "%M", msec) - p;
     v->valid = 1;
     v->no_cacheable = 1; // 不允许缓存变量结果
     v->not_found = 0;
     v->data = p; // 时间数据

     return NGX_OK;
}

以上就是全部逻辑,重新编译并安装nginx后就可以使用这个变量了,来看一个例子:

 location /start {
    return 200 "$msec -- $mymsec";
 }

curl http://127.0.0.1/start

1528025269.481 — 1528025269481836

后面的数据就是$mymsec变量打印的微秒数据,但是目前通过nginx自带的模块很难用例子证明我们上面说的“$msec”有缓存而“$mymsec”是实时获取的,这个等后面我们涉及到lua模块的时候再回过头细说这一块,这里就不展开了。



3.变量如何做到请求间隔离



通过上面的内容我们知道,变量的定义最终会放在cmcf->variables容器中,并且只此一份(这里我们不考虑前面提到的hash容器),而各个请求又都要用到这一份定义,但是请求之间又是相互独立的,同一个变量在不同的请求中肯定要展示跟当前请求相关的变量值,做到这一点的简单方式就是每个请求都保存属于自己的变量值,实际上nginx也是这么做的,下面就来看看具体实现细节。



3.1创建数组容器



在nginx中有一个专门用来表示请求的结构体,每当一个请求过来的时候都会创建一个这样的结构体,然后把相关的请求信息封装到该结构体中,所以把保存变量值的容器放到该结构体中也是最合情合理的,该结构体的定义大致如下:

typedef struct ngx_http_request_s  ngx_http_request_t;
struct ngx_http_request_s {
    ngx_http_variable_value_t        *variables;
}

为了节省篇幅我们省略了不必要的成员字段,只列出了我们需要的字段,该结构体的定义在/src/http/ngx_http_request.h文件中,需要的读者请自行查看。

数组容器是在/src/http/ngx_http_request.c#ngx_http_create_reqeust()方法中完成创建的,该方法的逻辑不算复杂,我们这里其实只需要关心其中的两个语句,一个是用来为ngx_http_request_t结构体分配内存空间的:

r = ngx_pcalloc(pool, sizeof(ngx_http_request_t));

另一个是为r->variables这个数组容器分配空间的:

r->variables = ngx_pcalloc(r->pool, cmcf->variables.nelts * sizeof(ngx_http_variable_value_t));

其中cmcf->variables.nelts表示容器大小,乘以每个变量值结构体需要的空间就是整个数组容器需要占用的内存空间,这样容器的创建就算完成了。

其中r->variables这个指针变量在这里实际是一个数组,这对于不了解c语法的同学来说会产生一点困惑,这里我们做一个简单的介绍。在c语言中也存在表示数组的语法,正常情况下我们可以这样定义一个数组:

ngx_http_variable_value_t  variables[n];

其中n表示该数组可以容纳的元素个数,这是一个必填值,并且一旦定义完毕后variables这个变量是不允许被修改的,它的结构在内存中大致是这样的:




这个结构和内存空间在程序运行后就已经确定了,其中第一个小方块中的“*”号代表地址,但是由于c语言中数组的特性这个值是不允许被修改的。

另一种使用数组的方式就是我们上面的请求结构体中表示的那样,这种定义方式在程序启动后在内存的结构体是这样的:




可以看到,只是为这个变量分配了一个块内存空间,这个内存空间里面放的是地址(此时是空),并且这个内存空间是可以被修改的,对这样一个指针变量,我们可以把它指向一个变量值结构体(ngx_http_variable_value_t),也是指向多个,比如这样:




这就和上面定义数组的形式就一样了,并且variables这个值是可以被改变的,所以以上两种方法都可以表示一个数组。


3.2使用数组容器


当请求创建完成后,在当前请求过程中所有变量值都会存在该数组容器中,其中也包括动态变量的取值,下面通过一个例子来看一下变量值是如何围绕该数组容器工作的:


location /var {
    set $a “aaa”;
    return 200 “$a+$a”;
}




上面这个配置中,内容的输出是由return指令负责的(实际上是有该指令翻译后的脚本负责的),当我们向nginx发送请求后,return指令最终需要做的就是通过变量“$a”的索引值获取变量值,对应的方法就是我们前面提到的ngx_http_get_indexed_variable()方法。针对我们当前的例子,当取第一个变量“$a”的值的时候,因为r->variables容器中还没有对应的值,所以该变量会通过绑定的get_handler()方法来获取变量值,然后把变量值放入到r->variables容器中,我们假设index是变量“$a”的索引值,那么该变量值就是r->variables[index]。当取第二个变量值的时候,因为该变量已经存在容器中了,并且用set指令设置的变量值是允许缓存的,所以这次的变量值直接从容器中获取就可以了。最后因为每个请求都会有自己所属的数组容器,所以不会相互干扰。



4.子请求中的变量



上篇文章中我们说过,nginx中子请求的变量跟父请求的变量大部分是共享的,是否共享其实取决于该变量是否被允许缓存,下面我们来看看它的具体实现。

nginx中创建子请求的方法是/src/http/modules/ngx_http_core_module.c#ngx_http_subrequest(),该方法声明如下:


ngx_int_t ngx_http_subrequest(ngx_http_request_t *r,ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **psr,ngx_http_post_subrequest_t *ps, ngx_uint_t flags)



解释一下这些参数的意思:


  • r:客户端发过来的请求(父请求)

  • uri:子请求要访问的资源,比如/sub/uri

  • args:子请求访问某个资源时携带的查询参数querystring

  • psr:用来接收当前方法创建好的子请求对象,可以看到它既是入参也是出参,最终*psr指向的就是创建好的ngx_http_request_t对象(子请求)

  • ps:子请求处理完毕后需要回调的方法和一些携带的信息会放在这个结构体中

  • flags:一些标记



刨去其它逻辑,目前我们仅关心该方法中跟变量有关的部分,其实也就一句话:


sr->variables = r->variables;




其中sr是在该方法中创建的子请求对象ngx_http_request_t,从这句我们可以看到,在子请求中,用来存放变量值的数组容器就是用的父请求的数组容器。所以好像也没什么可说的了,只要变量允许被缓存,那么变量值在父请求和子请求中就是同一个东西。

按照这个推论,如果变量不允许缓存,那么变量值就应该是跟各自请求相关的值,比如$uri等变量,在主请求和子请求中应该会表现出不同的值。遗憾的是并不是所有的变量都按照常理出牌,就像我们之前提到的“$request_method”变量。为了一探究竟,我们从ngx_http_variables.c文件中找到了这个变量的定义如下:


{ ngx_string("request_method"), NULL,
  ngx_http_variable_request_method, 0,
  NGX_HTTP_VAR_NOCACHEABLE, 0 }




这里我们只关注两个地方,一个是该变量打上了不可缓存标记,所以它每次获取变量值都会调用对应的handler方法;另一个就是变量对应的get_handler方法,这个方法就是用来获取实际变量值的,我们看一下它都做了什么:


   20    if (r->main->method_name.data) {
   21       v->len = r->main->method_name.len;
   22       v->valid = 1;
   23       v->no_cacheable = 0;
   24       v->not_found = 0;
   25       v->data = r->main->method_name.data;
   26
   27    } else {
   28        v->not_found = 1;
   29    }




其中r->main表示主请求(根请求),可以看到它只关心主请求的方法是否有值,如果有则赋值,没有则标记未发现。所有不管最开始的主请求派生出了多少子请求,该变量始终表示的是主请求的请求方法。

另一个需要注意是,我们在定义该变量的时候为该变量打了一个NGX_HTTP_VAR_NOCACHEABLE标记,但是该变量对应的get_handler方法在成功获取到值后且把v->no_cacheble设置为了0,这等于又把变量值设置为了可缓存的,有兴趣的读者自己分析以下这个逻辑,看看在请求过程中是个什么效果,以及为什么这么做,试着总结一下结论。



5.总结



这篇文章主要介绍了变量在nginx中的具体实现,以及实现这些需要的基本方法和数据结构。

其中两个主要的数据结构是:


typedef struct ngx_http_variable_s  ngx_http_variable_t;
typedef ngx_variable_value_t  ngx_http_variable_value_t;




几个重要的容器是:


cmcf->variables;
cmcf->variables_keys;
r->variables;




还有几围绕这些数据结构和容器干活的方法:


ngx_http_get_variable_index()
ngx_http_get_indexed_variable()
ngx_http_get_flushed_variable()





至此,关于变量实现原理就算介绍完了。实际上我们上面介绍的这些功能,

都是通过nginx中的脚本引擎来执行的,甚至包括整个http_rewrite模块,它里面所有的指令功能,最终都是通过脚本引擎串起来并执行的。关于nginx中的脚本的内容我会在下一篇做一个详细的介绍,感兴趣的同学可以关注下面这个目录:


http://deyimsf.iteye.com/blog/2419833


我会持续对它更新,同时也欢迎其它同学提供好的写作案例和素材。







版权声明:本文为deyimasf原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。