nginx虚拟主机实现原理

概述

本篇博客我们将阐述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机制是如何实现的,在下一篇博客中我会尝试去弄懂这块并给大家分享。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值