nginx listen指令浅析之add listen
前言
我们在上篇文章中介绍了address:port
的解析过程,这篇文章继续讲解解析listen
指令的后续过程。
解析listen
指令的函数是 ngx_http_core_listen()
,这个函数的前半部分是解析address:port
,我们在上篇文章中介绍过。后面紧接着就是解析各种 default_server
, recvbuf
等字段,这些很简单,只需要设置ngx_http_listen_opt_t
结构体中的相应字段即可,主要的功能在于下面的 ngx_http_add_listen()
函数。
函数功能
我们知道ngx_http_core_main_conf_t
结构体中有一个ports
字段,这个字段是一个ngx_http_conf_port_t
类型的数组。这个数组保存了当前http
块指令监听的所有端口。ngx_http_add_listen()
函数就是把监听的端口信息保存到这个数组中。
数据结构体
牵涉到的结构体如下:
端口数据结构:
typedef struct {
// 协议族类型,我们只讨论IPV4,所以这里是AF_INET
ngx_int_t family;
// 监听的端口号
in_port_t port;
/* array of ngx_http_conf_addr_t */
ngx_array_t addrs;
} ngx_http_conf_port_t;
address
数据结构:
typedef struct {
ngx_http_listen_opt_t opt;
ngx_hash_t hash;
// 下面三个字段都是哈希表,用来保存当前address:port对应的server_name
// 其中key是server_name对应的字符串
// value 是 ngx_http_core_srv_conf_t 结构体
ngx_hash_wildcard_t *wc_head;
ngx_hash_wildcard_t *wc_tail;
ngx_uint_t nregex;
ngx_http_server_name_t *regex;
/* the default server configuration for this address:port */
ngx_http_core_srv_conf_t *default_server;
/* array of ngx_http_core_srv_conf_t */
ngx_array_t servers;
} ngx_http_conf_addr_t;
源码分析
下面的ngx_http_add_listen()
源码删除了IPV6
和Unix Domain
代码,只保留了IPV4
相关的代码,如下:
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;
struct sockaddr_in *sin;
ngx_http_conf_port_t *port;
ngx_http_core_main_conf_t *cmcf;
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
// 为 ngx_http_core_main_conf_t 结构的 port 分配空间,保存所有的端口
if (cmcf->ports == NULL) {
cmcf->ports = ngx_array_create(cf->temp_pool, 2,
sizeof(ngx_http_conf_port_t));
}
sa = &lsopt->u.sockaddr;
switch (sa->sa_family) {
default: /* AF_INET */
sin = &lsopt->u.sockaddr_in;
p = sin->sin_port; // 拿到当前listen指令的端口号
break;
}
port = cmcf->ports->elts;
// 遍历所有的port,查看该端口号是否已经存在
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 */
// 如果执行到这里,说明之前已经存在了一个协议版本和端口号都相同的listen,这样的话我们应该把当前监听的
// address 加入到已有的port->addrs 数组中,统一管理当前端口对应的 address
return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);
}
/* add a port to the port list */
// 如果当前端口不存在,那么就向 ngx_http_core_main_conf_t->port 中添加一个元素
// 表示一个我们监听了一个新的端口
port = ngx_array_push(cmcf->ports);
if (port == NULL) {
return NGX_ERROR;
}
port->family = sa->sa_family;// port中保存了协议类型,因为我们区分不同的协议
port->port = p;
port->addrs.elts = NULL;
// 将当前listen的address加入到port->addr中
return ngx_http_add_address(cf, cscf, port, lsopt);
}
其实这个函数很简单,具体可以分为以下几个步骤:
- 遍历所有的
ports
,查找是否存在相同协议类型和端口的元素 - 如果存在上述的元素,就调
ngx_http_add_addresses()
- 如果不存在上述元素,就调用
ngx_http_add_address()
上面的两个函数名字也很有意思,
ngx_http_add_addresses()
使用了复数形式的addresses()
,其实也说明了相同的元素已经存在,这里需要添加多个元素。ngx_http_add_address()
使用单数形式的address
,说明添加的应该是第一个元素。
端口不存在
上面我们分析过,如果端口不存在,那么会调用ngx_http_add_address()
函数,代码如下:
//这个函数是将一个listen的addr加入到port->addrs数组中
static ngx_int_t
ngx_http_add_address(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
{
ngx_http_conf_addr_t *addr;
if (port->addrs.elts == NULL) {
if (ngx_array_init(&port->addrs, cf->temp_pool, 4,
sizeof(ngx_http_conf_addr_t))
!= NGX_OK)
{
return NGX_ERROR;
}
}
// 下面的代码就是将listen的address添加到port->addrs数组中.
// 然后初始化这个元素
addr = ngx_array_push(&port->addrs);
addr->opt = *lsopt;
addr->hash.buckets = NULL;
addr->hash.size = 0;
addr->wc_head = NULL;
addr->wc_tail = NULL;
#if (NGX_PCRE)
addr->nregex = 0;
addr->regex = NULL;
#endif
//先给default_server赋一个默认值,也即是当前address:port对应的ngx_http_core_srv_conf_t结构体
// 下面的 ngx_http_add_server会根据 ngx_http_listen_opt_t 结构体的值重新给 default_server赋值.
// 如果我们在listen后面没有设置default_server指令,那么所有相同 address:port 的server的第一个server就是default_server
addr->default_server = cscf;
addr->servers.elts = NULL;
// 下面的函数是将一个ngx_http_core_srv_conf_t 结构体添加到 addr->servers 中
// 由于同一个 address:port 可以对应于多个server,所以这里通过 port->addr->server字段
//将相同的 address:port 对应的所有虚拟主机关联起来
return ngx_http_add_server(cf, cscf, addr);
}
添加server
每个address:port
都可能对应多个server
,ngx_http_add_server
函数就是server
和address:port
对应起来。
static ngx_int_t
ngx_http_add_server(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
ngx_http_conf_addr_t *addr)
{
ngx_uint_t i;
ngx_http_core_srv_conf_t **server;
if (addr->servers.elts == NULL) {
if (ngx_array_init(&addr->servers, cf->temp_pool, 4,
sizeof(ngx_http_core_srv_conf_t *))
!= NGX_OK)
{
return NGX_ERROR;
}
} else {
server = addr->servers.elts;
for (i = 0; i < addr->servers.nelts; i++) {
if (server[i] == cscf) {
// 这一部分的意思就是:防止同一个server块内有两个listen指令
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"a duplicate listen %s", addr->opt.addr);
return NGX_ERROR;
}
}
}
// 下面的代码实在是没有什么意思,就是将 listen 指令所在的 ngx_http_core_srv_conf_t
// 结构体添加到 addr->servers 数组中
server = ngx_array_push(&addr->servers);
*server = cscf;
return NGX_OK;
}
端口已存在
上面我们说过,如果相同的端口已经存在,那么就会调用ngx_http_add_addresses()
函数将当前的端口添加到相应的数组元素中。我们下面看一下这个函数。
再次重申:执行
ngx_http_add_addresses
的时候,表示当前listen
的port
已经存在。
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)
{
u_char *p;
size_t len, off;
ngx_uint_t i, default_server;
struct sockaddr *sa;
ngx_http_conf_addr_t *addr;
/*
* we cannot compare whole sockaddr struct's as kernel
* may fill some fields in inherited sockaddr struct's.
* 不能比较整个sockaddr结构体,因为内核可能在sockaddr中填充了其他的字段。
* 我的理解:不同版本的内核,sockaddr结构体中可能包含不同的字段,所以不能比较整个结构体,只能比较一些必然会存在的字段.
* 这应该也是为了兼容性
*/
sa = &lsopt->u.sockaddr;
switch (sa->sa_family) {
default: /* AF_INET */
// off 字段指向了 sockaddr_in 中 sin_addr 的起始地址
// len 字段说明了 sin_addr 的长度
// 下面我们要比较相同的address是否已经存在了
off = offsetof(struct sockaddr_in, sin_addr);
len = 4;// sin_addr的长度
break;
}
p = lsopt->u.sockaddr_data + off;
addr = port->addrs.elts;
for (i = 0; i < port->addrs.nelts; i++) {
// 遍历当前port所有的addrs,查找是否存在相同 address
if (ngx_memcmp(p, addr[i].opt.u.sockaddr_data + off, len) != 0) {
continue;
}
/* the address is already in the address list */
// 如果address已经存在,也就是说 address 和 port 全部相同
// 那么我们就只需把 ngx_http_core_srv_conf_t 添加到 addrs->servers
// 数组中即可
if (ngx_http_add_server(cf, cscf, &addr[i]) != NGX_OK) {
return NGX_ERROR;
}
/* preserve default_server bit during listen options overwriting */
// 看一下当前遍历到的listen指令是否设置了 default_server
default_server = addr[i].opt.default_server;
if (lsopt->set) {
// 这里的作用我们在 关于listen指令中的bind和set.note 文章中已经说过了
if (addr[i].opt.set) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"duplicate listen options for %s", addr[i].opt.addr);
return NGX_ERROR;
}
addr[i].opt = *lsopt;
}
/* check the duplicate "default" server for this address:port */
if (lsopt->default_server) {
if (default_server) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"a duplicate default server for %s", addr[i].opt.addr);
return NGX_ERROR;
}
default_server = 1;
addr[i].default_server = cscf;
}
addr[i].opt.default_server = default_server;
return NGX_OK;
}
/* add the address to the addresses list that bound to this port */
// 如果执行到这里,说明该address尚不存在,所以新建一个address
return ngx_http_add_address(cf, cscf, port, lsopt);
}
函数的关系
上面的几个函数错综复杂,容易弄混,我对这几个函数的功能进行了一些总结。
ngx_http_add_addresses()
函数的功能呢:
遍历当前端口
addrs
数组的所有元素, 查看是否有addrs
元素和当前的address
相同
1、如果不存在相同的address
元素,则调用ngx_http_add_address()
完成如下功能:
- 将当前
listen
指令的address
作为一个新元素添加到port->addrs
数组中 - 调用
ngx_http_add_server()
函数完成如下功能
如果调用ngx_http_add_server()
则说明address
和port
都是相同的,那么我们应该将当前address:port
对应的ngx_http_core_srv_conf_t
结构体添加到port->addrs->servers
数组中。
2、如果存在相同的
address
元素,那么就调用ngx_http_add_server()
函数完成功能。
测试用例
该配置文件仅用于测试listen
指令
user root;
daemon on;
worker_processes 1;
master_process on;
events {
use epoll;
worker_connections 1024;
multi_accept on;
accept_mutex on;
}
http{
# server_1,和server_2监听的 ip:port 相同
server {
listen 127.0.0.1:8088;
server_name first;
location / {
root first;
}
}
# server_2,和server_1监听的 ip:port 相同
server {
listen 127.0.0.1:8088;
server_name second;
location / {
root second;
}
}
# 先配置一个虚拟ip: ifconfig eth0:1 192.168.10.5 netmask 255.255.255.0
# server_3,和server_1监听的 port 相同,但是ip不同
server {
listen 192.168.10.5:8088;
server_name third;
location / {
root third;
}
}
# server_4,和server_3监听的 ip:port 相同
server {
listen 192.168.10.5:8088;
server_name fourth;
location / {
root fourth;
}
}
# server_5
server {
listen 127.0.0.1:8089;
server_name fiveth;
location /{
root fiveth;
}
}
# server_6 与server_5监听的端口相同,ip不同
server {
listen 192.168.10.5:8089;
server_name sixth;
location /{
root sixth;
}
}
# server_7 与server_5监听的端口相同,但是监听全部ip
server {
listen 8089;
server_name seventh;
location / {
root seventh;
}
}
}