点赞
评论
收藏
分享
举报
NGINX脚本语言原理及源码分析(四)
发表于2021-04-09 13:47

浏览 1.6k

概述

前几篇文章,我们分别介绍了NGINX变量的基本特性实现原理以及NGINX复杂变量求值的原理。 本篇,我们继续分析NGINXrewrite模块定义的系列指令比如if/set/break/return等的实现原理。

基本原理

我们在分析NGINX复杂变量求值时,已经介绍和分析了NGINX脚本执行的基本原理。除了复杂变量求值涉及到NGINX脚本语言以外,另外一种显式地通过在配置脚本中配置的指令,比如NGINX rewrite模块或者geo模块定义的指令也需要NGINX的脚本语言功能的支持。

NGINX在启动阶段,会把一些指令比如set/rewrite/return等编译成一系列指令集,并且存放到每一个location中的ngx_http_rewrite_loc_conf_t结构体中。

typedef struct {

    ngx_array_t  *codes;//保存着所属location下的所有编译后脚本

    ngx_uint_t    stack_size;//变量值栈sp的大小

     ……

} ngx_http_rewrite_loc_conf_t;

这个结构其实就是rewrite模块在location级别下面的配置结构体。如果匹配的location下面没有脚本语言配置,则codes数组是空的,否则codes成员就会存放着解析后的脚本指令的结构体。这样每当一个HTTP请求到来时就可以在这个请求本身的上下文执行对应的指令。

这些指令对应的执行阶段是在HTTPrewrite阶段,也就是在函数ngx_http_rewrite_handler中执行的。当函数执行时,首先创建一个ngx_http_script_engine_t结构,然后开始执行ngx_http_rewrite_loc_conf_t结构中codes数组中的指令。所以对应的代码的执行非常简单,只需要遍历和执行codes数组中的函数就可以。所以最重要的工作还是指令的编译和解析阶段,也就是如何把匹配的指令解析成一系列的指令结构体。

源码分析

我们将以如下的配置指令为例来进行原理分析。

if $a =‘test’{

    set $a ${a} again

    return 200 $a

}

基本原理

上面的例子中设计到了if/set/return三个指令,一个复杂变量和一个常量值。

对应这些指令和变量元素对应的指令结构体分别是:

1.     变量常量字符串的结构体

typedef struct {

        ngx_http_script_code_ptcode; //code指向获取变量字符串的方法

        uintptr_tlen;     //常量字符串长度

 } ngx_http_script_var_code_t;

2.     编译变量值的结构体

typedef struct {

        ngx_http_script_code_ptcode;   //code指向获取变量值的脚本指令

         /*外部变量值如果为整数,则转为整数后赋值给value,否则value0*/

        uintptr_tvalue;

        uintptr_ttext_len;  //外部变量值(set的第二个参数)的长度

        uintptr_ttext_data; //外部变量值的起始地址

} ngx_http_script_value_code_t;

3.     编译复杂变量值的结构体

typedef struct {

        ngx_http_script_code_pt code;//code指向编译复杂变量值的脚本指令方法

        ngx_array_t*lengths; //lengths存放的是复杂变量值中内嵌变量的值长度

} ngx_http_script_complex_value_code_t;

4.     指令if的结构体

typedef struct {

     ngx_http_script_code_ptcode;

    uintptr_tnext;

    void**loc_conf;

} ngx_http_script_if_code_t;

5.     指令set的结构体

根据set后面的变量是否有set_handler函数,可以分别使用如下两种指令结构体。

typedef struct {

    ngx_http_script_code_pt     code;

    uintptr_tindex;

} ngx_http_script_var_code_t;

typedef struct {

    ngx_http_script_code_pt     code;

    ngx_http_set_variable_pt    handler;

    uintptr_t                   data;

} ngx_http_script_var_handler_code_t;

6.     指令return的结构体

typedef struct {

    ngx_http_script_code_pt     code;

    uintptr_t                   status;

    ngx_http_complex_value_t    text;

} ngx_http_script_return_code_t;

7.     指令if的结构体

typedef struct {

    ngx_http_script_code_pt     code;

    uintptr_t                   next;

    void                      **loc_conf;

} ngx_http_script_if_code_t;

指令解析

NGINX启动过程中,通过函数ngx_http_rewrite_if函数解析配置中的if指令。对应这个例子具体流程是:

1.     在解析if指令时,会把if当做location来处理,并且把这个if对应的locationtype被设置成了noname,所以,在进行url匹配时并不会查找到此location

2.     函数ngx_http_rewrite_if通过ngx_http_rewrite_if_condition来具体解析if指令中的条件。

a.     对于本例,首先解析到变量$a,这时通过函数ngx_http_rewrite_variable生成一个结构为ngx_http_script_var_code_t的指令结构体。结构体对应的处理函数是ngx_http_script_var_code,并且结构体也存了变量$a在变量数组中的index

b.    然后解析到了后面的=号,此时通过函数ngx_http_rewrite_value添加了指令结构体ngx_http_script_value_code_t用来获取=号后面变量的值。函数ngx_http_rewrite_value本身可以处理复杂变量也就是变量嵌入的情况。对应于我们的例子中“test”是一个常量字符串,此时结构体ngx_http_script_value_code_t的指令回调函数是ngx_http_script_value_code,并且结构体也存储了常量字符串的数值和长度。

c.     解析完=号后面的变量以后,再为=号生成一个结构体ngx_http_script_code_pt,并且设置此结构体的指令回调函数是ngx_http_script_equal_code

3.     指令if的条件解析完毕以后,再对if指令本身生成一个指令结构体ngx_http_script_if_code_t,结构体的指令回调函数是ngx_http_script_if_code

至此,整个if语句本身已经解析完毕,解析完毕后,对应if所在的location的指令数组如下图所示:

4.     完整的if指令以及条件解析完毕以后,需要对if本身这个block中的指令进行解析。并且把解析后的指令存放到if_code结构体中的loc_conf中,并且调用函数ngx_conf_parse开始解析。

5.     现在开始解析if块中的set指令,对应的解析函数是ngx_http_rewrite_set。具体流程是:

a.     对于set后面的变量,分别通过函数ngx_http_add_variable以及函数ngx_http_get_variable_index把变量$a加入系统数组中,并且获得其对应的index

b.    对于set指令后面的第二个参数${a}test,其自身是一个复杂变量。通过函数ngx_http_rewrite_value添加指令结构体ngx_http_script_complex_value_code_t,并且设置其指令回调函数为ngx_http_script_complex_value_code

c.     通过ngx_http_script_compile生成对复杂变量${a}test求值的指令数组并且存放到指令结构体ngx_http_script_complex_value_code中的lengths数组中。

d.    对于set指令本身,生成指令结构体ngx_http_script_var_code_t并且设置其指令回调函数为ngx_http_script_set_var_code,并且把5a中生成的变量$aindex存放到此结构体中。

6.     接着解析if块中的return指令,对应的解析函数是ngx_http_rewrite_return。具体流程是:

a.     return指令生成指令结构体ngx_http_script_return_code_t, 并且设置其指令回调函数为ngx_http_script_return_code

b.    存储return指令后面的状态码到结构ngx_http_script_return_code中。

c.     return的第二个参数,通过函数ngx_http_compile_complex_value存放到指令结构提中的text字段中。此text字段是ngx_http_complex_value_t类型,其本身存放这复杂变量求值的指令结构。

至此,if块中的所有指令都已经生成完毕,if本身的codes数组此时的内容如下。其中return指令结构体return中的text字段还存放这对于复杂变量求值的指令集:

指令运行

在经过指令解析后,配置文件中的各种指令都被解析成各种指令回调函数。这些指令在rewrite模块的执行函数ngx_http_rewrite_handler中得到执行。具体逻辑非常简单就是循环执行指令数组中的指令。

在执行到if指令的回调函数ngx_http_script_if_code时,如果if的条件成立,则通过函数ngx_http_update_location_config把当前执行的context切换到if指令块中的指令数组,从而可以继续执行if块中的指令。

if is evail

NGINX if 指令被认为是“邪恶”的。甚至官方有一篇 著名的If is Evial 来劝告使用者慎用if指令。其实,造成这种印象的主要原因就是NGINX 是分 phase(阶段) 的,并不像真正的编程语言比如C语言一样按照指令出现的顺序执行。指令执行的顺序和配置文件中出现的顺序没有太大关系,而是与具体模块的实现有关。

具体说来,If 是属于 rewrite 模块的,所以对于 if 来讲,会和其他的 rewrite 模块执行全部执行完之后再进行下一阶段。如果 if 指令的结果是 match 的,那么 if 会创建一个内嵌的 location 块,只有这里面的 content 处理指令(NGX_HTTP_CONTENT_PHASE 阶段)会执行。

章宜春有一篇文章很好地解释了if指令执行的原理,通过理解if运行的原理,可以更好地使用if指令进行配置NGINX

结语

通过四篇系列文章分析了NGINX的变量以及脚本语言实现原理。这些变量和脚本语言的支持方便和丰富了NGINX的配置和使用。但是,这些NGINX自定义的脚本语言,在表达能力和功能上远不及现有流行的脚本语言比如LuaJavaScript等。这也促成了OpenrestyNJS的出现。这两种产品极大方便了用户使用脚本语言开发新功能,降低了编写模块的门槛。对NGINX本身的推广和方便使用也起到了极大的作用。


已修改于2023-03-03 09:46
本作品系原创
创作不易,留下一份鼓励
皮皮鲁

暂无个人介绍

关注



写下您的评论
发表评论
全部评论(0)

按点赞数排序

按时间排序

关于作者
皮皮鲁
这家伙很懒还未留下介绍~
85
文章
2
问答
41
粉丝
相关文章