NGINX脚本语言原理及源码分析(四)

21 篇文章 2 订阅

概述

前几篇文章,我们分别介绍了NGINX变量的基本特性实现原理以及NGINX中复杂变量求值的原理。 本篇,我们继续分析NGINX中rewrite模块定义的系列指令比如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请求到来时就可以在这个请求本身的上下文执行对应的指令。

这些指令对应的执行阶段是在HTTP的rewrite阶段,也就是在函数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_pt  code; //code指向获取变量字符串的方法

                        uintptr_t  len;     //常量字符串长度

             } ngx_http_script_var_code_t;

  1. 编译变量值的结构体

         typedef struct {

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

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

                        uintptr_t  value;

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

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

      } ngx_http_script_value_code_t;

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

      typedef struct {

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

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

      } ngx_http_script_complex_value_code_t;

  1. 指令if的结构体   

       typedef struct {

ngx_http_script_code_pt     code;

uintptr_t                   next;

void                      **loc_conf;

        } ngx_http_script_if_code_t;

  1. 指令set的结构体

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

typedef struct {

    ngx_http_script_code_pt     code;

    uintptr_t                   index;

} 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;

  1. 指令return的结构体

typedef struct {

    ngx_http_script_code_pt     code;

    uintptr_t                   status;

    ngx_http_complex_value_t    text;

} ngx_http_script_return_code_t;

  1. 指令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对应的location的type被设置成了noname,所以,在进行url匹配时并不会查找到此location。
  2. 函数ngx_http_rewrite_if通过ngx_http_rewrite_if_condition来具体解析if指令中的条件。
  1. 对于本例,首先解析到变量$a,这时通过函数ngx_http_rewrite_variable生成一个结构为ngx_http_script_var_code_t的指令结构体。结构体对应的处理函数是ngx_http_script_var_code,并且结构体也存了变量$a在变量数组中的index。
  2. 然后解析到了后面的=号,此时通过函数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,并且结构体也存储了常量字符串的数值和长度。
  3. 解析完=号后面的变量以后,再为=号生成一个结构体ngx_http_script_code_pt,并且设置此结构体的指令回调函数是ngx_http_script_equal_code。

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

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

  1. 完整的if指令以及条件解析完毕以后,需要对if本身这个block中的指令进行解析。并且把解析后的指令存放到if_code结构体中的loc_conf中,并且调用函数ngx_conf_parse开始解析。
  2. 现在开始解析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中生成的变量$a的index存放到此结构体中。

     3.接着解析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自定义的脚本语言,在表达能力和功能上远不及现有流行的脚本语言比如Lua,JavaScript等。这也促成了Openresty和NJS的出现。这两种产品极大方便了用户使用脚本语言开发新功能,降低了编写模块的门槛。对NGINX本身的推广和方便使用也起到了极大的作用。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值