熟悉Perl、Bourne Shell、C/C++等命令式编程语言的朋友一定知道,变量简单来说就是存储“值”的容器。很多编程语言中所谓的“值”可以是像3.14这样的数值,也可以是像hello world这样的字符串,甚至可以是像数组、哈希表这样的复杂数据结构。然而,在Nginx配置中,变量只能存储一种类型的值,因为只有一种类型的值,那就是字符串。
例如,我们的nginx.conf 文件有以下一行配置:
设置$a"helloworld";
我们使用标准ngx_rewrite 模块的set 配置指令为变量$a 赋值。特别是,我们将字符串hello world 分配给它。
我们看到Nginx变量名前面有一个$符号,这是表示法的要求。所有Nginx 变量在Nginx 配置文件中引用时都必须以$ 为前缀。这种表示方法与Perl、PHP等语言类似。
虽然像$这样的变量前缀修饰会让正统的Java和C#程序员感到不舒服,但是这种表示方法的好处也很明显,那就是变量可以直接嵌入到字符串常量中来构造新的字符串:
设置$你好;
设置$b"$a,$a";
这里我们通过现有的Nginx 变量$a 的值构造变量$b 的值。那么这两条指令依次执行后,$a的值为hello,$b的值为hello,hello。这种技术在Perl 世界中被称为“变量插值”,使得专门的字符串连接运算符不再那么必要。我们不妨在这里采用这个术语。
我们来看一个更完整的配置示例:
服务器{
听8080;
地点/测试{
设置$foohello;
回声"foo:$foo";
}
}
本示例省略了nginx.conf 配置文件中最外层的http 配置块和events 配置块。使用curl HTTP客户端在命令行请求这个/test接口,我们可以得到
$curl"http://localhost:8080/test"
foo:你好
这里我们使用第三方ngx_echo模块的echo配置指令来输出$foo变量的值作为当前请求的响应体。
我们看到echo配置指令的参数也支持“变量插值”。但需要注意的是,并非所有配置指令都支持“变量插值”。事实上,指令参数是否允许“变量插值”取决于指令的实现模块。
如果我们想通过echo命令直接输出包含“美元符号”($)的字符串,有没有办法转义特殊的$字符呢?答案是否定的(至少从最新的Nginx 稳定版本1.0.10 开始)。幸运的是,我们可以绕过这个限制,例如通过不支持“变量插值”的模块配置指令专门构造一个值为$ 的Nginx 变量,然后在echo 中使用这个变量。考虑以下示例:
地理$美元{
默认"$";
}
服务器{
听8080;
地点/测试{
echo"Thisisadollarsign:$dollar";
}
}
测试结果如下:
$curl"http://localhost:8080/test"
这是美元标志:$
这里使用了标准模块ngx_geo提供的配置指令geo将字符串"$"赋值给变量$dollar,这样当我们下面需要使用美元符号时,就可以直接引用我们的$dollar变量。事实上,ngx_geo模块最常见的用途是根据客户端的IP地址为指定的Nginx变量赋值。这里我们只是借用它来“无条件”地将“美元符号”的值赋给我们的$dollar 变量。
在“变量插值”的语境中,有一种特殊情况,即当引用的变量名后跟变量名的组成字符时(比如后面跟字母、数字、下划线),我们需要使用特殊符号。消除歧义,例如:
服务器{
听8080;
地点/测试{
设置$first"你好";
echo"${first}world";
}
}
这里,当我们在echo配置指令的参数值中引用变量$first时,它后面会跟着单词world,所以如果我们直接写"$firstworld",Nginx“变量插值”计算引擎会将其识别为引用一个变量。 $第一世界。为了解决这个问题,Nginx的字符串表示法支持使用大括号将$后面的变量名括起来,比如这里的${first}。上面例子的输出是:
$curl"http://localhost:8080/测试
你好世界
set指令(还有前面提到的geo指令)不仅有赋值的功能,它还有创建Nginx变量的副作用,即当作为赋值对象的变量还不存在时,会自动创建变量。例如,在上面的例子中,如果变量$a还没有创建,set指令会自动创建用户变量$a。如果我们不创建它而直接使用它的值,就会报错。例如
?服务器{
?听8080;
?
?位置/不好{
?回声$foo;
?}
?}
此时Nginx服务器会拒绝加载配置:
[emerg]未知的“foo”变量
是的,我们甚至无法启动服务!
有趣的是,Nginx 变量的创建和分配发生在完全不同的时间阶段。 Nginx变量的创建只能在Nginx配置加载时,或者Nginx启动时发生;并且赋值操作只能在实际处理请求时发生。这意味着在不创建变量的情况下使用变量会导致启动失败,也意味着我们无法在请求处理过程中动态创建新的Nginx变量。
Nginx变量一旦创建,其变量名的可见范围就是整个Nginx配置,甚至可以跨越不同虚拟主机的服务器配置块。让我们看一个例子:
服务器{
听8080;
位置/foo{
echo "foo=[$foo]";
}
地点/酒吧{
设置$foo32;
echo "foo=[$foo]";
}
}
这里我们使用set指令在location /bar创建了变量$foo,所以这个变量在整个配置文件中都是可见的,所以我们可以直接在location /foo引用这个变量,而不用担心Nginx报错。
下面是在命令行使用curl工具访问这两个接口的结果:
$curl"http://localhost:8080/foo"
富=[]
$curl"http://localhost:8080/bar"
富=[32]
$curl"http://localhost:8080/foo"
富=[]
从这个例子中我们可以看出,由于set指令是在/bar位置使用的,所以只有在访问/bar的请求中才会进行赋值操作。当请求/foo 接口时,我们总是得到一个空的$foo 值,因为如果在没有赋值的情况下输出用户变量,我们将得到一个空字符串。
从这个例子中我们可以看到的另一个重要特征是,虽然Nginx变量名的可见范围是整个配置,但每个请求都有一个所有变量的独立副本,或者换句话说,每个变量都有一个独立的容器来存储其值。副本之间不会互相干扰。例如,我们之前请求/bar接口后,$foo变量被赋值为32,但是它完全不会影响后续请求/foo接口对应的$foo值(它仍然是空的!),因为每个请求都有自己独立的$foo 变量副本。
Nginx 新手最常见的错误之一是将Nginx 变量理解为在请求之间全局共享的东西,或者“全局变量”。事实上,Nginx 变量的生命周期不能跨越请求边界。
关于Nginx 变量的另一个常见误解是变量容器的生命周期与位置配置块绑定。并不真地。我们来看一个涉及“内部跳转”的例子:
服务器{
听8080;
位置/foo{
设置$你好;
echo_exec/栏;
}
地点/酒吧{
echo "a=[$a]";
}
}
这里我们使用位置/foo 中第三方模块ngx_echo 提供的echo_exec 配置指令来发起到位置/bar 的“内部跳转”。所谓“内部跳转”就是在请求处理过程中从服务器内部的一个位置跳转到另一个位置的过程。这与使用HTTP状态码301、302的“外部跳转”不同,因为后者是由HTTP客户端进行跳转的,而在客户端,用户可以通过浏览器地址栏等界面进行查看。请求的URL 已更改。内部跳转与Bourne Shell(或Bash)中的exec 命令非常相似,都是“没有返回”。另一个类似的例子是C语言中的goto语句。
由于是内部跳转,所以当前正在处理的请求仍然是原来的请求,但是当前位置发生了变化,因此仍然是原来的一组Nginx 变量的容器副本。对应上面的例子,如果我们请求的是/foo接口,那么整个工作流程如下:首先通过location /foo中的set指令将$a变量的值赋给字符串hello,然后启动内部通过echo_exec指令请求。跳转,输入location /bar,然后输出$a变量的值。因为$a 仍然是原来的$a,所以我们可以期望得到输出的hello 行。测试证实了这一点:
$curllocalhost:8080/foo
一个=[你好]
但如果我们直接从客户端访问/bar 接口,我们将得到$a 变量的空值,因为它依赖位置/foo 来初始化$a。
从上面的例子中,我们可以看到,即使一个请求在处理过程中经历了多个不同的位置配置块,它仍然使用同一组Nginx变量的副本。在这里,我们还首次触及“内部跳跃”的概念。值得一提的是,标准ngx_rewrite 模块的rewrite 配置指令实际上可以发起“内部跳转”。例如,可以使用rewrite 配置指令将上面的示例重写为以下形式:
服务器{
听8080;
位置/foo{
设置$你好;
重写^/栏;
}
地点/酒吧{
echo "a=[$a]";
}
}
效果与使用echo_exec完全相同。后面我们还会介绍这个重写指令的更多用途,比如发起301、302这样的“外部跳转”。
从上面的例子我们看到,Nginx变量值容器的生命周期与当前正在处理的请求绑定,与位置无关。
我们之前遇到的是通过set 指令隐式创建的Nginx 变量。这些变量一般称为“用户定义变量”,或者更简单地称为“用户变量”。既然有“用户定义的变量”,自然就有Nginx核心和各个Nginx模块提供的“预定义变量”,或者说“内置变量”。
Nginx 内置变量最常见的用途是获取有关请求或响应的各种信息。例如,ngx_http_core模块提供的内置变量$uri可用于获取当前请求的URI(已解码且不包含请求参数),而$request_uri用于获取请求的原始URI(未解码并包含请求参数)。看一下下面的例子:
地点/测试{
echo"uri=$uri";
echo "request_uri=$request_uri";
}
为了简单起见,这里甚至省略了服务器配置块。与之前的所有示例一样,我们仍然在侦听端口8080。在本示例中,我们将$uri 和$request_uri 的值输出到响应正文中。让我们用不同的请求来测试这个/test 接口:
$curl"http://localhost:8080/test"
uri=/测试
request_uri=/测试
$curl"http://localhost:8080/test?a=3b=4"
uri=/测试
request_uri=/测试?a=3b=4
$curl"http://localhost:8080/test/hello%20world?a=3b=4"
uri=/测试/helloworld
request_uri=/test/hello%20world?a=3b=4
另外一个特别常用的内置变量其实并不是单个变量,而是一组具有无限变化的变量,即所有名称以arg_开头的变量,我们称之为$arg_XXX变量组。一个例子是$arg_name。该变量的值是当前请求中名为name 的URI 参数的值,以其原始的、未解码的形式。让我们看一个更完整的例子:
地点/测试{
echo"name:$arg_name";
回声"class:$arg_class";
}
然后在命令行上使用各种参数组合来请求/test接口:
$curl"http://localhost:8080/test"
姓名:
:级
$curl"http://localhost:8080/test?name=Tomclass=3"
姓名:汤姆
类:3
$curl"http://localhost:8080/test?name=hello%20worldclass=9"
name:hello%20world
:9类
其实$arg_name不仅可以匹配name参数,还可以匹配NAME参数,或者Name等:
$curl"http://localhost:8080/test?NAME=Marry"
姓名:结婚
:级
$curl"http://localhost:8080/test?Name=Jimmy"
姓名:Jimmy
:级
Nginx在匹配参数名之前会自动将原始请求中的参数名调整为全部小写。
如果要解码URI参数值中的%XX等编码序列,可以使用第三方ngx_set_misc模块提供的set_unescape_uri配置指令:
地点/测试{
set_unescape_uri$name$arg_name;
set_unescape_uri$class$arg_class;
echo"name:$name";
回声"class:$class";
}
现在我们来看看效果:
$curl"http://localhost:8080/test?name=hello%20worldclass=9"
name:helloworld
:9类
空间确实被破译了!
从这个例子我们也可以看出,set_unescape_uri指令也和set指令一样具有自动创建Nginx变量的功能。后面我们还会具体介绍ngx_set_misc模块。
$arg_XXX 类型的变量有无限多个可能的名称,因此它们不对应于任何值的容器。而且这种变量在Nginx核心中是经过特殊处理的。第三方Nginx模块无法提供如此神奇的内置变量。
与$arg_XXX类似的内置变量有很多,比如用于获取cookie值的$cookie_XXX变量组、用于获取请求头的$http_XXX变量组、用于获取响应头的$sent_http_XXX变量组。这里我就不一一介绍了。有兴趣的读者可以参考ngx_http_core模块的官方文档。
需要指出的是,很多内置变量都是只读的,比如我们刚刚介绍的$uri和$request_uri。应绝对避免给只读变量赋值,因为会出现意想不到的后果,例如:
?位置/不好{
?设置$uri/等等;
?回声$uri;
?}
这个有问题的配置将导致Nginx 在启动时报告一个奇怪的错误:
[emerg]重复的“uri”变量.
如果尝试覆盖其他只读内置变量,例如$arg_XXX 变量,在某些版本的Nginx 中甚至可能会导致进程崩溃。
还有一些内置变量支持重写,一个例子是$args。该变量在读取时返回当前请求的URL参数字符串(即请求URL中问号后面的部分,如果有的话),而在赋值时可以直接修改参数字符串。让我们看一个例子:
地点/测试{
设置$orig_args$args;
设置$args"a=3b=4";
echo"originalargs:$orig_args";
回声"args:$args";
}
这里我们首先将原始的URL参数字符串保存在$orig_args变量中,然后通过重写$args变量来修改当前的URL参数字符串,最后使用echo命令输出$orig_args和$args变量的值分别。接下来我们像这样测试/test 接口:
$curl"http://localhost:8080/test"
原始参数:
args:a=3b=4
$curl"http://localhost:8080/test?a=0b=1c=2"
原始参数:a=0b=1c=2
args:a=3b=4
在第一个测试中,我们没有设置任何URL 参数字符串,因此在打印$orig_args 变量的值时,我们得到了空。在第一个和第二个测试中,无论我们是否提供URL参数字符串,参数字符串都会被强制重写为位置/test中的a=3b=4。
需要注意的是,这里的$args变量与$arg_XXX相同,不再使用自己的容器来存储值。当我们读取$args时,Nginx会执行一小段代码,从Nginx核心中存储当前URL参数字符串的位置读取数据;而当我们重写$args时,Nginx会执行另一小段代码来读取相同的位置并进行重写。当Nginx的其他部分需要当前URL参数字符串时,它们就会从该位置读取数据,所以我们对$args的修改会影响所有部分的功能。让我们看一个例子:
地点/测试{
设置$orig_a$arg_a;
设置$args"a=5";
回声"originala:$orig_a";
回声"a:$arg_a";
}
这里我们首先将内置变量$arg_a的值,即原始请求的URL参数a的值保存到用户变量$orig_a中,然后将当前请求的参数字符串重写为a为内置变量$args 赋值。=5,最后使用echo命令分别输出$orig_a和$arg_a变量的值。因为对内置变量$args的修改会直接导致当前请求的URL参数字符串发生变化,所以内置变量$arg_XXX自然也会发生相应的变化。测试结果证实了这一点:
$curl"http://localhost:8080/test?a=3"
原版a:3
a:5
我们看到,因为原始请求的URL参数串是a=3,所以$arg_a的初始值为3,但是随后通过重写$args变量,将URL参数串强行修改为a=5,所以最终的结果$arg_a 的值值自动更改为5。
我们来看另一个通过修改$args 变量来影响标准HTTP 代理模块ngx_proxy 的示例:
服务器{
听8080;
地点/测试{
设置$args"foo=1bar=2";
proxy_passhttp://127.0.0.1:8081/args;
}
}
服务器{
听8081;
位置/参数{
回声"args:$args";
}
}
这里我们在http配置块中定义了两个虚拟主机。第一个虚拟主机监听8080端口,其/test接口通过重写$args变量,无条件地将当前请求的URL参数字符串更改为foo=1bar=2。然后通过ngx_proxy模块的proxy_pass指令配置/test接口。指向本地计算机端口8081 上的HTTP 服务/参数的反向代理。默认情况下,ngx_proxy模块将HTTP请求转发到远程HTTP服务时,当前请求的URL参数字符串会自动转发到远程位置。
本地8081端口上的HTTP服务是由我们定义的第二个虚拟主机提供的。我们使用echo命令在第二个虚拟主机的/args位置输出当前请求的URL参数串,通过ngx_proxy模块查看/test接口实际转发的URL请求参数串。
我们来实际访问第一个虚拟主机的/test接口:
$curl"http://localhost:8080/test?blah=7"
args:foo=1bar=2
我们看到,虽然请求本身提供了URL参数字符串blah=7,但在位置/test中,参数字符串被强制重写为foo=1bar=2。然后我们重写的参数字符串被转发到第二个虚拟主机上配置的/args接口,然后输出/args接口的URL参数字符串。事实证明,我们对$args变量的赋值操作也成功影响了ngx_proxy模块的行为。
读取变量时执行的这段特殊代码在Nginx 中称为“get handler”;而重写变量时执行的这段特殊代码称为“set handler”处理程序)。不同的Nginx 模块通常对其变量有不同的“访问处理程序”,这使得这些变量的行为变得神奇。
事实上,这种技术在计算领域并不罕见。例如,在面向对象编程中,类的设计者一般不会将类的成员变量直接暴露给类的使用者。相反,它提供了两个额外的方法(methods)来进行成员变量的读写操作。这两种方法通常称为“访问器”。下面是一个C++ 语言的例子:
包括
使用命名空间std;
类人{
公共:
conststringget_name(){
returnm_name;
}
voidset_name(conststringname){
m_name=名称;
}
私人:
字符串名称;
};
在这个名为Person 的C++ 类中,我们提供了两个公共方法get_name 和set_name,作为私有成员变量m_name 的“访问器”。
这种设计的好处是显而易见的。类的设计者可以执行“访问器”中的任意代码来实现所需的业务逻辑和“副作用”,比如自动更新与当前成员变量有依赖关系的其他成员变量,或者直接修改某个成员变量取决于当前的成员变量。数据库表中与当前对象关联的相应字段。对于后一种情况,也许“
存取器”所对应的成员变量压根就不存在,或者即使存在,也顶多扮演着数据缓存的角色,以缓解被代理数据库的访问压力。 与面向对象编程中的“存取器”概念相对应,Nginx 变量也是支持绑定“存取处理程序”的。Nginx 模块在创建变量时,可以选择是否为变量分配存放值的容器,以及是否自己提供与读写操作相对应的“存取处理程序”。 不是所有的 Nginx 变量都拥有存放值的容器。拥有值容器的变量在 Nginx 核心中被称为“被索引的”(indexed);反之,则被称为“未索引的”(non-indexed)。 我们前面在(二)中已经知道,像$arg_XXX这样具有无数变种的变量群,是“未索引的”。当读取这样的变量时,其实是它的“取处理程序”在起作用,即实时扫描当前请求的 URL 参数串,提取出变量名所指定的 URL 参数的值。很多新手都会对$arg_XXX的实现方式产生误解,以为 Nginx 会事先解析好当前请求的所有 URL 参数,并且把相关的$arg_XXX变量的值都事先设置好。然而事实并非如此,Nginx 根本不会事先就解析好 URL 参数串,而是在用户读取某个$arg_XXX变量时,调用其“取处理程序”,即时去扫描 URL 参数串。类似地,内建变量$cookie_XXX也是通过它的“取处理程序”,即时去扫描Cookie请求头中的相关定义的。 在设置了“取处理程序”的情况下,Nginx 变量也可以选择将其值容器用作缓存,这样在多次读取变量的时候,就只需要调用“取处理程序”计算一次。我们下面就来看一个这样的例子: map$args $foo { default 0; debug 1; } server{ listen8080; location/test { set$orig_foo $foo; set$args debug; echo "orginal foo: $orig_foo"; echo "foo: $foo"; } } 这里首次用到了标准ngx_map模块的map配置指令,我们有必要在此介绍一下。map在英文中除了“地图”之外,也有“映射”的意思。比方说,中学数学里讲的“函数”就是一种“映射”。而 Nginx 的这个map指令就可以用于定义两个 Nginx 变量之间的映射关系,或者说是函数关系。回到上面这个例子,我们用map指令定义了用户变量$foo与$args内建变量之间的映射关系。特别地,用数学上的函数记法y = f(x)来说,我们的$args就是“自变量”x,而$foo则是“因变量”y,即$foo的值是由$args的值来决定的,或者按照书写顺序可以说,我们将$args变量的值映射到了$foo变量上。 现在我们再来看map指令定义的映射规则: map$args $foo { default 0; debug 1; } 花括号中第一行的default是一个特殊的匹配条件,即当其他条件都不匹配的时候,这个条件才匹配。当这个默认条件匹配时,就把“因变量”$foo映射到值0. 而花括号中第二行的意思是说,如果“自变量”$args精确匹配了debug这个字符串,则把“因变量”$foo映射到值1. 将这两行合起来,我们就得到如下完整的映射规则:当$args的值等于debug的时候,$foo变量的值就是1,否则$foo的值就为0. 明白了map指令的含义,再来看location /test. 在那里,我们先把当前$foo变量的值保存在另一个用户变量$orig_foo中,然后再强行把$args的值改写为debug,最后我们再用echo指令分别输出$orig_foo和$foo的值。 从逻辑上看,似乎当我们强行改写$args的值为debug之后,根据先前的map映射规则,$foo变量此时的值应当自动调整为字符串1, 而不论$foo原先的值是怎样的。然而测试结果并非如此: $ curl "http://localhost:8080/test" original foo: 0 foo: 0 第一行输出指示$orig_foo的值为0,这正是我们期望的:上面这个请求并没有提供 URL 参数串,于是$args最初的取值就是空,再根据我们先前定义的映射规则,$foo变量在第一次被读取时的值就应当是0(即匹配默认的那个default条件)。 而第二行输出显示,在强行改写$args变量的值为字符串debug之后,$foo的条件仍然是0,这显然不符合映射规则,因为当$args为debug时,$foo的值应当是1. 这究竟是为什么呢? 其实原因很简单,那就是$foo变量在第一次读取时,根据映射规则计算出的值被缓存住了。刚才我们说过,Nginx 模块可以为其创建的变量选择使用值容器,作为其“取处理程序”计算结果的缓存。显然,ngx_map模块认为变量间的映射计算足够昂贵,需要自动将因变量的计算结果缓存下来,这样在当前请求的处理过程中如果再次读取这个因变量,Nginx 就可以直接返回缓存住的结果,而不再调用该变量的“取处理程序”再行计算了。 为了进一步验证这一点,我们不妨在请求中直接指定 URL 参数串为debug: $ curl "http://localhost:8080/test?debug" original foo: 1 foo: 1 我们看到,现在$orig_foo的值就成了1,因为变量$foo在第一次被读取时,自变量$args的值就是debug,于是按照映射规则,“取处理程序”计算返回的值便是1. 而后续再读取$foo的值时,就总是得到被缓存住的1这个结果,而不论$args后来变成什么样了。 map指令其实是一个比较特殊的例子,因为它可以为用户变量注册“取处理程序”,而且用户可以自己定义这个“取处理程序”的计算规则。当然,此规则在这里被限定为与另一个变量的映射关系。同时,也并非所有使用了“取处理程序”的变量都会缓存结果,例如我们前面在(三)中已经看到$arg_XXX并不会使用值容器进行缓存。 类似ngx_map模块,标准的ngx_geo等模块也一样使用了变量值的缓存机制。 在上面的例子中,我们还应当注意到map指令是在server配置块之外,也就是在最外围的http配置块中定义的。很多读者可能会对此感到奇怪,毕竟我们只是在location /test中用到了它。这倒不是因为我们不想把map语句直接挪到location配置块中,而是因为map指令只能在http块中使用! 很多 Nginx 新手都会担心如此“全局”范围的map设置会让访问所有虚拟主机的所有location接口的请求都执行一遍变量值的映射计算,然而事实并非如此。前面我们已经了解到map配置指令的工作原理是为用户变量注册 “取处理程序”,并且实际的映射计算是在“取处理程序”中完成的,而“取处理程序”只有在该用户变量被实际读取时才会执行(当然,因为缓存的存在,只在请求生命期中的第一次读取中才被执行),所以对于那些根本没有用到相关变量的请求来说,就根本不会执行任何的无用计算。 这种只在实际使用对象时才计算对象值的技术,在计算领域被称为“惰性求值”(lazy evaluation)。提供“惰性求值” 语义的编程语言并不多见,最经典的例子便是 Haskell. 与之相对的便是“主动求值” (eager evaluation)。我们有幸在 Nginx 中也看到了“惰性求值”的例子,但“主动求值”语义其实在 Nginx 里面更为常见,例如下面这行再普通不过的set语句: set$b "$a,$a"; 这里会在执行set规定的赋值操作时,“主动”地计算出变量$b的值,而不会将该求值计算延缓到变量$b实际被读取的时候。 前面在(二)中我们已经了解到变量值容器的生命期是与请求绑定的,但是我当时有意避开了“请求”的正式定义。大家应当一直默认这里的“请求”都是指客户端发起的 HTTP 请求。其实在 Nginx 世界里有两种类型的“请求”,一种叫做“主请求”(main request),而另一种则叫做“子请求”(subrequest)。我们先来介绍一下它们。 所谓“主请求”,就是由 HTTP 客户端从 Nginx 外部发起的请求。我们前面见到的所有例子都只涉及到“主请求”,包括(二)中那两个使用echo_exec和rewrite指令发起“内部跳转”的例子。 而“子请求”则是由 Nginx 正在处理的请求在 Nginx 内部发起的一种级联请求。“子请求”在外观上很像 HTTP 请求,但实现上却和 HTTP 协议乃至网络通信一点儿关系都没有。它是 Nginx 内部的一种抽象调用,目的是为了方便用户把“主请求”的任务分解为多个较小粒度的“内部请求”,并发或串行地访问多个location接口,然后由这些location接口通力协作,共同完成整个“主请求”。当然,“子请求”的概念是相对的,任何一个“子请求”也可以再发起更多的“子子请求”,甚至可以玩递归调用(即自己调用自己)。当一个请求发起一个“子请求”的时候,按照 Nginx 的术语,习惯把前者称为后者的“父请求”(parent request)。值得一提的是,Apache 服务器中其实也有“子请求”的概念,所以来自 Apache 世界的读者对此应当不会感到陌生。 下面就来看一个使用了“子请求”的例子: location/main { echo_location /foo; echo_location /bar; } location/foo { echo foo; } location/bar { echo bar; } 这里在location /main中,通过第三方ngx_echo模块的echo_location指令分别发起到/foo和/bar这两个接口的GET类型的“子请求”。由echo_location发起的“子请求”,其执行是按照配置书写的顺序串行处理的,即只有当/foo请求处理完毕之后,才会接着处理/bar请求。这两个“子请求”的输出会按执行顺序拼接起来,作为/main接口的最终输出: $ curl "http://localhost:8080/main" foo bar 我们看到,“子请求”方式的通信是在同一个虚拟主机内部进行的,所以 Nginx 核心在实现“子请求”的时候,就只调用了若干个 C 函数,完全不涉及任何网络或者 UNIX 套接字(socket)通信。我们由此可以看出“子请求”的执行效率是极高的。 回到先前对 Nginx 变量值容器的生命期的讨论,我们现在依旧可以说,它们的生命期是与当前请求相关联的。每个请求都有所有变量值容器的独立副本,只不过当前请求既可以是“主请求”,也可以是“子请求”。即便是父子请求之间,同名变量一般也不会相互干扰。让我们来通过一个小实验证明一下这个说法: location/main { set$var main; echo_location /foo; echo_location /bar; echo "main: $var"; } location/foo { set$var foo; echo "foo: $var"; } location/bar { set$var bar; echo "bar: $var"; } 在这个例子中,我们分别在/main,/foo和/bar这三个location配置块中为同一名字的变量,$var,分别设置了不同的值并予以输出。特别地,我们在/main接口中,故意在调用过/foo和/bar这两个“子请求”之后,再输出它自己的$var变量的值。请求/main接口的结果是这样的: $ curl "http://localhost:8080/main" foo: foo bar: bar main: main 显然,/foo和/bar这两个“子请求”在处理过程中对变量$var各自所做的修改都丝毫没有影响到“主请求”/main. 于是这成功印证了“主请求”以及各个“子请求”都拥有不同的变量$var的值容器副本。 不幸的是,一些 Nginx 模块发起的“子请求”却会自动共享其“父请求”的变量值容器,比如第三方模块ngx_auth_request. 下面是一个例子: location/main { set$var main; auth_request /sub; echo "main: $var"; } location/sub { set$var sub; echo "sub: $var"; } 这里我们在/main接口中先为$var变量赋初值main,然后使用ngx_auth_request模块提供的配置指令auth_request,发起一个到/sub接口的“子请求”,最后利用echo指令输出变量$var的值。而我们在/sub接口中则故意把$var变量的值改写成sub. 访问/main接口的结果如下: $ curl "http://localhost:8080/main" main: sub 我们看到,/sub接口对$var变量值的修改影响到了主请求/main. 所以ngx_auth_request模块发起的“子请求”确实是与其“父请求”共享一套 Nginx 变量的值容器。 对于上面这个例子,相信有读者会问:“为什么‘子请求’/sub的输出没有出现在最终的输出里呢?”答案很简单,那就是因为auth_request指令会自动忽略“子请求”的响应体,而只检查“子请求”的响应状态码。当状态码是2XX的时候,auth_request指令会忽略“子请求”而让 Nginx 继续处理当前的请求,否则它就会立即中断当前(主)请求的执行,返回相应的出错页。在我们的例子中,/sub“子请求”只是使用echo指令作了一些输出,所以隐式地返回了指示正常的200状态码。 如ngx_auth_request模块这样父子请求共享一套 Nginx 变量的行为,虽然可以让父子请求之间的数据双向传递变得极为容易,但是对于足够复杂的配置,却也经常导致不少难于调试的诡异 bug. 因为用户时常不知道“父请求”的某个 Nginx 变量的值,其实已经在它的某个“子请求”中被意外修改了。诸如此类的因共享而导致的不好的“副作用”,让包括ngx_echo,ngx_lua,以及ngx_srcache在内的许多第三方模块都选择了禁用父子请求间的变量共享。 Nginx 内建变量用在“子请求”的上下文中时,其行为也会变得有些微妙。【Nginx配置变量深入解析(第十九篇学习笔记)】相关文章:
用户评论
nginx 的变量真神奇,能这么灵活地控制网页内容。
有11位网友表示赞同!
一直想好好了解一下 nginx 的变量,这次笔记终于帮到了!
有14位网友表示赞同!
学习笔记十九?感觉自己距离大神还很远...
有15位网友表示赞同!
Nginx 变量的使用确实会提升网站的定制化程度。
有12位网友表示赞同!
记录学习笔记真是个好习惯,方便日后回顾和整理 。
有6位网友表示赞同!
这篇笔记能帮助我更好地理解 Nginx 的工作原理吗?
有14位网友表示赞同!
分享学习笔记,这是一种很好的知识交流方式。
有19位网友表示赞同!
Nginx 变量太复杂了,希望笔记能给我讲解清楚。
有5位网友表示赞同!
看了标题,我觉得这篇文章一定很有深度。
有14位网友表示赞同!
我想学习如何使用 Nginx 变量来实现动态内容展示。
有17位网友表示赞同!
记录学习过程不仅可以巩固知识,还可以帮助他人学习。
有8位网友表示赞同!
Nginx 的变量用法确实有些复杂,需要反复练习才会熟练掌握。
有17位网友表示赞同!
这篇笔记能让我更了解如何利用 Nginx 提高网站的性能吗?
有7位网友表示赞同!
希望笔记能详细解释不同类型变量的使用场景。
有17位网友表示赞同!
学习笔记十九!佩服作者的坚持和探索精神!
有5位网友表示赞同!
Nginx 是一个非常强大的 WEB 服务器,变量功能确实很实用。
有7位网友表示赞同!
这篇文章刚好可以帮助我解决最近在项目中遇到的 Nginx 变量问题。
有20位网友表示赞同!
学习笔记十九?感觉作者已经很深入了...
有18位网友表示赞同!
我很想了解 Nginx 变量是如何被定义和识别的?
有14位网友表示赞同!
分享知识,造福他人!感谢作者的分享!
有8位网友表示赞同!