点赞
评论
收藏
分享
举报
NGINX Server匹配原理及源码分析
发表于2020-11-23 17:00

浏览 1.4k

一概述

在Bigip中,我们有virtual server的概念,在Openstack Neutron里面,我们有listener的概念。它们共同表示的是主机提供的虚拟服务。在NGINX中,与之对应的是配置文件中定义的server。

NGINX最重要的功能之一把用户的服务请求转发到正确的服务server上去。如果说二层的转发是依据数据包的二层mac地址,三层的转发依据的是数据包三层的IP地址,那么NGINX的服务转发依靠的是数据包的四七层信息。

NGINX对请求的路由转发,可以分成两个阶段。

第一个阶段是匹配定义的server。首先根据请求中的目的地址和端口进行匹配。如果相同的目的地址和端口同时还会对应多个servers,再根据server_name属性进行进一步匹配。需要注意的是,只有当listen指令无法找到最佳匹配时才会考虑评估server_name指令

第二阶段是在匹配到server后,NGINX根据请求URL中的path信息再匹配server中定义的某一个location。

本文试着从配置,原理和源代码的角度对NGINX服务匹配的第一阶段,server匹配进行分析。

二配置指令

相关指令

在NGINX中,我们可以通过server指令定义一系列不同的virtual server。在server中通过listen指令定义这个virtual server服务的地址和端口。在相同的地址和端口可以定义多个server时,通过server_name指令来进一步进行区分。

server {

            listen 80;

            server_name example.com;

}

匹配server需要数据包的四七层信息。与四层相关的是数据包的目的地址和port,与七层有关的信息是hostname和url中的path。其中对应四层的配置指令是listen指令。对应七层的配置指令是server_name和location两个指令。

其中与server匹配相关的四七层两个指令分别是是listen和server_name。

listen指令

Listen指令语法:listen [address|*][:port] [default_server] [flags]

  1. 第一个参数是指定NGINX运行的主机地址。一条listen指令中可以指定一个主机地址,如果配置的主机地址是*或者忽略,默认绑定主机所有的地址。如果要选择主机众多地址中的某几个,可以通过多个listen指令来实现。不能通过一条listen指令配置服务器众多地址中的某几个不连续地址。主机地址可以是本机的hostname或者域名。在NGINX启动期间会对配置hostname或者域名进行解析。如果配置的地址或者域名和本机ip地址不相配,NGINX会启动失败。地址还可以是Unix socket地址。
  2. 第二个参数指定要监听的端口。可以是单独的一个数字比如8080,也可以规定一个联系的范围,比如8080-9090。如果需要监听多个非连续的端口,可以通过多个listen指令实现。
  3. flags参数大多数是用来控制监听端口socket参数和网络行为的。其中default_server是指定当相同地址和端口对有多个server对应时,如果在匹配过程中通过server_name也不能匹配到相关的server,带有default_server标志的server就成为该匹配的结果。
  4. 相同的IP以及端口可以设置一个默认虚拟服务器。如果相同IP以及端口对应的server都没有标注default_server,那么配置文件中对于这个ip和端口对的第一个定义的server就是default_server。

需要注意以下几点:

  1. 如果指定ip或者domain_name而不指定端口号,比如listen 1.1.1.1; listen my_host_name; 如此配置对应的端口对于root用户是80,对于非root用户是8000。
  2. 指定端口号而不指定ip或者domain_name,比如listen 80;默认对NGINX所在主机的所有的ip地址的80端口都监听。
  3. 通过通配符*指定所有的主机地址。比如listen *:80;默认对所有的主机地址的端口都进行监听。
  4. 如果server中没有配置listen指令,对应root用户默认是执行listen *,对应非root用户执行的是listen *:8000。
  5. 另外listen之类也支持unix socket路径,用于和同主机之间的服务交互。

server_name指令

指令server_name的语法是:server_name name1 [name2] ..[namen];

可以配置同时配置一个或者多个name。如果不配置server_name对应的默认值是server_name““

参数name的配置形式有如下几种:

  1. 全字符串。
  2. 特殊变量$hostname。当使用变量$hostname时,在NGINX启动运行时,会把$hostname变量替换成本机的主机名。所以从本质上讲,在匹配时,这种情况也是一种全字符匹配。
  3. 带有通配符的字符串。对于这种字符串,通配符*的位置只能出现在头部或者末尾,而且不能和别的字符租户。比如*.test.com;  www.test.*是合法的。但是 *test.com; www.*.comwww.*a.comwww.abc.*om;都是不合法的。
  4. 正则表达式,NGINX要求正则表达式必须以~开头进行表示,而且正则表达式的字符全部按照小写字符进行匹配。在使用正则表达式时,通常会以 ^ 开头以 $ 结尾,虽然正则语法上并不要求这样配置,但是会大大提升解析效率。另外,点符号(.)是正则表达式的一个关键字,所以域名中的点需要使用反斜线来转意(\.),比如~^(?.+)\.domain\.com$;就是一个合理的正则表达式定义的server_name

另外,server_name字符是不区分大写小的。无论是全字符,前后缀通配符还是正则表达式,所有配置的server_name都按照小写字母来处理

对于这几种配置类型,匹配时的优先级顺序是:

  1. 首选匹配全字符串。
  2. 如果全字符串没有命中,则进行最长前缀通配符匹配。
  3. 如果前缀通配字符没有命中,则进行最长后缀通配符匹配。
  4. 如果后缀通配字符没有命中,则进行正则表达式类型匹配。按照在配置文件中出现的顺序,第一个匹配到的正则表达式就是匹配结果。
  5. 如果正则表达式也没有命中,则选择这个ip和port对的default_server。

对于用来进行匹配参数的server_name, 有如下的几种获取途径:

  1. 对于http1.1,协议规定必须携带host头部,此头部就可以用来进行server_name的匹配。
  2. 对于HTTP/1.0请求来说就没有这个要求,所以对于HTTP/1.0,只能把absolute URL中携带域名用来匹配。
  3. 如果host头部和absolute URL都可以提取server_name,按照absolute URL为准。如果两者都没有,使用server_name  “”;来匹配。
  4. 对于HTTPS请求来说,可以从TLS握手过程中获取到域名。

三Server匹配效果举例

对应ip地址和port的匹配,过程相对清晰简单,我们就不再举例说明。下面通过几个例子解释server_name匹配的原则。

1.如果有server_name正好完全匹配http中的Host头部,则定义这个完整字符串的server block就被选择处理请求。

如下配置,如果server_name值是host1.jikui.com,则第二个server block被选中用来处理请求。

     server {

            listen 80;

            server_name   *.jikui.com;

           …

       }

       server {

             listen 80;

             server_name   host1.jikui.com;

       }

2. 如果完全字符串没有匹配,则在前缀通配符中进行最长匹配。

如下配置,如果请求中的server_name数值是 www.jikui.org”, 下面第二个服务器就会被选中。

    server {

        listen 80;

        server_name www.jikui.*;

        . . .

    }

    server {

        listen 80;

        server_name *.jikui.org;

        . . .

    }

    server {

        listen 80;

        server_name *.org;

        . . .

    }

3.如果前缀匹配没有成功,则进行最长后缀匹配。

如下列配置如果server_name值是www.jikui.com”, 第三个服务器将会被选中。

    server {

        listen 80;

        server_name host1.jikui.com;

        . . .

    }

    server {

        listen 80;

        server_name jikui.com;

        .  . .

    }

    server {

        listen 80;

        server_name www.jikui.*;

        . . .

    }

4. 如果后缀匹配没有成功,则进行正则表达式匹配。第一个匹配成功的正则表达式所定义的server将会被用来处理请求。

比如,如果server_name值是www.jikui.com”, 第二个定义的服务器被选中用来处理服务。

     server {

            listen 80;

            server_name jikui.com;

            . . .

    }

    server {

            listen 80;

            server_name   ~^(www|host1).*\.jikui\.com$;

            . . .

    }

    server {

            listen 80;

            server_name   ~^(subdomain|set|www|host1).*\.jikui\.com$;

            . . .

    }

5. 如果正则表达式也没有匹配成功,则会使用ip和port对的default_server来处理请求。

四源代码分析

数据结构

与 server匹配相关的数据结构有:ngx_listening_t, ngx_http_port_t, ngx_http_in_addr_t, ngx_http_addr_conf_t, ngx_http_server_name_t, ngx_http_conf_port_t, ngx_http_conf_addr_t, ngx_http_request_t, ngx_http_connection_t等。

他们相互的关系如下图所示。

配置层面源码分析

与指令listen对应的解析函数是ngx_http_core_listen。而与指令server_name对应的是ngx_http_core_server_name。

指令listen解析流程

  1. ngx_http_core_listen 首先调用ngx_parse_url来解析listen指令后的ip地址或者domain_name和端口。如果有多个连续端口,在u->last_port会记录最后一个port。然后解析剩下的配置项并且存放在ngx_http_listen_opt_t结构中。在使用ngx_parse_url解析时,可能得到多个地址。函数会把每一个地址连同生成的属性调用ngx_http_add_listen存放到ngx_http_core_main_conf_t结构中的port数组中。从上面数据结构图中,我们可以看到,一个port结构对应着一个addr数组。这个一对多的关系就是对一个主机有多个IP地址然后每一个IP地址都可以在某一个port上启服务这种模型进行建模。
  2. 函数ngx_http_add_listen会遍历ngx_http_core_main_conf_t中的ports数组对于每一个port调用ngx_http_add_addresses为每一个port添加对应的ngx_http_core_srv_conf_t结构。
  3. 在函数ngx_http_add_addresses中,如果当前port已经存在一个相同的地址,则调用ngx_http_add_server把解析对应对的server结构ngx_http_core_srv_conf_t添加到port对应的server数组中如果没有找到相同的地址,则在ports里新加一个port再调用ngx_http_add_address添加一个addr到此port中。

至此,portaddr之间以及addrserver之间1对多关系都已经建立起来了。 一个具体的端口比如80可能对应主机的多个ip地址。然后即使ip和端口相同,也可以定义不同的servers

指令server_name解析流程

server_name指令对应的解析函数是 ngx_http_core_server_name。函数会把配置的server_name添加到ngx_http_core_srv_conf_t结果的server_name数组中。 如果server_name 指令后面有host_name变量,则直接解析成真正的hostname添加到server_name数组中。

监听端口创建

所有listen和server_name相关配置解析完毕后,在http解析函数ngx_http_block的最后会调用函数ngx_http_optimize_servers来生成监听端口来启动http服务。具体流程是:

  1. 函数ngx_http_optimize_servers会遍历ngx_http_core_main_conf_t结构中的port数组。 对于每一个port的所有的地址进行排序。
  2. 然后遍历此port的所有地址,如果某一个对应的地址的server数量大于1,则调用函数ngx_http_server_names进行处理。
  3. 函数ngx_http_server_names会遍历对应地址所有的server_name,然后把所有的全字符,前缀通配符字符串,后缀通配字符串对应server_name加入到对应addr结构的hash, wc_head, wc_tail 三种hash表中。然后把所有的正则表达式的字符串加入到对应的addr结构的regex数组中。
  4. 然后函数ngx_http_optimize_servers会对每一个port调用ngx_http_init_listening来创建监听socket接收服务连接。函数ngx_http_optimize_servers会对于每个port进行判断这个port对应的addr中有没有是通过通配符进行监听的。如果有,则所有的此端口对应的地址只会生成一个ngx_listening_t结构socket来监听服务连接。如果没有,则对于port对应的所有的addr都会生成一个ngx_listening_t结构端口进行监听。

     例一:

         server {

                         listen *:2121;

                         proxy_timeout 65534;

                         proxy_pass vpnftp1;

            }

            server {

                         listen 10.250.64.103:2121;

                         proxy_timeout 65534;

                         proxy_pass vpnftp;

            }

            server {

                         listen 60.60.60.77:2121;

                         proxy_timeout 65534;

                         proxy_pass vpnftp;

           }

    上述例子中,因为有通配符的存在只会1个ngx_listen_t。

    例二:

            server {

                         listen 10.250.64.103:2121;

                         proxy_timeout 65534;

                         proxy_pass vpnftp;

            }

            server {

                         listen 60.60.60.77:2121;

                         proxy_timeout 65534;

                         proxy_pass vpnftp;

            }

上述例子中,因为没有通配符所以生成2个ngx_listen_t。

5. 使用函数ngx_http_add_listening创建监听端口。在此函数中,会通过ngx_create_listening函数create一 个socket,然后设置监听端口的属性。其中最重要的把监听端口的handler设置为 nginx_http_init_connection。当有新的连接建立时,此函数就会得到调用。 

6. 然后创建ngx_http_port_t结构关联到监听端口的servers成员中。然后再调用ngx_http_add_addrs把端口 所有的addr复制到servers数组中的每一个成员中。

至此,port和address以及port,address对和server_name之间的1对多关系就建立起来。对于所有配置的listen端口也都创建了相关的监听端口用来服务连接。

数据层面源码分析

配置层面的数据结构构建好以后,我们再来分析用户连接到达以后,对应的server是如何被匹配的。

  1. 对于每一个http连接请求都会对应一个ngx_http_requst结构,在这个结构中有一个ngx_http_connection_t的成员。这个成员中的addr_conf成员在函数ngx_http_init_connection中被赋值,这样数据结构图中A标志的关系就建立起来。
  2. 因为server_name是从数据报文的应用层中获取的,所以对server进行选择是发生在HTTP连接已经完成,开始读取client的请求头部信息时。对应的具体代码就是从函数ngx_http_process_request_line或者ngx_http_process_request_header调用ngx_http_set_virtual_server开始的。
  3. 函数ngx_http_set_virtual_server会通过ngx_http_request结构找到对应的ngx_http_connection_t成员,然后通过ngx_http_connection_t成员找到对应的ngx_http_addr_conf_t结构,这个结构中存放这为这一地址端口对存储的所有server_name数组。
  4. 然后调用函数ngx_http_find_virtual_server来找到某一个具体的server定义。函数ngx_http_find_virtual_server通过ngx_hash_find_combined来查找具体的server定义。
  5. 函数ngx_hash_find_combined按顺序依次找查找全字符hash(通过函数ngx_hash_find),前缀匹配hash(通过函数ngx_hash_find_wc_head),后缀匹配hash(通过函数ngx_hash_find_wc_tail)。如果查找不到,函数ngx_http_find_virtual_server再一次查找正则表达式数组。
  6. 最后,如果函数ngx_hash_find_combined也没有匹配成功,就选取default_server。

至此,请求所对应的server被正确匹配。

五结语

作为反向代理,NGINX如何对请求进行服务匹配是一个核心过程。其本质上就是对数据包进行四七层的请求转发。本文分析了服务匹配的第一阶段的原理,下一篇,我们将分析服务转发的第二阶段,也就是NGINX如何进行location匹配的。


已修改于2023-03-09 02:21
本作品系原创
创作不易,留下一份鼓励
皮皮鲁

暂无个人介绍

关注



写下您的评论
发表评论
全部评论(2)

按点赞数排序

按时间排序

server_name只支持使用$host_name这个变量。或者前后缀通配符,全字符串这几种情况。不运行使用其他变量或者其他变量作为局部的情况。

赞同

0

回复举报

发表于2020-12-04 15:05



回复皮皮鲁
回复

感谢博主的一系列讲解。如何在server_name模块使用变量?

server{
server_name ~$(variable).jingkaimori.net
}



赞同

0

回复举报

发表于2020-12-04 14:50



回复jingkaimori
回复
关于作者
皮皮鲁
这家伙很懒还未留下介绍~
85
文章
2
问答
41
粉丝
相关文章
1.为什么要用nginx?它是怎么来的?首先我们知道,在网站输入网址的时候访问某一个网站,可以得到想要的结果,如果是淘宝呢,淘宝购物每天用户量非常大,达到百万或者千万级怎么办了,这个时候人太多了,访问的时候操作系统的多线程和进程建的切换消耗了大量的CPU资源,严重会导致服务器宕机,失去用户量,企业面临破产。所以就有一个能解决并发访问服务器的东西,所以这个东西就横空出世了,它就是nginx高性能服务器。是由俄罗斯的工程师IgorSysoev,他在为RamblerMedia工作期间,使用C语言开发了Nginx。Nginx作为WEB服务器一直为RamblerMedia提供出色而又稳定的服务。2.什么叫nginx高性能服务器?有什么特点?nginx高性能服务器:是一种自由的,开源的,高性能的HTTP服务器;同时也是一个IMAP,POP3,SMTP代理服务器;用来实现负载均衡的。特点:高可用,高并发,热部署,高扩展,低消耗。3.nginx的下载安装和各种命令输入?在百度里输入nginx,找到官网并进行下载(如果不会安装,网上有很多详细的教程,这里就不多说)。下载安装(本机的安装在”/usr
点赞 1
浏览 628
原文作者:Raye原文链接: 全面了解Nginx到底能做什么转载来源:简书前言本文只针对Nginx在不加载第三方模块的情况能处理哪些事情,由于第三方模块太多所以也介绍不完,当然本文本身也可能介绍的不完整,毕竟只是我个人使用过和了解到过得。所以还请见谅,同时欢迎留言交流Nginx能做什么1.反向代理2.负载均衡3.HTTP服务器(包含动静分离)4.正向代理以上就是我了解到的Nginx在不依赖第三方模块能处理的事情,下面详细说明每种功能怎么做反向代理反向代理应该是Nginx做的最多的一件事了,什么是反向代理呢,以下是百度百科的说法:反向代理(ReverseProxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。下面贴上一段简单的实现反向代理的
点赞 0
浏览 1.2k
圣火昭昭,圣火耀耀,凡我弟子,喵喵喵喵
点赞 0
浏览 361