Agentzh的Nginx教程(OpenxResty三部曲1/3)

作者:春哥
读者:锅巴GG

Nginx的配置文件使用的就是一门微型的编程语言
许多真实世界里的Nginx配置文件其实就是一个一个的小程序。

很久之前就拜读过春哥的很多文章,以及Nginx的相关博文,大部分内容都是来自持续关注的OpenResty内容,但是本教程确实是基础中的基础。

由于最近工作的关系,需要系统的学习掌握Nginx架构特性以及利用lua-nginx-module进行二次开发,不仅开始重读相关的资料,而重读的时候,尤其以此教程入手获益匪浅,故记录之。

本笔记只按自己的理解记录,和原书差异较大,有问题请参阅原书。

Nginx配置指令的11个阶段

  • 重要的事情放前面

我的理解,应该要掌握的内容重点是在:

  1. Ngnix配置文件的自身生命式特性
  2. 变量的作用域
  3. 变量书写顺序依赖阶段和模块实现
Agentzh的Nginx教程(OpenxResty三部曲1/3)_第1张图片
Nginx配置指令的11个阶段

谈谈变量

在 Nginx 配置中,变量只能存放一种类型的值,因为也只存在一种类型的值,那就是字符串。

# 使用了标准ngx_rewrite模块的set配置指令对变量$a进行了赋值操作          
set $a "hello world";
# “变量插值”(variable interpolation)
set $b "$a, $a";
#使用特别的记法来消除歧义,当引用的变量名之后紧跟着变量名的构成字符时(比如后跟字母、数字以及下划线)
set $first "hello "; 
echo "${first}world";

set 指令(以及 geo 指令)不仅有赋值的功能,它还有创建 Nginx 变量的副作用,即当作为赋值对象的变量尚不存在时,它会自动创建该变量。

Nginx 变量的创建和赋值操作发生在全然不同的时间阶段。Nginx 变量的创建只能发生在 Nginx 配置加载的时候,或者说 Nginx 启动的时候;而赋值操作则只会发生在请求实际处理的时候。这意味着不创建而直接使用变量会导致启动失败,同时也意味着我们无法在请求处理时动态地创建新的 Nginx 变量。

  • Nginx 变量一旦创建,其变量名的可见范围就是整个 Nginx 配置,甚至可以跨越不同虚拟主机的 server 配置块。
  • Nginx 变量名的可见范围虽然是整个配置,但每个请求都有所有变量的独立副本,或者说都有各变量用来存放值的容器的独立副本,彼此互不干扰。

Nginx 变量的生命期是不可能跨越请求边界的。

Nginx 变量的一个常见误区是认为变量容器的生命期,是与 location
配置块绑定的,其实不然:

server {
    listen 8080; 
    location /foo { 
      set $a hello; 
      #使用第三方模块 [ngx_echo]提供的 [echo_exec]配置指令
      #发起到 location /bar的“内部跳转”
      echo_exec /bar;
    } `        
    location /bar {
      echo "a = [$a]";        
    }    
}

一个请求在其处理过程中,即使经历多个不同的 location
配置块,它使用的还是同一套 Nginx 变量的副本。

标准 ngx_rewrite 模块的 rewrite 配置指令其实也可以发起“内部跳转”,例如上面那个例子用 rewrite 配置指令可以改写成下面这样的形式:

server {
        listen 8080;

        location /foo {
            set $a hello;
            rewrite ^ /bar;
        }

        location /bar {
            echo "a = [$a]";
        }
    }
  • 效果和使用 echo_exec 是完全相同的。
  • Nginx 变量值容器的生命期是与当前正在处理的请求绑定的,而与 location无关。

通过 set 指令隐式创建的 Nginx 变量。这些变量称为“用户自定义变量”,或者“用户变量”。既然有“用户自定义变量”,自然也就有由 Nginx 核心和各个 Nginx 模块提供的“预定义变量”,或者说“内建变量”(builtin variables)。

  • 内建变量

Nginx 内建变量最常见的用途就是获取关于请求或响应的各种信息。例如由 ngx_http_core 模块提供的内建变量 $uri,可以用来获取当前请求的 URI(经过解码,并且不含请求参数),而 $request_uri 则用来获取请求最原始的 URI (未经解码,并且包含请求参数)。

另一个特别常用的内建变量其实并不是单独一个变量,而是有无限多变种的一群变量,即名字以 arg_开头的所有变量,我们估且称之为 $arg_XXX 变量群。一个例子是$arg_name,这个变量的值是当前请求名为 name
的 URI 参数的值,而且还是未解码的原始形式的值。
1. Nginx 会在匹配参数名之前,自动把原始请求中的参数名调整为全部小写的形式。
2. 尝试改写另外一些只读的内建变量,比如$arg_XXX变量,在某些 Nginx 的版本中甚至可能导致进程崩溃。

有一些内建变量是支持改写的,其中一个例子是 $args. 这个变量在读取时返回当前请求的 URL 参数串(即请求 URL 中问号后面的部分,如果有的话),而在赋值时可以直接修改参数串。这里的 $args 变量和 $arg_XXX 一样,也不再使用属于自己的存放值的容器。当我们读取 $args 时,Nginx 会执行一小段代码,从 Nginx 核心中专门存放当前 URL 参数串的位置去读取数据;而当我们改写 $args 时,Nginx 会执行另一小段代码,对相同位置进行改写。Nginx 的其他部分在需要当前 URL 参数串的时候,都会从那个位置去读数据,所以我们对 $args 的修改会影响到所有部分的功能。

与面向对象编程中的“存取器”概念相对应,Nginx 变量也是支持绑定“存取处理程序”的。Nginx 模块在创建变量时,可以选择是否为变量分配存放值的容器,以及是否自己提供与读写操作相对应的“存取处理程序”。

不是所有的 Nginx 变量都拥有存放值的容器。拥有值容器的变量在 Nginx 核心中被称为“被索引的”(indexed);反之,则被称为“未索引的”(non-indexed)。

在设置了“取处理程序”的情况下,Nginx 变量也可以选择将其值容器用作缓存,这样在多次读取变量的时候,就只需要调用“取处理程序”计算一次。

Nginx 模块可以为其创建的变量选择使用值容器,作为其“取处理程序”计算结果的缓存。显然, ngx_map 模块认为变量间的映射计算足够昂贵,需要自动将因变量的计算结果缓存下来,这样在当前请求的处理过程中如果再次读取这个因变量,Nginx 就可以直接返回缓存住的结果,而不再调用该变量的“取处理程序”再行计算了。

  • 注意缓存和惰性求值以及主动求值

只在实际使用对象时才计算对象值的技术,在计算领域被称为“惰性求值”(lazy evaluation)。提供“惰性求值” 语义的编程语言并不多见,最经典的例子便是 Haskell. 与之相对的便是“主动求值” (eager evaluation)。

Nginx 变量的值只有一种类型,那就是字符串,但是变量也有可能压根就不存在有意义的值。没有值的变量也有两种特殊的值:一种是“不合法”(invalid),另一种是“没找到”(not found)。

  • 输出特殊值“找不到”的效果和空字符串是相同的。因为这一回是 Nginx 的“变量插值”引擎自动把“找不到”给忽略了。那么我们究竟应当如何捕捉到“找不到”这种特殊值的踪影呢?

通过第三方模块 ngx_lua,我们可以轻松地在 Lua 代码中做到这一点。请看下面这个例子:

location /test {
content_by_lua '
if ngx.var.arg_name == nil then
ngx.say("name: missing")
else
ngx.say("name: [", ngx.var.arg_name, "]")
end
';
}



* ngx_lua
[ngx_lua](http://wiki.nginx.org/HttpLuaModule) 模块将 Lua 语言解释器(或者 [LuaJIT](http://luajit.org/luajit.html) 即时编译器)嵌入到了 Nginx 核心中,从而可以让用户在 Nginx 核心中直接运行 Lua 语言编写的程序。我们可以选择在 Nginx 不同的请求处理阶段插入我们的 Lua 代码。这些 Lua 代码既可以直接内联在 Nginx 配置文件中,也可以单独放置在外部 .lua 源码文件(或者 Lua 字节码文件)里,然后在 Nginx 配置文件中引用这些文件的路径。

虽然反复指出 Nginx 变量只有字符串这一种数据类型,但这并不能阻止像 ngx_array_var 这样的第三方模块让 Nginx 变量也能存放数组类型的值。
下面就是这样的一个例子:

location /test {
array_split "," $arg_names to=$array;
array_map "[$array_it]" $array;
array_join " " $array to=$res;

echo $res;

}

这个例子中使用了 ngx_array_var 模块的 array_split、 array_map 和 array_join 这三条配置指令,其含义很接近 Perl 语言中的内建函数 split、map 和 join(当然,其他脚本语言也有类似的等价物)。

我们来看看访问 /test 接口的结果:

$ curl 'http://localhost:8080/test?names=Tom,Jim,Bob
[Tom] [Jim] [Bob]

使用 ngx_array_var 模块可以很方便地处理这样具有不定个数的组成元素的输入数据,例如此例中的 names URL 参数值就是由不定个数的逗号分隔的名字所组成。不过,这种类型的复杂任务通过 ngx_lua 来做通常会更灵活而且更容易维护。

----
### 谈谈顺序

> 因为要理解11个阶段的顺序,需要做大量的实验,所以特此附上我测试用的完整样例,供大家学习参考。如果你不能直接说出每个location直接调用会返回的结果,不妨重新看看教程。

nginx.conf

worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
map $args $foo {
default 0;
debug 1;
}
server {
listen 8084;
# set_real_ip_from 127.0.0.1;
# real_ip_header X-My-IP;

    location /test {
        set $addr $remote_addr;
        echo "from: $addr";
    }
    location / {
    }
}
server {
    listen 8083;
    location / {
        root /Users/gosber/sloth;
    }
}
server {
    listen 8082;
    location / {
        root /Users/gosber/sloth;
        autoindex on; # 故意写在index前面,如果存在index.html,优先级应该index高
        index index.html;
    }
    location /index.html {
        set $a 32;
        echo "a = $a";
    }
    location /test {
        set $args "foo=1&bar=2";
        proxy_pass http://127.0.0.1:8081/args;
    }
    
    location /main {
        #curl --data helo  'localhost:8082/main'
        set $var main;
        echo_location /foo;
        echo_location /bar;
        echo "main: $var";
        echo "main method: $request_method";
        echo "main method(父子请求独立): $echo_request_method";
        echo_location /sub;
        # curl --data helo  --cookie user=gosber 'localhost:8082/main?name='
        content_by_lua '
            if ngx.var.arg_name == nil then
                ngx.say("name: missing")
            else
                ngx.say("name: [", ngx.var.arg_name, "]")
            end
            if ngx.var.cookie_user == nil then
                ngx.say("cookie user: missing")
            else
                ngx.say("cookie user: [", ngx.var.cookie_user, "]")
            end
            ';
    }
    location /foo {
        set $var foo;
        echo "foo: $var";
    }
    location /bar {
        set $var bar;
        echo "bar: $var";
    }
    location /sub {
        echo "sub method: $request_method";
        echo "sub method(父子请求独立): $echo_request_method";
    }
    location /test1 {
        array_split "," $arg_names to=$array;
        array_map "[$array_it]" $array;
        array_join " " $array to=$res;
        echo $res;
    }
    location /test2 {
        set $a 32;
        set $b 56;
        set_by_lua $c "return ngx.var.a + ngx.var.b";
        set $quation "$a + $b = $c";
        echo $quation;
    }
    location /test3 {
        set $a 1;
        rewrite_by_lua "ngx.var.a = ngx.var.a + 1";
        # 第三方模块 ngx_lua 提供的 rewrite_by_lua 配置指令也和 more_set_input_headers 一样运行在 rewrite 阶段的末尾。我们来验证一下
        set $a 56;
         echo $a;
    }
    location /hello {
        allow 127.0.0.1;
        deny all;
        echo "hello world";
    }
    location /hellolua {
        access_by_lua '
        if ngx.var.remote_addr == "127.0.0.1" then
            return
        end
        ngx.exit(403)
        ';
        echo "hello world";
    }
    location /shunxu {
        # 完全反写,不会影响执行顺序

        # content phase
        echo "age = $age";

        # access phase
        deny 10.32.168.49;
        access_by_lua "ngx.var.age = ngx.var.age * 3";
        
        # rewrite phase
        set $age 1;
        rewrite_by_lua "ngx.var.age = ngx.var.age + 1";
    }
    location /echo {
        echo_before_body "before...";
        proxy_pass http://127.0.0.1:8082/echofoo;
        echo_after_body "after...";
    }
    location /echofoo {
        echo "contents to be proxied";
    }
}
server {
    listen 8081;

    location /test1 {
        set $orig_foo $foo;
        set $args debug;
        echo "original foo: $orig_foo";
        echo "foo: $foo";
    }

    location /args {
        echo "args: $args";
    }

    location /t {
        echo "uri = $uri";
        echo "request_uri = $request_uri";
        echo "name1 = $arg_name1";# Nginx 会在匹配参数名之前,自动把原始请求中的参数名调整为全部小写的形式,可以接收Name,Name等
        set_unescape_uri $name $arg_name;
        set_unescape_uri $class $arg_class;
        echo "name: $name";
        echo "class: $class";
    }
    location / {
        default_type text/html;
        echo "d=[$d]";
        content_by_lua '
            ngx.say("

hello, world $d

") '; } location /test { set $a 测试; # echo "This is a dollar sign: $a"; set $b foo; echo_exec /bar; } location /bar { set $c ccc; set $d ddd; echo "b = [$b] d=[$d]"; } location /c { set $c myccc; echo "my c and d c=[$c] d=[$d]"; #变量全局可见,所以声明一次,全局可用,但是赋值不能跨越边界,除非是内部跳转可以传递 } location /d { rewrite ^ /c; } }

}


* index.html和hello.html内容
```bash
 gosber@freedamadeMBP  ~/sloth  cat index.html
just index.html
 gosber@freedamadeMBP  ~/sloth  cat hello.html
just hello.html

好吧,技术书籍并不适合写书评或者笔记,因为细节特别的多。
尤其投入的学习之后,基本仍不住想要动手coding的冲动,根本不想再写笔记。下次一定注意,不再写技术学习的笔记了。 —— 锅巴GG


想加入更多乐读创业社的活动,请访问网站→ http://ledu.club
或关注微信公众号选取:

Agentzh的Nginx教程(OpenxResty三部曲1/3)_第2张图片
乐读微信公众号

你可能感兴趣的:(Agentzh的Nginx教程(OpenxResty三部曲1/3))