nginx脚本引擎与变量设计

http://blog.csdn.net/dingyujie/article/details/7065194

http://blog.csdn.net/dingyujie/article/details/7065244

http://blog.csdn.net/dingyujie/article/details/7106524

http://blog.csdn.net/dingyujie/article/details/7109269

前言:

本来的想法是写一个针对经典配置的情景分析,写到一半的时候就力不从心了,感觉很难给出一个完整全面的展示,主要是这块内容牵扯到整个系统的很多方面,比较复杂。所以就打算从一个有代表性的rewrite配置出发,讲一下在这个情景下,nginx的脚本引擎是如何工作的。

nginx的脚本解析系统,虽然不能跟专业的脚本解析器相比,但是它的设计却是非常巧妙,而且抽象性很高。先大体说一下它的大体思路。

 

设计思路:

我们知道nginx在解析配置时,每次根据读出的配置信息都会查找相应模块的特定处理函数,而rewrite的配置一般都是要针对具体的url来做特定的处理,如正则匹配等等,所以不能在解析的时候不能像其他配置那样得到一个相对静态的配置信息。nginx的做法是,为每个配置设置一个后续处理所需要的结构体,结构体中包含了与该配置相关的重要信息,并设置相应的处理handler,之后在处理请求的某个阶段(主要REWRITE_PHASE,这方面的介绍可以参考其他博客,这里就不重复了),会集中对这些在解析时设置的处理handler做一个集中处理。

在这个脚本引擎中,rewrite的处理或者其他使用使用脚本引擎的地方,都是通过ngx_http_script_engine_t结构体来驱动执行和保存执行结果的。我们通过下面这段代码来看一下,它的核心处理是怎么样的。

[cpp]  view plain copy
  1. // sp是一个ngx_http_variable_value_t的数组,里面保存了从配置中分离出的一些变量  
  2. // 和一些中间结果,在当前处理中可以可以方便的拿到之前或者之后的变量(通过sp—  
  3. // 或者sp++)  
  4. e->sp = ngx_pcalloc(r->pool, rlcf->stack_size * sizeof(ngx_http_variable_value_t));  
  5. // 包含了在配置解析过程中设置的一些处理结构体,下面的rlcf->codes是一个数组,  
  6. // 注意的是,这些结构体的第一个成员就是一个处理handler,这里处理时,都会将该  
  7. // 结构体类型强转,拿到其处理handler,然后按照顺序依次执行之  
  8. e->ip = rlcf->codes->elts;  
  9. // 需要处理的请求  
  10. e->request = r;  
  11. // 初始时认为uri需要特殊处理,如做escape,或者urldecode处理。  
  12. e->quote = 1;  
  13. e->log = rlcf->log;  
  14. // 保存处理过程时可能出现的一些http response code,以便进行特定的处理   
  15. e->status = NGX_DECLINED;  
  16.   
  17. // 依次对e->ip 数组中的不同结构进行处理,在处理时通过将当前结构进行强转,就  
  18. // 可以得到具体的处理handler,因为每个结构的第一个变量就是一个handler。我们看  
  19. // 的出来这些结构成员的设计都是有它的意图的。  
  20. while (*(uintptr_t *) e->ip) {  
  21. code = *(ngx_http_script_code_pt *) e->ip;  
  22. code(e);  
  23. }  


说了这些抽象的理论之后,下面给出一个具体的例子来更加形象的分析一下。

情景分析:

这里我们以下面这个例子来看一下,在一个特定的rewrite情景下,nginx的脚本引擎是如何工作的:

location / {

    if ($uri ~* "(.*).html$" ) {

       set  $file  $1;

           rewrite ^(.*)$ http://$http_host$file.mp4 break;

    }

}

我想对于这个示例配置,大家应该都知道它的作用,主要是将一个对html请求,通过302跳转,让客户端重新取一个同名的mp4文件(是不是很无聊-_-!)。

这里通过一个图来展现在整个解析过程得到的结构及其相应的处理handler,然后我们再对一些重点的地方加以分析,对于具体的逻辑,大家可以自己看。

说明:

1.       图中蓝色的部分是if行,它的解析主要通过ngx_http_rewrite_if,set和rewirte类推。

2.       图中带有小圆圈的标记,是脚本引擎中处理handler的执行顺序,即前面的e->ip数组。

接下来我们针对这个情景下的处理流程中的一些重点进行探讨。 

重点探讨:

(一)  if行的解析

1.       在解析if时, nginx会把它当做一个location来对待的,并且它的location type为noname。通过ngx_http_add_location将该“location”添加到上层的locations中。这里将if看做location自然有它的合理性,因为if的配置也是需要进行url匹配的。

2.       if中具体的解析通过ngx_http_rewrite_if_condition来实施。

3.       在(1)的处理时,对于$uri,ngx_http_rewrite_variable 函数会进行处理。主要的工作是将当前的这个变量(即$uri),放到全局的变量数组中,并得到其下标。还有一点就是ngx_http_script_var_code_t结构中的index,这个用来记录该下标,这样后面用到的时候就可以很快的取到该变量。

4.       为什么(2)没用设置结构体和handler呢?那是因为根据(2)的配置,就可以确定后面这个配置的处理方式了。所以这里不是每个分句都有对应的结构和handler的设置,能简单处理的,就可以捎带处理掉了,同理的还有像“break”等。

配置中凡是出现‘~’的,都是属于正则的处理。这里提醒一点是带有‘*’的,是不区分大小写的。

5.       这里先提一点相关的背景。较新的PCRE库中实现了perl和python中叫做named subpattern的东西,我们通常在引用一个pattern中的()部分时,通常使用$1,$2…等,这里的named subpattern是指在一个pattern使用类似(?<name>...)的形式,这样我们在引用这个()的内容是,就可以通过这个name来指定了,使用更加灵活。这里在(3)的处理的时候,会通过ngx_http_regex_compile函数来进行正则的编译,里面封装了一些PCRE正则库的一些API,相关的细节可以到官网http://www.pcre.org/中的document上查看。这里提一点的是,由于我们的情景中都是比较常规的正则表达式,所以与像named subpattern之类的处理就先跳过,如rc->named_captures的处理,我们这里只关注rc->captures这个值,它是正则表达式里,出现的()的个数,这个数目在对于处理后面出现的$1,$2…等非常有用。

 

在(3)的处理中关键的的其实就是为模式匹配提供信息,所以通过regex->test = 1来标记实际处理时应该采取的处理方式。

下一篇我们将分析set指令和rewrite执行。望关注!


继续上一篇的分析,下面我们将主要谈论set和rewrite两行的解析处理。为了方便分析,这里把示例配置和调用图列出来

location / {

    if ($uri ~* "(.*).html$" ) {

       set  $file  $1;

           rewrite ^(.*)$ http://$http_host$file.mp4 break;

    }

}

(一)  set行的解析

1.       我们首先看这里这句:

ngx_http_add_variable(cf, &value[1], NGX_HTTP_VAR_CHANGEABLE);

为什么这个函数的第三个参数flag,被设置成NGX_HTTP_VAR_CHANGEABLE,那是因为set之后的第一个的变量可以在一个if配置里面出现多次,每次可以改变它的值,当然还有nochangeable的,后面碰到再说。紧接着,把这个变量放到全局变量数组中,同时记录它的所在位置(即下标index),即函数ngx_http_get_variable_index。

2.       通过图中所示,先处理的是set后面的第二个变量,那第一个怎么办呢?先处理它使用原因的,一会就可以看到了。这个变量通过ngx_http_rewrite_value函数来处理的,在处理时带有$的变量,nginx里面称作“complex value”,于是分配ngx_http_script_complex_value_code_t结构同时使用ngx_http_script_complex_value_code来处理,接下来有几行代码需要注意下:

[cpp]  view plain copy
  1. // lengths和values数组会在ngx_http_script_compile的执行过程中被填充  
  2.   
  3. sc.lengths = &complex->lengths;   
  4.   
  5. sc.values = &lcf->codes;  
  6.   
  7. // complete_lengths置1,是为了给lengths数组结尾(以null指针填充),因为在运  
  8.   
  9. // 行这个数组中的成员时,碰到NULL时,执行就结束了。  
  10.   
  11. sc.complete_lengths = 1;  
  12.   
  13.    

3.       ngx_http_script_compile函数算是这里的一个核心,主要是分离当前“complex value”中的一些不同类型的变量,具体逻辑不难看懂,我们只把一些需要注意的地方强调下就行了。

在开始的时候有个函数叫ngx_http_script_init_arrays,这个函数对flushes,lengths和values三个数组的初始容量给出了一个估计值,当然nginx的数组结构在初始空间不够的时候会自动扩充的,这个就不用太追究了。

在函数ngx_http_script_add_capture_code中,有个这样的赋值动作code->n = 2 * n,为什么是2*n呢?这个跟PCRE保存匹配结果集的结构有关,后面我们会讲到。

4.       在处理set 中的第一个参数的时候,它的handler被置为了ngx_http_script_set_var_code,通过这个函数,我们也就知道了为什么要先处理后一个参数:

[cpp]  view plain copy
  1. // e->ip就是之前在解析时设置的各种结构体  
  2. code = (ngx_http_script_var_code_t *) e->ip;   
  3. e->ip += sizeof(ngx_http_script_var_code_t);  
  4. r = e->request;  
  5. // e->sp是通过解析得到的变量处理结果的一个数组,变量的放置顺序跟  
  6. // ip中的顺序一致,而且随着处理而递增,所以为了保持中处理的一致性(这样  
  7. // 就可以保证许多地方使用一致的处理方式)。这里sp—就可以得到之前的处  
  8. // 理值,得到我们想要的结果了。  
  9. e->sp--;  
  10. r->variables[code->index].len = e->sp->len;  
  11. r->variables[code->index].valid = 1;  
  12. r->variables[code->index].no_cacheable = 0;  
  13. r->variables[code->index].not_found = 0;  
  14. r->variables[code->index].data = e->sp->data;  


 

(二)  rewrite行的解析

有了前面对if和set的介绍,rewrite分析起来就简单多了,大多数的处理前面都已经出现过了,这里只说几个重点。

1.       在rewrite后面第二个参数中,凡是以“http://”,“https://”,“$scheme”,或者是最后的参数是“redirect”的,在后续的处理都是产生302的响应,其次最后的参数里,如“permanent”会产生301响应,last和break将会产生nginx内部的重定向(相当于重新做一次http的请求),会重新进行location的匹配等,但是它们的区别在于break在重定向时,就不会在做重定向的处理,而last将会继续。

2.       对于rewrite后面第二个参数的处理,跟处理set中第二个变量的过程是大致一样的。需要强调的有以下几点:

// 刚开始的时候已经给regex做了初始化,为什么后面有重新赋值了一次呢?原因就是“它可能变了”。。。

regex = ngx_http_script_start_code(cf->pool, &lcf->codes,

sizeof(ngx_http_script_regex_code_t));

regex = sc.main;

 

对于上面这个问题,可以追溯到ngx_http_script_add_code函数,我们知道nginx在对数组push时,如果发现空间不足会做扩充,在扩充时会分配原来两倍大小的空间,并把原有数据copy过来。这样一样原来执行这个数组中的一些指针值此时就不能再用了,而要指向他们的“新家”,这也就是这里regex重新赋值以及以&regex为参数传递给ngx_http_script_add_code的目的所在。

3.       到这里我们的if指令到了结束的时候,那么就以一个NULL来做结尾。

if (last) {

code = ngx_http_script_add_code(lcf->codes, sizeof(uintptr_t), &regex);

        if (code == NULL) {

            return NGX_CONF_ERROR;

        }

        *code = NULL;

}

看最后这一句:

regex->next = (u_char *) lcf->codes->elts + lcf->codes->nelts - (u_char *) regex;

这里其实说明了当前位置距离codes数组的大小,这样在必要的时候,例如e->ip += code->next,跳过当前结构去处理下一个,很方便。

 

到这里,基本的轮廓就是这样子,至于各个handler的执行细节,大家可以自己去阅读代码,提示一点的是,要结合e->ip,e->sp等,ngx_http_script_engine_t结构中的一些成员来看,就容易懂了。

 下一篇,我们针对脚本解析这块涉及的一些细节,做下探讨,来结束这块的分析



这一部分我们将探讨一些较细节的东西,加上前面(一),(二)两篇文章对典型情景的分析,我相信应该会对大家理解nginx的脚本解析机制有很大的帮助的。

 

        这里我们关注ngx_http_core_main_conf_t(下面简写为cmcf)结构中的这两个成员,cmcf->variables_keys和cmcf->variables,其中variables_keys是个hash数组,变量hash值相同的放到一个数组中,而且是唯一的,重复时会报错。重要的是这个hash数组,即cmcf->variables_keys,保存了整个系统中所有预定义的,自定义,几乎所有的变量(一些特定前缀的变量除外,如“http_”等,在函数ngx_http_variables_init_vars中都列出来了)。

 

我们分条目来看:

1.  系统中所有可能用到变量都会放到cmcf->variables_keys中。

 

2.  配置中出现的变量(在处理请求时会用到的),会放到cmcf->variables中。总体上来看,除去一些有特定前缀的变量(这类变量后面会讲),cmcf->variables可以看做是ngx_http_variables_init_vars的子集。从这里的设计可以看出来,系统定义的变量在请求处理时,并不会全部用到,我们只需根据配置,拿到有用的就可以了,所以cmcf->variables_keys使用了temp_pool(实际的元素则使用cf->pool),当variables收集变量完成之后,variables_keys结构也就没什么用处了。

 

3.  cmcf->variables是一个数组,它的元素类型为ngx_http_variable_t。

[cpp]  view plain copy
  1. struct ngx_http_variable_s {  
  2.   
  3.    // 变量名字符串  
  4.   
  5.     ngx_str_t                    name;   
  6.   
  7. // 使用变量中的值设置request的某个成员的值,所谓set  
  8.   
  9.     ngx_http_set_variable_pt   set_handler;  
  10.   
  11. // 根据request中成员(如uri,args等)的值来设置,r->variables中对应变量的内容。   
  12.   
  13.     ngx_http_get_variable_pt   get_handler;  
  14.   
  15. // 需要具体处理的具体内容,因为这个结构是通用的,那么具体处理的信息就放到// data成员中  
  16.   
  17.     uintptr_t                    data;  
  18.   
  19. // 一些标识信息,区别不同处理,后面会详细讲  
  20.   
  21.     ngx_uint_t                   flags;  
  22.   
  23. // 该变量在cmcf->variables数组中的下标  
  24.   
  25.     ngx_uint_t                   index;  
  26.   
  27. };  

4.  在init_request阶段,我们看到有这样的处理:

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

这里的设计思想是这样的,cmcf->variables是每个变量的处理信息的封装,而r-> variables里面则是处理该变量得到的内容(即value),所以cmcf->variables跟r->variables是一一对应的,成员之间就是var对value的关系,这不过这里的var和value是封装了很多信息的结构,不是单纯的值或字符串。这里我们看下r->variables的成员。

[cpp]  view plain copy
  1. typedef struct {  
  2.   
  3.     unsigned    len:28;  
  4.   
  5.     unsigned    valid:1;          // 表示该变量在数组中并且有效,可以使用  
  6.   
  7. // 表示使用该变量时,需要重新通过get_handler来获取,因为该变量的实际内容// 可能已经改变了。  
  8.   
  9.     unsigned    no_cacheable:1;   
  10.   
  11. // 特定位置上的变量不存在(实际上是没有设置),那么它就是“not found”!  
  12.   
  13.     unsigned    not_found:1;  
  14.   
  15.     unsigned    escape:1; // 这里不管它,跟我们的情景没有多大关系  
  16.   
  17.     u_char     *data;  
  18.   
  19. } ngx_variable_value_t;  

 

我们看到这个结构中处理包含代表直接变量值的指针和大小(即data和len)外,剩下的都是一些标记位。

 

下面我们看几个宏:

#define NGX_HTTP_VAR_CHANGEABLE   1

#define NGX_HTTP_VAR_NOCACHEABLE  2

#define NGX_HTTP_VAR_INDEXED       4

#define NGX_HTTP_VAR_NOHASH        8

 

1.  NGX_HTTP_VAR_CHANGEABLE

使用这个宏的时候意味着,该变量可以重复配置,一般后配置的会覆盖前面的配置。如:

set $file $1

set $file $2

那么结果就是file的最终值就是$2所代表的。

2.  NGX_HTTP_VAR_NOCACHEABLE

这个宏所影响的正是ngx_variable_value_t结构中的no_cacheable,前面注释里面提到过,凡是有该标记的变量,都要通过get_handler来获取变量的值。如下面函数:

[cpp]  view plain copy
  1. ngx_http_get_flushed_variable(ngx_http_request_t *r, ngx_uint_tindex)  
  2.   
  3. {  
  4.   
  5.    ngx_http_variable_value_t  *v;  
  6.   
  7.    v = &r->variables[index];  
  8.   
  9. // 找到变量,发现是有效的,但是由于属于NGX_HTTP_VAR_NOCACHEABLE,所以// 需要在cmcf->variables找到它的处理结构来重新获取新值。  
  10.   
  11.    if (v->valid) {  // 变量有效,具备可以使用的潜质  
  12.   
  13.        if (!v->no_cacheable) { //可以直接用,就返回了  
  14.   
  15.             return v;  
  16.   
  17.        }  
  18.   
  19.        // 这是一个NGX_HTTP_VAR_NOCACHEABLE类型的变量,那么…  
  20.   
  21.        v->valid = 0;  
  22.   
  23.        v->not_found = 0;  
  24.   
  25.    }  
  26.   
  27. // 重新获取,这个函数就不看了,大家可以自己分析  
  28.   
  29.    returnngx_http_get_indexed_variable(r, index);  
  30.   
  31. }  

 

这种变量往往是跟特定的请求紧密相关的,如host,uri,args之类的,处理时每次重新获取新值时必要的。

3.  NGX_HTTP_VAR_INDEXED和NGX_HTTP_VAR_NOHASH

这两个宏主要是在SSI相关处理中用到,而ssi的处理需要用到cmcf->variables_hash,这是hash表,可以高效的找到变量。NGX_HTTP_VAR_NOHASH类型的变量压根就不会被放到这个hash表中,而从hash表中找到的变量,是ngx_http_variable_t结构,如果我们从它flags发现NGX_HTTP_VAR_INDEXED标记,那么意味着我们可以直接到r->variables中去找,至于找到后可用与否,那要另作判断了!


  前面的几篇文章,从一个情景来大体上呈现出了nginx在变量设计上面的技巧和机制,但是这远远没有覆盖全部的内容,后面我会再找一些情景来进一步的分析补充。

在这方面的研究领域,我不得不提一个人,agentzh(中文名叫章亦春)。agentzh在国内nginx研究领域可以算的上首屈一指了,特别推荐大家看一下他在sina博客上,讲nginx变量的文章,名叫“nginx变量漫谈”,虽然里面极少涉及代码,但是在宏观上把握的很好,深入浅出,这里算是做个宣传吧,跟大家分享!:)

http://blog.sina.com.cn/s/blog_6d579ff40100wgm3.html



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值