双面if
在介绍nginx变量时我们说过,nginx具有语言的特性,并为此举了大量的例子,以及讲解了一些它的实现方式。而今天,我们将要介绍nginx的另外一个语言特性:if判断语句。以及if的另外一个非语言特性:location,是的,你没有看错,就是location,只不过它“隐藏”的很深,只有通过代码才能看到它作为location的“影子”。
好,我们开始吧。
1作为判断语句的if
任何接触过计算机语言的同学,对于if判断语句都不会陌生,几乎所有的计算机语言都会有if语句,只不过语法上或多或少有些区别,比如下面这种带分支的if形式:
if (condition) {
// something
}else{
// something
}
这种形式在java和c中都是合法的,细微的区别体现在“condition”上,在c中condition只要是非0都代表true,并且对condition形式没有过多的限制。而在java中condition必须是一个判断表达式或明确的ture/false,比如这种:
if (“hello”) {
// something
}else{
// something
}
这种形式在c中是合法的,但在java中且不是合法的。这只是在condition中的差别,还有在形式上的差别。比如上面例子中的形式在lua中就是非法的,lua中if语句的形式应该是这样的:
if condition then
// something
else
// something
end
我们可以看到,不同的计算机语言,在if的表现形式和condition处理上是不尽相同的。而作为一个非完全计算机语言的nginx来说,它对if的实现自然也不尽相同。
nginx的if在表现形式上比较单一,没有分支,只支持简单的非分支形式,具体形式如下:
if (condition) {
// something
}
看样子是不是跟java和c的语法形式是一样的?嗯,确实看起来没有什么区别,不过,一个细微的差别是,nginx中的if和紧跟其后的“(”之间至少要有一个空格,否则nginx根本无法启动成功。
由于if在nginx中的形式比较简单,所以本节重点是对if中condition的介绍。
在nginx中,if中的condition主要有两种形式,一种是用来做比较(或对比)的(比如1是否大于2);另一种是用来做检查的(比如某个文件是否存在)。具体是如何比较的,又是如何检查的,下面我们就来一探究竟。
1.1做比较的condition
先来看一个简单的“比较“形式的例子【=】:
location / {
if ($uri = “/a”) {
return 200 “I am [/a]”;
}
return 200 “I am [/]”;
}
这个例子中用到了$uri这个变量,它表示当前请求的uri。如果请求是这个:
http://127.0.0.1/get/name
那么,该变量的值就是“/get/name”。所以上面这个配置的作用是:用当前请求的uri跟“/a”比对,如果比对成功则走当前if中的逻辑,否则走if之外的,具体效果如下:
curl http://127.0.0.1/a
I am [/a]
curl http://127.0.0.1/b
I am [/]
我们知道,在真正的计算机语言中,判断语句除了有正向的比较,一般都会有反向的比较。比如lua中的不等于“~=”和java中的不等于“!=”运算符,虽然形式上不一样,但都表示的是不等于(或不相同)。
nginx中的反向比较符号是“!=”,把上面的配置拿过来,然后把它变成反向比较后就是这样:
location / {
if ($uri != “/a”) {
return 200 “I am [/a]”;
}
return 200 “I am [/]”;
}
用上面同样的测试用例,你可以看到最终展现的结果也是相反的。
到目前为止,上面的两种对比模式【=】和【!=】,在nginx和通常的计算机语言中都是存在的,但基本上也就这两种是一样的,其它的像“大于”、“小于”等符号,在nginx中是不存在的。因为nginx中if的condition比较(对比)只能比对字符。
实际上,if的对比模式跟location中的匹配模式比较相似,它们都只能做字符对比。比如【=】这种形式,在if和location中都是区分大小写并严格对比的,只不过location的匹配只有正向的,没有与之对应的反向匹配【!=】。
除了正反向对比外,if还有5种比对模式,分别是【无】、【~】、【~*】、【!~】、【!~*】。嗯!前三个是不是很熟悉?因为在location中也有同样的符号。
除了【无】外,后两个的比对模式跟location中的其实是一样的:一个表示正则匹配区分大小写,另一个表示正则匹配不区分大小写。更详细的对比规则可以参看“深入理解location匹配规则”的“正则匹配”这一小节,这里就不在赘述。而最后的两种模式则分别是与其对应(【~】【~*】)的反向规则,同【=】的反向规则类似。目前只有【无】模式在if中稍显特殊,这种【无】并不表示if中的condition是空,而是表示没有运算符号,像这样:
if ($uri){
return 200 “hello”;
}
这种形式表示的意思是,只要变量$uri存在值,则该条件成立。不过上面这个配置在实际的业务似乎并没有什么实际用处,因为只要有请求,那么变量$uri必然有值。一个比较符合实际情况的例子可能像这样:
location / {
if ($arg_flag) {
return 200 “I am [$arg_flag]”;
}
return 200 “I am default”;
}
这种可以根据请求中是否存在入参flag来决定if条件是否成立,比如:
curl http://127.0.0.1/abc?flag=aaa
I am [aaa]
curl http://127.0.0.1/abc
I am default
可以看到,带入参flag的请求,打印的就是if中的内容,反之则打印if之外的内容。
现在关于if中可用于比较的“运算符”算是已经介绍完毕了,但是nginx似乎总会有一些让你意想不到“秘密”。比如在计算机语言中的一个普通的判断语句:
if (a== “b”) {
// do something
}
应该不会有人怀疑a和“b”是不能交换位置吧?而在nginx中,它确实是不能交换位置的,比如下面的例子:
location / {
if (“/a” = $uri) {
return 200 “I am [/a]”;
}
return 200 “I am [/]”;
}
此时当你试图启动或reload的时候,你会发现报错了…。报错信息如下:
nginx: [emerg] invalid condition ""/a"" in /xxx/conf/nginx.conf:26
报错信息也并没有很清楚,只是说“/a”无效。但是一旦你将 $uri和“/a”交换位置后就又正常了,是不是很烦? 其实if还有这样一个“隐形”规则:if语句如果只是用来作比较的,那么,它的conditon必须以有效变量作为开始,也就是用“$”标识的变量。除了我们下面将要提到的“做检查的conditon”,其它形式都是非法的。
1.2做检查的condition
if的这种condition,在表现形式上跟用来做比较的condition不一样,用于检查的正向if必须以“-”字符作为开头,而反向检查则是以“!-”字符开头。
目前,if支持四种正向检查表达式,分别是“-f”、“-d”、“-e”和“-x”,这四种前面加上“!”号就表示其反向操作,它们的具体用法如下:
location / {
if (【-f|-d|-e|-x】 “/path/aa”) {
return 200 “I am a xxx”;
}
return 200 “I am nothing”;
}
1.其中,“-f”表示if会检查“/path/aa”是否是一个文件,如果是则返回if块内对应的内容
2.“-d”则检查“/path/aa”是否是一个目录
3.如果是“-e”则检查“/path/aa”是否是文件、目录或软连接(符号链接)
4.最后一个“-x”,表示“/path/aa”是否“可执行”,不过这个“可执行”在linux中并不代表一定是可以运行的。我们知道,在nginx中描述一个文件或目录的权限时有如下三组数据:
rwxrwxrwx
其中,每三个是一组,第一组表示拥有者的权限,第二组表示该文件(或目录)所属的用户组拥有的权限,第三组则表示其它用户拥有的权限。而每一组又有三个权限,分别是读(r)、写(w)、执行(x)。我们这里的“-x”检查的就是第一组权限中是否有x(可执行)存在。
对于做检查的condition,在实际的工作中用的并不是太多,所以并没有花太多篇幅去列举例子,觉得意犹未尽的同学可以自己去搞一些例子来验证,说不定在验证的过程中也会发现一些让你意想不到“秘密”。
2作为location的if
看到这个标题你可能会觉得奇怪,if作为一个判断语句,怎么会跟用于uri匹配的location扯上关系呢?
是因为if中的condition有几种对比(比较)方式,跟location中的uri匹配方式用了同样的符号,所以才说扯上关系的吗?当然不是,注意我们标题里的“作为”这个词,意思是它就是location。
因为在某些情况下,if是完全可看做一个location的,那具体在什么情况下是location,又在什么情况下是单纯的判断语句呢?下面我们会从两个方面来阐述这个特点:1.它的配置和使用,2.它的实现原理。
2.1从配置上看if的location特性
来看一个特殊的location配置:
server {
// 用来模拟一个8080端口上的web服务
listen 8080;
location / {
return “I am $uri”;
}
}
server {
listen 80;
location / {
location /a {
proxy_pass http://127.0.0.1:8080;
}
location /b {
proxy_pass http://127.0.0.1:8080;
}
return 200 “I am /”;
}
}
从这个配置可以看到,location里面又嵌套了location,并且最外层的location匹配范围要大于且包含内层的location匹配。那么当我们向该配置发起不同的请求时会有什么结果呢?下面我们用三个不同的请求来试一下:
curl http://127.0.0.1/a
I am /a
curl http://127.0.0.1/b
I am /b
curl http://127.0.0.1/c
I am /
对于请求“/a”来说,它先匹配到了最外层的location“/”,然后内层又匹配到了“/a”,最后通过prox_pass指令将请求转发到8080端口,并匹配到其中的location后输出结果;请求“/b”跟请求“/a”的匹配方式相同,都是先匹配到外层,然后再匹配到本身,并最终转发到8080端口;而最后一个“/c”,因为在外层location“/”内部没有对应的匹配,所以最终打印出了“I am /”。
仔细看上面的例子和其输出结果,试想一下,内层的两个location换成if是不是可以达到同样的效果?为了产生更强的对比性,我们置换其中一个location,如下:
location / {
location /a {
proxy_pass http://127.0.0.1:8080;
}
if ($uri ~ /b) {
proxy_pass http://127.0.0.1:8080;
}
return 200 “I am /”;
}
对于这样一个配置,如果用和上面同样的url发起请求,你会看到它得到的结果也是同样的。
看完上面两个例子后,有的同学可能会说,虽然“if ($uri ~ /b){}”和“location /b{}”在这种情况下打印出了同样的结果,但只能说他们看上去相似,并不能说明此时if“就是”location。而且这里location和if都是嵌套在一个location内的,如果if真的可以作为location对待,那是不是也可以不用嵌套,将其直接暴露在server{}块内呢?
既然有疑问,那我们就把内嵌的配置拿出来,像这样:
server {
listen 80;
location /a {
proxy_pass http://127.0.0.1:8080;
}
if ($uri ~ /b) {
proxy_pass http://127.0.0.1:8080;
}
}
遗憾的是我们无法成功reload这个配置,并且后台输出这样一条错误日志:
nginx:[emerg]"proxy_pass" directive is not allowed here in /xxx /conf/nginx.conf:30
说proxy_pass这条指令不允许出现在第30行,而我本地的nginx.conf这个文件的第30行正好指向“if ($uri ~ /b) {}”中的proxy_pass指令,通过文档可以看到该指令能够出现的范围如下:
location, if in location, limit_except
第一个表示location{}块内;第三个表示limit_except{}块内;而第二则表示只能在location下的if{}块内,也就是说proxy_pass指令可以出现在if{}块内,但前提条件是这个if本身是在某个location{}下才可以。比如这样是可以的:
location / {
if ($uri) {
proxy_pass http://xxxxx;
}
}
但是去掉location是万万不能的。为了把这个例子跑通,我们把proxy_pass换成同等输出效果的return指令(该指令没有过多的限制),如下:
if ($uri ~ /b) {
return 200 “I am /b”;
}
此时再reload这个配置是完全可以的,并且当发送请求“/a”和“/b”的时候跟前面的例子输出的结果一样。
通过上面这个例子似乎又能证明if和location在这种情况又是不同的,因为如果相同,那if{}和location{}块内应该可以接受相同的指令才对。出现这种“混乱”的情况主要是因为if的特殊实现方式造成的,它的这种特殊实现方式,会让if在location{}块中表现出location的一些特性,而在server{}块中则仅会展现其作为判断语句的特性。
到目前为止有些同学可能会觉得,仅凭这几个例子就把if往location特性上扯,实在有些牵强,为了更有力更详细的说明这个问题,下面就来看看它的具体实现原理。
2.2if的实现原理
介绍if的实现原理前,我们先来简单看看location的基本工作流程。
2.2.1关于location
我们在前面的文章中有提到过,在nginx内部,每一个location都有一个结构体(ngx_http_core_loc_conf_t,后续用loc_conf代表)表示。nginx每从配置文件中解析出一个location{}块,就会为其创建一个loc_conf结构体,然后把它放到一个集合容器中,之后又会根据location的具体匹配模式(比如【=】、【~】等),将其分解为三个容器,后续通过这三个容器进行location匹配,具体匹配规则可以参看“深入理解location匹配规则”。我们下面要重点介绍的是在匹配完成后,它的配置块内指令的一些运行规则。
我们知道,对于一个正常的location块,在其内部都会有一些指令,比如这样一个location:
location /a {
set $myhost www.xxx.com;
proxy_set_header myhost $my_host;
proxy_pass http://127.0.0.1:8080;
}
我们假设在8080端口处有一个这样的location:
location / {
return 200 “I am [$http_myhost]”;
}
当我们用curl访问80端口时会看到如下结果:
curl http://127.0.0.1/a
I am [www.xxx.com]
对于“/a”这个location,其内部共有三条指令,其中set指令属于http_rewrite模块,另外两个属于http_proxy模块。
而上面例子中location指令的匹配和其内部指令的“执行”时机,以及执行的时候指令的信息从何而来,对后续理解if的location特性至关重要,所以先来大致了解一下。
指令的“执行”时机:(这块内容涉及到nginx中的另一个知识点“阶段”。关于阶段更详细的内容,后续会有专门的文章进行详细介绍,这里只是简单提及)如果把整个请求过程比喻成一条“流水线”,那么,在流水线上的“工人”就是nginx中的“阶段”,不同的阶段做不同的事。
我们上面例子中的指令总共会涉及到三个阶段:
FIND_CONFIG:用来匹配合适的location
REWRITE:重写uri、执行一些该阶段的脚本等
CONTENT:处理要输出的内容
当有一个“/a”请求时,执行过程大致如下:
-
请求过来后,nginx会在FIND_CONFIG阶段,从有效的location中匹配一个合适的location(比如例子中的“/a”)
-
location匹配成功后开始执行REWRITE阶段,此时例子中的set指令会被执行,该指令会把变量值放到指定的位置共后续使用
-
如果一切水利,最终会走到CONTENT阶段开始执行,该阶段会用到上例中剩下的两条指令信息,并发起转发动作
以上这些指令的“执行”时机,都是严格按照阶段顺序先后“执行”的,除非某些指令有特殊逻辑或发生某些错误,否则是不会随便跳跃阶段的。
运行时指令的信息从何而来:nginx在配置文件中存放了各种指令,在运行时会把各种指令信息按照某种布局放到内存中。每个模块都会有一个用来存放指令或其他信息的结构体(信息结构体),根据指令所在的区块(比如server{}块或location{}块)的不同将其注册到对应的区块上,比如本例中的set指令对应的“信息结构体”(这里存放了set指令编译后的脚本信息)以及两个proxy_xxx指令对应的“信息结构体”,都会间接注册到loc_conf中。当某个请求一旦匹配到合适的location后,nginx内部就可以拿到与其对应loc_conf,此后通过它又可以间接的拿到该location下所有的指令信息,从而按阶段完成整个请求过程。
为了便于理解这些指令信息的存放规则,用一个图来展示一下nginx执行时是如何获取指令信息的(这只是一个概念图,实际情况会更复杂):
loc_conf : 代表一个location{}(例子中的“/a”)
lcf:代表http_rewrite模块的配置信息结构体
lcf->codes: 存放set指令编译后的脚本的容器
plcf :代表http_proxy模块的配置信息结构体
plcf->proxy_pass:代表proxy_pass指令信息
plcf->proxy_set_header:代表proxy_set_header指令信息
2.2.2关于if的实现
上面做了那么多的铺垫,总算轮到主角儿上场了。
关于if的实现方式,我们可以从它的解析入手。当nginx解析到一个“if{}”指令后,会先为其创建一个loc_conf结构体(是的你没有看错,跟代表一个“location{}”块的结构体是同一个)并为其打上noname标记(用来区分其它location模式)。然后同样会把它放到一个集合容器中,这个容器同存放location结构体的容器也是一样的(前提是他们在同一个server{}块下)。
到目前为止if的这个实现方式同location基本完全一致,但是之前我们说这个容器最终会被拆分成三个不同的容器,其实不然,而是四个。第四个就是存放“if{}”的,只不过第四个容器在启动成功后就丢弃了,因为这种匹配方式不需要依赖容器,而是依赖if中的condition。
紧接着对if中condition的编译工作才是后续匹配的关键,而condition最终也会像变量一样被编译成脚本,这个脚本存放的容器和location下其它可编译指令存放的容器(用loc代替)相同。不但如此,该“if{}”块下所有的可编译指令都会存放到该容器中(同loc),但其它指令(比如proxy_pass)切会被放到当前“if{}”所代表的loc_conf中(其实是一个间接关联)。
为了更好的理解上面描述的内容,我们用一个实际的例子来看:
location / {
set $a /a;
if ($uri = $a) { //用~不行,因为if中的正则不支持变量
set $ip 127.0.0.1;
proxy_pass http://$ip:8080;
}
proxy_pass http://127.0.0.1:9090;
}
针对这个例子,我们有以下假设:
loc_conf : 代表这个location对应的结构体(或叫区块容器)
loc_conf_if: 代表这个”if{}”对应的结构体(或叫区块容器)
lcf->codes: 存放当前location内所含指令编译后的脚本(比如set指令)
此时,当nginx启动后,lcf->codes中存放的脚本大致如下:
(为了便于理解,我们用配置指令来代替实际的脚本,脚本详情可参考ngx中脚本相关文章)
而loc_conf和loc_conf_if容器的内容分别如下(这只是一个概要示意图,箭头都是间接指向):
有了上面这个例子在nginx内部的一个概要表示情况,接下来就可以更详细的描述它的实际执行情况了,来看一下基本执行流程图:
其中,在执行完if语句之后,也就是在上图的“跳出if”之后,后续执行需要获取的大部分信息,基本都是从对应的区块容器中接获取的,而这个区块容器又是通过location的匹配或if语句的执行获取到的。
看到这里,我们基本上会有这样一种感觉:if语句其实就是location的一种特殊匹配模式。
不过我们前面也说过,不在location中的if,是没有location特性的,比如这样:
server{
// something
if ($uri){
// something
}
}
出现这种情况是因为这个if的脚本是在server_rewrite阶段执行的,该阶段又会在find_config阶段前执行,而此时(server_rewrite阶段)还根本没有location的影子,因此也就不会有location的特性。
3总结
这篇文章主要从三个角度阐述了if的location特性(双面性)。
1.首先是把if作为一个常规的判断语句,阐述了一下它的使用规则和注意事项
2.其次是从使用的角度,在配置上引出if的双面性
3.最后则是从if的实现入手
上一篇: Nginx指令的执行顺序
下一篇: Web开发中文乱码深入分析