顺风详解Nginx系列—Ngx中的变量
在计算机语言中,变量是用来存储和表示数据的,但不同的语言表示变量的方式不同,像java语言会把变量抽象成各种类型,并且每种类型都会用一个特殊的符号表示,比如表示一个整数需要这样:int age= 25;
用int去声明age是一个变量,并且是一个表示整数的变量。
另外一种语言比如lua,在使用的时候并不需要预先声明其类型,他可以在程序运行的时候确定变量的类型,甚至在变量前面都不需要任何关键字直接拿来就用,比如:
age = 25;
name = “张三”;
在没有任何征兆的情况下就定义了两个变量,而且该语言会动态的识别变量的数据类型。
可以看到,虽然都是变量,但不同的语言表示变量的方式且是不一样的。既然nginx中也有变量的概念,自然也会有自己的一套变量的规则。比如nginx中可以使用set指令定义一个变量:
set $a “hello”;
可以在return指令中使用这个变量:
return 200 “$a world”;
那nginx中的变量跟其他编程语言有什么不同?以及nginx中的变量又有那些规则?使用的时候应该注意些什么?接下来我会用一些例子来做详细说明。
变量表示和变量插入
nginx中变量的表示方法和真正语言的不同,它不像java语言那样需要用一个修饰符,也不想lua语言那么随意。nginx使用“$”符号作为前缀来表示一个变量,并且它还有一个其它语言没有的特性:变量可以直接插入到一个字符串中,插入后并不会改变变量的特性,并且对插入变量的个数没有限制。比如这个例子:
location / {
set $a“hello”;
set $b“world”;
return 200 “$a $b”;
}
在上面这个例子中,return这个指令可以识别出它后面字符串中的变量值,因此它的输出结果会是这样
curl http://127.0.0.1/
helloworld
除了直接在变量名字前加“$”符号表示一个变量外,nginx中还有另外一种形式来表示变量:在“$”符号的基础上加上一对花括号,并把变量名放在花括号中,比如
set ${a} “hello”
set ${b} “world”
现在可能你会有一个疑问:用“$”表示变量已经很简洁了,为什么又要多出一对花括号?这样岂不是更啰嗦了?而且其它语言中好像也没什么先例。
其实nginx引入花括号来表示变量正是为了满足其它语言中没有的一种变量特性-----变量插入,而设计的。
假设现在有这样一个无聊的需求:当用户输入一个英语单词后,我们会给出这个单词的复数形式。为了使例子简单这里只考虑后缀是‘s’的复数单词。下面的例子是一种实现方式:
location /{
set $suffix “s”;
set $word “$arg_word”;
return 200 “The plural of theword $word is $word$suffix”;
}
这里需要简单说一下“$argword”这个变量,nginx以“arg”开头的变量表示的是http请求中查询参数中的入参,比如有一个如下的请求:
http://127.0.0.1/get?name=1&age=2
那么在nginx就可以使用“$arg_name”获取这个请求中入参name的值1,而用“$arg_age”获取请求中入参age的值2。
现在我们用curl来测一下上面的例子:
curl http://127.0.0.1/?word=dog
打印结果如下:
The plural of the word dog is dogs
可以看到结果符合我们的预期。
回过头来再仔细看一下需求我们发现需求中只有一个未知变量----一个英语单词,而我们为了实现这个功能在nginx中用到了两个变量,其中变量“$suffix”是一个固定值,也就是说这个变量并不是必须的,我们完全可以直接使用“s”这个字符。
在我刚接触nginx的时候,我曾经的想法是直接在变量后边加上字符“s”, 就像这样:
location /{
return 200 “$arg_word plural is$arg_words”;
}
我寄希望于nginx可以自动分辨出$arg_word是个入参变量,因为在查询参数中确实存在word这个入参,这样在加上紧跟其后的字符‘s’这个功能就算完成了。现在想想,还真是错的一塌糊涂。
我们用curl测试一下这个错误的例子,看看他会发生什么:
curl http://127.0.0.1/?word=book
响应结果如下:
bookplural is
很明显,nginx并没有识别出变量“$arg_words”是“$arg_word”变量和“s”字符的组合,而是把他们当成了一个整体“words”,而请求中又没有这个入参,因此nginx就用空字符代替了这个变量。
实际上在nginx内部对于这种查询入参中没有的变量值都会打一个特殊的标记:not_found,表示在查询参数中没有找到对应的入参,因此对应的变量值也就没有。
简单验证一下是不是真的如我们所说的,这次我们使用两个入参值来验证一下效果:
curl “http://127.0.0.1/?word=book&words=books”
这次因为有两个入参,所以我们需要用引号把curl后面的url引起来,然后来看一下结果:
book plural is books
好了,错误的例子示范完了现在看看正确的方式:使用变量的另一种表示形式-----花括号,它就是nginx专门用来处理变量和字符拼接而设计的。
location /{
return 200 “$arg_word plural is${arg_word}s”;
}
验证一下:
curl http://127.0.0.1/?word=book
book plural is books
这样看起来是不是简洁了很多?
我们上面一直在说nginx是支持变量插入的,我们举的例子也确实如此,但就此得出nginx支持变量插入的结论其实是不严谨的。因为nginx是一个高度模块化的程序软件,是不是支持这种变量插入的形式其实完全区取决于每个模块具体实现,我们上面提到的set和return两个指令都属于同一个nginx模块----ngx_http_rewrite,该模块确实又赋予了这两个指令支持变量插入的功能,所以我们就看到了上面的效果。其它模块是不是支持这种特性其实是不确定的,等后续把nginx中变量是如何实现的阐述完毕后读者就会有一个更清晰的认识,这里就不再展开了。
表示变量的有效字符
在大部分语言中并不是所有的字符都可以用来表示变量名,一般会有一个范围限制。nginx对表示变量名的字符也是有规定的,nginx中仅允许四种类型的字符或他们的组合做为变量名,分别是大写字母(A-Z)、小写字母(a-z)、数字(0-9)、下划线(_),其它都是非法的。
我们用一个无聊的例子来验证一下:
location / {
set $0101 “我是0101”;
set$_0_1_4“我是_0_1_4”;
set $A_a_0 “我是A_a_0”;
return200“$0101$_0_1_4 $A_a_0”;
}
用curl访问一下这个资源看看:
curl http://127.0.0.1/
我是0101 我是_0_1_4我是A_a_0
可以看到这些变量名看上去奇奇怪怪,但它们确实做到了正确的输出。
那如果在配置文件中出现了不是上面提到的四种字符nginx是如何应对的呢?不妨用一个例子看验证一下:
location /{
set$变量“我是变量”;
}
当我们试图启动nginx的时候发现是可以正常启动的,此时你可能开始怀疑之前说的变量的四种字符限定类型是错误的,因为nginx似乎并没有认为这是一个非法的变量名,但事实真的是这样吗?
现在我们对这个例子稍微改动一下,为它加上一个return指令再看看是什么效果,这次我们在这个例子的左边标上行号:
40: location/ {
41: set$变量“我是变量”;
42: return 200 “$变量”;
43: }
此时当我们再次试图启动nginx的时候你会发现nginx根本无法启动,并且会打印一条日志:
nginx: invalid variable name in /path/conf/nginx.conf:42
意思是说在nginx.conf配置文件中有一个无效的变量名,根据行号可以看到正是我们刚加上的return指令的位置。
从表面看我们似乎可以得出这样一个结论:set指令在nginx的启动阶段不会校验变量的有效性,只有return指令才会校验其有效性。遗憾的是这样的结论仍然是错误的,我们用一个例子来反驳一下这个错误结论:
40: location / {
41: set$变量“我是变量”;
42: set $a “$变量”;
43: }
在这里例子中我们去掉了return指令,用另一个set指令取而代之。此时我们再次试图启动ngnx的时候发现nginx仍然无法启动成功,并且跟用return指令时一样,后台打印了一条同样的日志:
nginx: invalid variable namein /path/conf/nginx.conf:42
同样的行数,同样的错误。同样都是set指令,但只有42行的set指令被提示出错误。
把这两个报错的指令拿过来跟没报错的指令对比一下:
set$a “$变量”;
return 200“$变量”;
set$变量“我是变量”;
可以看到两个报错的指令都是在使用“$变量”这个变量,而不报错的指令且是在定义这个变量,这其实就是nginx内部用来检验变量名是否合法的策略。只有某个变量在真正被使用的时候nginx才会检查变量名的合法性,比如set指令中的为定义的变量赋值就是一种“使用”,而被定义的变量不能叫“使用”;再比如像return指令这样的行为,它没有发生任何变量定义行为,所以这种也叫“使用”。
你以为这样就结束了吗?咱们再看一个例子:
location/{
set$arg_变量 “我是变量”;
return200“$arg_变量”;
}
这个例子使用了中英文混合字符作为变量,此时我们试图启动nginx的时候发现nginx不但可以正常启动的,而且还可以正常访问:
curlhttp://127.0.0.1/
变量
此时你可能怀疑我们上面刚刚结论又是错误,但是先别急。再仔细看看输出结果我们会发现,这并不是一个我们想要的结果,我们想要的正确结果应该是输出“我是变量”这个四个汉字,但是这个例子且少了两个字。
出现这种情况其实是因为涉及到了nginx中的动态变量,动态变量和非变量字符混合到一起后的效果让我们产生了一种变量名可以是中文字符的错觉,我们的结论其实是没有错的。
关于动态变量会在后面的小节中详细的讲解,读者可以先保留这个疑问继续向下看,或者暂停一下自己去研究一下出现这种情况的原因。
内置变量和自定义变量
几乎所有的编程语言在使用变量前都需要先定义,即使像前面介绍的lua那样“随便”的语言,在变量使用前都需要先定义并初始化以下,比如:
age = 25;
print(age);
25
那如果不定义它会发生什么呢?直接打印看看是什么效果:
>print(name);
nil
看,它没有报错,而是直接返回了一个字符串“nil”,该字符类似于其它语言中的空值,也就是说lua把未定义的变量设置成了空值。当然了,像java、c等这种编程语言对这种情况也会有自己的处理方式,比如当他们遇到了一个未定义的变量时候在编译阶段就会直接给你“怼”回去,直接告诉你编译不通过。
那么在nginx中是如何处理这种情况的呢?我们在nginx.conf中搞一个未定义的变量试试,看看nginx会做什么反应:
location / {
return200“$a”;
}
当启动nginx的时候会发现,nginx又是无法启动,并且会打印一条日志:
nginx: unknown "a"variable
意思是说我nginx不认识变量a。仔细分析一下这句话会发现这里有一个隐含信息,那就是起码nginx承认这是一个变量,只不过它不认识这个变量。这个提示跟上面我们使用“$变量”这个中文字符定义变量时提示的信息是不一样的,之前直接提示这是一个无效的变量,相同的地方是这两种使用变量的方式都会导致nginx无法正常启动。
因此我们得出结论nginx中的变量在使用之前也是需要预先定义的。在有些语言中当你使用了未定义的变量后可能是编译无法通过,而在nginx则会导致nginx无法正常启动。
在nginx中变量的定义又分了两种:一种是自定义变量,就是上面用set指令设置的变量,它会在配置文件中明确指出这是一个被定义的变量。另外是内置变量,它在nginx启动之前就已经被设置好了,不需要在配置文件中明确定义。
但是要注意,并不是说自定义变量就一定要使用set指令,nginx中可以自定义变量的模块有很多,之所以一直在用set指令讲解变量,是因为我希望读者把更多的注意里放到变量本身上来,尽量避免为了说明一个问题而又引入其它额外的问题,比如我们下面要用到的geo模块。
ngx_geo模块是nginx的自带的一个标准模块,该模块只包含一个指令geo,作用是根据客户端ip来定义一个变量,比如下面的例子:
http {
geo$a{
default “我是geo默认值”;
127.0.0.1“客户端ip是127.0.0.1”;
}
location / {
return 200 “$a”;
}
}
我们用curl访问以下这个资源看看效果:
curlhttp://127.0.0.1/
客户端ip是127.0.0.1
可以看到变量$a的值变成了geo指令中设定的值。
同样是定义变量,geo指令跟set指令且有很大的不同,比如指令的放置位置,set指令可以放在location块中,而geo指令则只能放在http块中。
另外一个显著的不同是set指令定义的变量值是一个字符串形式,而geo定义的变量值则需要使用花括号括起来,并且该指令内部还隐含的做了逻辑判断。比如如果客户端ip地址是127.0.0.1则该变量值是“客户端ip是127.0.0.1”,如果不是则就是默认值“我是geo默认值”。
默认情况下geo指令会自己获取客户端的ip,然后根据相应的配置去映射变量,但其实它也可以接收一个指定ip,比如下面的例子:
geo$arg_name $a{
default “我是geo默认值”;
127.0.0.1 “我是张三”;
192.168.1.1 “我是李四”;
}
location / {
return 200 “$a”;
}
验证一下看看效果:
curl http://127.0.0.1/?name=127.0.0.1
我是张三
curl http://127.0.0.1/?name=192.168.1.1
我是李四
把入参name去掉再看看效果:
curl http://127.0.0.1/
我是geo默认值
这里既然用到ngx_geo模块,那我们就回过头来在看看之前提到的变量插入的问题,之前说过并不是所有的模块都支持变量插入的,ngx_geo就是这样一个模块。在geo指令中的花括号中是没有变量这一说的,在geo的花括号中放入的变量只会原样展示,比如下面的例子
geo$a{
default “我是geo默认值 $arg_name”;
127.0.0.1 “我是张三 $arg_name”;
}
location / {
return 200 “$a”;
}
当你试图用一个带着name参数的请求访问这个locaiton的时候,它会把花括号中对应的值原样输出:
curl http://127.0.0.1
我是张三$arg_name
除了自定义变量,nginx中的另一种变量就是内置变量了,内置变量在nginx启动之前就已经被设置好了,不需要在配置文件中明确定义。
来看一个内置变量的例子:
location /{
return200 “$uri”
}
按照我们目前的知识,基于上面的配置nginx应该无法启动才对,因为在配置文件中我们没有对变量“$uri”做定义,但事实上它不但可以启动成功,而且还可以很好的工作,用curl检测一下:
curl http://127.0.0.1/abc
打印结果如下:
/abc
这其实就是因为变量“$uri”是一个内置变量,他在nginx内部已经提前定义好了。
另外内置变量也是分模块的,每个模块都可以有自己的内置变量,比如$uri这个内置变量就属于ngx_http_core这个http核心模块中的变量,关于这个模块的其它内置变量读者可以关注nginx的官方文档:
http://nginx.org/en/docs/http/ngx_http_core_module.html#variables
变量的可见性
nginx中变量的另一个比较奇特的地方是每一个变量都是全局可见的,但它又不是全局变量。所谓全局可见,是指不管变量定义在配置文件的哪个地方,它在整个配置文件中都是可见的,但这个并不表示他是全局变量。
上面这句话的描述可能还是比较抽象,举个例子:
location/a {
return200 “I am $a”;
}
location/b {
set $a “b”;
return 200 “I am $a”;
}
在这个例子中第一个location中的变量“$a”既不是自定义变量也不是内置变量,按照目前了解到的知识,nginx应该是无法启动的。
而第二个location中可以看到用set指令定义了一个变量“$a”,从语法上看这是一个合法的配置,所以它是可以正常启动的。那如果把这两个location放在同一个配置文件中,nginx是不是可以正常启动呢?
答案是肯定的,原因就是nginx中的变量是全局可见的,第一个location中的变量“$a”看到了第二个location中对它的定义。那它又不是全局变量又是怎么回事呢?我们用curl访问以下第二个location:
curl http://127.0.0.1/b
打印结果是:
I am b
这个结果应该是毫无疑问的。
现在不确定的应该是访问第一个location的时候应该出现什么结果,如果变量“$a”是一个全局变量,那很显然它的值应该也是“b”。但它不是全局变量,那应该是什么值呢?用curl测试一下:
curl http://127.0.0.1/a
打印结果是:
I am
从表面上看此时变量“$a”应该是空字符或者空格之类非可见性字符,但是因为在当前的例子中,变量“$a”的前后不存在可见的字符,导致没办法区分此时变量“$a”到底是个什么内容。
现在我们把第一个locaiton例子稍微改动一下:
location /a {
return 200 “I am -->$aI am f.htmlI am sub [$a]I am sub Iam f.htmlI am sub [$a]I am sub2 [$a]I am sub I am f.htmlI am sub2 I am sub I am f.htmlI am sub2 I am sub [$uri]I am sub2 [$uri]I am sub I am f.htmlI am sub2 ${binary_remote_addr}
页:
[1]