当前位置: 首页 > 工具软件 > ngx-masonry > 使用案例 >

顺风详解Nginx系列—Ngx中的变量

郑承恩
2023-12-01

 

在计算机语言中,变量是用来存储和表示数据的,但不同的语言表示变量的方式不同,像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”;

}


这里需要简单说一下“$arg_word”这个变量,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: [emerg] 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: [emerg] invalid variable namein /path/conf/nginx.conf:42


同样的行数,同样的错误。同样都是set指令,但只有42行的set指令被提示出错误。

 

把这两个报错的指令拿过来跟没报错的指令对比一下:

     set  $a     “$变量”;

     return 200  “$变量”;


     set  $变量  “我是变量”;


可以看到两个报错的指令都是在使用“$变量”这个变量,而不报错的指令且是在定义这个变量,这其实就是nginx内部用来检验变量名是否合法的策略。只有某个变量在真正被使用的时候nginx才会检查变量名的合法性,比如set指令中的为定义的变量赋值就是一种“使用”,而被定义的变量不能叫“使用”;再比如像return指令这样的行为,它没有发生任何变量定义行为,所以这种也叫“使用”。

 

你以为这样就结束了吗?咱们再看一个例子:

location  /  {

    set  $arg_变量 “我是变量”;

    return  200  “$arg_变量”;

}


这个例子使用了中英文混合字符作为变量,此时我们试图启动nginx的时候发现nginx不但可以正常启动的,而且还可以正常访问:

   curl  http://127.0.0.1/ 

   变量


此时你可能怀疑我们上面刚刚结论又是错误,但是先别急。再仔细看看输出结果我们会发现,这并不是一个我们想要的结果,我们想要的正确结果应该是输出“我是变量”这个四个汉字,但是这个例子且少了两个字。

 

出现这种情况其实是因为涉及到了nginx中的动态变量,动态变量和非变量字符混合到一起后的效果让我们产生了一种变量名可以是中文字符的错觉,我们的结论其实是没有错的。

 

关于动态变量会在后面的小节中详细的讲解,读者可以先保留这个疑问继续向下看,或者暂停一下自己去研究一下出现这种情况的原因。

 

内置变量和自定义变量

几乎所有的编程语言在使用变量前都需要先定义,即使像前面介绍的lua那样“随便”的语言,在变量使用前都需要先定义并初始化以下,比如:

> age = 25;

> print(age);

25


那如果不定义它会发生什么呢?直接打印看看是什么效果:

    >print(name);

   nil


看,它没有报错,而是直接返回了一个字符串“nil”,该字符类似于其它语言中的空值,也就是说lua把未定义的变量设置成了空值。当然了,像java、c等这种编程语言对这种情况也会有自己的处理方式,比如当他们遇到了一个未定义的变量时候在编译阶段就会直接给你“怼”回去,直接告诉你编译不通过。

 

那么在nginx中是如何处理这种情况的呢?我们在nginx.conf中搞一个未定义的变量试试,看看nginx会做什么反应:

location / {

    return  200  “$a”;

}


当启动nginx的时候会发现,nginx又是无法启动,并且会打印一条日志:

    nginx: [emerg] 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访问以下这个资源看看效果:

   curl  http://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 -->$a<--”;

}


在变量前后都加入了可视的字符,然后再用curl测验一下:

    curl http://127.0.0.1/a

结果如下:

    I am --><--


通过结果可以推断出变量“$a”变成了一个空字符,这个现象其实间接的说明了变量“$a”在nginx并不是一个全局变量,因为它没有打印出b这个字符。

 

另外通过后台日志可以看到如下一条相关的日志信息:

[warn] 1733#0: *3 using uninitialized "a" variable,

(这条日志只是节选了跟当前变量相关的信息)


日志说nginx正在使用一个未初始化的变量,该变量的名字是a。从这条日志看nginx中的变量也有初始化这个概念。从变量“$a”的打印结果看nginx会把未初始化的变量设置为空字符。

 

关于空字符,我们这里不妨再弄一个小插曲。变量变成空字符我们之前说过,nginx会把请求入参中不存在的变量也当成空字符对待,比如这样一个配置:

location / {

    return 200“$arg_words”;

}


如果我们请求这个locaiton的时候不带words这个请求入参,那么该locaiton就会打印出空字符。但它跟我们这里提到的变量“$a”有所不同,他不会有相应的日志打出,它只是在nginx内部打了一个标记----not found,这个标记用户是看不到的。所以虽然同是空字符,但它们在nginx内部且有不同的含义,一个是未初始化,一个是未找到(not found)。


通过以上阐述,大多数读者可能对变量的全局可见性有了一个较清晰的认识,但对全局可见的同时又不是“全局变量”这个概念可能还会有点模糊,其实这个又涉及到了变量的隔离性问题,变量隔离性这个概念我单独抽出了一个小节来介绍,等后续看完这个小节后读者应该就会对这个概念有一个更清晰的认识,本小节就不再赘述了。

 

动态内置变量

在之前的小节中有用到“$uri”这个变量来说明内置变量,但是并没有提到内置变量的另外一种形式,即动态内置变量。这里所谓“动态”指的是变量的名字是不确定的,这个不确定性发生在nginx的运行过程中。比如对一个http请求,同一个请求可以有不同的查询参数,而查询参数的不同又可以返回不同的结果,举个例子,有如下一个查询功能:

   /query?name=xxx

   /query?age=yyy


该查询功能有两个入参,一个是name,一个是age,当仅有name的时候返回所有名字是xxx的人;而当仅有age的时候返回所有年龄是yyy的人;当两个参数都存在的时候返回的是名字是xxx且年龄是yyy的人。当请求实际发生的时候,在nginx内部肯定可以解析出所有的查询入参和对应的值的,但是在配置文件中如何得个这个入参的值就比较费劲,有人可能会说可以直接把入参名字做成内置变量名,比如像如下这样

location /query {

    return 200 “$name and $age”;

}


看起来问题迎刃而解,可问题是nginx需要内置多少这种内置变量呢?

 

http中的查询参数是一个自定义行为,每个使用者都可以随意决定自己请求中的查询参数,即便同一个功能,有着同样意义的查询参数,查询参数的实际值也可以不一样。比如上面的例子,完全可以把两个查询参数name和age替换为n和a,按照这种变化程度,nginx根本不可能完全猜测出用户对查询参数的定义,所以这种方案是行不通的。

 

nginx的解决方案是使用前缀的方式来表示http模块中各种动态内置变量,比如上面例子中的两个查询入参name和age,可以分别用arg_name和arg_age来表示其对应的变量,而arg_就是查询参数中某个入参的变量前缀。如此一来nginx只需要在内部内置一个以arg_开头的规则就可以方便的表示这类数据了。

 

目前在nginx的http模块中有六种内置动态变量,分别是“http_”、“sent_http_”、“upstream_http_”、“upstream_cookie”、“cookie_”,“arg_”。其中以“upstream”开头的动态变量需要涉及到额外的知识,为了不分散读者的注意力这里就不再介绍了,本小节主要介绍一下其它四种内置动态变量。

 

以“http_”开头的动态内置变量可以表示http请求过程中的任意请求头,使用的过程中不区分大小写,并且请求头中如果有“-”字符需要用“_”字符替代。  

 

我们先用curl去访问以下nginx的官方文档页,来看看请求过程中都发送了哪些请求头:

   curl http://nginx.org/en/dosc/ -v

去掉其它部分,只保留请求头部分,打印结果如下:

   > GET /en/docs HTTP/1.1

    > User-Agent: curl/7.19.7(x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.14.0.0 zlib/1.2.3 libidn/1.18libssh2/1.4.2

    >Host: nginx.org

    > Accept: */*


可以看到有三个请求头,根据nignx的规则,在配置文件中获取这三个请求头值只需要在对应的请求头名字前加上“http_”前缀就可以了,示例如下:

location / {

    return 200 “User-Agent: $http_user_agent ”;

}


用curl测是结果如下:

curl http://127.0.0.1/

     User-Agent: curl/7.19.7(x86_64-redhat-linux-gnu) libcurl/7.19.7    NSS/3.14.0.0 zlib/1.2.3 libidn/1.18libssh2/1.4.2


同样如果想获取其它两个请求头,使用$http_host和$http_accept就可以了。

 

以“sent_http_”开头的动态内置变量可以表示http响应过程中的任意响应头,规则跟“http_”动态内置变量一样。

 

使用如下的配置来看一下响应过程中包括哪些响应头:

location /a {

   return 200 “test sent_http_”;

}

用带-v参数的curl访问该资源

   curl http://127.0.0.1/a -v

仅保留响应头部分:

   < Server: nginx/1.9.4

    < Date: Sat, 21 Apr 2018 09:04:36 GMT

    <Content-Type: application/octet-stream

    <Content-Length: 15

    < Connection: keep-alive


现在来看一个在配置文件中使用connection这个响应头的一个例子:

location /a {

    return 200 “I am $sent_http_connection”;

}

用curl测试一下,结果如下:

   curl http://127.0.0.1/a

   I amkeep-alive

 

看起来一切都很顺畅,貌似不管哪个响应头,加上对应的前缀就可以轻而易举的获取。那我们再换一个响应头验证一下,看看能不能获取content_length这个头的值:

location/a {

    return 200 “I am $sent_http_content_length”;

}


用curl验证一下:

   curl http://127.0.0.1/a

   I am


结果并没有像我们预想的那样,content_length这个响应头的内容不见了,翻阅nginx文档好像也没有不妥的地方,白纸黑字写上的规则怎么说不行就不行了呢?此时你可能怀疑是nginx的bug,遗憾的是并不是这样的。

 

出现这种现象是因为涉及到了nginx中http模块阶段执行的模式。实际运行的时候nginx把整个请求过程分成了多个阶段,各个阶段对应完成不同的功能,我们这里出现的情况是因为return这个指令对应的阶段运行时,用来设置content_length这个响应头内容的阶段还没有执行,所以出现了该响应头内容“不见了”的情况。


关于http模块的阶段执行会在后续的文章中做详细的介绍,此时读者有这么一个概念就可以。目前读者只需要知道,虽然nginx提供了这种动态获取变量值得功能,但并不是在任何时候都能取到这个值的就行了。

 

以“cookie_”开头的动态内置变量可以表示http请求过程中的某个cookie值。需要和“$http_cookie”这个内置变量(非动态内置变量)区分一下,它代表请求中整个cookie值,比如:

location/a {

    return 200 “cookie:$http_cookie”;

}  


使用curl模拟一下带cookie的请求:

  curl http://127.0.0.1/a  -H “cookie:a=b;b=c”

  cookie:a=b;b=c


可以看到它把cookie名字是a和cookie名字是b的值都打印出来了。

 

而以“cookie_”开头的变量则代表某个实际cookie值,比如“$cookie_a”代表本次请求中cookie名字是a的对应值,一个获取某个cookie值的例子:

location/a {

    return200  “ cookie b 的值是 [$cookie_b]”

}


使用curl模拟带cookie的请求:

    curl http://127.0.0.1/a -H “cookie:a=b;b=哈哈哈”


输出结果如下:

   cookie b 的值是 [哈哈哈]

 

最后一个是以“arg_”开头的动态内置变量,用法跟以“cookie_”开头的变量类似,就不再赘述了。这里需要说的是在“变量的有效字符”小节中用到的“$arg_变量”这个变量,之前的配置例子是这样的:

location  /  {

    set  $arg_变量 “我是变量”;

    return  200  “$arg_变量”;

}


这个配置输出的结果是“变量”而非我们认为的“我是变量”,这是因为“$arg_变量”并不是一个变量,而是变量“$arg_”和文本字符“变量”的一个拼接。在nginx中变量“$arg_”不代表任何入参值,它会被nginx转换成空字符,所以最终结果就是一个文本“变量”。

  

可变变量和不可变变量

大部分编程语言在声明变量时一般会有特定的修饰符来标识变量是否可变,比如java中的“final”修饰符和C中的“const”修饰符。如果某个变量在声明时加上了这些修饰符,那么它在后续是无法再被修改的,这体现了变量的不可变性。

 

nginx中的变量也存在可变和不可变之分,但是它并没有显著的修饰符,所以从表面上你根本看不出来该变量是否可变。不过nginx在启动过程中提供了一个自检查机制,当在配置文件中试图修改一个不可变变量时,nginx是不会顺利启动的。通过这种机制可以间接的判断某个变量是否可变,这种机制我们在前面已经体验过好多次,其实也算是nginx的一种自我保护机制,尽早发现错误尽早制止错误。

 

前面讲自定义变量的时候涉及到了两个指令set和geo,现在我们来看看用set指令定义的变量是否可改变,一个例子如下:

location/a {

    set $a“old a”;

    set $a “new a”;

    return 200 “$a”;

}


用curl测验一下:

   curl http://127.0.0.1/a

   new a

 

再看一个geo指令的例子:

http {

   geo $a {default “old a”}

   geo $a {default “new a”}

   location /a {

     return 200 “$a”;

   }

}


用curl测验一下:

   curl http://127.0.0.1/a

   new a

 

两个例子都可以成功启动并打印数据,因此我们判定这两个指令定义的变量可以被改变。

 

记住上面的这个结论,然后咱们再看一个例子:

location/a {

   set $host “I am host”;

   return 200 “$host”;

}


nginx启动失败,并且打印了一条错误日志:

  nginx: [emerg] the duplicate "host"variable in /path/conf/nginx.conf:49

 

看到这种结果你可能开始怀疑刚刚得出的结论似乎又是错误的。查阅nginx文档会发现“$host”这个变量是http核心模块中的一个内置变量,此时你可能会猜测nginx中的内置变量是不可以改变的。为了验证这个结论我们再找一个内置变量验证一下:

location /a {

   set $args “I am querystring”;

   return 200 “$args”;

}


例子中的“$args”是一个内置变量,表示请求中的查询参数。当我们试图启动nginx的时候发现完全没有问题,而且用curl也可以正确访问,这时候你可能已经懵了,感觉nginx变量的这些行为毫无章法。

 

实际上这个问题的答案仅从做实验和文档上是找不到的,只能从代码上一窥究竟,不过我不打算带着读者读代码,后面会有专门的文章来介绍变量在代码层的实现,这里简单说一下原理:


nginx中每个变量在被定义的时候都会打上一个是否可以被改变的标记,然后把放到一个容器中,当后续有人试图再次定义用一个变量的时候,nginx会首先从这个容器中查找这个变量,如果找到相同的变量则需要判断容器中的变量是否存在可改变的标记,如果有则定义的变量会把容器中的变量覆盖掉,如果没有则返回错误并终止nginx启动。


另一个要注意的是http模块中的内置变量放入该容器中的时机,内置变量要先于“set”或“geo”指令,如果某个内置变量被打上了不可改变的标记,后续其它指令就无法再定义相同名字的变量了。

 

目前nginx的核心http模块中几乎所有内置变量都是不可改变的,只有“$args”和“$limit_rate”这两个内置变量可以被改变。

 

另外由于http模块的动态内置变量并不会把自己放入到容器中,所以它看起来是可以被改变的,比如:

location/a {

   set $arg_a “I am a”;

   return 200 “$arg_a”;

}


用curl验证下:

   curl http://127.0.0.1/a?a=b

   I am a


可以看到这个包含了一个内置变量的例子可以正常启动,并且输出了数据,

所以关于大部分内置变量不可改变这个结论,似乎需要再加上一条:除动态内置变量外。

 

实际上是因为动态变量被重新定义后它就不再是动态变量了,它之所以不再是动态变量,那是因为动态变量的“定义”发生在所有内置变量和自定义变量之后。在nginx中,一旦某个变量被认定为自定义或内置变量,后续就不会再被赋予动态变量的特性。

 

比如例子中的“$arg_a”,其实已经变成了一个自定义变量,相应的动态变量特征也就不存在了,但其它以“$arg_”开头的变量仍然是动态变量。  

 

可缓存变量和不可缓存变量

nginx中所有的变量在定义的时候都会被关联上一个get_handler()方法,所有变量在第一次获取值的时候,都是通过这个handler方法获取的,后续再次获取变量值的时候,是否仍然调用该handler方法则取决于该变量是否可以被缓存。

 

不可缓存的变量在获取值的时候都是实时计算的,比如“$arg_”开头的动态变量,每次获取值的时候都会从查询参数中重新解析对应的值;而可以缓存的变量并不会每次都调用这个handler方法,在它的整个生命周期中,如果这个变量没有被刷新过,那么自始至终只会调用一次。

 

nginx中用set指令定义的变量都是可以缓存的,但set指令不会改变已有变量的缓存特性(比如内置变量,但动态变量除外),而所有以“arg_”开头的动态变量都是不可缓存的,这两种变量结合在一起的时候会产生一种有意思的现象,来看一个简单的例子:

location/a {

   set $a “$arg_name”;

   return 200 “$a = $arg_name”;

}


用curl测试一下:

   curl http://127.0.0.1/a?name=zhangsan

   zhangsan =zhangsan


这个结果看起来并没有超出我们的预期,跟变量是不是可以缓存好像也没啥关系。

 

下面我们把这个例子稍微改造一下,改成如下形式:

location /a {

   set  $a “$arg_name”;

   set  $args “name=lisi”;

   return 200 “$a = $arg_name”;

}


再次用curl测一下:

   curl http://127.0.0.1/a?name=zhangsan

   zhangsan = lisi


这时候我们可以看到,“$a”和“$arg_name”这两个变量虽然都是在表示入参name的值,但是且输出了不同的结果。


这其实就是变量是否可缓存的特性引起的,因为变量“$a”是一个可缓存的变量,当被设置后变量值就被保存下来了;而“$arg_name”是一个不可被缓存的变量,每次获取该值的时候都会调用其对应的handler方法。


我们看到第一次调用的时候查询参数值是“name=zhangsan”,这个值被赋值给了变量“$a”,在第二次获取该变量值之前,我们把查询参数改成了“name=lisi”,当它再次调用对应的handler方法的时候获取到的值就变成了“lisi”。

 

动态内置变量此时仍然是一个特殊的存在,我们之前说过,动态变量被重新定义后它就不再是动态变量了,所以它也就不再保有不可缓存的特性,看个例子就知道了:


location /a {

    set $arg_name “$arg_name”;

    set $a “$arg_name”;

    set $args “name=lisi”;

    return 200 “$a = $arg_name”;

}


用跟上面同样的入参访问以下该location:

   curl http://127.0.0.1/a?name=zhangsan

   zhangsan =zhangsan


可以看到这两个变量的值又一样了。其实原因很简单,用set指令重新定义“$arg_name”后它就不再是动态变量了,它原本的不可缓存特性也就不存在了,所以此时查询参数的更改对他也就不起任何作用。

 

变量的隔离性

nginx中变量的隔离性类似于其它编程语言中变量的作用域,但它又不像其它语言那样有全局和局部变量之分。nginx中的变量隔离是基于请求的,同一个变量在不同的请求中毫无关系,即A请求不会读到(或改变)B请求中的变量值,B也不会读到(改变)A的,比如下面一个例子:

server {

   set $a “$uri”;

   location /a {

     return 200 “I am $a”;

   }

   location /b {

     return 200 “I am $a”;

   }

}


我们在server块定义了一个看似是“全局变量”的“$a”,如果它有全局性,那么访问上面的两个location的时候肯定会得到相同的值,但nginx中不是这样的。


在nginx中两个location都可以看到这个变量“$a”,这体现了nginx变量的全局可见性;但两个location看到的变量值确实是不一样的,这体现了隔离性。用curl验证一下结论是否正确:

   curl http://127.0.0.1/a

   /a

   curl http://127.0.0./b

   /b

可以看到结果跟预期一致。

 

在同一个请求中nginx的变量是有全局性的,但仅限于当前请求中。不管变量的更改发生在配置文件的哪个位置,在同一个请求中都可以被看到,看下面一个例子:

server {

   set $a “server”;

   location / {

         set $a “location”;

         if ($uri) {

            set $a “if”;

         }

         return 200 “$a”;

   }

}

从上面的例子可以看到,变量“$a”被更改了三次。因为“$uri”总会有值,所以if块中的set指令也会执行。这种情况如果在其它语言中一般是输出字符串“location”的,因为每块作用域都会关联一块内存空间来存放本作用域内的变量值。但是nginx在整个请求过程中只会为某个变量保留一份存储空间,所以变量值也会只保留最后一次修改的值,因此上面的例子一定是输出字符串“if”。

 

子请求中的变量

子请求这个概念并不属于http协议,在nginx中它不像http协议中的301、302那样会重新发起一个新的请求,而是一个简单的方法调用,而且nginx在发起子请求的时候不需要再次解析http请求头协议,直接共享父请求的,所以它比浏览器直接发起的请求要节省资源。

 

当nginx在内部发起一个子请求的时候,父请求会把自己的变量共享给子请求,但是这个共享并不是共享变量的值。我们之前说过每个变量都会对应一个handler方法,只有当这个变量允许被缓存的时候,我们才可以认为主子请求共享同一个变量值,否则他们都会在各自的环境中执行相同的handler方法,最终计算的值也会因为环境的不同而不同。

   

根据当前了解到的知识以及nginx中自带的模块,很难把变量在子请求中的特性详尽的描述出来,为了不引入过多新的知识,这里仅引用nginx自带的一个ngx_http_addition模块来阐述这个知识。这个模块默认没有安装,需要读者根据文档自行安装一下。

 

先来看一个子请求共享父请求变量的例子,首先需要在nginx的安装目录下找到一个名字叫html的目录,然后在该目录下创建一个f.html,在我这里该文件的绝对路径如下:

  /path/html/f.html

然后在这个文件中输入一行字,内容如下:

  -->I am f.html<--

然后在nginx.conf配置文件中做如下配置:

location /f.html {

    set $a “father”;

    add_before_body  /sub;

}

location /sub {

    return 200 “ -->I am sub [$a]<-- ”;

}


其中指令add_before_body的作用是发起一个子请求,并且把获取到的子请求的内容放置到父请求内容的最前面。现在我们要关注的是当访问“/f.html”时,变量“$a”的传递性。根据之前对变量规则的介绍我们知道变量“$a”是可以被缓存的,所以它在主请求中的值会被共享到子请求中,所以子请求“/sub”中的变量“$a”会被替换成父请求中的“father”,下面用curl验证一下:

  curl http://127.0.0.1/f.html

可以看到输出结果如下

  -->I am sub [father]<-- -->Iam f.html<--

跟预测结果一致。

 

既然主子请求中的变量可以共享,那就表示在其中一个子请求中改变变量的值时,该值也会反应到当前主请求和当前主请求发起的其它子请求中,但是就目前掌握的知识,我们还无法用nginx自带的模块模拟第一种情况(该值也会反应到当前主请求)。我们把上面的例子稍作改造,来模拟一下第二种情况:

location /f.html {

    set $a “father”;

    add_before_body  /sub;

    add_after_body   /sub2;

}

  

location /sub {

    set $a “sub”;

    return 200 “ -->I am sub [$a]<-- ”;

}

 

location /sub2 {

    return 200 “ -->I am sub2 [$a]<-- ”;

}


这个例子中引入了一个新的指令add_after_body,它的作用是把子请求“/sub2”中获取的内容放到主请求的最后。根据我们已知的规则,当访问主请求“/f.html”的时候,会发生如下的过程:

  1. 主请求中会存在一个变量“$a”值是“father”

  2. 然后主请求对“/sub”发起子请求,在该子请求中变量“$a”的值被改变成了“sub”,由于变量“$a”是主子请求共享的,所以此时主请求看到的值和其它之请求看到的值都是“sub”

  3. 然后继续向下走,当前子请求获取的输出内容为“-->I am sub [sub]<-- ”

  4. 然后继续回到主请求,此时主请求的输出内容是“-->I am f.html<--”

  5. 接着继续往下走,在主请求中又发起了另一个子请求“/sub2”,在该请求中又用到了变量“$a”,我们知道这个变量已经在第一个子请求中被设置成了“sub”,而这个变量又是可共享的,所以此时该子请求获得的内容是“ -->I am sub2 [sub]<--”

  6. 最后生成的内容就是上面3到5步生成的字符串顺序相加


用curl验证一下:

  curl http://127.0.0.1/f.html

  -->I am sub [sub]<--  -->I am f.html<--

  -->I am sub2 [sub]<--

 

对于不可缓存的变量而言,在主子请求中变量是不存在共享的,因为在任何时候,这些变量值都是调用其对应的handler方法实时计算出来的,来看一个例子:

locaiotn /f.html {

    add_before_body  /sub;

    add_after_body   /sub2;

}

  

location /sub {

    return 200 “ -->I am sub [$uri]<-- ”;

}

 

location /sub2 {

    return 200 “ -->I am sub2 [$uri]<--”;

}


用curl访问以下主请求“/f.html”

  curl http://127.0.0.1/f.html

  -->I am sub [/sub]<--  -->I am f.html<--

  -->I am sub2 [/sub2]<--


因为内置变量“$uri”是不可缓存变量,所以每次获取变量值时都会调用它对应的handler方法来重新计算,这样就得到了不同的值。也正是因为它是不可缓存的才获取到了我们期望的值。

 

nginx中还有另外一种变量,不管存在于哪个请求中,它始终只表示父请求中的值。比如核心http模块中的“$request_method”变量,不过目前在nginx自带的标准模块中好像也就这么一个“奇怪”的存在。感兴趣的同学可以找几个例子去验证一下,本小节就不再赘述了。

 

其它

nginx中变量类型比较单调,不像其它真正编程语言那样有各种类型。nginx中的变量不管是内置变量还是自定义变量,几乎都是字符型的。这里既然用了“几乎”俩字,那说明一定有例外,来个例子看一下:

location /a {

    return200 “-->${binary_remote_addr}<--”;

}

 

location /b {

    return200 “--><--”;

}


分别访问两个location,看看是什么结果

   curl http://127.0.0.1/a

  --><--

   curl http://127.0.0.1/b

  --><--


从表面上看,两个请求输出了同样的结果。似乎可以推断出这个变量的作用是输出空字符,但是想想又觉得不可能。nginx怎么可能这个大费周章的用一个这么长的变量来表示一个空字符。既然不可能是空字符,那应该是什么呢?咱们换一种访问方式,这次使用curl访问的时候带上-v,然后我们只看响应头:

   curl http://127.0.0.1/a -v

   < HTTP/1.1 200 OK

    < Server: nginx/1.9.4

    < Date: Mon, 23 Apr 2018 13:48:34 GMT

    < Content-Type: application/octet-stream

    < Content-Length: 12

   < Connection: keep-alive    

 

   curl http://127.0.0.1/b -v  

   < HTTP/1.1 200 OK

   <Server: nginx/1.9.4

   <Date: Mon, 23 Apr 2018 13:50:57 GMT

   <Content-Type: application/octet-stream

   <Content-Length: 8

   < Connection: keep-alive


从结果上可以看到,这两个请求的响应头中除了日期就只有“Content-Length”值是不一样的。很明显变量“${binary_remote_addr}”的内容长度是4个字节,但是从输出结果上看不出这4个字节是什么。nginx的官方文档对这个变量的解释是,这是一个二进制的IP地址,如果是IPv4则长度是4字节,如果是IPv6则长度是6字节。因此我们知道了变量“${binary_remote_addr}”并不是一个字符型的,而是一个4字节的ip地址,并且是一个IPv4形式的二进制数据。它之所以显示成了空字符,是因为我的终端无法把这个二进制数据解释成可视的字符。

 

在整篇文章举例说明问题的时候,关于变量的使用,我都是用双引号括起来的,这并不表示必须使用双引号,单引号或不用引号都是可以的,只有在不加引号就无法表示某个字符串是一个整体的时候加引号才是必须的,比如字符串

I am a uri

在不加引号的情况下,nginx根本无法判断它是一个整体,比如这样

   return  200  Iam a uri;

完全是一个不正确的使用方式,nginx是无法启动成功的。

 

实际上如果你愿意,nginx配置文件中几乎任何字符串都可以用双引号括起来,比如下面的例子:

 “location”  “/a”  {

   “return”  “200”  “Iam a”;

 }


虽然这种形式看起来乖乖的,但在nginx中它仍然是一个合法且正确的配置形式。

 

总结

以上内容从宏观上介绍了nginx中变量的一些特性,啰里啰嗦说了一大堆,其实主要说了以下内容:

  1. nginx中使用“$”或“${}”符号来表示一个变量

  2. nginx中的变量支持变量插入,比如“I am a $uri”

  3. 可以表示变量的有效字符只有四种:“a-z”、“A-Z”、“0-9”、“_”

  4. nginx中变量可分为内置变量(比如$uri)和自定义变量(比如用set定义的变量)

  5. nginx 中所有的变量都是全局可见的,但它又不是全局变量

  6. nginx中有六种动态内置变量,分别是“http_”、“sent_http_”、“upstream_http_”、“upstream_cookie”、“cookie_”,“arg_”。(在nginx的1.13.2版本中又多一个“$sent_trailer_”)

  7. nginx中几乎所有的内置变量都是不可变的,除了“args”和“$limt_rate”

  8. nginx中所有的变量都会关联一个get_handler()方法,不可缓存的变量每次获取值时都会调用这个方法,可缓存的变量只会调用一次

  9. nginx中的变量在各个请求之前是相互隔离的(主子请求除外)

  10. 变量在主子请求之间是共享的,但最终值是否相同则取决于该变量是否可缓存

  11. nginx中的变量值都是字符型的(除了“${binary_remote_addr}”变量)

 

接下来我会写两篇从代码级别分析nginx中变量实现的文章,感兴趣的同学可以关注下面这个目录:

http://deyimsf.iteye.com/admin/blogs/2419833

我会持续对它更新,同时也欢迎其它同学提供好的写作案例和素材。

 

 


 类似资料: