`
deyimsf
  • 浏览: 66704 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

双面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”请求时,执行过程大致如下:

  1. 请求过来后,nginx会在FIND_CONFIG阶段,从有效的location中匹配一个合适的location(比如例子中的“/a”)

  2. location匹配成功后开始执行REWRITE阶段,此时例子中的set指令会被执行,该指令会把变量值放到指定的位置共后续使用

  3. 如果一切水利,最终会走到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的实现入手

  • 大小: 178.6 KB
  • 大小: 44.7 KB
  • 大小: 97.9 KB
  • 大小: 242.9 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics