欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

双面if

程序员文章站 2022-05-22 21:18:40
...

  在介绍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指令信息

     双面if
            
    
    博客分类: HttpNginxC  

         

 

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中脚本相关文章)

       双面if
            
    
    博客分类: HttpNginxC  

而loc_conf和loc_conf_if容器的内容分别如下(这只是一个概要示意图,箭头都是间接指向)

         

       双面if
            
    
    博客分类: HttpNginxC  

 

有了上面这个例子在nginx内部的一个概要表示情况,接下来就可以更详细的描述它的实际执行情况了,来看一下基本执行流程图:

 

      双面if
            
    
    博客分类: HttpNginxC  

 

其中,在执行完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的实现入手

  • 双面if
            
    
    博客分类: HttpNginxC  
  • 大小: 178.6 KB
  • 双面if
            
    
    博客分类: HttpNginxC  
  • 大小: 44.7 KB
  • 双面if
            
    
    博客分类: HttpNginxC  
  • 大小: 97.9 KB
  • 双面if
            
    
    博客分类: HttpNginxC  
  • 大小: 242.9 KB