概述
如下配置,设置了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
结构体,前者主要保存了三个指针数组,存储相关模块的配置。后者存储了虚拟主机相关的配置,例如:listen
和 server_name
的配置。
listen
对应的处理函数为ngx_http_core_listen
,listen
的变量解析到ngx_http_listen_opt_t
结构体中,调用ngx_http_add_listen
添加到http对应配置结构体ngx_http_core_main_conf_t
的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) { 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
,该函数会遍历cycle
的listening
数组调用socket-bind-listen
一系列函数监听端口。
虽然此时已经监听端口,有连接请求过来还是不能建立连接,因为对应的sd还没有添加到epoll中,nginx还不能响应可读事件。ngx会调用事件模块的函数ngx_event_process_init
,遍历cycle
的listening
数组,添加读事件到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_t
的srv_conf
指针,让其指向查找到的虚拟主机ngx_http_core_srv_conf_t
的ctx
指向的srv_conf
指针数组。
转载请注明:爱开源 » nginx 虚拟主机 源码解析