`
deyimsf
  • 浏览: 67940 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Nginx中变量的实现(下)

 
阅读更多

这是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
我会持续对它更新,同时也欢迎其它同学提供好的写作案例和素材。

 

 

 

  • 大小: 19.6 KB
  • 大小: 6.6 KB
  • 大小: 18 KB
分享到:
评论

相关推荐

    nginx自定义变量与内置预定义变量的使用

    **可见性** 是Nginx变量的另一个重要特性。每个变量都是全局可见的,但它们并不是真正的全局变量。这意味着在一个配置块中定义的变量可以在其他配置块中被访问,但它们的值只在声明它们的块及其子块中有效。例如: ...

    NGINX conf 配置文件中的变量大全

    其配置文件是实现NGINX功能的核心,而变量则是配置文件中的关键元素,用于动态地响应请求或执行特定操作。 ### 重要变量详解 #### 1. $args $args 变量包含了请求URL中的查询字符串部分,即URL中“?”之后的所有...

    window下nginx配置

    一直以来,许多开发者都认为Nginx只能在Linux环境下运行,实际上,在Windows系统中同样可以部署并高效运行Nginx。随着网站流量的增长,单台服务器可能无法满足需求,此时就需要通过增加服务器数量来实现负载均衡。...

    nginx 内置变量表 Excel版.rar

    经常需要配置Nginx ,其中有许多以 $ 开头的变量,经常需要查阅nginx 所支持的变量。 可能是对 Ngixn资源不熟悉,干脆就直接读...Nginx支持的http变量实现在 ngx_http_variables.c 的 ngx_http_core_variables存储实现

    Nginx中rewrite实现二级域名、三级域名、泛域名、路径的重写[文].pdf

    Nginx 中 rewrite 实现二级域名、三级域名、泛域名...Nginx 中的 rewrite 模块可以实现二级域名、三级域名、泛域名、路径的重写,并且可以使用 rewrite flags、正则表达式匹配、全局变量等来实现复杂的 URL 重写逻辑。

    使用Nginx实现灰度发布1

    Nginx通过其强大的反向代理功能,能够轻松地实现灰度发布。以下是一个简单的Nginx配置示例,用于展示如何配置灰度发布: ```nginx upstream hilinux_01 { server 192.168.1.100:8080 max_fails=1 fail_timeout=60;...

    Nginx+lua通过url传参的方式实现动态代理

    - `ngx.var.remotePort = arg["port"]`:通过`arg["port"]`获取名为`port`的参数值,并将其赋值给Nginx变量`$remotePort`。 #### 总结 通过以上步骤,我们成功实现了基于Nginx+Lua的动态代理功能。这种方案不仅...

    nginx 内置变量详解及隔离进行简单的拦截

    本篇文档详细介绍了nginx内置变量的定义和用途,并进一步阐述了如何使用这些变量来实现简单的拦截功能,包括对请求头、请求参数、客户端信息等进行过滤和阻断,以增强Web应用的安全性和稳定性。 nginx内置变量涉及...

    浅析Nginx配置文件中的变量的编写使用

    例如,如果尝试使用`echo`指令输出包含美元符号`$`的字符串时,由于Nginx的限制,不能直接输出美元符号,但可以使用不支持“变量插值”的模块配置指令构造出一个取值为`$`的Nginx变量,然后在`echo`中使用这个变量,...

    agentzh写的Nginx教程

    12. Nginx变量系列:该系列包含了至少八篇专注于Nginx变量的文章,Nginx变量是Nginx配置中非常重要的一个方面,变量可以用于配置文件中的各种指令里,以实现更灵活的配置。 13. Nginx指令执行顺序系列:该系列文章...

    nginx中带问号(?) 带参数的rewrite规则

    在Nginx服务器配置中,`rewrite`指令用于URL重写,这在创建动态到静态页面的重定向、实现URL路由、或者根据特定条件改变URL结构时非常有用。当URL中包含问号(`?`)以及参数时,处理起来可能会有些复杂,因为问号及其...

    Nginx中全局变量整理小结

    这些全局变量在Nginx的配置文件中非常实用,可以根据它们的值来实现如重定向、访问控制、日志记录、缓存策略等多种功能。了解和熟练运用这些变量能帮助我们更好地优化Nginx服务器的性能,提供更高效、安全的服务。

    nginx包(windows 下使用)

    2. **环境变量**:为了方便命令行操作,可以将Nginx的可执行文件路径添加到系统的PATH环境变量中。 三、配置文件修改 1. **nginx.conf**:解压后的目录中,主要的配置文件是`nginx.conf`。在这个文件中,你可以...

    nginx利用referer指令实现防盗链配置

    如果 `Referer` 不在这些预设的合法列表中,Nginx 设置 `$invalid_referer` 变量为 1。在 `if` 语句中,如果 `$invalid_referer` 等于 1,则返回一个 403 错误给用户,用户会看到一个 403 页面。如果使用 `rewrite` ...

    Windows系统下,将nginx注册为本地服亲测可用

    在这里,`%BASE%`是一个环境变量,表示WinSW的安装目录,`nginx.exe`是Nginx的主程序,`nginx.conf`是Nginx的配置文件路径。 3. **注册服务**: 打开命令提示符,使用WinSW执行以下命令来注册Nginx服务: ```bash ...

    nginx结合tomcat实现负载分担入门

    ### Nginx 结合 Tomcat 实现负载分担的基础知识点 #### 1. Nginx 简介与原理 Nginx 是一款轻量级的 Web 服务器 / 反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并发处理能力特别强。其核心特性包括但不限于...

    Tomcat+memcached+Nginx实现session共享

    总结来说,Tomcat+memcached+Nginx实现session共享是通过MSM中间件,配合Nginx的负载均衡,将session数据存储在memcached中,实现跨服务器的用户状态一致性。配置过程涉及到Tomcat、memcached和Nginx的安装、配置...

Global site tag (gtag.js) - Google Analytics