熟悉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 内建变量用在“子请求”的上下文中时,其行为也会变得有些微妙。 前面在(三)中我们已经知道,许多内建变量都不是简单的“存放值的容器”,它们一般会通过注册“存取处理程序”来表现得与众不同,而它们即使有存放值的容器,也只是用于缓存“存取处理程序”的计算结果。我们之前讨论过的$args变量正是通过它的“取处理程序”来返回当前请求的 URL 参数串。因为当前请求也可以是“子请求”,所以在“子请求”中读取$args,其“取处理程序”会很自然地返回当前“子请求”的参数串。我们来看这样的一个例子: location/main { echo "main args: $args"; echo_location /sub "a=1&b=2"; } location/sub { echo "sub args: $args"; } 这里在/main接口中,先用echo指令输出当前请求的$args变量的值,接着再用echo_location指令发起子请求/sub. 这里值得注意的是,我们在echo_location语句中除了通过第一个参数指定“子请求”的 URI 之外,还提供了第二个参数,用以指定该“子请求”的 URL 参数串(即a=1&b=2)。最后我们定义了/sub接口,在里面输出了一下$args的值。请求/main接口的结果如下: $ curl "http://localhost:8080/main?c=3" main args: c=3 sub args: a=1&b=2 显然,当$args用在“主请求”/main中时,输出的就是“主请求”的 URL 参数串,c=3;而当用在“子请求”/sub中时,输出的则是“子请求”的参数串,a=1&b=2。这种行为正符合我们的直觉。 与$args类似,内建变量$uri用在“子请求”中时,其“取处理程序”也会正确返回当前“子请求”解析过的 URI: location/main { echo "main uri: $uri"; echo_location /sub; } location/sub { echo "sub uri: $uri"; } 请求/main的结果是 $ curl "http://localhost:8080/main" main uri: /main sub uri: /sub 这依然是我们所期望的。 但不幸的是,并非所有的内建变量都作用于当前请求。少数内建变量只作用于“主请求”,比如由标准模块ngx_http_core提供的内建变量$request_method. 变量$request_method在读取时,总是会得到“主请求”的请求方法,比如GET、POST之类。我们来测试一下: location/main { echo "main method: $request_method"; echo_location /sub; } location/sub { echo "sub method: $request_method"; } 在这个例子里,/main和/sub接口都会分别输出$request_method的值。同时,我们在/main接口里利用echo_location指令发起一个到/sub接口的GET“子请求”。我们现在利用curl命令行工具来发起一个到/main接口的POST请求: $ curl --data hello "http://localhost:8080/main" main method: POST sub method: POST 这里我们利用curl程序的--data选项,指定hello作为我们的请求体数据,同时--data选项会自动让发送的请求使用POST请求方法。测试结果证明了我们先前的预言,$request_method变量即使在GET“子请求”/sub中使用,得到的值依然是“主请求”/main的请求方法,POST. 有的读者可能觉得我们在这里下的结论有些草率,因为上例是先在“主请求”里读取(并输出)$request_method变量,然后才发“子请求”的,所以这些读者可能认为这并不能排除$request_method在进入子请求之前就已经把第一次读到的值给缓存住,从而影响到后续子请求中的输出结果。不过,这样的顾虑是多余的,因为我们前面在(五)中也特别提到过,缓存所依赖的变量的值容器,是与当前请求绑定的,而由ngx_echo模块发起的“子请求”都禁用了父子请求之间的变量共享,所以在上例中,$request_method内建变量即使真的使用了值容器作为缓存(事实上它也没有),它也不可能影响到/sub子请求。 为了进一步消除这部分读者的疑虑,我们不妨稍微修改一下刚才那个例子,将/main接口输出$request_method变量的时间推迟到“子请求”执行完毕之后: location/main { echo_location /sub; echo "main method: $request_method"; } location/sub { echo "sub method: $request_method"; } 让我们重新测试一下: $ curl --data hello "http://localhost:8080/main" sub method: POST main method: POST 可以看到,再次以POST方法请求/main接口的结果与原先那个例子完全一致,除了父子请求的输出顺序颠倒了过来(因为我们在本例中交换了/main接口中那两条输出配置指令的先后次序)。 由此可见,我们并不能通过标准的$request_method变量取得“子请求”的请求方法。为了达到我们最初的目的,我们需要求助于第三方模块ngx_echo提供的内建变量$echo_request_method: location/main { echo "main method: $echo_request_method"; echo_location /sub; } location/sub { echo "sub method: $echo_request_method"; } 此时的输出终于是我们想要的了: $ curl --data hello "http://localhost:8080/main" main method: POST sub method: GET 我们看到,父子请求分别输出了它们各自不同的请求方法,POST和GET. 类似$request_method,内建变量$request_uri一般也返回的是“主请求”未经解析过的 URL,毕竟“子请求”都是在 Nginx 内部发起的,并不存在所谓的“未解析的”原始形式。 如果真如前面那部分读者所担心的,内建变量的值缓存在共享变量的父子请求之间起了作用,这无疑是灾难性的。我们前面在(五)中已经看到ngx_auth_request模块发起的“子请求”是与其“父请求”共享一套变量的。下面是一个这样的可怕例子: map$uri $tag { default 0; /main 1; /sub 2; } server{ listen8080; location/main { auth_request /sub; echo "main tag: $tag"; } location/sub { echo "sub tag: $tag"; } } 这里我们使用久违了的map指令来把内建变量$uri的值映射到用户变量$tag上。当$uri的值为/main时,则赋予$tag值 1,当$uri取值/sub时,则赋予$tag值 2,其他情况都赋0. 接着,我们在/main接口中先用ngx_auth_request模块的auth_request指令发起到/sub接口的子请求,然后再输出变量$tag的值。而在/sub接口中,我们直接输出变量$tag. 猜猜看,如果我们访问接口/main,将会得到什么样的输出呢? $ curl "http://localhost:8080/main" main tag: 2 咦?我们不是分明把/main这个值映射到1上的么?为什么实际输出的是/sub映射的结果2呢? 其实道理很简单,因为我们的$tag变量在“子请求”/sub中首先被读取,于是在那里计算出了值2(因为$uri在那里取值/sub,而根据map映射规则,$tag应当取值2),从此就被$tag的值容器给缓存住了。而auth_request发起的“子请求”又是与“父请求”共享一套变量的,于是当 Nginx 的执行流回到“父请求”输出$tag变量的值时,Nginx 就直接返回缓存住的结果2了。这样的结果确实太意外了。 从这个例子我们再次看到,父子请求间的变量共享,实在不是一个好主意。 在(一)中我们提到过,Nginx 变量的值只有一种类型,那就是字符串,但是变量也有可能压根就不存在有意义的值。没有值的变量也有两种特殊的值:一种是“不合法”(invalid),另一种是“没找到”(not found)。 举例说来,当 Nginx 用户变量$foo创建了却未被赋值时,$foo的值便是“不合法”;而如果当前请求的 URL 参数串中并没有提及XXX这个参数,则$arg_XXX内建变量的值便是“没找到”。 无论是“不合法”也好,还是“没找到”也罢,这两种 Nginx 变量所拥有的特殊值,和空字符串("")这种取值是完全不同的,比如 JavaScript 语言中也有专门的undefined和null这两种特殊值,而 Lua 语言中也有专门的nil值: 它们既不等同于空字符串,也不等同于数字0,更不是布尔值false. 其实 SQL 语言中的NULL也是类似的一种东西。 虽然前面在(一)中我们看到,由set指令创建的变量未初始化就用在“变量插值”中时,效果等同于空字符串,但那是因为set指令为它创建的变量自动注册了一个“取处理程序”,将“不合法”的变量值转换为空字符串。为了验证这一点,我们再重新看一下(一)中讨论过的那个例子: location/foo { echo "foo = [$foo]"; } location/bar { set$foo 32; echo "foo = [$foo]"; } 这里为了简单起见,省略了原先写出的外围server配置块。在这个例子里,我们在/bar接口中用set指令隐式地创建了$foo变量这个名字,然后我们在/foo接口中不对$foo进行初始化就直接使用echo指令输出。我们当时测试/foo接口的结果是 $ curl "http://localhost:8080/foo" foo = [] 从输出上看,未初始化的$foo变量确实和空字符串的效果等同。但细心的读者当时应该就已经注意到,对于上面这个请求,Nginx 的错误日志文件(一般文件名叫做error.log)中多出一行类似下面这样的警告: [warn] 5765#0: *1 using uninitialized "foo" variable, ... 这一行警告是谁输出的呢?答案是set指令为$foo注册的“取处理程序”。当/foo接口中的echo指令实际执行的时候,它会对它的参数"foo = [$foo]"进行“变量插值”计算。于是,参数串中的$foo变量会被读取,而 Nginx 会首先检查其值容器里的取值,结果它看到了“不合法”这个特殊值,于是它这才决定继续调用$foo变量的“取处理程序”。于是$foo变量的“取处理程序”开始运行,它向 Nginx 的错误日志打印出上面那条警告消息,然后返回一个空字符串作为$foo的值,并从此缓存在$foo的值容器中。 细心的读者会注意到刚刚描述的这个过程其实就是那些支持值缓存的内建变量的工作原理,只不过set指令在这里借用了这套机制来处理未正确初始化的 Nginx 变量。值得一提的是,只有“不合法”这个特殊值才会触发 Nginx 调用变量的“取处理程序”,而特殊值“没找到”却不会。 上面这样的警告一般会指示出我们的 Nginx 配置中存在变量名拼写错误,抑或是在错误的场合使用了尚未初始化的变量。因为值缓存的存在,这条警告在一个请求的生命期中也不会打印多次。当然,ngx_rewrite模块专门提供了一条uninitialized_variable_warn配置指令可用于禁止这条警告日志。 刚才提到,内建变量$arg_XXX在请求 URL 参数XXX并不存在时会返回特殊值“找不到”,但遗憾的是在 Nginx 原生配置语言(我们估且这么称呼它)中是不能很方便地把它和空字符串区分开来的,比如: location/test { echo "name: [$arg_name]"; } 这里我们输出$arg_name变量的值同时故意在请求中不提供 URL 参数name: $ curl "http://localhost:8080/test" name: [] 我们看到,输出特殊值“找不到”的效果和空字符串是相同的。因为这一回是 Nginx 的“变量插值”引擎自动把“找不到”给忽略了。 那么我们究竟应当如何捕捉到“找不到”这种特殊值的踪影呢?换句话说,我们应当如何把它和空字符串给区分开来呢?显然,下面这个请求中,URL 参数name是有值的,而且其值应当是空字符串: $ curl "http://localhost:8080/test?name=" name: [] 但我们却无法将之和前面完全不提供name参数的情况给区分开。 幸运的是,通过第三方模块ngx_lua,我们可以轻松地在 Lua 代码中做到这一点。请看下面这个例子: location/test { content_by_lua " if ngx.var.arg_name == nil then ngx.say("name: missing") else ngx.say("name: [", ngx.var.arg_name, "]") end "; } 这个例子和前一个例子功能上非常接近,除了我们在/test接口中使用了ngx_lua模块的content_by_lua配置指令,嵌入了一小段我们自己的 Lua 代码来对 Nginx 变量$arg_name的特殊值进行判断。在这个例子中,当$arg_name的值为“没找到”(或者“不合法”)时,/foo接口会输出name: missing这一行结果: curl "http://localhost:8080/test" name: missing 因为这是我们第一次接触到ngx_lua模块,所以需要先简单介绍一下。ngx_lua模块将 Lua 语言解释器(或者LuaJIT即时编译器)嵌入到了 Nginx 核心中,从而可以让用户在 Nginx 核心中直接运行 Lua 语言编写的程序。我们可以选择在 Nginx 不同的请求处理阶段插入我们的 Lua 代码。这些 Lua 代码既可以直接内联在 Nginx 配置文件中,也可以单独放置在外部.lua文件里,然后在 Nginx 配置文件中引用.lua文件的路径。 回到上面这个例子,我们在 Lua 代码里引用 Nginx 变量都是通过ngx.var这个由ngx_lua模块提供的 Lua 接口。比如引用 Nginx 变量$VARIABLE时,就在 Lua 代码里写作ngx.var.VARIABLE就可以了。当 Nginx 变量$arg_name为特殊值“没找到”(或者“不合法”)时,ngx.var.arg_name在 Lua 世界中的值就是nil,即 Lua 语言里的“空”(不同于 Lua 空字符串)。我们在 Lua 里输出响应体内容的时候,则使用了ngx.say这个 Lua 函数,也是ngx_lua模块提供的,功能上等价于ngx_echo模块的echo配置指令。 现在,如果我们提供空字符串取值的name参数,则输出就和刚才不相同了: $ curl "http://localhost:8080/test?name=" name: [] 在这种情况下,Nginx 变量$arg_name的取值便是空字符串,这既不是“没找到”,也不是“不合法”,因此在 Lua 里,ngx.var.arg_name就返回 Lua 空字符串(""),和刚才的 Luanil值就完全区分开了。 这种区分在有些应用场景下非常重要,比如有的 web service 接口会根据name这个 URL 参数是否存在来决定是否按name属性对数据集合进行过滤,而显然提供空字符串作为name参数的值,也会导致对数据集中取值为空串的记录进行筛选操作。 不过,标准的$arg_XXX变量还是有一些局限,比如我们用下面这个请求来测试刚才那个/test接口: $ curl "http://localhost:8080/test?name" name: missing 此时,$arg_name变量仍然读出“找不到”这个特殊值,这就明显有些违反常识。此外,$arg_XXX变量在请求 URL 中有多个同名XXX参数时,就只会返回最先出现的那个XXX参数的值,而默默忽略掉其他实例: $ curl "http://localhost:8080/test?name=Tom&name=Jim&name=Bob" name: [Tom] 要解决这些局限,可以直接在 Lua 代码中使用ngx_lua模块提供的ngx.req.get_uri_args函数。 与$arg_XXX类似,我们在(二)中提到过的内建变量$cookie_XXX变量也会在名为XXX的 cookie 不存在时返回特殊值“没找到”: location/test { content_by_lua " if ngx.var.cookie_user == nil then ngx.say("cookie user: missing") else ngx.say("cookie user: [", ngx.var.cookie_user, "]") end "; } 利用curl命令行工具的--cookie name=value选项可以指定name=value为当前请求携带的 cookie(通过添加相应的Cookie请求头)。下面是若干次测试结果: $ curl --cookie user=agentzh "http://localhost:8080/test" cookie user: [agentzh] $ curl --cookie user= "http://localhost:8080/test" cookie user: [] $ curl "http://localhost:8080/test" cookie user: missing 我们看到,cookieuser不存在以及取值为空字符串这两种情况被很好地区分开了:当 cookieuser不存在时,Lua 代码中的ngx.var.cookie_user返回了期望的 Luanil值。 在 Lua 里访问未创建的 Nginx 用户变量时,在 Lua 里也会得到nil值,而不会像先前的例子那样直接让 Nginx 拒绝加载配置: location/test { content_by_lua " ngx.say("$blah = ", ngx.var.blah) "; } 这里假设我们并没有在当前的nginx.conf配置文件中创建过用户变量$blah,然后我们在 Lua 代码中通过ngx.var.blah直接引用它。上面这个配置可以顺利启动,因为 Nginx 在加载配置时只会编译content_by_lua配置指令指定的 Lua 代码而不会实际执行它,所以 Nginx 并不知道 Lua 代码里面引用了$blah这个变量。于是我们在运行时也会得到nil值。而ngx_lua提供的ngx.say函数会自动把 Lua 的nil值格式化为字符串"nil"输出,于是访问/test接口的结果是: curl "http://localhost:8080/test" $blah = nil 这正是我们所期望的。 上面这个例子中另一个值得注意的地方是,我们在content_by_lua配置指令的参数中提及了$bar符号,但却并没有触发“变量插值”(否则 Nginx 会在启动时抱怨$blah未创建)。这是因为content_by_lua配置指令并不支持参数的“变量插值”功能。我们前面在(一)中提到过,配置指令的参数是否允许“变量插值”,其实取决于该指令的实现模块。 设计返回“不合法”这一特殊值的例子是困难的,因为我们前面在(七)中已经看到,由set指令创建的变量在未初始化时确实是“不合法”,但一旦尝试读取它们时,Nginx 就会自动调用其“取处理程序”,而它们的“取处理程序”会自动返回空字符串并将之缓存住。于是我们最终得到的是完全合法的空字符串。下面这个使用了 Lua 代码的例子证明了这一点: location/foo { content_by_lua " if ngx.var.foo == nil then ngx.say("$foo is nil") else ngx.say("$foo = [", ngx.var.foo, "]") end "; } location/bar { set$foo 32; echo "foo = [$foo]"; } 请求/foo接口的结果是: $ curl "http://localhost:8080/foo" $foo = [] 我们看到在 Lua 里面读取未初始化的 Nginx 变量$foo时得到的是空字符串。 最后值得一提的是,虽然前面反复指出 Nginx 变量只有字符串这一种数据类型,但这并不能阻止像ngx_array_var这样的第三方模块让 Nginx 变量也能存放数组类型的值。下面就是这样的一个例子: location/test { array_split "," $arg_names to=$array; array_map "[$array_it]" $array; array_join " " $array to=$res; echo $res; } 这个例子中使用了ngx_array_var模块的array_split、array_map和array_join这三条配置指令,其含义很接近 Perl 语言中的内建函数split、map和join(当然,其他脚本语言也有类似的等价物)。我们来看看访问/test接口的结果: $ curl "http://localhost:8080/test?names=Tom,Jim,Bob [Tom] [Jim] [Bob] 我们看到,使用ngx_array_var模块可以很方便地处理这样具有不定个数的组成元素的输入数据,例如此例中的namesURL 参数值就是由不定个数的逗号分隔的名字所组成。不过,这种类型的复杂任务通过ngx_lua来做通常会更灵活而且更容易维护。 至此,本系列教程对 Nginx 变量的介绍终于可以告一段落了。我们在这个过程中接触到了许多标准的和第三方的 Nginx 模块,这些模块让我们得以很轻松地构造出许多有趣的小例子,从而可以深入探究 Nginx 变量的各种行为和特性。在后续的教程中,我们还会有很多机会与这些模块打交道。 通过前面讨论过的众多例子,我们应当已经感受到 Nginx 变量在 Nginx 配置语言中所扮演的重要角色:它是获取 Nginx 中各种信息(包括当前请求的信息)的主要途径和载体,同时也是各个模块之间传递数据的主要媒介之一。在后续的教程中,我们会经常看到 Nginx 变量的身影,所以现在很好地理解它们是非常重要的。【Nginx配置变量深入解析(第十九篇学习笔记)】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
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位网友表示赞同!