Nginx 是目前最流行的 Web 服务器, 由于具备高性能、高可靠以及支持热部署等特性被人们所青睐。Nginx 用途广泛, 其可作为静态资源服务器,也可充当代理服务器 (HTTP/TCP/UDP/MAIL 等),还可以用来实现一些简单的 API 服务。Nginx 主要是通过其配置文件来控制它的行为,本文主要介绍其 http 模块下的 server_namelocation 这两条指令的配置。

server 指令块与虚拟主机

虚拟主机是一种在单一主机或主机群上运行多个网站或服务的技术,可以用来解决 IP 地址资源有限而网站数目日益增多的问题。实现方式主要有以下三种:

  • 基于域名(Name-based)
  • 基于 IP 地址(IP-based)
  • 基于 Port 端口(Port-based)

其中使用最广泛无疑是基于域名的方式, 不同的域名通过 DNS 最终可以解析到相同的 IP 地址, 在对应的机器上我们可以使用 Nginx 等 Web 服务器软件对不同的域名请求进行相应的处理。这里再提及一点, 我们平时访问一个网站,是通过 DNS 将其解析到某一个 IP 上, 我们的客户端(通常是浏览器)最终是和这个 IP 对应的机器建立连接,从而发送请求的。那么 Nginx 等服务器是如何知道一个请求对应的是哪个域名的呢?

答案在于 HTTP 协议中的 Host 请求头,其值为我们要访问的域名。这里需要注意的是, 在 HTTP/1.0 中是不支持 Host 请求头字段的,所以 HTTP/1.0 是不支持虚拟主机技术的,而根据rfc2616 规范HTTP/1.1 协议中客户端发送的请求必须带上 Host 这个请求头, 否则服务器必须返回400 Bad Request响应。

而 nginx 正是通过 http 模块下的 server 指令块来配置虚拟主机。

server_name 指令

配置语法:

  1. Syntax: server_name name ...
  2. Default:
  3. server_name ""
  4. Context: server

server_name 形式

sever_name 指令后面的参数值可以是以下几种:

  • 精确的域名,例如 www.example.com
  • 通配符名称,可用 * 表示任意多字符(类似 Linux Shell 中的 *),但是通配符必须在域名的最前面或者最后面,例如*.example.comwww.example.*
  • 正则表达式,最前面是一个波浪号~,例如 ~^www\d+\.example\.com$ 表示可以匹配以 www 开头,后跟一个到多个数字,然后以 .example.com 结尾的域名

除了以上几种形式,还有下面几种表示特殊含义的域名:

  • .example.com,相当于*.example.com+example.com
  • "",可以匹配没有带 Host 头的请求
  • 国际化域名 (用得不多, 了解即可),用 ASCII 码表示,例如xn--e1afmkfd.xn--80akhbyknj4f可表示пример.испытание
  • ___ 或者 !@# 等无效的域名,可以理解为其可以匹配任意域名,但是优先级最低,最常见的用法是用来设置默认的 server,即当一个请求的 Host 没有命中其他规则时,会采用默认 server 的配置。配置如下:
    1. server {
    2. listen 80 default_server
    3. server_name _
    4. return 444
    5. }

    server_name 匹配顺序

    当需要决定采用哪个 server 块的配置处理请求时, 会根据以下的顺序查找:
  1. 精确匹配
  2. *开头的最长通配符名称
  3. *结尾的最长通配符名称
  4. 根据在配置文件出现的顺序第一个匹配上的正则表示式名称
  5. 默认配置,在 listen 指令中指明了 default_server 的 server 块,若无,为配置文件中第一个声明的 server 块

示例,假设 nginx 只有以下 server 配置:

  1. # 这里主要是方便下面输出结果可以直接在浏览器显示
  2. default_type text/plain;
  3. # 这里使用geo指令主要是为了输出$,直接在return输出$会报错
  4. # 参见https://stackoverflow.com/questions/57466554/how-do-i-escape-in-nginx-variables
  5. geo $dollar {
  6. default "$";
  7. }
  8. server {
  9. listen 80;
  10. server_name ~^www\.a\..*$;
  11. return 200 "~^www\.a\..*$dollar";
  12. }
  13. server {
  14. listen 80;
  15. server_name ~^.*a\..*$;
  16. return 200 "~^.*a\..*$dollar";
  17. }
  18. server {
  19. listen 80;
  20. server_name www.code.a.*;
  21. return 200 "www.code.a.*";
  22. }
  23. server {
  24. listen 80;
  25. server_name *.a.com;
  26. return 200 "*.a.com";
  27. }
  28. server {
  29. listen 80;
  30. server_name www.a.com;
  31. return 200 "www.a.com";
  32. }

修改本机 hosts 文件,在 hosts 文件上加上以下配置:

  1. 127.0.0.1 www.a.com www.code.a.com www.code.a.cn www.a.oa.com dev.a.cn www.b.com

我们可以直接用浏览器访问或者借助 curl 工具来进行测试,测试结果如下,可对照上面的查找顺序进行分析:

input output 匹配类型
http://www.a.com www.a.com 精确匹配
http://www.code.a.com *.a.com 前导*匹配
http://www.code.a.cn www.code.a.* 后导*匹配
http://www.a.oa.com ~^www\.a\..*$ 正则匹配
http://dev.a.cn ~^.*a\..*$ 正则匹配
http://www.b.com ~^www\.a\..*$ 默认匹配

值得说明的是,由于上面的配置没有显示指定默认 server,所以会默认匹配到第一个配置,假如我们在配置最后再添加如下配置:

  1. server {
  2. listen 80 default_server;
  3. server_name _;
  4. return 200 "default_server";
  5. }

重启后,再访问 http://www.b.com ,会输出 default_server, 其他访问结果不变。注意这里的 default_server 是配置在 listen 指令下的。

关于 listen 指令, 有几点需要注意的地方:

  1. 如果 server 指令块里没有指定 listen 指令, 则根据运行 nginx 的用户不同,默认监听的端口也不同,root 用户启动默认监听 80 端口,否则默认监听 8000 端口
  2. 如果配置了 listen 且只指定了 IP, 则监听端口为 80,此时操作系统可能会不允许非 root 用户启动 nginx,提示:

    1. nginx: [emerg] bind() to 127.0.0.1:80 failed (13: Permission denied)
  3. 以上说的配置查找规则前提是请求需要跟 listen 指令配置的 IP 跟端口相匹配

关于以上注意事项,这里举两个例子:

  • 假设运行 nginx 的是非 root 用户,且上面最后我们加的配置里 listen 指令没有指定 80 端口,即:```nginx server {
    1. listen default_server;
    2. server_name _;
    3. return 200 "default_server";
    } ```

这时访问 http://www.b.com ,由于上面这个 server 监听的是 8000 端口,跟请求的 80 端口不匹配,结果将会变回 ~^www\.a\..*

  • 假设最后的默认 server 配置改成如下配置(注意端口前有 IP):```nginx server {
    1. listen 公网IP:80 default_server;
    2. server_name _;
    3. return 200 "default_server";
    } ```

这时如果是在公网访问的话,不管访问上面的哪个域名都会返回 “default_server”。理由是不设置 IP 的话 nginx 默认会监听该机器的所有 IP 的特定端口,设置了的话只会监听该 IP 的特定端口。
本地访问同理,不能匹配到 listen 了公网 IP 的 server。

location 配置

了解完 server_name 和 listen 的配置规则,我们知道了一个请求过来会对应哪个 server。接下来我们要讨论的是某个 server 下不同请求 URI 对应的 location 配置查找规则。

配置语法

  1. Syntax: location \[ = | ~ | ~\* | ^~ \] uri { ... }
  2. location @name { ... }
  3. Default:
  4. Context: server, location

根据配置语法我们知道 location 可以有以下几种形式:

  • \=,精确匹配
  • ~,正则匹配,大小写敏感
  • ~*,正则匹配,大小写不敏感
  • ^~,忽略正则表达式的前缀匹配
  • 没有修饰符,前缀匹配
  • @,命名 location,可用来做内部重定向

其中 = 和 ^~ 修饰符都可以认为是特殊形式的前缀匹配

匹配过程

根据请求的 URI 和 location 的配置, 查找请求对应的 location 过程如下:

  1. 将请求 URI 标准化,包括将 %xx 形式编码的文本进行解码,解析相对路径 ... 以及合并两个或多个相邻的 / 成单个 /
  2. 根据请求 URI 找到并记录匹配上的最长前缀匹配,这里有两个特殊的场景:
    • 找到了 = 修饰的精确匹配,结束查找,采用它的配置
    • 如果该步骤最终记录下的前缀以 ^~ 修饰,则采用它的配置,不会进行后续的查找
  3. 根据在配置文件出现的顺序,检查相应的正则匹配,若有一个匹配上,则应用该配置,且不会继续检查后续的正则配置
  4. 若第 3 步没有找到匹配上的正则匹配,则采用第 2 步中找到的最长前缀匹配对应的配置

根据上面的查找过程,可以得到一些配置优化点:

  • 对于经常要访问的路径,可以使用精确匹配或 ^= 修饰的匹配, 可以避免进行正则匹配检查
  • 如果一定要用到正则表达式,可以把最经常被访问的 location 规则配置在最前面,因为正则匹配命中一个就不会继续验证后续的匹配规则

    示例

    假设有如下配置:

    1. server {
    2. listen 80 default_server;
    3. server_name _;
    4. # A
    5. location = / {
    6. return 200 "A";
    7. }
    8. # B
    9. location / {
    10. return 200 "B";
    11. }
    12. # C
    13. location /docs {
    14. return 200 "C";
    15. }
    16. # D
    17. location ^~ /imgs {
    18. return 200 "D";
    19. }
    20. # E
    21. location ~* \.(gif|jpg|png)$ {
    22. return 200 "E";
    23. }
    24. # F
    25. location ~ /a/.*$ {
    26. return 200 "F";
    27. }
    28. }

    测试结果如下:

input output 说明
http://127.0.0.1 A 匹配到 A 跟 B, 精确匹配优先级较高
http://127.0.0.1/test B 只匹配到 B
http://127.0.0.1/docs/1 C 匹配到 B 跟 C,C 前缀比 B 长
http://127.0.0.1/docs/2.jpg E 匹配到 B、C、E,正则匹配比普通前缀匹配优先级高
http://127.0.0.1/imgs/1 D 只匹配到 B、D,D 前缀比 B 长
http://127.0.0.1/imgs/1.jpg D 匹配到 B、D、E,由于 D 是最长匹配且有 ^~ 修饰符,所以不会再检查正则匹配
http://127.0.0.1/docs/a/1 F 匹配到 B、C、F

关于最后一条测试结果,需要注意的是,/a/.*$ 这个正则表达式,并不要求请求 URI 以 /a 开头,这也是很容易疏漏的地方,若想匹配以 /a 开头的请求,应改为^/a/.*$ 此时最后一条测试结果会变为 C

location @name 的用法

@ 前缀可以用来定义一个命名的 location,该 location 不处理正常的外部请求,一般用来供内部重定向使用。它们不能嵌套,也不能包含嵌套的 location。
例如:

  1. location /try {
  2. try_files $uri $uri/ @name;
  3. }
  4. location /error {
  5. error_page 404 = @name;
  6. return 404;
  7. }
  8. location @name {
  9. return 200 "@name";
  10. }

这时访问 /try 或者 /error 都会返回 “@name”

总结

本文主要介绍了 nginx 关于server_namelocation的配置以及匹配规则, 并举例说明。server_namelocation指令是 nginx 中非常重要的两条指令,掌握这两条指令对于我们配置 nginx 以及排查问题都是非常重要的,希望本文能帮到大家。

参考文档