点赞
评论
收藏
分享
举报
NGINX 脚本语言原理及源码分析(二)
发表于2021-03-22 11:21

浏览 2.1k

概述

上篇文章,我们介绍了NGINX变量的基本特性。本篇,我们继续分析变量的实现原理,来分析变量是如何被定义以及如果被使用的。

因为NGINX变量本身支持变量插入,比如set $abc ${host}abc{url}类型的变量。这种复杂变量abc的求值也是NGINX变量实现的一个特色和难点。对于这个复杂变量求值,我们将在下一篇分析脚本语言实现时进行分析。

数据结构

针对每一个变量,NGINX内部对应有两个数据结构,一个是NGX_HTTP_VARIABLE_S用来存储变量的名称等信息,另外一个是NGX_HTTP_VARIABLE_VALUE_S用来存储变量的本身的值。其中NGX_HTTP_VARIABLE_VALUE_S结构只存在于每一个具体的请求中。

1.     结构ngx_http_variable_s的结构定义如下所示。它用来表示变量的描述和管理信息。


上面这个结构体中:

name代表变量名字,比如$host,$url等。

set_handler用来设置变量值的回调函数。

get_handler获取变量值的回调函数。

data是传递给set_handler/get_handler回调函数的参数。

flags表示此变量遵循的一些规则,比如NGX_HTTP_VAR_CHANGEABLE

index index提供了一个索引(数组的脚标),从而可以迅速定位到对应的变量。

·      NGX_HTTP_VAR_CHANGEABLE表示这个变量是可变的Nginx有很多内置变量是不可变的,比如arg_xxx这类变量,如果你使用set指令来修改这类变量,那么NGINX就会报错

·      NGX_HTTP_VAR_NOCACHEABLE表示这个变量每次都要实时去计算变量的值,而不是直接返回上次缓存的值

·      NGX_HTTP_VAR_INDEXED表示这个变量是用索引读取的

·      NGX_HTTP_VAR_NOHASH表示这个变量不需要被hash

·      NGX_HTTP_VAR_PREFIX 表示这个变量是内置前缀变量比如$args_等。

2.     结构ngx_http_variable_value_s如下所示。它用来表示变量的数值本身。

上面这个结构体中:

len表示变量的长度。

valid标志变量的值是否有效,在获取变量值时,如果标志为无效,则需要通过变量描述结构ngx_http_variable_s中的get_handler函数再次去获得变量值。

no_cachable标志变量是否可以缓存。有些变量比如动态内置变量需要每次都去实时获取数值。

escape标志变量值是否经过转义处理。

data变量的具体值。

3.     NGINX定义的变量,根据变量的不同类型需要存放到下述结构的不同的容器中。

无论通过模块preconfiguration回调函数定义的内置变量,还是通过显示的配置指令定义的变量,在定义时,都会先把定义的变量存放到左边核心数据结构ngx_http_core_main_conf_t定义的容器中。

具体说来,对于所有的非前缀变量,都存放在上述结构的variables_keys容器中。对于所有的前缀变量,都存放在prefix_variables容器中。也有模块会使用variables_hash容器存放变量。

NGINX本身支持非常多的变量,但是在某一个具体的用户配置中,只有少数的变量会实际被使用到。所以,所有实际被系统使用的变量在初始化阶段会存放到上述结构中的variables数组中。上述我们提到的这些变量容器,都是存放的变量的定义。

具体的数值都存放于每一个具体的http请求的请求ngx_http_request_t结构中。

变量定义和初始化

变量定义

在使用一个NGINX变量之前,需要先定义这个变量。对于自定义变量,是通过NGINX的特定模块的指令,比如rewrite模块的set指令来定义,这些变量的是在系统解析配置文件阶段加入到NGINX系统中的。对于内置变量,是在每一个模块的preconfiguration函数执行时定义一些固定名称的变量并且把它们加入到系统中的。

无论哪一种定义方式,都会生成一个ngx_http_variable_s的数据结构用来表示此变量。然后把定义的变量加入到cmcf结构中的容器中。具体来说,对于动态前缀内置变量,比如args_等变量,加入到cmcf-> prefix_variables数组中。对非动态内置变量,则加入到cmcf中的variable_keys数组中。

对于自定义的变量,比如通过set指令定义的变量,他们加入cmcf容器中的时机要晚于内置变量。他们发生在当NGINX启动解析配置文件时,此时所有模块的preconfiguration回调函数都已经执行完毕。

定义的变量最终要通过函数ngx_http_add_variable添加到系统中。我们以HTTP核心模块添加变量的流程来分析内置变量是如何添加到系统中的。

1.     在配置解析阶段,在处理http{}配置时,会执行所有HTTP模块的preconfiguraton回调函数。

2.     执行到ngx_http_core_module时,回调函数ngx_http_core_preconfiguraiton 函数会把在数组ngx_http_core_variables中预先定义的变量添加到系统中。

3.     函数ngx_http_core_preconfiguration函数调用ngx_http_variables_and_core_vars完成下列功能。

4.     函数ngx_http_core_preconfiguraiton函数初始化cmcf结构中的variable_keys哈希键值数组和prefix_variables数组,两个数组分别用来存放非前缀变量和前缀变量。

5.     遍历数组ngx_http_core_variables中定义的元素,然后通过函数ngx_http_add_variables函数做具体的添加动作。

函数ngx_http_add_variables的流程为:

1.     判断要添加的变量是否有NGX_HTTP_VAR_PREFIX标志,如果有,则通过ngx_http_add_prefix_variable函数把函数添加到prefix_variables数组中。如果没有,则添加到variable_keys哈希数组中。

2.     无论添加到哪个容器,具体的添加逻辑是一致的。

a.     查看原来的容器中是否已经存在同名的变量。

b.    如果存在相同名称的变量,并且变量标志有NGX_HTTP_VAR_CHANGALBE变量,则去除变量的NGX_HTTP_VAR_WEAK标志,然后把变量添加到容器中。如果没有NGX_HTTP_VAR_CHANGALBE标志,则打印出错信息"the duplicate *** variable"

c.     如果不存在相同名称变量,则直接完成变量插入。

除了HTTP core模块以外,也有其他模块也通过各自如下的preconfiguration回调函数完成自定义变量的添加。

ngx_http_upstream_add_variables()

ngx_http_proxy_add_variables()

ngx_http_fastcgi_add_variables()

ngx_http_browser_add_variable()

ngx_http_gzip_add_variables()

ngx_http_ssi_preconfiguration()

ngx_http_userid_add_variables()

以上是内置变量的添加流程,而自定义变量一般是通过自己定义指令来实现的。比如通过set指令添加的变量发生在rewrite的配置解析阶段通过回调函数ngx_http_rewrite_set中调用ngx_http_add_variable函数完成的。

对于系统中实际使用的变量都会通过函数ngx_http_get_variable_index添加到cmcfvariables数组中的。如前面所述,此数组存储的是NGINX在实际运行中会使用到的变量。函数ngx_http_get_variable_index的流程是非常简单,主要是查找某一个变量在variables数组中的index。如果变量已经存在直接返回变量的下标。如果不存在,则重新创建一个新变量并且返回变量的下标。获取index的主要目的是为了以后可以直接通过index进行快速查询。

变量初始化

配置解析完毕以后,所有的定义的变量变量都存放到variable_keys或者prefix_variables。而被实际使用的变量都存放到了variables中。这时通过函数ngx_http_variables_init_vars来完成对variables数组中的变量的二次初始化。初始化的主要目的是设置variables中变量的set/get_handler回调函数。而且把原来存放在variable_keys中的变量定义改放到variable_hash中,方便后续系统通过名字直接查找变量定义。

在函数ngx_http_variables_init_vars初始化的过程中,还要要检查所有在variables中实际被使用的变量是否在variable_keys或者prefix_variables或者variables_hash变量定义的容器中存在。如果不存在,说明我们在试图使用一个没有定义的变量。系统会打印NGINX: [emerg] unknown "**" variable” 错误并且终止启动。

函数ngx_http_variables_init_vars的流程:

1.     遍历variables数组中所有的变量。

2.     对于每一个变量,查找其是否存在于变量定义的数组variables_keysprefix_variables.

3.     如果存在,则把变量定义的set/get_handler函数赋值给variables数组中的变量。

4.     如果不存在,意味着试图使用一个没有定义的变量,则启动出错。

5.     最后,对于所有的不带有ngx_http_get_variable_index标志的变量生成一个新的结构variable_hash。最后把variables_keys数组清空。

这样在NGINX启动阶段,所有实际被使用的变量都存放到cmcf结构中的variables数组中。当每次新的HTTP请求到来时会给每个请求根据cmcfvariables数字创建一个变量数组。数组的个数就是上述variables数组的个数。这个针对每一个请求的数组,用来存放每一个请求具体的变量值。这样实现了每一个请求的变量值的隔离。

总结说来,对于变量的定义是全局的,但是对于每一个变量的值每一个请求是不一样的。对于每一个请求中的variables数组,其中的每一个变量都没有变量名称。只有对应的变量所在数组的下标。所以,在配置解析阶段就会通过函数ngx_http_get_variable_index解析出要使用的变量的index然后把这个index存放在相关的数据结构中。这样可以在运行时就可以通过index来获取变量具体的变量信息。也有些应用,比如ssi_filter模块,它在需要根据运行时的动态数据来决定使用的变量是什么。对应这种情况,只能通过variable_hashprefix_variables容器去查找具体的变量的定义,然后再通过变量的get_handler回调函数去获取变量的值。

变量取值和使用

当每个请求到来时会给每个请求创建一个存储变量值的数组,数组的长度就是 cmc->variables 数组中变量的个数。只有第一次取变量值的时候,才会将变量值保存在数组的对应位置上。这也就是所谓的惰性求值。

在函数ngx_http_create_request中,会为请求r结构申请和cmcf->variables数组一样大小的数组用来存放针对此请求的变量的具体值。请求结构r中的variables中的每一个变量ngx_http_variable_value_s只存储变量的值而不存储包括名称在内的描述信息。

NGINX中,可以通过配置文件或者模块代码中使用变量。无论哪种使用方式,最终需要有接口来完成获取变量的具体值。

NGINX提供了三种不同的API接口来完成变量取值功能。

ngx_http_variable_value_t *ngx_http_get_indexed_variable(ngx_http_request_t *r,

ngx_uint_t index);

ngx_http_variable_value_t *ngx_http_get_flushed_variable(ngx_http_request_t *r,

ngx_uint_t index);

ngx_http_variable_value_t *ngx_http_get_variable(ngx_http_request_t *r,

ngx_str_t *name, ngx_uint_t key);

三个接口的具体功能是:

ngx_http_get_variable() 通过变量名获取变量值。

ngx_http_get_indexed_variable() ngx_http_get_flushed_variable() 通过变量索引获取变量值。

函数ngx_http_get_flushed_variable

通过变量索引直接获取缓存中变量的值,若该变量的值不可缓存,则调用 ngx_http_get_indexed_variable() 重新获取。

函数ngx_http_get_indexed_variable

通过变量索引直接获取缓存中变量的值,不判断变量是否可以缓存。只要有结果就返回。如果没有结果,则调用get_handler函数重新查找。并且把查找结果存放到ngx_http_variable_value_s结构中。

不过他们的区别是后一个函数会处理 NGX_HTTP_VAR_NOCACHEABLE这个标记,也就是说如果你想要cache你的变量值,那么你的变量属性就不能设置NGX_HTTP_VAR_NOCACHEABLE, 并且通过ngx_http_get_flushed_variable来获取变量值。

函数ngx_http_get_flushed_variable变量取值的大体流程为:

1.     根据index获取对应请求variables数组中的值。

2.     判断数值的标志是否已经有数值或者已经取值失败过。

3.     如果变量本身可缓存,则直接返回已经获取的数值。

4.     如果数值本身没有标志获取过,则调用ngx_http_get_indexed_variable来获取变量的值。

5.     函数ngx_http_get_indexed_variable的流程为:

a.     或者全局的cmcf->variables结构。

b.    判断index是否超过上述数组结构的范围。

c.     如果数值已经有获取结果,直接返回结果。

d.    反之通过变量的get_handler函数获取变量的值。

e.     更新变量的结果信息和标志。

上述的三个接口都是针对某一个单独变量的求值。在NGINX中,我们还支持变量的嵌套,比如在proxy_pass指令中可以如下定义要连接的URL

proxy_pass https://${host}/path/${args_path}

其中proxy_pass指令要连接的URL由两个变量和常量字符串组成。关于这种复杂变量的求值,是通过NGINX的脚本语言机制实现的。简单地说就是把复杂字符串比如上述的https://${host}/path/${args_path} 分解成几个部分分别有字符串https://,变量 $host,常量字符串/path/以及变量$args_path。对于每一部分生成一个脚本结构,结构中存放求字符串或者变量数值和长度的回调函数,然后在运行的时候,顺序执行这些回调函数就可以获得整个字符串的数值。具体实现细节,我们将在后续的文章中进行分析。

结语

通过两篇文章,我们大体分析了变量的特性和具体实现。在NGINX的变量实现机制中有明显的两个阶段,一个是启动和配置解析阶段,另一个是运行阶段。变量的定义,创建,索引工作都是在第一阶段完成的,主要是为第二阶段变量的取值提供支持。对于变量的取值是在第二阶段完成的,也就是变量只有在使用时才尝试去获得数值,这就是所谓的惰性求值。

NGINX的变量值是针对每一个具体的请求的,在每一个请求中都有自己存储变量值的容器。而所有的变量的描述信息,包括取值方法等,都是全局一份。这样的设计一方面节约了资源,另一方面也实现了变量的基于请求的隔离。

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

暂无个人介绍

关注



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

按点赞数排序

按时间排序

关于作者
皮皮鲁
这家伙很懒还未留下介绍~
85
文章
2
问答
41
粉丝
相关文章
Nginx的stream模块提供了TCP负载均衡的功能,最初的stream模块比较简单,在nginx-1.11.4后也开始采用类似HTTP模块中分阶段处理请求的方式。stream模块的处理阶段在ngx_stream.h中定义了stream模块的7个阶段。如下面所示typedefenum{NGX_STREAM_POST_ACCEPT_PHASE=0,NGX_STREAM_PREACCESS_PHASE,NGX_STREAM_ACCESS_PHASE,NGX_STREAM_SSL_PHASE,NGX_STREAM_PREREAD_PHASE,NGX_STREAM_CONTENT_PHASE,NGX_STREAM_LOG_PHASE}ngx_stream_phases;与HTTP模块相同,每个阶段有相应的checker检查方法和handler回调方法,每个阶段都有零个或多个ngx_stream_phase_handler_t结构体。typedefstructngx_stream_phase_handler_sngx_s
点赞 2
浏览 4k
NginxRestrictAccessModuleAmoduletorestrictaccesstoaserver/locationusingthehostnameofremotehost,basedonNginxaccessmodule.ThismoduleisnotdistributedwiththeNginxsource.See theinstallationinstructions.ConfigurationAnexample:pidlogs/nginx.pid; error_loglogs/nginx-main_error.logdebug; #DevelopmentMode #master_processoff; #daemonoff; worker_processes1; worker_rlimit_core500M; working_directory/tmp; debug_point
点赞 1
浏览 949
妇女节半天假,谁还在工作,举个手。。。 看网上说三八节假期不能调休,还有人问说给不给加班费。。。
点赞 0
浏览 529