概述

NGINX本身提供了简单的脚本解析功能来提高配置使用的灵活性。与常用的语言一样,可以通过在NGINX配置文件中使用变量和指令来完成对NGINX的编程,来实现特定的功能。

简单变量使用例子:

location /test {

     if ($args_name = “test”) {

         return 200 “welcome”;

    }

}

上面的配置完成的功能是,如果客户访问的服务器的/test URI并且携带的name参数值是test, 则给client返回状态码200并且输出“welcome”。

上述简单的例子中,if对应的是指令,args_name对应的是变量。变量和指令也是任何编程语言必备的要素。通过使用脚本语言和变量,可以大大提高NGINX的灵活性,避免了通过大量添加新代码来支持新功能。

NGINX提供的变量机制,使得NGINX在处理每一个具体的HTTP请求时,可以在不同处理阶段直接传递数据。比如在POST_READ阶段在读取了client请求以后,我们就可以获取本次请求的相关host等信息。通过变量机制,后续的模块就可以通过变量$host来获取本次请求的host数据信息。

下面我们通过一系列的文章,来分析NGINX脚本语言是如何实现的。本篇文章,我们先介绍NGINX变量的基本特性。下一篇,我们继续分析变量的实现原理。最后我们将分析NGINX指令的实现原理以及变量是如何被使用的。

变量的定义

任何语言的变量都是用来存储和表示数据的。在定义变量时,我们首先需要给变量命名。比如在C语言中,我们通过int abc = 10;来定义一个整数类型的变量。其中int表示变量的类型,abc表示变量的名字。

关于定义变量的类型,在NGINX中所有的变量除了内置变量“${binary_remote_addr}”以外,剩余的变量都是字符串类型。

每种语言对于可以表示变量的字符要求不尽相同。在NGINX脚本语言中,变量的名字是使用“$”或“${}”符号来表示一个变量。可以用来定义变量的有效字符只有四种:“a-z”、“A-Z”、“0-9”、“_”。 比如 set $abc ‘abc’或者set ${abc} ‘abc’。 符号{}的意义在于,如果一个变量名需要和它相邻的字符进行区分时,需要显式地添加{}来实现。比如 变量${a}s 与$as和${as}两个变量表示的意义是不一样的,前者表示在变量a的值后面再添加一个s字符。 $as和${as}都表示变量as本身。

NGINX的变量支持变量插入,比如我们通过set $def “this is a test $abc”定义变量$def。在进行赋值时,需要首先计算出变量abc的值然后再和另外的字符串连接,最后把连接后的字符串赋值给变量def。如果变量abc的值是“yeah!”,那么变量def的值就是 “this is a test yeah!”。

变量的分类

NGINX变量可以分为内置变量和自定义变量两种。自定义变量是通过NGINX不同模块的进行显示定义。比如通过rewrite模块中的set指令可以如下定义: set $test “abc”;这个指令完成定义一个名为test的变量,并且在变量第一次被使用时把字符串abc赋值给test变量。还有比如geo模块也可以如下根据客户端的IP来定义一个新的变量。

geo $a  {

    default    “我是geo默认值”;

    127.0.0.1  “客户端ip是127.0.0.1”;

}

除了自定义变量以外,NGINX还支持大量的内置变量。这些内置变量根据系统的层次模型,可以分为系统相关的变量比如$pid、网络三层相关的变量,比如$remote_addr、四层相关的变量,比如$remote_port、表示层(SSL,TLS)相关的变量,比如$http_ssl_vars以及七层HTTP,比如$url相关的变量。

内置变量又可以分为静态内置变量和动态内置变量。静态内置变量是在不同的模块中通过代码预先定义的。比如在NGX_HTTP_CORE_MODULAR里面就定义了大量的系统变量如$HOST,  $URI等。

所谓“动态”指的是变量的名字是不确定的,这个不确定性发生在NGINX的运行过程中。比如对于一个HTTP请求,同一个请求可以有不同的查询参数,而查询参数的不同又可以返回不同的结果,比如这个查询功能:

/query?name=jikui

/query?occurpation=coder

该查询功能有两个输入参数,一个是name,一个是occupation。当请求发生的时候,在NGINX内部肯定可以解析出所有的查询入参和对应的值,但是在配置文件中如何得到和使用呢? NGINX通过使用前缀的方式来表示HTTP模块中各种动态内置变量。分别使用用arg_name和arg_occurpation来表示其对应的变量。而arg_就是查询参数中某个入参的变量前缀。如此一来NGINX只需要在内部内置一个以arg_开头的规则就可以方便的表示这类参数相关数据了。

目前在NGINX的http模块中有如下几种内置动态变量,分别是“http_”、“sent_http_”、“sent_trailer_”、“upstream_http_”、 “upstream_cookie_”、“cookie_”,“arg_”, “upstream_trailer_”。

以“http_”开头的动态内置变量可以表示http请求过程中的任意请求头,使用的过程中不区分大小写,并且请求头中如果有“-”字符需要用“_”字符替代。另外别的种类的动态内置变量也有相应的对应HTTP处理阶段。

变量的作用域

NGINX在启动时,会对变量进行静态检查,如果有指令使用未经定义的变量,NGINX启动会出错。并且打印如下的出错信息:“NGINX: [emerg] unknown "**" variable”。

NGINX变量在配置文件中是全局可见的,基于此,在如下的配置中,虽然我们是在location /test2中定义的变量name,在locaiton /test1中也进行了使用,但是这样在NGINX启动时是也不会出错。

        location /test1 {

               return 200 “I am $name”;

        }

        location /test2 {

             set $name“jikui”;

             return 200 “I am $name”;

         }

在实际的访问中,如果访问location test1返回的结果是 “I am”而访问location test2返回的结果是 “I am jikui”。这是因为,虽然静态的变量定义是全局可见的,但是对应每一个请求,都会有自己的一份变量的定义和数值。虽然对于不同的请求,都有各自不同的变量定义,但是在父子请求模型中,所有的子请求自动继承父请求所有的变量值。     

变量的可变性

根据变量是否可以被再次赋值,NGINX中的变量分为可变变量和不可变变量。在C语言中有特定的修饰符const用来描述这个变量的可变性。但是在NGINX中,并没有显著的修饰符来区分变量的可变性。只能从变量的实现代码实现中来判断某个变量是否可变。

具体来说,在定义NGINX的变量时都会打上一个是否可以被改变的标记,然后把这个变量放到一个容器中。当后续试图再次定义同一个变量时,NGINX会首先从这个容器中查找这个变量,如果找到相同的变量,再判断这个变量的可改变的标记。如果变量的可变标志是True,则会把容器中的变量覆盖掉,反之则返回错误并终止NGINX启动。具体的实现我们下篇变量实现中继续分析。

NGINX的内置变量要先于“set”或“geo”指令存放到系统中,如果某个内置变量,比如$host,被打上了不可改变的标记,后续其它指令就无法再定义相同名字的变量了。如果试图再次定义$host变量,则会出现如下错误:

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

目前NGINX的核心http模块中几乎所有静态内置变量都是不可改变的。只有“$args”和“$limit_rate”这两个内置变量可以被改变。另外由于http模块的动态内置变量并不会把自己放入到容器中,所以它是可以被改变。

比如:

location /name {

    set $arg_name “jikui”;

    return 200 “$arg_name”;

}

在访问location /name时,输出的是通过set指令设置的数值“jikui”而不是url参数中的“test”。

curl http://127.0.0.1/name?name=test

jikui

这是因为在NGINX中,一自定义或内置变量不会被赋予动态变量的特性。比如例子中的“$arg_name”,通过set指令赋值后,其实已经变成了一个自定义变量,相应的动态变量特征也就不存在了。但在这个请求中,其它以“$arg_”开头的变量仍然是动态变量。  

变量的可缓存性

变量的可缓存性是指,在获取变量值时,是否需要每次都实时计算变量的值。对于不可缓存变量,获取数值时都是实时计算的。对于可缓存的变量,不需要每次都实时计算。

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

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

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

比如:

location /url {

    set   $name   “$arg_name”;

    set   $args   “name=jikui”;

    return 200   “$name = $arg_name”;

}

访问url输出结果是:

curl http://127.0.0.1/url?name=test

test = jikui

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

这其实就是变量是否可缓存的特性引起的,因为变量“$name”是一个可缓存的变量,当被设置后变量值就被保存下来了;而“$arg_name”是一个不可被缓存的变量,每次获取该值的时候都会调用其对应的handler方法。我们看到第一次调用的时候查询参数值是“name=test”,这个值被赋值给了变量“$name”,在第二次获取该变量值之前,我们把查询参数改成了“name=jikui”,当它再次调用对应的handler方法的时候获取到的值就变成了“jikui”。

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

location /a {

    set     $arg_name     “$arg_name”;

    set     $name     “$arg_name”;

    set     $args     “name=jikui”;

    return   200    “$name = $arg_name”;

}

访问location:

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

test = test

可以看到这两个变量的值又一样了。原因是,在用set指令重新定义“$arg_name”后,它就不再是动态变量了,它原本的不可缓存特性也就不存在了,所以此时查询参数的更改对他也就不起任何作用。具体原因我们可以通过下篇代码分析来看实际的实现原理。

变量的隔离性

NGINX中变量的隔离性类似于其它编程语言中变量的作用域。比如C语言中的局部变量和全局变量。而NGINX中的变量的作用域是基于请求的。同一个变量在不同的请求中毫无关系(子请求例外,子请求继承了父请求所有的变量),即A请求不会读到(或改变)B请求中的变量值,B也不会读到(改变)A的,比如下面一个例子:

server {

    set $name “$uri”;

    location /test1 {

        return 200 “I am $name”;

    }

    location /test2 {

        return 200 “I am $name”;

    }

}

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

在NGINX中两个location都可以看到这个变量“$name”,这体现了NGINX变量的全局可见性;但两个location看到的变量值确实是不一样的,这体现了隔离性。上述配置的运行结果是:

curl http://127.0.0.1/test1

I am test1

curl http://127.0.0./test2

I am test2

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

server {

    set $name “server”;

    location / {

        set $name “location”;

        if ($uri) {

           set $name “if”;

        }

        return 200 “$name”;

    }

}

从上面的例子可以看到,变量“$name”被更改了三次。上面的例子一定是输出字符串“if”。从上面这个例子我们看到,NGINX 变量值容器的生命期是与当前正在处理的请求绑定的,而与 location 无关。

结语

本篇简单介绍了NGINX变量的一些特性。下篇我们将继续分析变量是如何实现的。然后我们还将通过分析NGINX的脚本语言的实现,来分析NGINX的变量是如何在系统中被使用的。