Nginx(17)之Nginx编程???

Nginx(17)之Nginx编程???

微信搜索 zze_coding 或扫描 👉 二维码关注我的微信公众号获取更多资源推送:

额。。。你第一眼看到“编程”想到了啥???

我的脑海中首先就是那啥?顺序、分支、循环、变量等等字眼。。。

Nginx 也支持这些东东???

额,,除了循环,在 Nginx 中基本上都提供了对应的指令来支撑我们在配置文件中完成类编程的操作,使我们的配置方式更加灵活。

这些指令主要由「ngx_http_rewrite_module」模块提供,那下面我们就来看一下它们怎么使用。

返回值 return

Nginx 配置中可使用 return 指令来配置类似返回值的东东,它的返回值是针对客户端(通常是浏览器)来说的。
这个返回值可不是简单的一个值而已,它可以返回给浏览器 HTML 内容,也能返回给浏览器一个动作,如临时重定向、永久重定向等。

它的使用规则如下:

语法: return code [text];
	return code URL;
	return URL;
默认: —
可使用的上下文: server, location, if

看示例咯~~~


先来测试一下返回文本内容,配置虚拟主机如下:

server {
    listen 80;
    charset utf8;

    return 404 '404 不存在的页面';
}

此时访问该虚拟主机,响应内容就一直是 404 不存在的页面 啦,并且响应状态码也为 404,如下:

$ curl -I 10.0.1.200
HTTP/1.1 404 Not Found
Server: nginx/1.16.1
Date: Sat, 14 Mar 2020 06:15:58 GMT
Content-Type: application/octet-stream
Content-Length: 22
Connection: keep-alive

$ curl 10.0.1.200
404 不存在的页面

再测试一下返回响应码和地址

server {
    listen 80;
    charset utf8;

    return 302 https://www.zze.xyz;
}

此时使用浏览器访问该虚拟主机就会临时重定向到我的博客首页啦~~~就不截图了。。


最后测试一下只返回地址是什么情况?修改虚拟主机配置如下:

server {
    listen 80;
    charset utf8;

    return https://www.zze.xyz;
}

使用 crul 访问看一下响应头:

$ curl -I 10.0.1.200
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.16.1
Date: Sat, 14 Mar 2020 09:06:55 GMT
Content-Type: text/html
Content-Length: 145
Connection: keep-alive
Location: https://www.zze.xyz

可以看到,当不指定响应状态码只指定 URL 时,默认就是临时重定向到指定的 URL。

定义变量 set

其实在之前已经用到过很多模块的内建变量了,常用变量可参考「整理一下 Nginx 中常用的变量」。

如果我们要自己定义变量,可通过 set 指令来完成,其使用规则如下:

语法: set $variable value;
默认: —
可使用的上下文: server, location, if

该指令为指定变量 $variable 设置一个值。该值可以包含文本、变量及其组合。

看下面示例吧~~~


配置如下虚拟主机:

server {
    listen 80;
    charset utf8;

    set $request_info '请求的 URL 为: $uri';
    return 200 $request_info;
}

此时对该主机发起 /test_variable,响应如下:

$ curl 10.0.1.200/test_variable
请求的 URL 为: /test_variable

回头再看一下 set $request_info '请求的 URL 为: $uri'; 这行,是不是不用我说也明白了,$uri 是引用当前请求 URI 的内建变量,它被嵌套在字符串中使用,并将这个字符串的结果赋值给了我们自定义的 $request_info 变量,最后通过 return 200 $request_info; 响应这个变量的内容。

$variable 也可以写作 ${variable},可防止与其它普通字符串混淆。

条件判断 if

说到条件判断,是不是下意识就想到了 if ... else ...,只不过在 Nginx 中是木有 else 的哦,因为 Nginx 配置中主要还是通过指令的形式来完成配置的。

那么这里对条件判断提供支持的就是 if 指令啦,其使用规则如下:

语法: if (condition) { ... }
默认: —
可使用的上下文: server, location

condition 是指定的判断条件,当该条件的结果为 true,则执行括号的指令。

if 指令内部的配置是从上级的配置级别继承的。

条件(condition)可以是以下任意一种:

  • 变量名,如果变量的值为空字符串或 0,则为 false,否则为 true(在 1.0.1 版之前,任何以 0 开头的字符串都被视为错误值)。
  • 使用 =!= 运算符将变量与字符串进行比较;
  • 使用 ~(用于区分大小写的匹配)和 ~*(用于区分大小写的匹配)运算符将变量与正则表达式进行匹配。正则表达式可以包含分组,这些分组可供以后使用 $1 .. $9 变量做后向引用。也可使用 ! 做取反操作,如 !~!~* 也可用。如果正则表达式包含 }; 字符,整个表达式应用单引号或双引号引起来;
  • 使用 -f!-f 运算符检查文件是否存在;
  • 使用 -d!-d 运算符检查目录是否存在;
  • 使用 -e!-e 运算符检查文件、目录或符号链接的是否存在;
  • 使用 -x!-x 运算符检查文件是否有执行权限;

看示例~~~


配置如下虚拟主机:

server {
    listen 80;
    charset utf8;

    set $user_agent 'PC 端';

    if ($http_user_agent ~* curl){
        set $user_agent 'CURL 命令';
    }

    if ($http_user_agent ~* iphone){
        set $user_agent 'IOS 客户端';
    }

    if ($http_user_agent ~* android){
        set $user_agent 'Android 客户端';
    }

    return 200 $user_agent;
}

在上述配置中,我们定义了变量 $user_agent 来存放客户端信息,并初始化其默认值为 PC 端
然后通过几个 if 指令判断客户端请求头信息中是否包含指定客户端文本内容,如果包含,则对 $user_agent 变量做重新赋值,最后响应 $user_agent 的值。

所以此时我们使用 curl 访问该主机,结果如下:

$ curl 10.0.1.200
CURL 命令

中断 break

这里的 break 可不像我们在 Java、Python、Shell 中使用的 break 哦~~毕竟这里也没有循环。

在说 break 指令是啥之前我们需要先了解一下 $uri 变量,与之对应的还有一个变量是 $request_uri,下面是对这两个变量的描述:

  • $request_uri:客户端发来的原始请求 URI,带完整的参数,如请求为 10.0.1.200/aaa.jsp?name=zhangsan 时它的值就为 /aaa.jsp?name=zhangsan
  • $uri:未必是用户的原始请求,如果使用了 rewrite 重写那么它的值就会是重写后的 URI,并且它是不带参数的。在不重写的情况下请求 10.0.1.200/aaa.jsp?name=zhangsan 时它的值就为 /aaa.jsp

break 指令的作用就是可以让 Nginx 停止往下执行并立即去查找当前 $uri 变量对应的资源文件并返回

懂了木有??有点感觉木有??没有就看下面例子吧。。


配置如下虚拟主机:

server {
    listen 80;
    root /nginx/;

    location / {
        rewrite ^/html/(.*\.html)$ /www/$1 last;
	break;
        rewrite ^/www/(.*\.html)$ /hdoc/$1 last;
    }
}

思考一下:如果我们此时访问 10.0.1.200/html/1.html 会返回什么?

如果忽略 break; 这行,按我们之前在「Nginx(15)之URL 重写」中的结论,它最终返回资源是 /nginx/hdoc/1.html

现在添加了 break; 这行,在 Nginx 执行到 break; 的上一行时当前请求的 URL 已被重写了,它重写后的 URI 为 /www/1.html,即此时的 $uri 的值是 /www/1.html,所以继续向下执行到 break; 时,会立即去寻找 $uri 对应的资源响应,即最终响应的资源为 /nginx/www/1.html

那就测试一下啦,我这里就不添加对应的页面了,请求的资源如果找不到会记录在错误日志,所以我就监听错误日志就行了,测试访问 10.0.1.200/html/1.html,此时错误日志如下:

2020/03/14 17:51:45 [error] 3400#0: *126 open() "/nginx/www/1.html" failed (2: No such file or directory), client: 10.0.1.1, server: , request: "GET /html/1.html HTTP/1.1", host: "10.0.1.200"

可以看到 open() "/nginx/www/1.html" failed,没毛病吧~~~

说到这里你应该明白了 break 指令的作用了,那么它到底有啥用呢?

再看个例子,修改上面虚拟主机配置如下:

server {
    listen 80;
    root /nginx/;

    location / {
        rewrite ^/html/(.*\.html)$ /www/$1 last;
        if (-f '$document_root$uri'){
            break;
        }
        rewrite ^/www/(.*\.html)$ /hdoc/$1 last;
    }
}

7 ~ 9 行我们添加了一个 if 判断块,'$document_root$uri' 拼接的结果是当前 $uri 对应的资源路径,-f 用来判断资源是否存在。

那么依旧是以访问 10.0.1.200/html/1.html为例,这个判断块的作用就是:

  • 如果当前 $uri 对应的资源存在,就执行 break 指令,即立即去响应当前 $uri 对应的资源;
  • 如果当前 $uri 对应的资源不存在,继续执行到第 10 行的重写指令,那么最后响应的资源就是 /nginx/hdoc/1.html 了;

要试一下吗,,那就试一下😂

创建对应资源目录:

$ mkdir /nginx/{www,hdoc} -p

先添加 URI /hdoc/1.html 对应的资源:

$ echo 'in hdoc' > /nginx/hdoc/1.html

此时访问 10.0.1.200/html/1.html 试试:

$ curl 10.0.1.200/html/1.html 
in hdoc

由于 URI /www/1.html 对应的资源不存在,所以 URI 继续被重写为 /hdoc/1.html,所以最后返回的也就是 URI /hdoc/1.html 对应的资源。

再添加 URI /www/1.html 对应的资源:

$ echo 'in www' > /nginx/www/1.html

访问 10.0.1.200/html/1.html

$ curl 10.0.1.200/html/1.html 
in www

没毛病,由于 URI /www/1.html 对应的资源存在,所以执行了 break 指令,立即找到了对应资源并响应~~~

变量映射 map

Nginx 中可以通过 map 指令实现变量映射,要注意哦,在上面介绍的几个指令其实都是「ngx_http_rewrite_module」模块提供,而 map 指令是由「ngx_http_map_module」提供的。

map 指令的使用规则如下:

语法: map string $variable { ... }
默认值: —
可使用的上下文: http

下面说一下它的几个参数:

  • 第一个参数 string 是一个字符串,也可以是一个变量;
  • 第二个参数 $variable 是我们自定义的一个变量;
  • 第三个参数是一个 { ... } 语句块,在该语句块中可定义两列,可有多行。第一列定义对第一个参数的匹配规则,第二列定义匹配成功时的返回结果,且返回结果会被赋值给第二个参数 $variable

我们这里暂称第三个参数中的第一列为“源值”,第二个参数为“结果值”。

map 指令会让第一个参数 string 与第三个参数中的每行的源值进行匹配,如果和某行的源值匹配成功,则将该行的结果值赋值给第二个参数 $variable,我感觉我描述清楚了😳。

源值可以是如下取值:

  • 普通字符串,不区分大小写匹配;
  • 正则表达式,对于区分大小写的匹配,正则表达式应以 ~ 开头,对于不区分大小写的匹配,正则表达式应以 ~* 符号开头。正则表达式可以包含命名分组和位置分组,这些分组捕获到的值可以在结果值中被后向引用。

源值位还可以用来指定特殊的参数:

  • default_value:设定默认结果值,当没有可匹配的源值时则返回默认的结果值,如果未指定默认结果值,那么返回一个空字符串;
  • include:引用一个定义了源值、结果值的文件;
  • hostnames:允许用前缀或者后缀掩码(*)指定域名作为源变量值,这个参数必须写在值映射列表的最前面;(这个参数我并没有搞懂到底怎么用。。

如果源值与以上描述的特殊参数名称之一相同,则应在其前面加上 \ 符号进行转义。

如果匹配到多个源值,那么会按照下面的顺序进行选择:

  • 没有掩码的字符串;
  • 最长的带前缀的字符串,例如:*.example.com
  • 最长的带后缀的字符串,例如:mail.*
  • 按定义顺序第一个先匹配的正则表达式;
  • 默认值;

那么啥是变量映射呢???额,,其实是我编的一个词🤣,空口白话也说不明白它的意思,咱们直接整一个 demo 吧~


这里我们用 map 指令实现上面「条件判断 if」中的示例功能,即判断发起请求的客户端。

map $http_user_agent $user_agent {
    default "PC 端";
    ~*curl "CURL 命令";
    ~*iphone "IOS 端";
    ~*android "Android 端";
}

server {
    listen 80;
    charset utf-8;

    return 200 $user_agent;
}

它的效果和 使用 if 指令实现的例子是一样的(不信你试一试😎),但它是不是清爽很多~

现在综合上面对 map 指令的概述,我想你应该能看懂这个例子。我还是做一下说明吧:

  • $http_user_agent 是内建变量,保存了客户端请求头中的客户端信息;
  • map 指令会让 $http_user_agent{ ... } 中的每一行的第一列进行匹配;
  • 如果匹配成功,则返回该行对应第二列的内容赋值给 $user_agent

我这里以使用 curl 发送请求为例,此时 $http_user_agent 的值类似为 curl/7.29.0,它被 { ... } 中第一列的 ~*curl 规则匹配到了,然后将对应的第二列即 "CURL 命令" 返回并赋值给 $user_agent,在虚拟主机配置中我们返回了 $user_agent 内容,所以响应内容就是 CURL 命令。。嗯。我确定我说清楚了🤓

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.zze.xyz/archives/nginx17.html

Buy me a cup of coffee ☕.