最新消息:

nginx 虚拟主机 源码解析

nginx admin 3378浏览 0评论

概述

如下配置,设置了4个虚拟主机分别是aa.com、bb.com、cc.com和dd.com。都绑定到80端口。其中aa.com和bb.com 绑定本地回环地址,cc.com和dd.com绑定外网地址。

通过该配置文件结合代码来思考以下问题:

  • 那么nginx是怎么存储地址端口和虚拟主机的,他们之间的关系又是怎么样的呢?
  • 当通过127.0.0.1:80 host是aa.com 又是怎么查找的对应虚拟主机的相关配置的?
  • 通过外网地址是否可以访问aa.com的这个域名呢?
http {
    access_log  logs/access.log;
    
    server {
        listen 127.0.0.1:80;
        server_name aa.com;
        ......
        location / {
            root html;
            index index.html;
        }
    }
    
    server {
        listen 127.0.0.1:80;
        server_name bb.com;
        ......
        location / {
            root html;
            index index.html;
        }
    }
    
    server {
        listen 192.168.23.11:80;
        server_name cc.com;
        
        location / {
            root html;
            index index.html;
        }
    }
    
    server {
        listen 192.168.23.11:80;
        server_name dd.com;
        
        location / {
            root html;
            index index.html;
        }
    }
}

 

解析配置

每配置一个server指令就对应配置了一个虚拟主机,对应源码的处理函数是ngx_http_core_server,每一个虚拟主机都对应一个ngx_http_conf_ctx_t结构体和一个ngx_http_core_srv_conf_t结构体,前者主要保存了三个指针数组,存储相关模块的配置。后者存储了虚拟主机相关的配置,例如:listenserver_name的配置。
listen 对应的处理函数为ngx_http_core_listenlisten的变量解析到ngx_http_listen_opt_t结构体中,调用ngx_http_add_listen添加到http对应配置结构体ngx_http_core_main_conf_tports数组中,具体看下代码:

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;
    ngx_http_conf_port_t       *port;
    ngx_http_core_main_conf_t  *cmcf;

    // cmcf 是 http对应的核心配置,一个http对应一个,所以根据上述配置文件启动的ngx仅有一个
    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;
        }
    }

    // p 是 listen的端口,sa 有对应的协议:ipv4还是ipv6。
    sa = &lsopt->sockaddr.sockaddr;
    p = ngx_inet_get_port(sa);

    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 已经添加到了cmcf->ports数组中,
        // 检查addr 是否已经添加到port->addrs数组中,
        // 如果已经添加则调用ngx_http_add_server函数添加虚拟主机到addr->servers数组
        // 如果每有添加则调用ngx_http_add_address函数添加addr。
        return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);
    }

    /* add a port to the port list */
    // 添加port到cmcf->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;
    // 添加addr到port->addrs数组,添加server到addr->servers数组。
    return ngx_http_add_address(cf, cscf, port, lsopt);
}

通过上述代码可知,上述配置文件的组织形式是:

port(80)->addr(127.0.0.1)->server(aa.com)
       |                |->server(bb.com)
       |->addr(192.168.23.11)->server(cc.com)
                            |->server(dd.com)

 

上述形式也不是最终的组织形式,是nginx启动的中间过程形式,ports数组是分配在临时内存上的,cmcf->ports = ngx_array_create(cf->temp_pool, 2, sizeof(ngx_http_conf_port_t));程序启动后会回收该临时内存。

在处理http配置对应的函数ngx_http_block中会调用ngx_http_optimize_servers继续完善上述说述的ports、addrs、servers。

该函数ngx_http_optimize_servers会检测每个addr的servers数组是否大于1,也就是说如果该ip:port绑定了多个虚拟主机,则调用ngx_http_server_names函数,把该addr->servers数组所有的虚拟主机初始化为一个hash表保存到addr->hash上。支持前置通配符的保存到addr->wc_head,后置通配符的保存到addr->wc_tail,正则表达式的保存到addr->regex

初始化好虚拟主机hash表后,接着会调用ngx_http_init_listening函数初始化监听结构体,该函数绑定ip:port时会判断一下*:port这种情况,如果有这种配置的,需要bind这个忽略指定了IP的其他的配置。bind前先调用ngx_http_add_listening函数在cycle结构体的listening数组添加该ip:port对应的数据结构,类型为ngx_listening_t,设置可读事件的回调函数指针handler指为ngx_http_init_connection函数,设置该listening的servers为包含的addr以及对应的虚拟主机,对应的数据结构为ngx_http_port_t结构体,具体设置调用了ngx_http_add_addrs函数,如下:

static ngx_int_t
ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport,
    ngx_http_conf_addr_t *addr)
{
    ngx_uint_t                 i;
    ngx_http_in_addr_t        *addrs;
    struct sockaddr_in        *sin;
    ngx_http_virtual_names_t  *vn;

    // addrs 数组包含了addr以及对应的虚拟主机
    hport->addrs = ngx_pcalloc(cf->pool,
                               hport->naddrs * sizeof(ngx_http_in_addr_t));
    if (hport->addrs == NULL) {
        return NGX_ERROR;
    }

    addrs = hport->addrs;

    for (i = 0; i < hport->naddrs; i++) {

        sin = &addr[i].opt.sockaddr.sockaddr_in;
        addrs[i].addr = sin->sin_addr.s_addr;
        addrs[i].conf.default_server = addr[i].default_server;
        ......
        addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;

        if (addr[i].hash.buckets == NULL
            && (addr[i].wc_head == NULL
                || addr[i].wc_head->hash.buckets == NULL)
            && (addr[i].wc_tail == NULL
                || addr[i].wc_tail->hash.buckets == NULL)
#if (NGX_PCRE)
            && addr[i].nregex == 0
#endif
            )
        {
            continue;
        }

        vn = ngx_palloc(cf->pool, sizeof(ngx_http_virtual_names_t));
        if (vn == NULL) {
            return NGX_ERROR;
        }

        // addr 下的虚拟主机
        addrs[i].conf.virtual_names = vn;

        vn->names.hash = addr[i].hash;
        vn->names.wc_head = addr[i].wc_head;
        vn->names.wc_tail = addr[i].wc_tail;
#if (NGX_PCRE)
        vn->nregex = addr[i].nregex;
        vn->regex = addr[i].regex;
#endif
    }

    return NGX_OK;
}

 

通过上述可知,端口和虚拟主机的最终组织形式是:

cycle->listening->ls(127.0.0.1:80)->servers->addr(127.0.0.1)->conf.vn(aa.com)
               |                                           |->conf.vn(bb.com)
               |->ls(192.168.23.11:80)->servers->addr(192.168.23.11)->conf.vn(cc.com)
                                                                   |->conf.vn(dd.com)

监听端口并添加事件处理

至此,nginx还没开始监听端口,nginx在启动过程中接着会调用ngx_open_listening_sockets,该函数会遍历cyclelistening数组调用socket-bind-listen一系列函数监听端口。

虽然此时已经监听端口,有连接请求过来还是不能建立连接,因为对应的sd还没有添加到epoll中,nginx还不能响应可读事件。ngx会调用事件模块的函数ngx_event_process_init,遍历cyclelistening数组,添加读事件到epoll中,对应的回调函数为ngx_event_accept。至此,nginx服务算是启动完毕,可以提供http服务了。

接收请求并查找虚拟主机

启动以后nginx会循环调用ngx_process_events_and_timers处理网络事件和定时器事件。处理网络事件其实就是调用epoll_wait可读可写事件的回调函数。当有连接请求到达时回调ngx_event_accept接受连接并调用ngx_listening_t设置的回调函数ngx_http_init_connection初始化http连接ngx_http_connection_t,并调用ngx_http_process_request_line函数,在该函数中又调用了ngx_http_parse_request_line解析http请求,根据解析的host调用ngx_http_set_virtual_server查找并设置虚拟主机,设置虚拟主机就是设置ngx_http_request_tsrv_conf指针,让其指向查找到的虚拟主机ngx_http_core_srv_conf_tctx指向的srv_conf指针数组。

FROM:https://github.com/vislee

转载请注明:爱开源 » nginx 虚拟主机 源码解析

您必须 登录 才能发表评论!