Nginx源码剖析--ngx_http_optimize_servers函数分析
前言
本章将继续介绍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建立哈希表来管理,方便请求的匹配。