/ 中存储网

Nginx rewrite if指令详细分析

2014-04-17 09:42:01 来源:itjs.cn
nginx的if功能确实是弱得可以,严重影响了生产效率。故此,先提出严正抗议!

对于这个功能奇弱的if指令,nginx实现得还特别复杂。下面将对其实现进行剖析。

1.1. 指令解析

if指令由ngx_http_rewrite_if函数负责解析。这个函数的主要工作是

543: ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));

552: ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);

582: ngx_http_add_location(cf, &pclcf->locations, clcf);

586: ngx_http_rewrite_if_condition(cf, lcf);

590: if_code = ngx_array_push_n(lcf->codes, sizeof(ngx_http_script_if_code_t));

618: ngx_conf_parse(cf, NULL);

632: if_code->next = (u_char *) lcf->codes->elts + lcf->codes->nelts

633: - (u_char *) if_code;

以上是基于nginx 1.2.0版本摘录出来的提纲语句。我们现在细细来讲:

i. 进入ngx_http_rewrite_if函数后,首先进行的是创建nginx http配置数据结构的过程,对应于上面的line 543和line 552两个标记行引导的程序段。为什么需要这样呢?因为nginx对于块指令的指令解析回调函数仅支持两种模式。第一种方式,块内指令解析和配置存储(数据结构和存储方式)全部由用户定义,这种模式见于ngx_http_geo_block()。而第二种方式,块内指令解析和配置存储(数据结构和存储方式)则全部由nginx定义,ngx_http_rewrite_if()就属于这种方式。这种方式要求模块配置存储在ngx_http_conf_ctx_t中。

ii. 产生了nginx要求的配置存储结构以后,流程执行到line 582为终结的阶段——创建匿名location配置。之所以要创建一个匿名location,是为了实现在if匹配成功后,使用if块中的location配置替换请求的location配置。注意,这个location配置不参与nginx匹配请求uri的过程,因为有这条语句的存在clcf->noname = 1;。另外,细心的读者可能可以意识到为什么可以写在if中的配置指令无一例外都是保存在location配置中的,因为if指令在匹配上以后只会替换请求的location配置。

iii. 接着,nginx开始解析if指令后面的条件。对应line 586。如果解析成功,nginx创建一个if_code的内部指令,对应line 590开始的程序段。这一部分可以这么理解,nginx将if指令解释成一个单目指令,产生类似于

mov [bp], val

test [bp], 0

这样的内部指令。其中test对应着if指令,mov则是if指令括号中条件的抽象。

iv. 处理完if指令本身,nginx开始处理if指令后面的块指令。这一步对应于line 618前后的程序段。注意这里,nginx根据location配置中pclcf->name.len == 0的条件,也就是location的uri,将后续的解析指令的类型设置成为NGX_HTTP_SIF_CONF和NGX_HTTP_LIF_CONF两种,也就是对应server中的if和location中的if了。

v. 全部解析完成以后,nginx设置if_code->next。这个指针的用途是记录这个if块中最后一条内部指令的后续地址,一旦请求处理时if条件不满足,nginx就直接跳到这个地址执行if块后面的指令。

1.2. 条件解析

条件解析在ngx_http_rewrite_if_condition中完成。因为条件中可能含有空格,所以这个函数里面也有单独的词法分析,有点不雅,不过也没有别的办法。

659: if (value[1].len < 1 || value[1].data[0] != '(') {

691: if (len > 1 && p[0] == '$') {

712: if (len == 1 && p[0] == '=') {

729: if (len == 2 && p[0] == '!' && p[1] == '=') {

745: if ((len == 1 && p[0] == '~')

746: || (len == 2 && p[0] == '~' && p[1] == '*')

747: || (len == 2 && p[0] == '!' && p[1] == '~')

748: || (len == 3 && p[0] == '!' && p[1] == '~' && p[2] == '*'))

785: } else if ((len == 2 && p[0] == '-')

786: || (len == 3 && p[0] == '!' && p[1] == '-'))

上面列举了这个条件解析过程的核心条件。

i. 从line 659开始,到line 691结束,nginx进行了两项预处理过程。第一是剥离括号,第二是条件正文的参数下标和在该参数字符串中的字符下标。

ii. line 691是开始一个重要分支,处理形如$var op $val这样的变量条件表达式。与之相对的是line 785开始的分支,处理文件检测类的条件。

iii. line 691和line 712之间,nginx解析变量,负责这件工作的是ngx_http_rewrite_variable()函数。解析完变量后,nginx再分析变量条件表达式。分三种情况,line 712开始的变量等式,line 729开始的变量不等式和line 745开始的变量正则匹配。

举例:

i. if ($var = ‘chen’) 这个条件解析完成以后生成的内部指令序列是:

ngx_http_script_var_code

ngx_http_script_value_code

ngx_http_script_equal_code

ngx_http_script_if_code

ii. if ($var ~ ‘test’) 这个条件解析完成以后生成的内部指令序列是:

ngx_http_script_var_code

ngx_http_script_regex_start_code

ngx_http_script_if_code

iii. if ( -f ‘test$i.sh’) 这个条件解析完成以后生成的内部指令序列是:

ngx_http_script_complex_value_code

ngx_http_script_file_code

ngx_http_script_if_code

表一:nginx条件总表

条件类型

内部指令集

备注

变量等式

ngx_http_script_var_code

值拷贝根据是否含变量分为两类

值拷贝

ngx_http_script_value_code

ngx_http_script_complex_value_code

ngx_http_script_equal_code

变量不等式

ngx_http_script_var_code

值拷贝根据是否含变量分为两类

值拷贝

ngx_http_script_value_code

ngx_http_script_complex_value_code

ngx_http_script_not_equal_code

变量正则表达式

ngx_http_script_var_code

ngx_http_script_regex_start_code

文件检测

值拷贝

ngx_http_script_value_code

值拷贝根据是否含变量分为两类

ngx_http_script_complex_value_code

ngx_http_script_file_code

1.3. 块解析

if条件后面是一个块。我们前面说了,这个块中的指令类型可能是NGX_HTTP_SIF_CONF或者NGX_HTTP_LIF_CONF。我们现在来看一下,究竟有哪些指令属于这个范畴。我们不具体列出这些指令,因为nginx发展过程中,这些指令肯定会发生变量,我们这里只看他们的特点。

所有指令分为两类:

l rewrite模块指令:rewrite模块中的所有指令都可以在if产生的这两种块中被解析。解析产生的内部指令序列接在if条件的内部指令序列后面,并不断向后延伸。直到块解析完成,if_code->next指向内部指令序列的尾部。当if条件满足时,顺序指令后面的指令。当条件不满足时,通过if_code->next指针跳过块中所有指令。

l 普通指令(其他模块指令):都是将配置保存在location配置中的指令。块中的指令配置保存在刚刚建立的匿名location配置中。当if条件满足时,使用此location配置替换处理请求的location配置。

if指令和其他rewrite模块指令一样,都是在处理请求的时候,在REWRITE_PHASE时被处理。处理函数是ngx_http_rewrite_handler。其核心

while (*(uintptr_t *) e->ip) {

code = *(ngx_http_script_code_pt *) e->ip;

code(e);

}

由此可见,nginx的指令执行的核心其实是各个内部指令的实现本身。接下来,我们就一起来分析if涉及到的这些内部指令。

2.1. nginx内部指令

nginx内部指令和CPU指令有点类似:op和操作数。我们刚刚看到的ngx_http_script_var_code等等就是op,其实就是个函数指针。每个op都有不同数量和类型的操作数。这些操作数和op一起放在各个不同的数据结构中,比如ngx_http_script_var_code对应的数据结构就是ngx_http_script_var_code_t。每个内部指令的整个数据结构都完整的放在nginx的内部指令序列中,就和C代码段中既有op,又有直接操作数一样。那么nginx如何识别指令呢?那就是所有数据结构的第一个属性都必须是op回调函数指针。这样一来,nginx通过ip指针指向内部指令序列的某一个地址,那个地址一定是op回调函数指针。在op回调函数,ip指针被修改,移到下一条指令的开始处,那么此地址也是下一条指令的op回调函数指针。

2.2. ngx_http_script_var_code

取得变量,*e->sp = *value; e->sp++; 这里有个问题,sp是什么?nginx内部指令处理过程中,ip指向内部指令序列,sp指向结果序列。看到sp,大家其实很容易联想到堆栈,nginx的sp实现确实像个堆栈,有压栈也有出栈。

2.3. ngx_http_script_value_code

核心代码是

1660: e->sp->len = code->text_len;

1661: e->sp->data = (u_char *) code->text_data;

1666: e->sp++;

这个很简单,就是字符串赋值。

2.4. ngx_http_script_complex_value_code

这个过程稍微复杂一点,但是原理还是一样的,可以看这段代码

1645: e->sp->len = e->buf.len;

1646: e->sp->data = e->buf.data;

1647: e->sp++;

甭管值是如何得到的,过程是一样的。在这里,值是通过执行另一段内部指令序列得到的,这里不作展开。

2.5. ngx_http_script_equal_code

1426: e->sp--;

1427: val = e->sp;

1428: res = e->sp - 1;

1432: if (val->len == res->len

1433: && ngx_strncmp(val->data, res->data, res->len) == 0)

1434: {

1435: *res = ngx_http_variable_true_value;

1436: return;

1437: }

1439: *res = ngx_http_variable_null_value;

因为存值的时候sp都加了1,所以判断相等时sp先减1,。接着取出比较的两个值val和res。注意比较的结果存放在res中,这个过程中sp的示意图是:

before:

after:

2.6. ngx_http_script_not_equal_code

它和ngx_http_script_equal_code流程完全相同,逻辑完全相反,不做赘述。

2.7. ngx_http_script_regex_start_code

ngx_http_script_regex_start_code在多种条件下使用,所以逻辑很杂。和if相关的流程如下:

931: e->sp--;

932: e->line.len = e->sp->len;

933: e->line.data = e->sp->data;

936: rc = ngx_http_regex_exec(r, code->regex, &e->line);

938: if (rc == NGX_DECLINED) {

947: if (code->test) {

965: }

978: if (code->test) {

992: }

正则表达式的处理和前面的等式以及不等式有些区别,不知道大家发现了没有?操作数的处理不同。后两者的操作数都是通过别的内部指令提前存放在结果序列中的,而正则表达式只有一个参数在结果序列中,另外一个参数在指令数据结构中保存引用。为什么需要这样呢?因为“~”后面的字符串只有在前面是正则运算符的时候才有意义,所以这个字符串就在处理正则运算符的时候连带处理掉了。处理过程前面已经遇到过,大家可以再回顾一下。

2.8. ngx_http_script_file_code

内部指令ngx_http_script_file_code从sp取得文件名,接着调用ngx_open_cached_file()函数测试文件,最后根据测试结果将真或假存回sp。

2.9. ngx_http_script_if_code

内部指令ngx_http_script_if_code很显著的特点是一个消费者,它从sp中取出数据,判断值是否为真,但是不产生新的值。我们前面提到过if条件为真时的处理逻辑。看到

1401: if (e->sp->len && (e->sp->len !=1 || e->sp->data[0] != '0')) {

1402: if (code->loc_conf) {

1403: e->request->loc_conf = code->loc_conf;

1404: ngx_http_update_location_config(e->request);

1405: }

1407: e->ip += sizeof(ngx_http_script_if_code_t);

1408: return;

1409: }

1415: e->ip += code->next;

分析这段代码可以得出if条件为真时的工作过程是使用if中的loc_conf替换请求loc_conf e->request->loc_conf = code->loc_conf;,更新请求参数,然后执行后面的内部指令e->ip += sizeof(ngx_http_script_if_code_t);。如果条件非真,则直接跳过解析if块中的指令得到的内部指令序列e->ip += code->next;。

代码中明显更新的是e->request,为什么说更新的是r->request呢?因为在ngx_http_rewrite_handler()中有e->request = r;。

至于nginx是如何通过location配置更新请求参数的,这个问题不在本文讨论范围内。

本文分析了nginx处理if条件的流程,见识了nginx内部指令的。虽然说并不是完全了解了nginx的脚本机制,但也不仅仅只是豹窥一斑的肤浅程度。

了解了这些有什么用呢?

稍微复杂点的条件写成nginx配置,怎得一个蛋疼可以形容。了解了这块的逻辑,搞个andornot难道是一个怎么复杂的事情?

有if没有else,没有这个比没有andor可能更麻烦。不过这个不那么容易做,但也不是不能做。