listen
配置指令用于指定虚拟主机,也就是 server {...}
监听的地址和端口。一个 虚似主机可以使用多个 listen
指令,也就是监听多对地址和端口;多个虚似主机可以 监听同一对地址和端口,这时,使用 server_name
来区分虚拟主机。
listen
指令的详细用法和支持的选项等内容可参见 官网手册 。
本篇主要来分析一下 listen
配置指令的解析过程、运行时监听地址端口的组织管理和虚 拟主机的实现过程。
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
处 理。
*:80
和 address: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 运行期间,存储监听 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
对应一个或多个 address
。 address
可以是绑定到本机的任何具体 IP 地址值 或者 0
(对应配置文件中的 *
)。 比如,一台有两个 IP 地址 1.1.1.1
和 2.2.2.2
机器上,对 Nginx 分别配置了三个 虚拟主机,分别监听 1.1.1.1:80
、 1.1.1.2:80
和 *:80
,那么 Nginx 会使用一个 ngx_listening_t
结构体对应这三个 address
。 ngx_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
- key
为 server name
, value
为虚拟主机信息的 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_addrs
将 ngx_http_conf_addr_t
结构转化为 ngx_http_in_addr_t
类型的结构体,并存储到 ngx_listening_t.servers.addrs
中。上面提到过,如果存在 wildcard binding
的情况下,多个 implicit bindings
会共用同一个 ngx_listening_t
结构体。这种情况下, hport->naddr
个 ngx_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
相关的组织结构图:
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