转载

Nginx 源代码笔记 - 虚拟主机

listen 配置指令用于指定虚拟主机,也就是 server {...} 监听的地址和端口。一个 虚似主机可以使用多个 listen 指令,也就是监听多对地址和端口;多个虚似主机可以 监听同一对地址和端口,这时,使用 server_name 来区分虚拟主机。

listen 指令的详细用法和支持的选项等内容可参见 官网手册 。

本篇主要来分析一下 listen 配置指令的解析过程、运行时监听地址端口的组织管理和虚 拟主机的实现过程。

  • 本篇暂时忽略正则表达式相关的内容
  • 本篇暂时忽略IPv6相关的处理

虚拟主机

Nginx Server Blocks

Note: "VirtualHost" is an Apache term. Nginx does not have Virtual hosts, it has "Server Blocks" that use the server_name and listen directives to bind to tcp sockets.

Apache Virtual Host

The term Virtual Host refers to the practice of running more than one web site (such as company1.example.com and company2.example.com ) on a single machine. Virtual hosts can be "IP-based", meaning that you have a different IP address for every web site, or "name-based", meaning that you have multiple names running on each IP address. Tha fact that they are running on the same physical server is not apparent to the end user.

server {}

Directive assigns configuration for the virtual server.

There is no separation of IP and name-based (the Host header of the request) servers.

Instead, the directive listen is used to describe all addresses and ports on which incoming connections can occur, and in directive server_name indicate all names of the server.

也就是说:

  • Nginx 没有 Virtual Host 的说法,但是实际上 server {} 实现的功能和 Apache VirutalHost 的功能基本上完全一致。 Nginx 把 server {} 称为 Virtual Server

  • 不同的 Virtual Server 可以用监听的 address:port 进行区分,也可以用 Server Name 进行区分 (在 Virtual Server 同时监听相同的 address:port 时)。

  • 一个 Virtual Server 可以监听多个 address:port ,这点是 Apache 的 Virual Host 不能支持的。

  • Virtual Server 支持 *:port 配置形式,也就是说一个 Virtual Server 可以设 置为监听其运行机器上的所有 IP 地址的 port 端口。

  • TCP/IP 协议栈下,网络程序能够使用 address:port 申请监听文件描述符。由于一 个 address:port 在 Nginx 配置中会对应多个 Virtual Server ,那么 Nginx 需要根 椐 Server Name 来判断来自 address:port 的请求要交给哪个 Virtual Server 处 理。

  • *:80address:80 同属一个 ngx_listening_t 。来自 80 端口的请求需要根 据接收此请求的本地 IP ( getsockname ) 和 ngx_http_in_addr_t.addr 进行匹配,以 便查找应该处理此请求的虚拟主机。 --- 后文详述

  • 本文将 Virtual Server 称为虚拟主机。

接下来的篇幅主要就用来分析一下 Nginx 如何建立起什么样的数据结构来实现上述的这些 功能,也即虚拟主机是如何实现的。

基础结构

  • listen 配置指令配置项的存储结构 ngx_http_listen_opt_t 。此结构由 listen 指令处理函数 ngx_http_core_listen 生成并初始化,其中的配置最终会转存到运行时 ngx_listening_t 结构体中。

    typedef struct {     union {         struct sockaddr     sockaddr;         struct sockaddr_in  sockaddr_in;         ...         u_char              sockaddr_data[NGX_SOCKADDRLEN];     } u;     socklen_t               socklen;     unsigned                set:1; /* 除了 `default_server` 和 addr:port                                     外还有其它选项被设置的话,set 都会                                     置 1 */     unsigned                default_server:1;     unsigned                bind:1;     unsigned                wildcard:1;     int                     backlog;     int                     rcvbuf;     int                     sndbuf;     u_char                  addr[NGX_SOCKADDR_STRLEN + 1]; } ngx_http_listen_opt_t;
  • 配置解析过程中,用于临时记录 port 相同的虚拟主机信息的 ngx_http_conf_port_t 结构体。

    typedef struct {     ngx_int_t               family;     in_port_t               port;     ngx_array_t             addrs; /* array of ngx_http_conf_addr_t */ } ngx_http_conf_port_t;
  • 配置解析过程中,用于临时记录 address:port 相同的虚拟主机信息的 ngx_http_conf_addr_t 结构体。

    typedef struct {     ngx_http_listen_opt_t       opt;     ngx_hash_t                  hash;     ngx_hash_wildcard_t         *wc_head;     ngx_hash_wildcart_t         *wc_tail;     ...                /* the default server configuration for this address:port */     ngx_http_core_srv_conf_t    *default_server;      ngx_array_t                 servers; /* array of ngx_http_core_srv_conf_t */ } ngx_http_conf_addr_t;
  • 在配置解析完成后,和监听地址端口相关的临时结构图如下

Nginx 源代码笔记 - 虚拟主机

  • Nginx 运行期间,存储监听 socket 相关信息的结构体 ngx_listening_t 。其中存储 了此 socket 对应的 address:port (也可能是 *:port )、来自此 socket 的请求 都可以由哪些虚拟主机进行处理 以及 socket 的由 listen 指令设置的相关选项参数。

    struct ngx_listening_s {     ngx_socket_t        fd;     struct sockaddr     *sockaddr;     ...     int                 backlog;     int                 rcvbuf;     void                *servers; /* ngx_http_port_t */     ... };
  • ngx_http_port_t - 每个 ngx_listening_t 对应一个或多个 addressaddress 可以是绑定到本机的任何具体 IP 地址值 或者 0 (对应配置文件中的 * )。 比如,一台有两个 IP 地址 1.1.1.12.2.2.2 机器上,对 Nginx 分别配置了三个 虚拟主机,分别监听 1.1.1.1:801.1.1.2:80*:80 ,那么 Nginx 会使用一个 ngx_listening_t 结构体对应这三个 addressngx_http_port_t 记录了这三个 address 的具体信息:它们的 IP 值是多少,都对应了哪些虚拟主机。

    typedef struct {     void            *addrs; /* plain array of ngx_http_in_addr_t */     ngx_uint_t      naddrs;
  • ngx_http_in_addr_t - 每个 address 对应的虚拟主机、默认虚拟主机等信息。 address 可以是具体的 IP 地址,也可以是 0 (对应配置文件中的 * )。

    typedef struct {     in_addr_t           addr;     ngx_http_in_addr_t  conf; } ngx_http_in_addr_t;
  • ngx_http_addr_conf_t - 包含默认虚拟主机、可按照虚拟主机 server name 查找虚 拟主机的 ngx_hash_combined_t 类型的 hash 表结构。

    typedef struct {     ngx_http_core_srv_conf_t    *default_server;     ngx_http_virtual_names_t    *virtual_names; } ngx_http_addr_con_t;
  • ngx_http_virtual_names_t - keyserver namevalue 为虚拟主机信息的 hash 数据结构。

  • ngx_hash_combined_t - 支持头模糊 ( *.example.com ) 和尾模糊 ( www.example.* ) 查找的 hash 表结构。其具体实现这里略过。

结构重组

配置文件解析过程中,根据 listen 指令配置构造监听地址相关的临时存储 (如上图) 过 程暂时不表 (配置文件解析篇 有间单分析)。

ngx_listening_t 是 Nginx 创建监听 socket 、处理监听 socket 事件使用的主要结 构体。不论一个 address:port 被多少个虚拟主机使用,Nginx 也只会分配唯一一个 ngx_listening_t 结构体。

同时,如果某个虚拟主机配置为监听所有本机地址某个端口 portA ,即使用 *:portA 的配置形式,此时 Nginx 会对所有监听了此端口的虚拟主机 (使用 address:portA*:portA 配置形式) 也只分配唯一一个 ngx_listening_t 结构体,并从系统中申请绑 定到地址 *:portA 上的监听 socket

下面分析一下由临时存储构造 ngx_listening_t 的过程。

回到 http 作用域的构造函数 ngx_http_block 中来。在配置文件解析完毕后,根据 上节描述的临时结构,Nginx 调用 ngx_http_optimize_servers 生成运行时存储结构 ngx_listening_t

    /* optimize the lists of ports, addresses and server names */     ---------------http/ngx_http.c:1356----------------     static ngx_int_t     ngx_http_optimize_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,         ngx_array_t *ports)     {         ...         ports = ports->elts;         for (p = 0; p < ports->nelts; p++) {              ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts,                      sizeof(ngx_http_conf_addr_t), ngx_http_cmp_conf_addrs);              addr = port[p].addrs.elts;             for (a = 0; a < port[p].addrs.nelts; a++) {                 if (addr[a].servers.nelts > 1) {                     ngx_http_server_names(cf, cmcf, &addr[1]);                 }             }              ngx_http_init_listening(cf, &ports[p]);         }         ...     }

对以上代码的补充说明:

  • ngx_sort 对使用相同端口的 ngx_conf_addr_t 进行排序。对应 listen 指令显式 设置 bind 选项的放到数组的开头。对应地址为通配形式的 ( * ) 放到数组的末尾。

  • 同一个 address:port 被多个虚拟主机使用时 ( addr[a].servers.nelts > 1 ),根 据 server name 生成 hash 查找表。

ngx_http_init_listening 函数根据排序后的 ngx_conf_addr_t 数组,决定针对一个 端口构造多少个 ngx_listening_t 结构体。

    --------------http/ngx_http_init_listening.c:1600---------------     static ngx_int_t     ngx_http_init_listening(ngx_conf_t *cf, ngx_http_conf_port_t *port)     {         ...         addr = port->addrs.elts;         last = port->addrs.nelts;          /*          * If there is a binding to an "*:port" then we need to bind() to          * the "*:port" only and ignore other implicit bindings.  The bindings          * have been already sorted: explicit bindings are on the start, then          * implicit bindings go, and wildcard binding is in the end.          */          if (addr[last - 1].opt.wildcard) {             addr[last - 1].opt.bind = 1;             bind_wildcard = 1;          } else {             bind_wildcard = 0;         }          i = 0;          while (i < last) {              if (bind_wildcard && !addr[i].opt.bind) {                 i++;                 continue;             }              ls = ngx_http_add_listening(cf, &addr[i]);             ...             hport = ngx_pcalloc(cf->pool, sizeof(ngx_http_port_t));             ...             ls->servers = hport;              if (i == last - 1) {                 hport->naddrs = last;              } else {                 hport->naddrs = 1;                 i = 0;             }              switch (ls->sockaddr->sa_family) {             ...             default: /* AF_INET */                 if (ngx_http_add_addrs(cf, hport, addr) != NGX_OK) {                     return NGX_ERROR;                 }                  break;             }              addr++;             last--;         }          return NGX_OK;     }

对上面代码的补充说明:

  • 如果出现了 *:port 形式的地址,Nginx 会将 implicit bindings 对应的虚拟主机 信息和此 wildcard binding 对应的虚拟主机信息合并到同一个 ngx_listening_t 结 构体中。

  • 上述代码中的注释说得很明白: explicit bindings 排到了数组的最前面,在此函数中 会被优先调用创建对应的 ngx_listening_t 结构体。 implicit bindings 排在了 wildcard binding 的前面,它们会和 wildcard binding (如果有的话, bind_wildcard 值为 1 ) 共用一个 ngx_listening_t 结构体。

ngx_http_add_addrsngx_http_conf_addr_t 结构转化为 ngx_http_in_addr_t 类型的结构体,并存储到 ngx_listening_t.servers.addrs 中。上面提到过,如果存在 wildcard binding 的情况下,多个 implicit bindings 会共用同一个 ngx_listening_t 结构体。这种情况下, hport->naddrngx_http_in_addr_t 被 转换成 ngx_http_in_addr_t 结构体,并统一存放到 hport->addrs 指向的连续空间中。

    ----------------http/ngx_http.c:1736---------------------     static ngx_int_t     ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport,         ngx_http_conf_addr_t *addr)     {         ...         ngx_http_virtual_names_t *vn;          hport->addrs = ngx_pcalloc(cf->pool,                                    hport->naddrs * sizeof(ngx_http_in_addr_t))         ...         addrs = hport->addrs;          for (i = 0; i < hport->naddrs; i++) {              sin = &addr[i].opt.u.sockaddr_in;             addrs[i].addr = sin->sin_addr.s_addr;             addrs[i].conf.default_server = addr[i].default_server;             ...             vn = ngx_palloc(cf->pool, sizeof(ngx_http_virtual_names_t));             ...             addrs[i].conf.virtual_names = vn;              vn->names.hash = addr[i].has;             vn->names.wc_head = addr[i].wc_head;             vn->names.wc_tail = addr[i].wc_tail;             ...         }          return NGX_OK;     }

上面过程中创建的 ngx_listening_t 结构体在创建时就已经被加入到了 cycle->listening 中。

    ------------core/ngx_connection.c:15----------------     ngx_listening_t *     ngx_create_listening(ngx_conf_t *cf, void *sockaddr, socklen_t socklen)     {         ...         ls = ngx_array_push(&cf->cycle->listening);         ...         ls->sockaddr = sa;         ls->socklen = socklen;         ...         ls->fd = (ngx_socket_t) -1;         ls->type = SOCK_STREAM;         ...     }

ngx_http_optimize_server 函数执行完毕后,所有需要的 ngx_listening_t 结构 体都已经准备完毕。确定了 Nginx 所有需要监听的 address:port 后,就可以从系统申 请对应的 socket 并开始 listen() 了。

下图是本节完成后, ngx_listening_t 相关的组织结构图:

Nginx 源代码笔记 - 虚拟主机

socket 创建

确定了 Nginx 需要监听的地址和端口后,Nginx 在 ngx_init_cycle 中申请 socket 并开始 bind()listen()

    -----------core/ngx_cycle.c:40--------------     ngx_cycle_t *     ngx_init_cycle(ngx_cycle_t *old_cycle)     {         ...         if (ngx_open_listening_sockets(cycle) != NGX_OK) {             goto failed;         }         ...     }      -----------core/ngx_connection.c:243---------     ngx_int_t     ngx_open_listening_sockets(ngx_cycle_t *cycle)     {         ...         for (tires = 5; tires; tries--) {             ...             ls = cycle->listening.elts;             for (i = 0; i < cycle->listening.nelts; i++) {                 ...                 s = ngx_socket(ls[i].sockadr->sa_family, ls[i].type, 0);                 ...                 setsockopt(s, SOL_SOCKET, SO_REUSEADDR,                            (const void *) &reuseaddr, sizeof(int));                 ...                 bind(s, ls[i].sockaddr, ls[i].socklen);                 ...                 listen(s, ls[i].backlog);                 ...                 ls[i].listen = 1;                 ls[i].fd = s;             }             ...         }         ...     }

在 Nginx 配置文件中指令的事件通知模块初始化时,会给 ngx_listening_t 分配对应的 ngx_connection_t 类型变量 ( ngx_listening_t.connection ),以便将它和普通连接统 一进行事件处理 (事件处理都是围绕 connection 进行的)。随后,对对应的 socket 读 事件进行注册和事件处理回调函数进行指定。

    -----------event/ngx_event.c:579------------     static ngx_int_t     ngx_event_process_init(ngx_cycle_t *cycle)     {         ...         ls = cycle->listening.elts;         for (i = 0; i < cycle->listening.nelts; i++) {             c = ngx_get_connection(ls[i].fd, cycle->log);             ...             c->listening = &ls[i];             ls[i].connection = c;              rev = c->read;             ...             rev->accept = 1;             ...             rev->handler = ngx_event_accept;             ...             ngx_add_event(rev, NGX_READ_EVENT, 0);             ...         }     }

到此为止,Nginx 和外界的门户打开了。等其它组件也都初始化完毕, worker 进程进入 事件处理循环时,Nginx 就开始接入新的连接了。

Category:Nginx Tagged:nginx notes virtual host virtual server

Comments

正文到此结束
Loading...