欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Nginx源码剖析--ngx_http_optimize_servers函数分析

程序员文章站 2022-07-15 15:50:49
...

前言

本章将继续介绍HTTP模块初始化函数:ngx_http_block中的内容。将会涉及到server块的组织,监听端口的管理,以及ip地址和server块之间的组织关系。下面我们将从listen关键字说起,然后根据listen配置项以及它的解析函数了解nginx组织server块和监听端口的过程。最后在介绍ngx_http_optimize_servers函数。所有这些工作都是为了实现Nginx的虚拟主机功能。看看nginx是怎么样使得每个请求可以迅速根据它的host,Ip匹配到它对应的虚拟主机server块的。

listen配置项

listen配置项在ngx_http_core_module中:

    { ngx_string("listen"),
      NGX_HTTP_SRV_CONF|NGX_CONF_1MORE,
      ngx_http_core_listen,
      NGX_HTTP_SRV_CONF_OFFSET,
      0,
      NULL },

因此可以看到,它只能存在与server块中。它可以包含读个参数,具体地:

listen 127.0.0.1:8000;
listen 127.0.0.1;
listen 8000;
listen *:8000;
listen localhost:8000;

主要是以listen [addr:] port的格式。
除此之外,他还可以有以下的格式:

listen 127.0.0.1 default_server accept_filter=dataready backlog=1024;

这个主要是对监听端口的属性的设置参数。

listen的配置指令函数是:

static char *
ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

也就是,当conf_parse函数解析完listen指令之后,就会调用这个函数处理这个指令以及指令对应的参数,其中,指令参数存储在

cf->args->elts

中。下面我们来解析ngx_http_core_listen函数。

首先调用ngx_parse_url函数解析listen的第一个参数,主要是为了解析出addr:port。

    u.url = value[1];
    u.listen = 1;
    u.default_port = 80;

    if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
    ....

这个函数执行结束,一般会初始化完成u。

然后nginx会根据listen的其他参数(除addr,port)和前面初始化过的u来初始化:

ngx_http_listen_opt_t   lsopt;

这部分主要是在下面的一个大for循环中完成:

   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;
    .....
    for (n = 2; n < cf->args->nelts; n++) {
       ....

最后,就是用这个lsopt初始化cmcf->ports 了。也就是ngx_http_core_module模块的main_conf结构体中的ports成员。这部分在

ngx_int_t
ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_listen_opt_t *lsopt)

函数中完成。下面分析一下这个函数。

ngx_http_add_listen函数解析

前面的所有工作本质上是解析listen的各个参数,并将这些参数封装在lsopt中。ngx_http_add_listen的工作就是完成将这个listen对应的数据组织到HTTP模块的组织结构中。

    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;
        }
    }

从这里可以看到,主要是用lsopt初始化cmcf->ports。从这段代码中我们还可以发现,cmcf->ports数组的元素类型是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;

也就是说,每个端口可以对应多个地址:


Nginx源码剖析--ngx_http_optimize_servers函数分析

后面就是一个for循环了:

    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 */

        return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);
    }

这个for循环是为了查找cmcf->ports中是否已经存在和当前的端口一样的监听端口了。如果已经存在这个端口的话,就不需要改变ports数组,只需要往对应ports[i]的addrs数组里面添加当前port对应的地址就可以了:

return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);

这个函数后面再看。
如果ports数组中没有和port相同的监听端口,则

    /* 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最新被加入ports数组中,因此ports数组中对应的这个port的addrs数组是空的。后面还需要根据lsopt将该listen对应的addr加入到这个port对应的addrs数组中:

 return ngx_http_add_address(cf, cscf, port, lsopt);

注意,这里的ngx_http_add_address和之前哪个ngx_http_add_addresses的区别。ngx_http_add_address是将addr加入一个还没有任何元素的addrs数组中;而ngx_http_add_addresses是将一个addr加入到一个已经存在其他元素的数组中。

ngx_http_add_addresses函数解析

这个函数的逻辑和ngx_http_add_listen的逻辑很相似。它的原型是:

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)

它的功能是将lsopt中的addr加入到port的addrs数组中。主体实现在一个大for循环中:

p = lsopt->u.sockaddr_data + off;
addr = port->addrs.elts;
for (i = 0; i < port->addrs.nelts; i++) {
    ....
}

for循环主要是为了检查p是否在addrs数组中。如果存在则执行for循环后面的语句,

if (ngx_http_add_server(cf, cscf, &addr[i]) != NGX_OK) {
            return NGX_ERROR;
        }
        ....

这里我们又可以看到addrs数组中的元素是属于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_wildcard_t       *wc_tail;

#if (NGX_PCRE)
    ngx_uint_t                 nregex;
    ngx_http_server_name_t    *regex;
#endif

    /* 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;

在这里我们只关注ngx_array_t servers;项。
所以我们可以对上面的插图做进一步的细化:


Nginx源码剖析--ngx_http_optimize_servers函数分析

这里的server表示的是配置文件中的server块。也就是说,每个地址可以对应多个server块。综上,每个addr:port可以在多个server块中出现。这样子的话,那对于一个对addr:port的请求,怎么知道用哪个server块来处理呢?这就和server name有关了。这就实现了一个ip:port可以对应到多台server虚拟机处理。虚拟机根据请求的host和server name来匹配,当然,一个server块中可以有多个server name。

这里注意的是,传入ngx_http_add_addresses参数cscf就是对应当前server块。因此下一步就是将这个server块加入到servers数组中。这些由函数ngx_http_add_server完成。

     .....
    server = ngx_array_push(&addr->servers);
    if (server == NULL) {
        return NGX_ERROR;
    }

    *server = cscf;

很简单了。当然前面要检查保证servers不存在这个server块,也就是servers不允许有重复的server块。这种情况应该只在一个server块中重复了多个相同的listen指令才会造成。

讲完ngx_http_add_addresses函数,ngx_http_add_address就很简单了,这里就不赘述了。

下面进入正题,ngx_http_optimize_servers函数的解析。

ngx_http_optimize_servers函数源码解析

这个函数主要做三件事情:

1. 对ports数组中的每个元素对应的addrs数组排序,使得addrs有如下形式:


Nginx源码剖析--ngx_http_optimize_servers函数分析

也就是将wildcard属性(*:port)的都排在addrs数组的最后,bind属性的都排在最前面。

2. 将addrs数组中每个元素对应的servers数组根据它的server_name组织到哈希表中。还记得ngx_http_conf_addr_t中有一个 ngx_hash_t hash;成员。 这个成员就是组织servers中元素的哈希表。目的是为了可以快速根据请求的host找到对应的server块。需要注意的是,每个server块可能对应多个server_name:

    for (s = 0; s < addr->servers.nelts; s++) { //对当前的ip:port的所有server块按照server_name组织到hash表中,hash表在addr->hash中

        name = cscfp[s]->server_names.elts;

        for (n = 0; n < cscfp[s]->server_names.nelts; n++) {

3.调用ngx_http_init_listening函数,根据listen的信息初始化套接字。这也是我们这一节要讲解的关键。

ngx_http_init_listening函数源码分析

函数的原型如下

static ngx_int_t
ngx_http_init_listening(ngx_conf_t *cf, ngx_http_conf_port_t *port)

简单地说,这个函数就是根据port->addrs初始化套接字。

首先判断addrs中是否有wildcard属性的地址存在。

    if (addr[last - 1].opt.wildcard) {
        addr[last - 1].opt.bind = 1; 
        bind_wildcard = 1; //对所有的ip地址,这个端口都会被监听  *:port

    } else {
        bind_wildcard = 0;
    }

下面进入一个大while循环,while循环将用每个bind属性的ip:port初始化一个监听套接字;而对于所有的wildcard属性的*:port则只用来初始化一个监听套接字,这也是当然的。

这里主要是看一下怎么初始化监听套接字(ngx_listening_t)的。而这里我们更主要关注的是监听套接字结构体的servers成员是怎么被初始化的:

ls->servers = hport;

servers是属于ngx_http_port_t类型。对serves的初始化主要是在ngx_http_add_addrs中完成。总的来说,对于每个用bind类型的ip:port初始化的servers它具有如下形式:


Nginx源码剖析--ngx_http_optimize_servers函数分析

对于bind属性naddrs=1,对于wildbind naddrs >=1。

nginx通过调用

static ngx_int_t
ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport,
    ngx_http_conf_addr_t *addr)

将addr的信息加入到hport中,也就是加入到ls->server中。这样,这个套接字中就有了该addr:port对应的所有信息了,包括它的server块:

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

        addrs[i].conf.virtual_names = vn; //addrs = hport->addrs;

        vn->names.hash = addr[i].hash; 

也就是说,当该监听套接字接收到新建连接时,就可以知道该连接应该交由哪些server块来处理,而具体由这些server块中的哪个server块处理,则需要根据请求的host来匹配server块的server_name来决定。

在ngx_http_init_listening函数调用ngx_http_add_listening创建新的监听结构体时,会设置监听套接字在接收到新建连接时的对连接初始化函数:

//ngx_http_add_listening
ls->handler = ngx_http_init_connection;

而在ngx_http_init_connection函数中,会设置新建连接的c->data成员使其包含处理该连接的那些server块:

//ngx_http_init_connection.
.....
ngx_http_connection_t  *hc;
....
c->data = hc;
....
port = c->listening->servers;
.....
addr = port->addrs;
.....
hc->addr_conf = &addr[0].conf;
.....

前面可以知道ls->serves->addrs中包含了该监听套接字关联的所有虚拟主机对应的server块,因此这里就相当于把处理这个连接的虚拟主机列表都传给了connection结构体。后面对于该连接上的http 请求,只需呀根据它的host匹配server_name,选择合适的虚拟机就可以了。

总结

本篇博文介绍了nginx虚拟主机的实现机制。虚拟主机的功能是根据请求的host不同选择不同的server块来处理请求,即使这些请求是来自同一条TCP连接。nginx实现虚拟主机的方式是为每个server块关联一个server_name,根据server_name和host的匹配来给请求寻找合适的server块来处理请求。而TC连接是由ip:port来标识的,这在nginx中是通过server块里面的listen指令来完成。相同的ip和port可以对应不同的server块,这些server块的server_name不同。这些server块根据server_name建立哈希表来管理,方便请求的匹配。