Nginx源码剖析--ngx_http_optimize_servers函数分析
2017-12-03 00:13
656 查看
前言
本章将继续介绍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;
也就是说,每个端口可以对应多个地址:
后面就是一个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;项。
所以我们可以对上面的插图做进一步的细化:
这里的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有如下形式:
也就是将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它具有如下形式:
对于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建立哈希表来管理,方便请求的匹配。相关文章推荐
- Nginx源码分析 - HTTP模块篇 - ngx_http_optimize_servers函数和TCP连接建立过程
- Nginx源码分析 - HTTP模块篇 - ngx_http_wait_request_handler函数和HTTP Request解析过程
- nginx源码剖析(三) —— ngx_queue_t分析
- nginx源码剖析(二) —— ngx_list_t分析
- nginx源码分析-ngx_http_phases说明
- Nginx源码分析 - HTTP模块篇 - ngx_http_block函数和HTTP模块的初始化
- Nginx源码剖析--ngx_cycle_s结构体分析
- 菜鸟nginx源码剖析数据结构篇(五) 基数树 ngx_radix_tree_t
- 菜鸟nginx源码剖析数据结构篇(四)红黑树ngx_rbtree_t
- 菜鸟nginx源码剖析数据结构篇(九) 内存池ngx_pool_t
- nginx源码分析—内存池结构ngx_pool_t及内存管理(精辟)
- Nginx源码分析-ngx_htttp_footer_filter_module
- nginx源码分析—hash结构ngx_hash_t(v1.0.4)
- Nginx源码剖析--ngx_cycle_t的初始化
- nginx源码分析—链表结构ngx_list_t
- nginx源码剖析-红黑树ngx_rbtree_t(添加lookup操作)
- Nginx源码分析与实践---ngx_command_t
- http框架核心之ngx_http.c源码分析
- Nginx源码分析---内存池结构ngx_pool_t及内存管理
- nginx源码分析—链表结构ngx_list_t