Nginx变量漫谈

Nginx变量漫谈(一)-Nginx变量初识

1.1 基本认识

1.1.1 引言

Nginx的配置文件使用的就是一门微型的编程语言, 许多真实世界里的Nginx配置文件其实就是一个一个的小程序. 当然, 是不是"图灵完全的"暂且不论, 至少据我观察, 它在设计上受PerlBourne Shell这两种语言的影响很大. 在这一点上, 相比Apache和Lighttpd等其他Web服务器的配置记法, 不能不说算是Nginx的一大特色了. 既然是编程语言, 一般也就少不了"变量"这种东西(当然, Haskell这样奇怪的函数式语言除外了).

熟悉Perl, Bourne Shell, C/C++等命令式编程语言的朋友肯定知道, 变量说白了就是存放"值"的容器. 而所谓"值", 在许多编程语言里, 既可以是3.14这样的数值, 也可以是hello world这样的字符串, 甚至可以是像数组, 哈希表这样的复杂数据结构. 然而, 在Nginx配置中, 变量只能存放一种类型的值, 因为也只存在一种类型的值, 那就是字符串.

比如我们的nginx.conf文件中有下面这一行配置:

set $a "hello world";

这里使用了标准ngx_rewrite模块的set配置指令对变量$a执行了赋值操作. 特别地, 我们把字符串hello world赋给了它. 我们看到, Nginx变量名前面有一个’ ′ 符号 , 这是记法上的要求 . 所有的 N g i n x 变量在 N g i n x 配置文件中引用时都须带上 '符号, 这是记法上的要求. 所有的Nginx变量在Nginx配置文件中引用时都须带上 符号,这是记法上的要求.所有的Nginx变量在Nginx配置文件中引用时都须带上前缀.这种表示方法和Perl, PHP这些语言是相似的.

1.1.2 变量插值

虽然$这样的变量前缀修饰会让正统的Java和C#程序员不舒服, 但这种表示方法的好处也是显而易见的, 那就是可以直接把变量嵌入到字符串常量中以构造出新的字符串:

set $a hello;
set $b "$a, $a";

这里我们通过已有的Nginx变量$a的值, 来构造变量$b的值, 于是这两条指令顺序执行完之后, $a的值是hello, 而$b的值则是hello, hello.这种技术在Perl世界里被称为变量插值(variable interpolation), 它让专门的字符串拼接运算符变得不再那么必要. 我们在这里也不妨采用此术语.

1.2 实例分析

1.2.1

我们来看一个比较完整的配置示例:

server {
    listen 8080;

    location /test {
        set $foo hello;
        echo "foo: $foo";
    }
}

这个例子省略了nginx.conf配置文件中最外围的http配置块以及events配置块. 使用curl这个HTTP客户端在命令行上请求这个/test地址, 我们可以得到

$ curl 'http://localhost:8080/test'
foo: hello

这里我们使用第三方ngx_echo模块的echo配置指令将$foo变量的值作为当前请求的响应体输出. 我们看到, echo配置指令的参数也支持"变量插值". 不过, 需要说明的是, 并非所有的配置指令都支持"变量插值".事实上, 指令参数是否允许"变量插值", 取决于该指令的实现模块.

1.2.2

如果我们想通过echo指令直接输出含有"美元符"($)的字符串, 那么有没有办法把特殊的$字符给转义掉呢? 答案是否定的(至少到目前最新的Nginx稳定版1.0.10). 不过幸运的是, 我们可以绕过这个限制, 比如通过不支持"变量插值"的模块配置指令专门构造出取值为$的Nginx变量, 然后再在echo中使用这个变量.

看下面这个例子:

geo $dollar {
    default "$";
}

server {
    listen 8080;

    location /test {
        echo "This is a dollar sign: $dollar";
    }
}

测试结果如下:

$ curl 'http://localhost:8080/test'
This is a dollar sign: $

这里用到了标准模块ngx_geo提供的配置指令geo来为变量$dollar赋予字符串’ ′ , 这样我们在下面需要使用美元符的地方 , 就直接引用我们的 ‘ ', 这样我们在下面需要使用美元符的地方, 就直接引用我们的` ,这样我们在下面需要使用美元符的地方,就直接引用我们的dollar变量就可以了. 其实ngx_geo模块最常规的用法是**根据客户端的IP地址对指定的Nginx变量进行赋值**, 这里只是借用它以便"无条件地"对我们的$dollar`变量赋予"美元符"这个值.

1.2.3

在"变量插值"的上下文中, 还有一种特殊情况, 即当引用的变量名之后紧跟着变量名的构成字符时(比如后跟字母, 数字以及下划线), 我们就需要使用特别的记法来消除歧义, 例如:

server {
    listen 8080;

    location /test {
        set $first "hello ";
        echo "${first}world";
    }
}

这里, 我们在echo配置指令的参数值中引用变量$first的时候, 后面紧跟着world这个单词. 所以如果直接写作"$firstworld", 则 Nginx"变量插值"计算引擎会将之识别为引用了变量$firstworld. 为了解决这个难题, Nginx的字符串记法支持使用花括号在 之后把变量名围起来 , 比如这里的 ‘ 之后把变量名围起来, 比如这里的` 之后把变量名围起来,比如这里的{first}`.

上面这个例子的输出是:

$ curl 'http://localhost:8080/test'
hello world

2.1 作用域

set指令(以及前面提到的geo指令)不仅有赋值的功能, 它还有创建Nginx变量的副作用, 即当作为赋值对象的变量尚不存在时, 它会自动创建该变量. 比如在前面的例子中, 如果$a这个变量尚未创建, 则set指令会自动创建$a这个用户变量. 如果我们不创建就直接使用它的值, 则会报错. 例如:

server {
    listen 8080;

    location /bad {
        echo $foo;
    }
}

此时Nginx服务器会拒绝加载配置:

[emerg] unknown "foo" variable

是的, 我们甚至都无法启动服务!

有趣的是, Nginx变量的创建和赋值操作发生在全然不同的时间阶段. Nginx变量的创建只能发生在Nginx配置加载的时候, 或者说Nginx启动的时候; 而赋值操作则只会发生在请求实际处理的时候. 这意味着不创建而直接使用变量会导致启动失败, 同时也意味着我们无法在请求处理时动态地创建新的Nginx变量.

Nginx变量一旦创建, 其变量名的可见范围就是整个Nginx配置, 甚至可以跨越不同虚拟主机的server配置块.

来看一个例子:

server {
    listen 8080;

    location /foo {
        echo "foo = [$foo]";
    }

    location /bar {
        set $foo 32;
        echo "foo = [$foo]";
    }
}

这里我们在location /bar中用set指令创建了变量$foo, 于是在整个配置文件中这个变量都是可见的, 因此我们可以在location /foo中直接引用这个变量而不用担心Nginx会报错.

下面是在命令行上用curl工具访问这两个接口的结果:

$ curl 'http://localhost:8080/foo'
foo = []
 
$ curl 'http://localhost:8080/bar'
foo = [32]
 
$ curl 'http://localhost:8080/foo'
foo = []

从这个例子我们可以看到, set指令因为是在location /bar中使用的, 所以赋值操作只会在访问/bar的请求中执行. 而请求/foo接口时, 我们总是得到空的$foo值, 因为用户变量未赋值就输出的话, 得到的便是空字符串.

从这个例子我们可以窥见的另一个重要特性是, Nginx变量名的可见范围虽然是整个配置, 但每个请求都有所有变量的独立副本, 或者说都有各变量用来存放值的容器的独立副本, 彼此互不干扰. 比如前面我们请求了/bar接口后, $foo变量被赋予了值32, 但它丝毫不会影响后续对/foo接口的请求所对应的$foo值(它仍然是空的!), 因为各个请求都有自己独立的$foo变量的副本.

对于Nginx新手来说, 最常见的错误之一, 就是将Nginx变量理解成某种在请求之间全局共享的东西, 或者说"全局变量". 而事实上, Nginx变量的生命期是不可能跨越请求边界的.

2.2 生命期

关于Nginx变量的另一个常见误区是认为变量容器的生命期, 是与location配置块绑定的. 其实不然. 我们来看一个涉及"内部跳转"的例子:

server {
    listen 8080;

    location /foo {
        set $a hello;
        echo_exec /bar;
    }

    location /bar {
        echo "a = [$a]";
    }
}

这里我们在location /foo中, 使用第三方模块ngx_echo提供的echo_exec配置指令, 发起到location /bar的"内部跳转". 所谓"内部跳转", 就是在处理请求的过程中, 于服务器内部, 从一个location跳转到另一个location的过程. 这不同于利用HTTP状态码301和302所进行的"外部跳转", 因为后者是由HTTP客户端配合进行跳转的, 而且在客户端, 用户可以通过浏览器地址栏这样的界面, 看到请求的URL地址发生了变化. 内部跳转和Bourne Shell(或Bash)中的exec命令很像, 都是"有去无回"(另一个相近的例子是C语言中的goto语句).

既然是内部跳转, 当前正在处理的请求就还是原来那个, 只是当前的location发生了变化, 所以还是原来的那一套Nginx变量的容器副本. 对应到上例, 如果我们请求的是/foo这个接口, 那么整个工作流程是这样的: 先在location /foo中通过set指令将 a 变量的值赋为字符串 h e l l o , 然后通过 e c h o e x e c 指令发起内部跳转 , 又进入到 l o c a t i o n / b a r 中 , 再输出 a变量的值赋为字符串hello, 然后通过echo_exec指令发起内部跳转, 又进入到location /bar中, 再输出 a变量的值赋为字符串hello,然后通过echoexec指令发起内部跳转,又进入到location/bar,再输出a变量的值. 因为 a 还是原来的 a还是原来的 a还是原来的a, 所以我们可以期望得到hello这行输出. 测试证实了这一点:

$ curl localhost:8080/foo
a = [hello]

但如果我们从客户端直接访问/bar接口, 就会得到空的$a变量的值, 因为它依赖于location /foo来对$a进行初始化.

从上面这个例子我们看到, 一个请求在其处理过程中, 即使经历多个不同的location配置块, 它使用的还是同一套Nginx变量的副本. 这里, 我们也首次涉及到了内部跳转这个概念. 值得一提的是, 标准ngx_rewrite模块的rewrite配置指令其实也可以发起内部跳转, 例如上面那个例子用rewrite配置指令可以改写成下面这样的形式:

server { 
    listen 8080; 
    location /foo { 
        set $a hello; 
        rewrite ^ /bar; 
    } 
    location /bar { 
        echo "a = [$a]"; 
    } 
}

其效果和使用echo_exec是完全相同的. 后面我们还会专门介绍这个rewrite指令的更多用法, 比如发起301和302这样的外部跳转.

从上面这个例子我们看到, Nginx变量值容器的生命期是与当前正在处理的请求绑定的, 而与location无关.

前面我们接触到的都是通过set指令隐式创建的Nginx变量. 这些变量我们一般称为用户自定义变量, 或者更简单一些, 用户变量. 既然有用户自定义变量, 自然也就有由Nginx核心和各个Nginx模块提供的预定义变量, 或者说内建变量(builtin variables).

Nginx内建变量最常见的用途就是获取关于请求或响应的各种信息. 例如由ngx_http_core模块提供的内建变量$uri, 可以用来获取当前请求的URI(经过解码, 并且不含请求参数), 而$request_uri则用来获取请求最原始的URI(未经解码, 并且包含请求参数).

请看下面这个例子(这里为了简单起见, 连server配置块也省略了):

location /test {
    echo "uri = $uri";
    echo "request_uri = $request_uri";
}

和前面所有示例一样, 我们监听的依然是8080端口. 在这个例子里, 我们把$uri$request_uri的值输出到响应体中去. 下面我们用不同的请求来测试一下这个/test接口:

$ curl 'http://localhost:8080/test'
uri = /test
request_uri = /test
 
$ curl 'http://localhost:8080/test?a=3&b=4'
uri = /test
request_uri = /test?a=3&b=4
 
$ curl 'http://localhost:8080/test/hello%20world?a=3&b=4'
uri = /test/hello world
request_uri = /test/hello%20world?a=3&b=4

另一个特别常用的内建变量其实并不是单独一个变量, 而是有无限多变种的一群变量, 即名字以arg_开头的所有变量, 我们估且称之为$arg_XXX变量群. 一个例子是$arg_name, 这个变量的值是当前请求名为name的URI参数的值, 而且还是未解码的原始形式的值.

来看一个比较完整的示例:

location /test {
    echo "name: $arg_name";
    echo "class: $arg_class";
}

然后在命令行上使用各种参数组合去请求这个/test接口:

$ curl 'http://localhost:8080/test'
name: 
class: 
 
$ curl 'http://localhost:8080/test?name=Tom&class=3'
name: Tom
class: 3
 
$ curl 'http://localhost:8080/test?name=hello%20world&class=9'
name: hello%20world
class: 9

其实$arg_name不仅可以匹配name参数, 也可以匹配NAME参数, 抑或是Name, 等等. Nginx会在匹配参数名之前, 自动把原始请求中的参数名调整为全部小写的形式.

如果你想对URI参数值中的%XX这样的编码序列进行解码, 可以使用第三方ngx_set_misc模块提供的set_unescape_uri配置指令:

location /test {
    set_unescape_uri $name $arg_name;
    set_unescape_uri $class $arg_class;

    echo "name: $name";
    echo "class: $class";
}

现在再看一下效果:

$ curl 'http://localhost:8080/test?name=hello%20world&class=9'
name: hello world
class: 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. 对只读变量进行赋值是应当绝对避免的, 因为会有意想不到的后果, 比如:

location /bad {
    set $uri /blah;
    echo $uri;
}

这个有问题的配置会让Nginx在启动的时候报出一条令人匪夷所思的错误:

[emerg] the duplicate "uri" variable in ...

如果你尝试改写另外一些只读的内建变量, 比如$arg_XXX变量, 在某些Nginx的版本中甚至可能导致进程崩溃.

4.1 取处理程序

4.1.1 可改写的内建变量

有一些内建变量是支持改写的, 其中一个例子是$args. 这个变量在读取时返回当前请求的URL参数串(即请求URL中问号后面的部分, 如果有的话), 而在赋值时可以直接修改参数串. 我们来看一个例子:

location /test {
    set $orig_args $args;
    set $args "a=3&b=4";

    echo "original args: $orig_args";
    echo "args: $args";
}

这里我们把原始的URL参数串先保存在$orig_args变量中, 然后通过改写$args变量来修改当前的URL参数串, 最后我们用echo指令分别输出$orig_args$args变量的值. 接下来我们这样来测试这个/test接口:

$ curl 'http://localhost:8080/test'
original args: 
args: a=3&b=4

$ curl 'http://localhost:8080/test?a=0&b=1&c=2'
original args: a=0&b=1&c=2
args: a=3&b=4

在第一次测试中, 我们没有设置任何URL参数串, 所以输出$orig_args变量的值时便得到空. 而在第一次和第二次测试中, 无论我们是否提供URL参数串, 参数串都会在location /test中被强行改写成a=3&b=4.

需要特别指出的是, 这里的$args变量和$arg_XXX一样, 也不再使用属于自己的存放值的容器. 当我们读取$args时, Nginx会执行一小段代码,从Nginx核心中专门存放当前URL参数串的位置去读取数据; 而当我们改写$args时, Nginx 会执行另一小段代码, 对相同位置进行改写. Nginx的其他部分在需要当前URL参数串的时候, 都会从那个位置去读数据, 所以我们对$args的修改会影响到所有部分的功能.

来看一个例子:

location /test {
    set $orig_a $arg_a;
    set $args "a=5";
    echo "original a: $orig_a";
    echo "a: $arg_a";
}

这里我们先把内建变量$arg_a的值, 即原始请求的URL参数a的值, 保存在用户变量 o r i g a 中 , 然后通过对内建变量 ‘ orig_a中, 然后通过对内建变量` origa,然后通过对内建变量args进行赋值, 把当前请求的参数串改写为a=5, 最后再用echo指令分别输出 o r i g a ‘ 和 ‘ orig_a`和` origaarg_a变量的值. 因为对内建变量$args的修改会直接导致当前请求的URL参数串发生变化, 因此内建变量$arg_XXX`自然也会随之变化.

测试的结果证实了这一点:

$ curl 'http://localhost:8080/test?a=3'
original a: 3
a: 5

我们看到, 因为原始请求的URL参数串是a=3, 所以 a r g a 最初的值为 3 , 但随后通过改写 arg_a最初的值为3, 但随后通过改写 arga最初的值为3,但随后通过改写args变量, 将URL参数串又强行修改为a=5, 所以最终$arg_a的值又自动变为了5.

再来看一个通过修改$args变量影响标准的HTTP代理模块ngx_proxy的例子:

server {
    listen 8080;

    location /test {
        set $args "foo=1&bar=2";
        proxy_pass http://127.0.0.1:8081/args;
    }
}

server {
    listen 8081;

    location /args {
        echo "args: $args";
    }
}

这里我们在http配置块中定义了两个虚拟主机. 第一个虚拟主机监听8080端口, 其/test接口自己通过改写$args变量, 将当前请求的URL参数串无条件地修改为foo=1&bar=2. 然后/test接口再通过ngx_proxy模块的proxy_pass指令配置了一个反向代理, 指向本机的8081端口上的HTTP服务/args. 默认情况下ngx_proxy模块在转发HTTP请求到远方HTTP服务的时候, 会自动把当前请求的URL参数串也转发到远方.

而本机的8081端口上的HTTP服务正是由我们定义的第二个虚拟主机来提供的. 我们在第二个虚拟主机的location /args中利用echo指令输出当前请求的URL参数串, 以检查/test接口通过ngx_proxy模块实际转发过来的URL请求参数串.

我们来实际访问一下第一个虚拟主机的/test接口:

$ curl 'http://localhost:8080/test?blah=7'
args: foo=1&bar=2

我们看到, 虽然请求自己提供了URL参数串blah=7, 但在location /test中, 参数串被强行改写成了foo=1&bar=2. 接着经由proxy_pass指令将我们被改写掉的参数串转发给了第二个虚拟主机上配置的/args接口, 然后再把/args接口的URL参数串输出. 事实证明, 我们对$args变量的赋值操作, 也成功影响到了ngx_proxy模块的行为.

4.1.2 取处理程序

上面提到的, 在读取变量时执行的这段特殊代码, 在Nginx中被称为取处理程序(get handler); 而改写变量时执行的这段特殊代码, 则被称为存处理程序(set handler). 不同的Nginx模块一般会为它们的变量准备不同的"存取处理程序", 从而让这些变量的行为充满魔法.

其实这种技巧在计算世界并不鲜见. 比如在面向对象编程中, 类的设计者一般不会把类的成员变量直接暴露给类的用户, 而是另行提供两个方法(method), 分别用于该成员变量的读操作和写操作, 这两个方法常常被称为存取器(accessor). 下面是C++语言中的一个例子:

#include <string>
using namespace std;

class Person {
public:
    const string get_name() {
        return m_name;
    }

    void set_name(const string name) {
        m_name = name;
    }

private:
    string m_name;
};

在这个名叫Person的C++类中, 我们提供了get_nameset_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请求头中的相关定义的.

4.2 变量缓存与惰性求值

在设置了"取处理程序"的情况下, Nginx变量也可以选择将其值容器用作缓存, 这样在多次读取变量的时候, 就只需要调用"取处理程序"计算一次. 我们下面就来看一个这样的例子:

map $args $foo {
    default     0;
    debug       1;
}

server {
    listen 8080;

    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)来说, 我们的 a r g s 就是 " 自变量 " x , 而 ‘ args就是"自变量"x, 而` args就是"自变量"x,foo则是"因变量"y, 即$foo的值是由$args的值来决定的, 或者按照书写顺序可以说, 我们将 a r g s ‘ 变量的值映射到了 args`变量的值映射到了 args变量的值映射到了foo变量上.

现在我们再来看map指令定义的映射规则:

map $args $foo {
    default     0;
    debug       1;
}

花括号中第一行的default是一个特殊的匹配条件, 即当其他条件都不匹配的时候, 这个条件才匹配. 当这个默认条件匹配时, 就把"因变量" f o o 映射到值 0. 而花括号中第二行的意思是说 , 如果 " 自变量 " foo映射到值0. 而花括号中第二行的意思是说, 如果"自变量" foo映射到值0.而花括号中第二行的意思是说,如果"自变量"args精确匹配了debug这个字符串, 则把"因变量" f o o 映射到值 1. 将这两行合起来 , 我们就得到如下完整的映射规则 : 当 foo映射到值1. 将这两行合起来, 我们就得到如下完整的映射规则: 当 foo映射到值1.将这两行合起来,我们就得到如下完整的映射规则:args的值等于debug的时候, f o o 变量的值就是 1 , 否则 foo变量的值就是 1, 否则 foo变量的值就是1,否则foo的值就为0.

明白了map指令的含义, 再来看location/test. 在那里, 我们先把当前 f o o 变量的值保存在另一个用户变量 foo变量的值保存在另一个用户变量 foo变量的值保存在另一个用户变量orig_foo中, 然后再强行把 a r g s 的值改写为 d e b u g , 最后我们再用 e c h o 指令分别输出 args的值改写为debug, 最后我们再用echo指令分别输出 args的值改写为debug,最后我们再用echo指令分别输出orig_foo和$foo的值.

从逻辑上看, 似乎当我们强行改写$args的值为debug之后, 根据先前的map映射规则, f o o 变量此时的值应当自动调整为字符串 1 , 而不论 foo变量此时的值应当自动调整为字符串1, 而不论 foo变量此时的值应当自动调整为字符串1,而不论foo原先的值是怎样的. 然而测试结果并非如此:

$ curl 'http://localhost:8080/test'
original foo: 0
foo: 0

第一行输出指示 o r i g f o o 的值为 0 , 这正是我们期望的 : 上面这个请求并没有提供 U R L 参数串 , 于是 orig_foo的值为0, 这正是我们期望的: 上面这个请求并没有提供URL参数串, 于是 origfoo的值为0,这正是我们期望的:上面这个请求并没有提供URL参数串,于是args最初的取值就是空, 再根据我们先前定义的映射规则, $foo变量在第一次被读取时的值就应当是0(即匹配默认的那个default条件).

而第二行输出显示, 在强行改写$args变量的值为字符串debug之后, f o o 的条件仍然是 0 , 这显然不符合映射规则 . 因为当 foo的条件仍然是0, 这显然不符合映射规则. 因为当 foo的条件仍然是0,这显然不符合映射规则.因为当args为debug时, $foo的值应当是1. 这究竟是为什么呢?

其实原因很简单, 那就是$foo变量在第一次读取时, 根据映射规则计算出的值被缓存住了. 刚才我们说过, Nginx模块可以为其创建的变量选择使用值容器, 作为其"取处理程序"计算结果的缓存. 显然, ngx_map模块认为变量间的映射计算足够昂贵, 需要自动将因变量的计算结果缓存下来, 这样在当前请求的处理过程中如果再次读取这个因变量, Nginx就可以直接返回缓存住的结果, 而不再调用该变量的"取处理程序"再行计算了.

为了进一步验证这一点, 我们不妨在请求中直接指定URL参数串为debug:

$ curl 'http://localhost:8080/test?debug'
original foo: 1
foo: 1

我们看到, 现在 o r i g f o o 的值就成了 1. 因为变量 orig_foo的值就成了1. 因为变量 origfoo的值就成了1.因为变量foo在第一次被读取时, 自变量 a r g s 的值就是 d e b u g , 于是按照映射规则 , " 取处理程序 " 计算返回的值便是 1. 而后续再读取 args的值就是debug, 于是按照映射规则, "取处理程序"计算返回的值便是1. 而后续再读取 args的值就是debug,于是按照映射规则,"取处理程序"计算返回的值便是1.而后续再读取foo的值时, 就总是得到被缓存住的1这个结果, 而不论$args后来变成什么样了.

注解: 这种情况应该是map模块在set与echo模块之前执行而发生, 这涉及到Nginx的内部执行过程.

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 " b " b"a,$a";

这里会在执行set规定的赋值操作时, "主动"地计算出变量 b 的值 , 而不会将该求值计算延缓到变量 b的值, 而不会将该求值计算延缓到变量 b的值,而不会将该求值计算延缓到变量b实际被读取的时候.

5.1 主请求与子请求

前面在chapter 2中我们已经了解到变量值容器的生命期是与请求绑定的, 但是我当时有意避开了"请求"的正式定义. 大家应当一直默认这里的"请求"都是指客户端发起的HTTP请求. 其实在Nginx世界里有两种类型的"请求", 一种叫做"主请求"(main request), 而另一种则叫做"子请求"(subrequest). 我们先来介绍一下它们.

所谓主请求, 就是由HTTP客户端从Nginx外部发起的请求. 我们前面见到的所有例子都只涉及到主请求. 包括(二)中那两个使用 echo_execrewrite指令发起内部跳转的例子.

子请求则是由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)通信. 我们由此可以看出"子请求"的执行效率是极高的.

5.2 父子请求之间的变量值容器

回到先前对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接口中先为 v a r 变量赋初值 m a i n , 然后使用 n g x a u t h r e q u e s t 模块提供的配置指令 a u t h r e q u e s t , 发起一个到 / s u b 接口的 " 子请求 " , 最后利用 e c h o 指令输出变量 var变量赋初值main, 然后使用ngx_auth_request模块提供的配置指令auth_request, 发起一个到/sub接口的"子请求", 最后利用echo指令输出变量 var变量赋初值main,然后使用ngxauthrequest模块提供的配置指令authrequest,发起一个到/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内建变量用在"子请求"的上下文中时, 其行为也会变得有些微妙.

6.1 父子请求不会相互干扰的内建变量

前面在(四)中我们已经知道, 许多内建变量都不是简单的"存放值的容器", 它们一般会通过注册"存取处理程序"来表现得与众不同, 而它们即使有存放值的容器, 也只是用于缓存"存取处理程序"的计算结果.

我们之前讨论过的$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. 这种行为正符合我们的直觉.

a r g s 类似 , 内建变量 args类似, 内建变量 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

这依然是我们所期望的.

6.2 只作用于主请求的内建变量

并非所有的内建变量都作用于当前请求. 少数内建变量只作用于"主请求", 比如由标准模块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内部发起的, 并不存在所谓的"未解析的"原始形式.

如果真如前面那部分读者所担心的, 内建变量的值缓存在共享变量的父子请求之间起了作用, 这无疑是灾难性的. 我们前面在chapter 5中已经看到ngx_auth_request模块发起的"子请求"是与其"父请求"共享一套变量的. 下面是一个这样的可怕例子:

map $uri $tag {
    default     0;
    /main       1;
    /sub        2;
}

server {
    listen 8080;

    location /main {
        auth_request /sub;
        echo "main tag: $tag";
    }

    location /sub {
        echo "sub tag: $tag";
    }
}

这里我们使用久违了的map指令来把内建变量 u r i 的值映射到用户变量 uri的值映射到用户变量 uri的值映射到用户变量tag上. 当 u r i 的值为 / m a i n 时 , 则赋予 uri的值为/main时, 则赋予 uri的值为/main,则赋予tag值1, 当 u r i 取值 / s u b 时 , 则赋予 uri取值/sub时, 则赋予 uri取值/sub,则赋予tag值2, 其他情况都赋0. 接着, 我们在/main接口中先用ngx_auth_request模块的auth_request指令发起到/sub接口的子请求, 然后再输出变量 t a g 的值 . 而在 / s u b 接口中 , 我们直接输出变量 tag的值. 而在/sub接口中, 我们直接输出变量 tag的值.而在/sub接口中,我们直接输出变量tag. 猜猜看, 如果我们访问接口/main, 将会得到什么样的输出呢?

$ curl 'http://localhost:8080/main'
main tag: 2

咦? 我们不是分明把/main这个值映射到1上的么? 为什么实际输出的是/sub映射的结果2呢?

其实道理很简单, 因为我们的 t a g 变量在 " 子请求 " / s u b 中首先被读取 , 于是在那里计算出了值 2 ( 因为 tag变量在"子请求" /sub中首先被读取, 于是在那里计算出了值2(因为 tag变量在"子请求"/sub中首先被读取,于是在那里计算出了值2(因为uri在那里取值/sub, 而根据map映射规则, t a g 应当取值 2 ) , 从此就被 tag应当取值2), 从此就被 tag应当取值2),从此就被tag的值容器给缓存住了. 而auth_request发起的"子请求"又是与"父请求"共享一套变量的, 于是当Nginx的执行流回到"父请求"输出$tag变量的值时, Nginx就直接返回缓存住的结果2了. 这样的结果确实太意外了.

从这个例子我们再次看到, 父子请求间的变量共享, 实在不是一个好主意.

在(一)中我们提到过, Nginx变量的值只有一种类型, 那就是字符串, 但是变量也有可能压根就不存在有意义的值. 没有值的变量也有两种特殊的值: 一种是"不合法"(invalid), 另一种是"没找到"(not found).举例来说:

当Nginx用户变量$foo创建了却未被赋值时, f o o 的值便是 " 不合法 " ; 而如果当前请求的 U R L 参数串中并没有提及 X X X 这个参数 , 则 foo的值便是"不合法"; 而如果当前请求的URL参数串中并没有提及XXX 这个参数, 则 foo的值便是"不合法";而如果当前请求的URL参数串中并没有提及XXX这个参数,arg_XXX内建变量的值便是"没找到".

无论是"不合法"也好, 还是"没找到"也罢, 这两种Nginx变量所拥有的特殊值, 和空字符串(“”)这种取值是完全不同的. 比如JavaScript语言中也有专门的undefined和null这两种特殊值, 而Lua语言中也有专门的nil值. 它们既不等同于空字符串, 也不等同于数字0, 更不是布尔值false. SQL语言中的NULL也是类似的一种东西.

7.1 Invalid

虽然前面在chapter 1中我们看到, 由set指令创建的变量未初始化就用在"变量插值"中时, 效果等同于空字符串, 但那是因为set指令为它创建的变量自动注册了一个"取处理程序", 将"不合法"的变量值转换为空字符串.

为了验证这一点, 我们再重新看一下chapter 1中讨论过的那个例子(为了简单起见, 省略了原先写出的外围server配置块):

location /foo {
    echo "foo = [$foo]";
}

location /bar {
    set $foo 32;
    echo "foo = [$foo]";
}

在这个例子里, 我们在/bar接口中用set指令隐式地创建了 f o o 变量这个名字 , 然后我们在 / f o o 接口中不对 foo变量这个名字, 然后我们在/foo接口中不对 foo变量这个名字,然后我们在/foo接口中不对foo进行初始化就直接使用echo指令输出. 我们当时测试/foo接口的结果是

$ curl 'http://localhost:8080/foo'
foo = []

从输出上看, 未初始化的$foo变量确实和空字符串的效果等同. 但细心的读者当时应该就已经注意到, 对于上面这个请求, Nginx的错误日志文件(一般文件名叫做error.log)中多出一行类似下面这样的警告:

[warn] 5765#0: *1 using uninitialized "foo" variable, ...

这一行警告是谁输出的呢? 答案是set指令为 f o o 注册的 " 取处理程序 " . 当 / f o o 接口中的 e c h o 指令实际执行的时候 , 它会对它的参数 " f o o = [ foo注册的"取处理程序". 当/foo接口中的echo指令实际执行的时候, 它会对它的参数"foo = [ foo注册的"取处理程序"./foo接口中的echo指令实际执行的时候,它会对它的参数"foo=[foo]" 进行"变量插值"计算. 于是, 参数串中的 f o o 变量会被读取 , 而 N g i n x 会首先检查其值容器里的取值 , 结果它看到了 " 不合法 " 这个特殊值 , 于是它这才决定继续调用 foo变量会被读取, 而Nginx会首先检查其值容器里的取值, 结果它看到了"不合法"这个特殊值, 于是它这才决定继续调用 foo变量会被读取,Nginx会首先检查其值容器里的取值,结果它看到了"不合法"这个特殊值,于是它这才决定继续调用foo变量的"取处理程序". 于是 f o o 变量的 " 取处理程序 " 开始运行 , 它向 N g i n x 的错误日志打印出上面那条警告消息 , 然后返回一个空字符串作为 foo变量的"取处理程序"开始运行, 它向Nginx的错误日志打印出上面那条警告消息, 然后返回一个空字符串作为 foo变量的"取处理程序"开始运行,它向Nginx的错误日志打印出上面那条警告消息,然后返回一个空字符串作为foo的值, 并从此缓存在$foo的值容器中.

细心的读者会注意到刚刚描述的这个过程其实就是那些支持值缓存的内建变量的工作原理, 只不过set指令在这里借用了这套机制来处理未正确初始化的Nginx变量. 值得一提的是, 只有"不合法"这个特殊值才会触发Nginx调用变量的"取处理程序", 而特殊值"没找到"却不会.

上面这样的警告一般会指示出我们的Nginx配置中存在变量名拼写错误, 抑或是在错误的场合使用了尚未初始化的变量. 因为值缓存的存在, 这条警告在一个请求的生命期中也不会打印多次. 当然, ngx_rewrite模块专门提供了一条uninitialized_variable_warn配置指令可用于禁止这条警告日志.

7.2 Not Found

上面提到, 内建变量$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参数的情况给区分开.

7.2.1 区分Not Found与空值

幸运的是, 通过第三方模块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变量 V A R I A B L E 时 , 就在 L u a 代码里写作 n g x . v a r . V A R I A B L E 就可以了 . 当 N g i n x 变量 ‘ VARIABLE时, 就在Lua代码里写作ngx.var.VARIABLE就可以了. 当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空字符串(“”), 和刚才的Lua nil值就完全区分开了.

这种区分在有些应用场景下非常重要, 比如有的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函数.

7.2.2 内建$cookie_XXX变量

$arg_XXX类似, 我们在chapter 1中提到过的内建变量$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

我们看到, cookie user不存在以及取值为空字符串这两种情况被很好地区分开了: 当cookie user不存在时, Lua代码中的ngx.var.cookie_user返回了期望的Lua nil值.

在Lua里访问未创建的Nginx用户变量时, 在Lua里也会得到nil值, 而不会像先前的例子那样直接让Nginx拒绝加载配置:

location /test {
    content_by_lua '
        ngx.say("$blah = ", ngx.var.blah)
    ';
}

这里假设我们并没有在当前的nginx.conf配置文件中创建过用户变量 b l a h , 然后我们在 L u a 代码中通过 n g x . v a r . b l a h 直接引用它 . 上面这个配置可以顺利启动 , 因为 N g i n x 在加载配置时只会编译 ‘ c o n t e n t b y l u a ‘ 配置指令指定的 L u a 代码而不会实际执行它 , 所以 N g i n x 并不知道 L u a 代码里面引用了 blah, 然后我们在Lua代码中通过ngx.var.blah直接引用它. 上面这个配置可以顺利启动, 因为Nginx在加载配置时只会编译`content_by_lua`配置指令指定的Lua代码而不会实际执行它, 所以Nginx并不知道Lua代码里面引用了 blah,然后我们在Lua代码中通过ngx.var.blah直接引用它.上面这个配置可以顺利启动,因为Nginx在加载配置时只会编译contentbylua配置指令指定的Lua代码而不会实际执行它,所以Nginx并不知道Lua代码里面引用了blah这个变量. 于是我们在运行时也会得到nil值. 而ngx_lua提供的ngx.say函数会自动把Lua 的nil值格式化为字符串"nil"输出, 于是访问/test接口的结果是:

curl 'http://localhost:8080/test'
$blah = nil

这正是我们所期望的.

上面这个例子中另一个值得注意的地方是, 我们在content_by_lua配置指令的参数中提及了 b a r 符号 , 但却并没有触发 " 变量插值 " ( 否则 N g i n x 会在启动时抱怨 bar符号, 但却并没有触发"变量插值"(否则 Nginx会在启动时抱怨 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时得到的是空字符串.

7.3 其他变量类型-数组

最后值得一提的是, 虽然前面反复指出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来做通常会更灵活而且更容易维护.

7.4 结语

至此,本系列教程对Nginx变量的介绍终于可以告一段落了. 我们在这个过程中接触到了许多标准的和第三方的Nginx模块, 这些模块让我们得以很轻松地构造出许多有趣的小例子, 从而可以深入探究Nginx变量的各种行为和特性. 在后续的教程中, 我们还会有很多机会与这些模块打交道.

通过前面讨论过的众多例子, 我们应当已经感受到Nginx变量在Nginx配置语言中所扮演的重要角色: 它是获取Nginx中各种信息(包括当前请求的信息)的主要途径和载体, 同时也是各个模块之间传递数据的主要媒介之一. 在后续的教程中, 我们会经常看到Nginx变量的身影, 所以现在很好地理解它们是非常重要的.

在下一个系列的教程, 即Nginx配置指令的执行顺序系列中, 我们将深入探讨Nginx配置指令的执行顺序以及请求的各个处理阶段, 因为很多Nginx用户都搞不清楚他们书写的众多配置指令之间究竟是按照何种时间顺序执行的, 也搞不懂为什么这些指令实际执行的顺序经常和配置文件里的书写顺序大相径庭.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨烦信息

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值