listen
配置指令用于指定虚拟主机,也就是 server {...}
监听的地址和端口。一个
虚似主机可以使用多个 listen
指令,也就是监听多对地址和端口;多个虚似主机可以
监听同一对地址和端口,这时,使用 server_name
来区分虚拟主机。
listen
指令的详细用法和支持的选项等内容可参见官网手册。
本篇主要来分析一下 listen
配置指令的解析过程、运行时监听地址端口的组织管理和虚
拟主机的实现过程。
- 本篇暂时忽略正则表达式相关的内容
- 本篇暂时忽略IPv6相关的处理
虚拟主机
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.
The term Virtual Host refers to the practice of running more than one web site (such as
company1.example.com
andcompany2.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.
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 directiveserver_name
indicate all names of the server.
也就是说:
-
Nginx 没有
Virtual Host
的说法,但是实际上server {}
实现的功能和 ApacheVirutalHost
的功能基本上完全一致。 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 就开始接入新的连接了。
Comments
不要轻轻地离开我,请留下点什么...