概述
本篇博客我们将阐述nginx的虚拟主机的实现原理。如果对nginx的虚拟主机所谓何物还不是很清楚的话,建议先阅读相关资料。本文我们将阐述nginx的虚拟主机的底层实现原理。
预备
nginx配置中与虚拟主机相关的配置主要如下
listen address[:port] [default_server] [setfib=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [ssl] [so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]];
好看,看起来一个listen指令携带的参数可是够复杂的。不过还好,我们一般很少关注那些不太常用的参数,而且,与本文所阐述的虚拟主机的参数只是第一个addr[:port]。我们需要注意一点:这个参数可以非常灵活地适用,可以只配置ip:port的任意一个,也可以兼而有之,以下几种配置都是合理的
listen 127.0.0.1:8000
listen 127.0.0.1 不加端口,默认监听80端口
listen 8000
listen *:8000
listen localhost:8000
实现
listen指令由ngx_http_core模块负责解析,指令解析函数为ngx_http_core_listen
static char *
ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_srv_conf_t *cscf = conf;
ngx_str_t *value, size;
ngx_url_t u;
ngx_uint_t n;
ngx_http_listen_opt_t lsopt;
cscf->listen = 1;
value = cf->args->elts;
ngx_memzero(&u, sizeof(ngx_url_t));
u.url = value[1];
u.listen = 1;
u.default_port = 80;
if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
if (u.err) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"%s in \"%V\" of the \"listen\" directive",
u.err, &u.url);
}
return NGX_CONF_ERROR;
}
ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
ngx_memcpy(&lsopt.u.sockaddr, u.sockaddr, u.socklen);
lsopt.socklen = u.socklen;
lsopt.backlog = NGX_LISTEN_BACKLOG;
lsopt.rcvbuf = -1;
lsopt.sndbuf = -1;
#if (NGX_HAVE_SETFIB)
lsopt.setfib = -1;
#endif
lsopt.wildcard = u.wildcard;
#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
lsopt.ipv6only = 1;
#endif
(void) ngx_sock_ntop(&lsopt.u.sockaddr, lsopt.addr,
NGX_SOCKADDR_STRLEN, 1);
// 以下代码解析listen后面的若干参数
for (n = 2; n < cf->args->nelts; n++) {
......
if (ngx_http_add_listen(cf, cscf, &lsopt) == NGX_OK) {
return NGX_CONF_OK;
}
return NGX_CONF_ERROR;
}
这个函数的大部分代码都在解析listen指令的参数,没什么课值得细说的地方。我们需要关注的是在解析完成后调用的ngx_http_add_listen。这个函数将这里解析生成的虚拟主机及其配置添加至全局统一管理。
ngx_http_add
ngx_int_t
ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
ngx_http_listen_opt_t *lsopt)
{
in_port_t p;
ngx_uint_t i;
struct sockaddr *sa;
struct sockaddr_in *sin;
ngx_http_conf_port_t *port;
ngx_http_core_main_conf_t *cmcf;
#if (NGX_HAVE_INET6)
struct sockaddr_in6 *sin6;
#endif
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
if (cmcf->ports == NULL) {
cmcf->ports = ngx_array_create(cf->temp_pool, 2,
sizeof(ngx_http_conf_port_t));
if (cmcf->ports == NULL) {
return NGX_ERROR;
}
}
sa = &lsopt->u.sockaddr;
switch (sa->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
sin6 = &lsopt->u.sockaddr_in6;
p = sin6->sin6_port;
break;
#endif
#if (NGX_HAVE_UNIX_DOMAIN)
case AF_UNIX:
p = 0;
break;
#endif
default: /* AF_INET */
sin = &lsopt->u.sockaddr_in;
p = sin->sin_port;
break;
}
port = cmcf->ports->elts;
for (i = 0; i < cmcf->ports->nelts; i++) {
if (p != port[i].port || sa->sa_family != port[i].family) {
continue;
}
<span style="white-space:pre"> </span>// 如果port已经位于ports数组中,那么不需要再将port添加一次
/* a port is already in the port list */
return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);
}
/* add a port to the port list */
port = ngx_array_push(cmcf->ports);
if (port == NULL) {
return NGX_ERROR;
}
port->family = sa->sa_family;
port->port = p;
port->addrs.elts = NULL;
return ngx_http_add_address(cf, cscf, port, lsopt);
}
这里的逻辑就相对简单了,解析出虚拟主机对应的port,判断该port是否已经被添加到全局的ports数组中。如果没有,添加一次。最终调用了ngx_http_add_address来完成后续工作。
从实现上来看,nginx的各个概念之间的关系如下:
port:每个port可被多个IP地址共同使用,即可出现如下此类的配置
server {
listen 192.168.1.1:80
}
server {
listen 192.168.1.2:80
}
因此,port与ip之间的关系是1:N。另外,每一个address(ip:port)甚至可对应多个虚拟主机。例如,下面的配置也是合理的
server {
listen 192.168.1.1:80;
server_name example.org www.example.org;
...
}
server {
listen 192.168.1.1:80;
server_name example.net www.example.net;
...
}
可以看到,上面的同一个address(192.168.1.1:80)对应了两个虚拟主机。因此,ip:port与虚拟主机之间也是1:N的关系。
理解了上面的两点,我们接下来就可以看看nginx的虚拟主机在内存中是如何管理的了。涉及虚拟主机管理相关的数据结构有如下几个:
ngx_http_core_main_conf_t :ports
ngx_http_core_srv_conf_t:server_names
在介绍这两个数据结构之前我们先来看看一些预备知识。不知道各位是否还记得,nginx配置文件中的每个指令均是由一个特定的nginx模块负责处理,而且每个指令有其作用域的限制。与虚拟主机相关的配置有如下两个listen和server_name
server {
listen 192.168.1.1:80
server_name example.org
}
我们接下来的主要任务就是看看nginx的模块是如何组织相关数据结构的。
首先,负责解析这两个指令的是http_core模块。我们前面说的与虚拟主机相关的数据结构有二:ngx_http_core_main_conf_t 和ngx_http_core_srv_conf_t。ngx_http_core_main_conf_t 的ports成员是一个数组,保存了所有监听的端口;而ngx_http_core_srv_conf_t的server_names则记录了当前虚拟主机的域名(可能会有多个)。
我们先来看看对于listen指令的解析:ngx_http_core_listen
static char *
ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
...
cscf->listen = 1;
value = cf->args->elts;
ngx_memzero(&u, sizeof(ngx_url_t));
u.url = value[1];
u.listen = 1;
u.default_port = 80;
//解析listen命令后面的参数,ip:port
if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
if (u.err) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"%s in \"%V\" of the \"listen\" directive",
u.err, &u.url);
}
return NGX_CONF_ERROR;
}
//根据上面解析的参数初始化ngx_http_listen_opt_t结构
ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
ngx_memcpy(&lsopt.u.sockaddr, u.sockaddr, u.socklen);
lsopt.socklen = u.socklen;
...
//解析其它参数,如default_server,bind等,并通过这些参数设置lsopt
...
//将解析到的虚拟主机的地址信息加入到监听列表中
if (ngx_http_add_listen(cf, cscf, &lsopt) == NGX_OK) {
return NGX_CONF_OK;
}
return NGX_CONF_ERROR;
}
实际上,我们可以将每个虚拟主机理解为一个ngx_http_core_srv_conf_t结构。在上面的实现中,主要是解析listen后面所携带的参数,并将其存储在结构ngx_http_listen_opt_t中,最终调用ngx_http_add_listen()完成接下来的工作。
ngx_int_t
ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
ngx_http_listen_opt_t *lsopt)
{
in_port_t p;
ngx_uint_t i;
struct sockaddr *sa;
struct sockaddr_in *sin;
ngx_http_conf_port_t *port;
ngx_http_core_main_conf_t *cmcf;
#if (NGX_HAVE_INET6)
struct sockaddr_in6 *sin6;
#endif
// 获取http_core模块的main_conf结构并初始化其ports数组,如果还是null的话
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
if (cmcf->ports == NULL) {
cmcf->ports = ngx_array_create(cf->temp_pool, 2,
sizeof(ngx_http_conf_port_t));
if (cmcf->ports == NULL) {
return NGX_ERROR;
}
}
sa = &lsopt->u.sockaddr;
switch (sa->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
sin6 = &lsopt->u.sockaddr_in6;
p = sin6->sin6_port;
break;
#endif
#if (NGX_HAVE_UNIX_DOMAIN)
case AF_UNIX:
p = 0;
break;
#endif
default: /* AF_INET */
sin = &lsopt->u.sockaddr_in;
p = sin->sin_port;
break;
}
port = cmcf->ports->elts;
for (i = 0; i < cmcf->ports->nelts; i++) {
if (p != port[i].port || sa->sa_family != port[i].family) {
continue;
}
/* a port is already in the port list */
// 说明当前要添加的port在数组中已经存在
// 那么将当前的ip添加到每个port下的addrs数组中
return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);
}
// 否则将port添加至http_core_main_conf的ports数组
port = ngx_array_push(cmcf->ports);
if (port == NULL) {
return NGX_ERROR;
}
port->family = sa->sa_family;
port->port = p;
port->addrs.elts = NULL;
return ngx_http_add_address(cf, cscf, port, lsopt);
}
上面函数的主要作用的将listen参数中的port添加至http_core模块的main_conf中的ports数组中,那可能会出现两种情况:
1. 该port已经在数组中;
2. port还不在数组中;
对于情况1,如果port已经在数组中,那么我们只需将当前ip添加到port下的addrs数组,诚如我们前面所说:每个port是可以对应多个ip的;对于情况2,如果port在数组中还未出现,那么分配一个ngx_http_conf_port_t结构来代表该port,并将该port添加至http_core_main_conf的ports中,调用了函数ngx_http_add_adress。
接下来我们分别来看看这两个函数的实现:
static ngx_int_t
ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
{
......
addr = port->addrs.elts;
for (i = 0; i < port->addrs.nelts; i++) {
if (ngx_memcmp(p, addr[i].opt.u.sockaddr_data + off, len) != 0) {
continue;
}
// 新加入的地址已经在地址列表中存在了,
// 将新的虚拟主机信息加入到这个地址的虚拟主机列表中
if (ngx_http_add_server(cf, cscf, &addr[i]) != NGX_OK) {
return NGX_ERROR;
}
/* preserve default_server bit during listen options overwriting */
default_server = addr[i].opt.default_server;
#if (NGX_HTTP_SSL)
ssl = lsopt->ssl || addr[i].opt.ssl;
#endif
#if (NGX_HTTP_SPDY)
spdy = lsopt->spdy || addr[i].opt.spdy;
#endif
if (lsopt->set) {
if (addr[i].opt.set) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"duplicate listen options for %s", addr[i].opt.addr);
return NGX_ERROR;
}
addr[i].opt = *lsopt;
}
/* check the duplicate "default" server for this address:port */
if (lsopt->default_server) {
if (default_server) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"a duplicate default server for %s", addr[i].opt.addr);
return NGX_ERROR;
}
default_server = 1;
addr[i].default_server = cscf;
}
addr[i].opt.default_server = default_server;
#if (NGX_HTTP_SSL)
addr[i].opt.ssl = ssl;
#endif
#if (NGX_HTTP_SPDY)
addr[i].opt.spdy = spdy;
#endif
return NGX_OK;
}
/* add the address to the addresses list that bound to this port */
//添加新地址信息到port的地址列表中
return ngx_http_add_address(cf, cscf, port, lsopt);
}
这个函数将虚拟主机对应的ngx_http_core_srv_conf_t结构添加到某个地址(ngx_http_conf_addr_t)下面的servers数组中。调用了函数ngx_http_add_server。完事后调用了ngx_http_add_address来将该地址添加到port下的addrs数组中
static ngx_int_t
ngx_http_add_address(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
{
ngx_http_conf_addr_t *addr;
if (port->addrs.elts == NULL) {
if (ngx_array_init(&port->addrs, cf->temp_pool, 4,
sizeof(ngx_http_conf_addr_t))
!= NGX_OK)
{
return NGX_ERROR;
}
}
#if (NGX_HTTP_SPDY && NGX_HTTP_SSL && !defined TLSEXT_TYPE_next_proto_neg)
if (lsopt->spdy && lsopt->ssl) {
ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
"nginx was built without OpenSSL NPN support, "
"SPDY is not enabled for %s", lsopt->addr);
}
#endif
addr = ngx_array_push(&port->addrs);
if (addr == NULL) {
return NGX_ERROR;
}
addr->opt = *lsopt;
addr->hash.buckets = NULL;
addr->hash.size = 0;
addr->wc_head = NULL;
addr->wc_tail = NULL;
#if (NGX_PCRE)
addr->nregex = 0;
addr->regex = NULL;
#endif
addr->default_server = cscf;
addr->servers.elts = NULL;
return ngx_http_add_server(cf, cscf, addr);
}
这个函数也比较简单:
1. 将ip:port对应的监听地址addr添加至port的addrs数组,这就是我们前面说的每个port可对应多个address;
2. 将当前的虚拟主机结构ngx_http_core_srv_conf_t添加到addr下面的servers数组中,这就是我们说的一个address可以对应多个虚拟主机。
通过上面的过程,我们可以为相关数据结构组织起他们之间的逻辑关系,如下图
上面对listen的指令处理函数就基本分析完成了,接下来我们分析下server_name指令的处理函数
static char *
ngx_http_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
...
value = cf->args->elts;
for (i = 1; i < cf->args->nelts; i++) {
...
sn = ngx_array_push(&cscf->server_names);
if (sn == NULL) {
return NGX_CONF_ERROR;
}
...
sn->name = value[i];
...
}
这个函数主要是将server_name指令后面的各个主机名放到当前虚拟主机配置结构体ngx_http_core_srv_conf_t的server_names数组中。于是,最终形成的结构体的拓扑结构如下图所示:
分析到这里,我们已经将所有的虚拟主机配置信息全部读取到http_core_module的结构体的ports数组中了。在http模块指令处理函数ngx_http_block的最后调用了ngx_http_optimize_servers,对上面的配置信息作了优化,下面具体来看:
static ngx_int_t
ngx_http_optimize_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
ngx_array_t *ports)
{
ngx_uint_t p, a;
ngx_http_conf_port_t *port;
ngx_http_conf_addr_t *addr;
if (ports == NULL) {
return NGX_OK;
}
port = 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);
/*
* check whether all name-based servers have the same
* configuration as a default server for given address:port
*/
addr = port[p].addrs.elts;
for (a = 0; a < port[p].addrs.nelts; a++) {
if (addr[a].servers.nelts > 1
#if (NGX_PCRE)
|| addr[a].default_server->captures
#endif
)
{
if (ngx_http_server_names(cf, cmcf, &addr[a]) != NGX_OK) {
return NGX_ERROR;
}
}
}
if (ngx_http_init_listening(cf, &port[p]) != NGX_OK) {
return NGX_ERROR;
}
}
return NGX_OK;
}
static ngx_int_t
ngx_http_server_names(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
ngx_http_conf_addr_t *addr)
{
#if (NGX_PCRE)
ngx_uint_t regex, i;
regex = 0;
#endif
ngx_memzero(&ha, sizeof(ngx_hash_keys_arrays_t));
ha.temp_pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, cf->log);
if (ha.temp_pool == NULL) {
return NGX_ERROR;
}
ha.pool = cf->pool;
if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK) {
goto failed;
}
cscfp = addr->servers.elts;
// 每个address可能对应多个虚拟主机配置
// 而每个虚拟主机配置则可能会配置多个域名
for (s = 0; s < addr->servers.nelts; s++) {
name = cscfp[s]->server_names.elts;
for (n = 0; n < cscfp[s]->server_names.nelts; n++) {
#if (NGX_PCRE)
if (name[n].regex) {
regex++;
continue;
}
#endif
rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server,
NGX_HASH_WILDCARD_KEY);
if (rc == NGX_ERROR) {
return NGX_ERROR;
}
if (rc == NGX_DECLINED) {
ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
"invalid server name or wildcard \"%V\" on %s",
&name[n].name, addr->opt.addr);
return NGX_ERROR;
}
if (rc == NGX_BUSY) {
ngx_log_error(NGX_LOG_WARN, cf->log, 0,
"conflicting server name \"%V\" on %s, ignored",
&name[n].name, addr->opt.addr);
}
}
}
//这个hash弄出来有什么用呢?
hash.key = ngx_hash_key_lc;
hash.max_size = cmcf->server_names_hash_max_size;
hash.bucket_size = cmcf->server_names_hash_bucket_size;
hash.name = "server_names_hash";
hash.pool = cf->pool;
if (ha.keys.nelts) {
hash.hash = &addr->hash;
hash.temp_pool = NULL;
if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) {
goto failed;
}
}
....
}
上面的逻辑也比较简单,从ports数组开始,每个port下面的每个address的每个server下面配置的每个域名将其使用hash的方式组织起来,便于后续根据域名直接找到对应的保存虚拟主机配置的ngx_http_core_srv_conf_t结构体。
但是有点遗憾的是我尚未完全清楚nginx内部的hash机制是如何实现的,在下一篇博客中我会尝试去弄懂这块并给大家分享。