nginx中的脚本(实战篇)
上一篇基本以理论为主,介绍了ngx中脚本实现的基本要素,这一篇以如何编译为切入点,通过实际例子,来详细介绍ngx如何通过脚本来完成变量支持的。
1从复杂值(complex value)开始
在nginx的整个配置文件中,并非所有的配置项都涉及编译概念,对于如下nginx配置:
location / {
return 200 “hello world”;
}
涉及到编译概念的,只有return这条配置指令,其后面的参数“hello world”在nginx内部作为一个复杂值被编译和获取。
1.1复杂值概念
复杂值(complex value)在nginx中是这样定义的:
A complex value, despite its name, provides an easy way to evaluate expressions which can contain text, variables, and their combination.
大概意思是它是一个表达式,这个表达式可以是纯文本、变量、或两者的组合,并且nginx内部提供了一种计算该表达式的简易方式。
从复杂值的概念中我们知道,它是支持变量插入的,比如这样:
“I am $uri”
“I am uri”
“$uri”
这三个字符串在nginx内部都可以被看成一个复杂值,并且nginx为其提供了一套完整的工具,用来编译和计算这种字符串的实际值。如此一来,当我们在开发自定义模块,并且需要支持变量和变量插入的时候,用nginx自带的工具就很容易实现。
1.2复杂值使用方式
对于一个确定的复杂值,nginx使用结构体ngx_http_compile_complex_value_t来存放原生值,使用结构体ngx_http_complex_value_t来存放编译后的值。而实际编译过程则由ngx_http_compile_complex_value()方法负责,最后的结果值可由ngx_http_complex_value()方法计算得出。
我们用如下例子来简单介绍一下这几个结构体和方法的大致使用方式:
location /a {
return 200 “I am $uri”;
}
对于这样一个配置,nginx会把整个return指令编译成一系列指令序列。其中复杂值的编译只是其中一环,本次我们只陈述复杂值的编译。用法如下:
ngx_http_complex_value_t cv;
ngx_http_compile_complex_value_t ccv;
上面是两个我们要用到的结构体,其中ccv中的value字段用来存放复杂值的原始值“I am $uri”,而ccv中的complex_value字段则指向cv,如果没有什么其他特殊设置的话,就可以调用编译方法了,具体如下:
if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
return NGX_CONF_ERROR;
}
当上面的方法正确执行后,我们放在ccv.value中的“I am $uri”就会被编译到cv中。
每次当我们用“/a”访问上面的locaiton的时候,nginx会用ngx_http_complex_value()方法从cv中获取结果值,使用方式如下:
ngx_str_t res;
if (ngx_http_complex_value(r, &cv, &res) != NGX_OK) {
return NGX_ERROR;
}
res是nginx中的字符表示形式,刚开始的时候res的结构如下:
当该方法执行完毕后,res的结构会如下:
以上就是nginx中复杂值概念和nginx提供的对应工具的一个基本用法,关于编译细节,我们从下一节的实际例子中开始。
2纯文本复杂值的例子
这一节会详细分析纯文本复杂值是如何通过ngx_http_compile_complex_value()方法进行编译的,以及如何通过ngx_http_complex_value()方法,从编译后的结果中获取值。
2.1纯文本复杂值的编译
看下面这个配置:
location /a {
return 200 “I am uri”;
}
在上面这个例子中,对于return这个配置指令来说,“I am uri”这个字符串就是一个复杂值,在配置解析阶段,当nginx遇到return时就会触发对“I am uri”进行编译。所以在本例中,return配置指令是了解复杂值编译方式的一个入口,在开始介绍复杂值编译过程之前,我们先来看一下return这个配置指令在nginx内部的表示形式:
01: { ngx_string("return"),
02: NGX_HTTP_SRV_CONF
|NGX_HTTP_SIF_CONF
|NGX_HTTP_LOC_CONF
03: |NGX_HTTP_LIF_CONF|NGX_CONF_TAKE12,
04: ngx_http_rewrite_return,
05: NGX_HTTP_LOC_CONF_OFFSET,
06: 0,
07: NULL }
上面的代码是nginx在内部定义配置指令的方式,关于配置指令定义的详细介绍会放在后续文章中,这里只简单介绍一下跟本次内容相关的第01行和04行的作用:其中01行表示配置指令名字是“return”;04行表示该配置指令对应的方法,该方法在nginx解析到对应的配置指令时会被执行。
所以在本例中ngx_http_rewrite_return()方法是整个编译动作的入口,编译的过程就从它开始,基本过程如下:
1.取出实现准备好的容器lcf->codes,该容器用来存放编译好的“结构体指令”序列,在当前例子中的此时,容器里面还没有存放任何“指令结构体”(或结构体指令),所以此时该容器内存结构可如下表示:
2.创建一个return配置指令对应的指令结构体ngx_http_script_return_code_t(后续有ret表示),然后把该指令结构体放入lcf->codes容器中,并设置指令结构体对应的code字段值:
此时容器中仅存放了一条配置指令return对应的指令结构体(ret),还并未真正涉及到复杂值的编译,下面的步骤才是关于复杂值的编译。
3. 准备好一个用来存放原生值的结构体ngx_http_compile_complex_value_t(后续用ccv表示),原生值(“I am uri”)我们用v(在进入ngx_http_rewrite_return方法后该值就可以被拿到)表示,然后对ccv做如下赋值:
ccv.value = v;
ccv.complex_value = &ret->text;
其中,ccv.complex_value和&ret->text都指向一个ngx_http_complex_value_t结构体,这个是用来存放编译后的结果的,他们的关系可用下图表示:
4. 所需“原料”准备好后,会调用ngx_http_compile_complex_value方法进行复杂值编译,该方法会首先计算原生值中所含“$”符号的个数,如果该符号后面紧跟着的是1-9的数字,则表示这是一个正则表达式子捕获,如果不是数字则表示这是一个变量。最终会计算出变量的个数用nv表示,子捕获的个数用nc表示,因为我们本例的原生值是纯文本字符串,所以nv和nc都是零。
5.对于纯文本编译,ngx_http_compile_complex_value()方法仅仅把原生值v拷贝到ngx_http_complex_value_t结构体的value字段,然后再把该结构体中用来存放指令结构体和变量索引的容器都设置为NULL,具体操作如下:
ccv->complex_value->value = *value;
ccv->complex_value->flushes = NULL;
ccv->complex_value->lengths = NULL;
ccv->complex_value->values = NULL;
其中对value字段的赋值就算是纯文本复杂值编译的核心了,而flushes、lengths、values这三个容器在此时并没有起太大的作用,关于这三个容器我们会在介绍“编译混合复杂值(带变量的复杂值)”的例子中介绍,
6.最后用nv和nc都是零来断定这是一个纯文本编译,并返回。最终在ngx_http_complex_value_t结构体中,只有value字段的值是“I am uri”,其它字段都是NULL;
从以上例子看,纯文本的编译还是及其简单的,仅仅将原生值拷贝到了目标结构体中。
2.2从编译好的复杂值中求结果
复杂值的获取就是已编译好的脚本执行的过程,对于我们上面的例子来说,就是执行return这条配置指令对应的脚本,也就是lcf->codes容器中的指令集。而return这条配置指令在nginx中属于ngx_http_rewrite_module这个模块,该模块有一个入口函数ngx_http_rewrite_handler(),该方法的作用就是执行容器中的指令集,下面我们就详细介绍下整个执行过程。
2.2.1开始执行脚本
我们在上一篇提到过,nginx中脚本执行其实就是一个while循环:
while (*(uintptr_t *) e->ip) {
code = *(ngx_http_script_code_pt *) e->ip;
code(e);
}
此时e->ip指向的就是容器lcf->codes的首地址,延续上一节的例子,那么此时容器中就只有一个ngx_http_script_return_code_t指令,并且code字段对应的是ngx_http_script_return_code()方法。
继续往下可以看到,nginx把e->ip强制转换成了一个方法,这对其它语言(比如java)来说是不可接受的,但是在c语言中完全合法,就像在nginx变量实现原理(上)中介绍的那样,c中的强转算是一种“认知”上的转换,因为ngx_http_script_return_code_t结构体的第一个字段正好是一个ngx_http_script_code_pt类型的指针函数,所以我们这里把e->ip直接“认知”为一个ngx_http_script_code_pt类型的函数是没有问题的。
强转完毕后紧接着就是执行code字段对应的函数ngx_http_script_return_code(),这个函数包含了return这个配置指令在nginx中的行为逻辑,大概的逻辑是这样:
1.首先看return配置指令后面的状态码,如果大于等于400,就直接把状态码传递给脚本引擎(e->status)并设置e->ip为NULL(这样就会结束执行脚本的while循环)。
2.如果状态码小于400(或非数字字符),则通过ngx_http_send_response()方法来获得状态码,并发状态码赋值给脚本引擎(e->status)并设置e->ip为NULL。
其中,第二个逻辑中的ngx_http_send_response()方法会接收一个ngx_http_complex_value_t结构体对象,这里存放的是我们前面小节的例子中编译好的复杂值(“I am uri”),该值最终由ngx_http_complex_value()方法得出。
2.2.2用ngx_http_complex_value()方法计算纯文本复杂值
该方法对纯文本复杂值的计算非常简单,就下面一个if语句:
if (val->lengths == NULL) {
*value = val->value;
return NGX_OK;
}
其中val就是传进来的ngx_http_complex_value_t结构体对象,而val->lengths字段的值为NULL是我们在编译纯文本值时设置的,所以可以使用这个字段来判断要计算的复杂值是否为纯文本值。如果是文本值,则直接把val->value中的值赋值给*value这个“值-结果(value-result)”参数就可以了。
3混合复杂值编译的例子
混合复杂值编译和求值的步骤跟纯文本复杂值基本一样,他们的不同具体表现在ngx_http_compile_complex_value()和ngx_http_complex_value()这两个方法中,混合复杂值因为要考虑的情况多,所以编译和求值就相对更复杂一些。
3.1混合复杂值的编译
对于下面这样一个例子:
location / {
return 200 “I am $uri”;
}
配置指令return后面的字符串“I am $uri”就是一个混合复杂值(combination complex value),它的编译过程跟纯文本基本一致,不同点都封装在了ngx_http_compile_complex_value()方法中,所以我们下面直接以该方法为入口来介绍混合复杂值的编译过程。
3.1.1三个容器
在ngx_http_compile_complex_value()方法中,nginx会为复杂复杂值的编译准备三个容器,分别是flushes、lengths、values其中:
容器flushes:用来存放复杂值中每个变量的索引值,比如有如下复杂值:
“$host and $uri ”
如果变量$host和$uri在容器(cmcf->variables)中的索引值是9和7,那么容器flushes中就会顺序的包含9和7这两个数字。
容器lengths:该容器存放的不再是数字了,而是编译好的指令集,该指令集用来计算混合复杂值的字符长度,比如例子中的“I am $uri”这个字符串,如果我们使用“/a”来访问,那么该容器中的指令执行完毕后,就能计算出该字符串的长度为7。
容器values:该容器存放的也是指令集,主要用来计算复杂值的最终值,同样拿例子中的“I am $uri”做解释,如果用“/a”来访问,那么该容器的最终计算值就是“I am /a”。
后面要介绍的编译工作主要就是填充这三个容器,nginx还专门为此准备了一个ngx_http_script_compile_t结构体(后续用sc表示)和ngx_http_script_compile()方法。其中sc用来携带原值(“I am $uri”)和以上的三个容器及其它相关信息,而ngx_http_script_compile()方法则用来负责具体编译工作。
3.1.2真正干活的ngx_http_script_compile()方法
该方法首先会确保sc中的三个容器是有值的,如果没有则会为这三个容器赋值并分配内容空间,之后才开始真正的编译工作。
假设要编译的字符串为“I am $uri”,那么该方法在编译的时候会把字符串分成两种不同的成分:文本值(I am )和变量($uri)。
文本值的编译工作由ngx_http_script_add_copy_code()方法来完成,它会向sc->lenghts和sc->values容器中添加对应的指令(指令结构体)。所以当分析出文本值“I am ”后,该方法会向sc->lengths中添加一个ngx_http_script_copy_code_t指令结构体,然后把指令中的code字段设置为ngx_http_script_copy_len_code()方法,指令中的len字段设置为计算出的当前文本的长度5。此后sc->lengths容器长成这样:
之后是向sc->values容器中添加指令结构体,用的同样是ngx_http_script_copy_code_t指令结构体,然后指令的len字段值没变,但是指令的code字段变成了ngx_http_script_copy_code()方法,另外为了存储文本值(I am ),又在容器中额外分配了len长度的内存空间,这个内存空间存放的就是“I am ”这个文本值。此后sc->values容器长这样:
变量的编译工作由ngx_http_script_add_var_code()方法来完成,该方法会同时向三个容器中添加值,只不过sc->flushes中存放的是当前要编译的变量的索引值(变量在cmcf->variable容器中的下标)。另外两个容器添加的都是ngx_http_script_var_code_t指令结构体,该指令有一个index字段用来存放当前变量的索引值,而指令的code字段对应的方法分别是用来计算变量长度的ngx_http_script_copy_var_len_code()方法和用来计算变量值的ngx_http_script_copy_var_code()方法。
最后结合文本值编译的结果,lengths和values这两个容器的结果如下:
所以从上面对整个编译过程的介绍可以看到,对于“I am $uri”这样一个字符串,nginx总共产生了四条指令结构体,每当我们访问一次对应的location的时候,nginx就会利用这四条指令计算出“I am $uri”的实际值,接下来我们就介绍下它的计算过程。
3.2从编译好的混合复杂值中求结果
这一过程跟2.2节的流程基本一致,唯一不同的是在ngx_http_complex_value()方法中的执行流程。
混合复杂值的计算会同时涉及flushes、lengths、values这三个容器,该方法会先用flushes中的变量索引来确定本次用到的变量是否需要缓存,具体逻辑在ngx_http_script_flush_complex_value()方法中。
然后用lengths中的脚本(指令)计算出整个复杂值的长度(用value->len表示),用这个长度来分配结果值需要的内存空间(用value->data表示),之后再用values中的脚本(指令)计算出复杂值最终结果,并复制到value->data中,最终value就是我们需要的值。
其中lengths容器中脚本的执行过程如下:
while (*(uintptr_t *) e.ip) {
lcode = *(ngx_http_script_len_code_pt *) e.ip;
len += lcode(&e);
}
此时e.ip的值为sc->lengths,该循环会顺序执行容器中的两个指令ngx_http_script_copy_code_t和ngx_http_script_var_code_t,并最终从其对应的方法中计算出整个复杂值的长度len。
values容器中的脚本执行过程跟lengths容器一样,也是一个循环:
while (*(uintptr_t *) e.ip) {
code = *(ngx_http_script_code_pt *) e.ip;
code((ngx_http_script_engine_t *) &e);
}
只不过此时e.ip的值是sc->values,并且e.pos的值是value->data,而该循环要顺序执行的指令是ngx_http_script_copy_code_t和ngx_http_script_var_code_t中对应的方法。其中,指令方法对数据传递是通过e.pos指针来完成的,因为e.pos的值和value->data是一致的,所以向e.pos指针中添加字符就是向value->data中添加字符,关于脚本引擎结构体(ngx_http_script_engine_t)个字段的更详细说明可以看这里:
https://github.com/deyimsf/nginx-1.9.4/blob/master/src/http/ngx_http_script.h
4用来定义和赋值的set指令
在之前的文章中提到过nginx中的配置指令set可以定义一个变量并为其赋值,并且变量值也支持变量插入,比如这样一个配置:
set $a “I am $uri”;
它的实现方式跟return一样,也是用脚本,但是他们处理复杂值的方式稍有不同,在set中并没有使用ngx_http_compile_complex_value()、ngx_http_complex_value()、ngx_http_complex_value_t等方法和结构体,我们接下来会从set的编译和求值两个角度来一探究竟。
4.1配置指令set的编译
来看这样一个配置:
location /a {
set $a “I am $uri”;
return 200 $a;
}
这里我们重点关注set的编译,所以需要先从nginx中找出在配置解析阶段,触发set的入口函数,而这个入口函数就在set配置指令的内部定义中,如下:
{ ngx_string("set"),
NGX_HTTP_SRV_CONF
|NGX_HTTP_SIF_CONF
|NGX_HTTP_LOC_CONF
|NGX_HTTP_LIF_CONF |NGX_CONF_TAKE2,
ngx_http_rewrite_set,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
其中的ngx_http_rewrite_set()就是我们要找的入口函数,在该函数中,用来存放脚本的容器是lcf->codes,其中主要的编译工作通过调用ngx_http_rewirte_value()方法来完成。
4.1.1 ngx_http_rewirte_value() 函数
为了方便表述,接下来我们用value表示“I am $uri”。
在该函数中,首先计算出value中变量的个数n,如果n等于零,则表示value中没有变量。该方法会按照文本的编译方式向容器(lcf->codes)中添加对应的指令,这里因为我们的value中存在变量,所以它用的是另外一种指令结构体ngx_http_script_complex_value_code_t(后续用complex表示),从名字可以看出,它是专门为复杂值搞的一个指令,其中,该指令中的lengths字段用来存放计算复杂值长度的脚本,该指令放入到lcf->codes容器中后是这样的:
随后nginx又会借助我们之前用到过的ngx_http_script_compile()方法来实时具体编译工作,执行完这个方法后lcf->codes容器会变成这样:
而该指令中的complex->lengths容器会变成这样:
到此,ngx_http_rewirte_value()方法基本流程执行完毕。
4.1.2添加赋值脚本(指令)
这一步比较简单,主要就是想lcf->codes容器中添加一个ngx_http_script_var_code_t脚本,而这个脚本的唯一左右就是为变量($a)赋值,等这一步执行完毕后lcf->codes容器会变成这个样子:
4.2从编译好的set指令脚本中求值
求值的入口跟之前的一样,都是ngx_http_rewrite_handler()方法,都是从lcf->codes容器中的第一个指令开始的,从4.1.2的图中可以知道,本次set配置指令的执行涉及到四个指令,假设我们的请求是“/a”,那么它们的大概执行过程如下:
1.调用ngx_http_script_complex_value_code_t指令对应的ngx_http_script_complex_value_code()方法,该方法第一次用到了我们之前提到过的栈(e->sp),该方法会入栈一个变量值结构体(ngx_http_variable_value_t)
2.调用ngx_http_script_copy_code_t指令对应的ngx_http_script_copy_code()方法,把“I am ”拷贝到栈(e->sp)中
3.调用ngx_http_script_var_code_t指令对应的ngx_http_script_copy_var_code()方法,把变量$uri的实际值“/a”拷贝到栈中,那么此时栈顶对应的数据就是“I am /a”
4.调用ngx_http_script_var_code_t指令对应的ngx_http_script_set_var_code()方法,该方法会从栈顶取值,并把值赋值给变量$a
5.至此,配置指令set涉及的脚本执行完毕
5总结
本篇主要从nginx中的复杂值(complex value)概念开始,描述了nginx如何进行脚本编译,以及如何执行脚本。
通过对return和set两个配置指令的学习,了解到了处理复杂值的不同方式。不管是哪种方式,最终它们都会涉及到三个容器sc->flushes、sc->lengths、sc->values,以及真正用来编译它们的ngx_http_script_compile()方法,所以当我们期望自己编写的模块也支持变量插入时,完全可以利用这个现成的方法。
最后,关于变量和脚本终于可告一段落了,下一篇我们开始介绍nginx中uri的匹配规则。
下一篇: 高并发下的读服务