nginx監聽事件流程
在前面的幾篇文章中已經分析了master程序、work程序的初始化流程。但一直沒有分析監聽socket的建立流程,nginx伺服器只有在建立socket, 繫結socet,監聽socket執行完成後,才能處理來自客戶端的連線。ngx_cycle_t結構中有一個listening成員,存放的就是所有監聽socket。接下來首先分析socket內部結構的維護,不同域名的管理,然後分析什麼時候把監聽socket新增到listening陣列中,最後分析何時建立監聽socket。
一、socket內部結構管理
static ngx_command_t ngx_http_core_commands[] = { { ngx_string("listen"), NGX_HTTP_SRV_CONF|NGX_CONF_1MORE, ngx_http_core_listen, NGX_HTTP_SRV_CONF_OFFSET, 0, NULL }, }
在ngx_http_core_module模組的命令列表中,listen命令的實現函式為ngx_http_core_listen。nginx.conf配置檔案中,每一個server塊都可以監聽不同的埠,listen命令函式的作用就一個,就是為了維護socket的內部結構。如果直接分析原始碼,則不好表達,也不太好理解。還是以一個例子來說明socket內部結構的維護吧!
http { #server1,監聽80埠,ip為192.168.100.1 server { listen 192.168.100.1:80; server_name www.server1.com; } #server2,監聽80埠,ip位192.168.100.2 server { listen 192.168.100.2:80; server_name www.server2.com; } #server3,監聽80埠,ip位192.168.100.1 server { listen 192.168.100.1:80; server_name www.server3.com; } #server4,監聽90埠,ip位192.168.100.4 server { listen 192.168.100.4:90; server_name www.server4.com; } }
假設有這樣一個nginx.conf配置檔案。192.168.100.1 ---192.168.100.2這兩個ip監聽同一個埠80, 而 192.168.100.4監聽埠90。nginx一共監聽了2個埠,則nginx維護的內部結構如下圖:
在這張圖中,192.168.100.1 ---192.168.100.2這兩個ip監聽埠80。192.168.100.1:80這個socket對應有www.server1.com; www.server3.com共兩個域名。 192.168.100.2:80這個socket對應有www.server2.com共一個域名。
192.168.100.4這個ip監聽90埠,對應的域名為www.server4.com
nginx維護這樣的結構是為了做什麼? 假設nginx在192.168.100.1 :80這個socket收到來自客戶端的連線,http請求頭部的host=www.server3.com。則查詢過程如下:
(1)先在ports陣列中查詢80埠對應的結構ngx_http_conf_port_t;
(2)接著查詢ngx_http_conf_port_t中的addrs陣列,從而獲取到192.168.100.1所在的ngx_http_conf_addr_t;
(3)然後查詢ngx_http_conf_addr_t結構中的servers陣列,從而獲取得到www.server3.com域名所在的server塊。
從這個例子可以看出,即便來自同一個ip:port的客戶端連線,由於請求域名的不同,從而會獲取到不同的server塊進行處理。而listen命令的函式ngx_http_core_listen就是用來維護這樣的一種socket結構
//解析listen命令
static char * ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
cscf->listen = 1; //表示server塊配置了listen配置項
value = cf->args->elts;
u.url = value[1];
u.listen = 1;
u.default_port = 80;
//解析listen的引數1,獲取監聽的ip地址以及埠資訊
ngx_parse_url(cf->pool, &u);
ngx_memcpy(&lsopt.u.sockaddr, u.sockaddr, u.socklen);
//給監聽選項結構賦值預設值
lsopt.socklen = u.socklen;
lsopt.backlog = NGX_LISTEN_BACKLOG;
lsopt.rcvbuf = -1;
lsopt.sndbuf = -1;
//將struct sockaddr結構轉為點分十進位制的ip地址格式,例如172.16.3.180
(void) ngx_sock_ntop(&lsopt.u.sockaddr, lsopt.addr,
NGX_SOCKADDR_STRLEN, 1);
//listen配置的引數儲存到監聽選項結構
for (n = 2; n < cf->args->nelts; n++)
{
//設定預設的server塊
if (ngx_strcmp(value[n].data, "default_server") == 0
|| ngx_strcmp(value[n].data, "default") == 0)
{
lsopt.default_server = 1;
continue;
}
//設定監聽佇列大小
if (ngx_strncmp(value[n].data, "backlog=", 8) == 0)
{
lsopt.backlog = ngx_atoi(value[n].data + 8, value[n].len - 8);
lsopt.set = 1;
lsopt.bind = 1;
if (lsopt.backlog == NGX_ERROR || lsopt.backlog == 0)
{
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid backlog \"%V\"", &value[n]);
return NGX_CONF_ERROR;
}
continue;
}
}
//將server塊新增到監聽埠中,表示有多少個server塊在監聽同一個埠
if (ngx_http_add_listen(cf, cscf, &lsopt) == NGX_OK)
{
return NGX_CONF_OK;
}
return NGX_CONF_ERROR;
}
而ngx_parse_url函式是從listen配置項格式中提取到需要監聽的ip地址,埠號等資訊。
//根據listen命令格式"listen ip:port"獲取ip, port,儲存到u對應的成員中
ngx_int_t ngx_parse_url(ngx_pool_t *pool, ngx_url_t *u)
{
u_char *p;
p = u->url.data;
//格式: listen unix:/var/run/nginx.sock
if (ngx_strncasecmp(p, (u_char *) "unix:", 5) == 0)
{
return ngx_parse_unix_domain_url(pool, u);
}
if ((p[0] == ':' || p[0] == '/') && !u->listen)
{
u->err = "invalid host";
return NGX_ERROR;
}
//格式: listen [fe80::1];
if (p[0] == '[')
{
return ngx_parse_inet6_url(pool, u);
}
//根據url獲取ipv4格式的地址資訊()
return ngx_parse_inet_url(pool, u);
}
ngx_http_add_listen這個函式開始就是要建立圖中的ports陣列內容了,使得nginx能監聽不同的埠。
//將server塊新增到監聽埠中,表示有多少個server塊在監聽同一個埠
ngx_int_t ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
ngx_http_listen_opt_t *lsopt)
{
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));
}
sa = &lsopt->u.sockaddr;
//獲取監聽埠
switch (sa->sa_family)
{
default: /* AF_INET */
sin = &lsopt->u.sockaddr_in;
p = sin->sin_port;
break;
}
//查詢是否存在相同的監聽埠,相同則只新增server塊
port = cmcf->ports->elts;
for (i = 0; i < cmcf->ports->nelts; i++)
{
if (p != port[i].port || sa->sa_family != port[i].family)
{
continue;
}
//查詢到有多個ip監聽同一個埠時的處理邏輯
return ngx_http_add_addresses(cf, cscf, &port[i], lsopt);
}
//指向到此,說明沒有查詢到相同的監聽埠,則需要建立一個埠
port = ngx_array_push(cmcf->ports);
port->family = sa->sa_family;
port->port = p;
port->addrs.elts = NULL;
//如果有多個不同的ip監聽同一個埠,則需要把這些ip資訊保持到ngx_http_conf_port_t結構中的addrs陣列中
return ngx_http_add_address(cf, cscf, port, lsopt);
}
如果有多個不同的ip監聽同一個埠,則需要把這些ip資訊保持到ngx_http_conf_port_t結構中的addrs陣列中。ngx_http_add_address函式完成這個功能
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)
{
if (port->addrs.elts == NULL)
{
ngx_array_init(&port->addrs, cf->temp_pool, 4, sizeof(ngx_http_conf_addr_t)
}
//同一個監聽埠,但對於不同的ip邏輯,則需要建立一個ngx_http_conf_addr_t結構。
//表示有多個ip地址監聽同一個埠
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;
addr->default_server = cscf; //多個server塊監聽同一個埠時,有一個預設的server塊
addr->servers.elts = NULL;
//將監聽同一個埠的server塊加入到server陣列中
return ngx_http_add_server(cf, cscf, addr);
}
最終將將監聽ip:port的server儲存到陣列中,則是由ngx_http_add_server這個函式完成。
//將監聽同一個埠的server塊加入到server陣列中
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)
{
ngx_array_init(&addr->servers, cf->temp_pool, 4, sizeof(ngx_http_core_srv_conf_t *)
}
else
{
//查詢是否同一個server塊兩次呼叫listen監聽同一個埠
//在同一個server塊中呼叫兩次listen監聽同一個埠是不允許的;例如:
//listen 80; listen 80;是非法的
server = addr->servers.elts;
for (i = 0; i < addr->servers.nelts; i++)
{
if (server[i] == cscf)
{
return NGX_ERROR;
}
}
}
//獲取一個server塊
server = ngx_array_push(&addr->servers);
if (server == NULL)
{
return NGX_ERROR;
}
//儲存server塊,表示這個server塊監聽ip:portsocket
*server = cscf;
}
到此為止,nginx對於監聽socket維護的內部結構已經分析完成了。接下里分析下nginx伺服器對不同域名的管理
二、域名管理
在解析http配置塊中,呼叫了ngx_http_optimize_servers函式,將把各個監聽埠下的所有域名加入到相應雜湊表中(例如:普通雜湊表,前置萬用字元雜湊表,後置萬用字元雜湊表)。這樣nginx在收到客戶端連線請求時,就可以直接根據http請求頭部host欄位查詢這些雜湊表,從而獲取到server塊,由這個server塊處理這個http請求。
//開始解析http塊
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//功能: 將所有監聽socket對應的伺服器名加入到雜湊表中。例如:有80?90兩個埠。則將80埠所有server塊下的
//所有伺服器名加入到雜湊表;將90埠所有server塊下的所有伺服器名加入到雜湊表
if (ngx_http_optimize_servers(cf, cmcf, cmcf->ports) != NGX_OK)
{
return NGX_CONF_ERROR;
}
}
ngx_http_optimize_servers這個函式負責將各個監聽埠下的所有域名加入到相應雜湊表中;該函式同時也會建立一個監聽物件
//功能: 將所有監聽socket對應的伺服器名加入到雜湊表中。例如:有80?90兩個埠。則將80埠所有server塊下的
//所有伺服器名加入到雜湊表;將90埠所有server塊下的所有伺服器名加入到雜湊表
static ngx_int_t ngx_http_optimize_servers(ngx_conf_t *cf,
ngx_http_core_main_conf_t *cmcf, ngx_array_t *ports)
{
//對於每一個監聽埠,都會有對應多個server塊監聽這個埠。這每一個server塊又可能有多少server名稱。
//下面這個迴圈處理每一個埠,構成一個雜湊表
port = ports->elts;
for (p = 0; p < ports->nelts; p++)
{
//將把監聽同一個埠的所有ip資訊排序
ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts,
sizeof(ngx_http_conf_addr_t), ngx_http_cmp_conf_addrs);
addr = port[p].addrs.elts;
for (a = 0; a < port[p].addrs.nelts; a++)
{
//處理監聽同一個埠的所有server塊,構成一個雜湊表
if (ngx_http_server_names(cf, cmcf, &addr[a]) != NGX_OK)
{
return NGX_ERROR;
}
}
//建立ngx_listening_s物件,並用port陣列給這個物件賦值
if (ngx_http_init_listening(cf, &port[p]) != NGX_OK)
{
return NGX_ERROR;
}
}
}
函式ngx_http_server_names將建立由所有域名構成的普通雜湊表,前置萬用字元雜湊表、後置萬用字元雜湊表。這樣在收到來自客戶端的請求時,可以根據http頭部的host欄位查詢這些雜湊表,進而獲取到server塊。
//建立服務名雜湊表
static ngx_int_t ngx_http_server_names(ngx_conf_t *cf,
ngx_http_core_main_conf_t *cmcf,
ngx_http_conf_addr_t *addr)
{
//建立一個雜湊陣列
if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK)
{
goto failed;
}
cscfp = addr->servers.elts;
//多個server塊監聽同一個埠
for (s = 0; s < addr->servers.nelts; s++)
{
name = cscfp[s]->server_names.elts;
//每一個server塊又可能有多個伺服器名
for (n = 0; n < cscfp[s]->server_names.nelts; n++)
{
//將伺服器名加入到雜湊陣列中
rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server,NGX_HASH_WILDCARD_KEY);
}
}
//建立普通雜湊表
if (ha.keys.nelts)
{
hash.hash = &addr->hash;
hash.temp_pool = NULL;
if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK)
{
goto failed;
}
}
//建立前置雜湊表
if (ha.dns_wc_head.nelts)
{
ngx_qsort(ha.dns_wc_head.elts, (size_t) ha.dns_wc_head.nelts,
sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);
ngx_hash_wildcard_init(&hash, ha.dns_wc_head.elts, ha.dns_wc_head.nelts);
addr->wc_head = (ngx_hash_wildcard_t *) hash.hash;
}
//建立後置萬用字元雜湊表
if (ha.dns_wc_tail.nelts)
{
ngx_hash_wildcard_init(&hash, ha.dns_wc_tail.elts,ha.dns_wc_tail.nelts);
addr->wc_tail = (ngx_hash_wildcard_t *) hash.hash;
}
}
三、監聽物件的建立
而ngx_http_init_listening函式將建立一個建立一個ngx_listening_t物件,並個這個物件的成員賦值。同時將建立後的這個物件存放到cf->cycle->listening這個監聽陣列中儲存。
static ngx_int_t ngx_http_init_listening(ngx_conf_t *cf, ngx_http_conf_port_t *port)
{
while (i < last)
{
//建立一個ngx_listening_t物件,並個這個物件的成員賦值。例如設定監聽回撥
ls = ngx_http_add_listening(cf, &addr[i]);
if (ls == NULL)
{
return NGX_ERROR;
}
}
}
ngx_http_add_listening函式建立監聽物件,並設定監聽的回撥為ngx_http_init_connection。在監聽到客戶端的連線時,這個回撥將被呼叫。
//建立一個ngx_listening_t物件,並個這個物件的成員賦值。例如設定監聽回撥
static ngx_listening_t * ngx_http_add_listening(ngx_conf_t *cf, ngx_http_conf_addr_t *addr)
{
//建立一個ngx_listening_t物件
ls = ngx_create_listening(cf, &addr->opt.u.sockaddr, addr->opt.socklen);
ls->addr_ntop = 1;
//監聽回撥
ls->handler = ngx_http_init_connection;
//在有多少server塊監聽同一個埠時,使用預設塊的配置
cscf = addr->default_server;
ls->pool_size = cscf->connection_pool_size;
ls->post_accept_timeout = cscf->client_header_timeout;
return ls;
}
四、監聽socket
在函式ngx_init_cycle有這樣的程式碼段,用來建立socket,繫結socekt,監聽socket
//初始化ngx_cycle_t結構
ngx_cycle_t * ngx_init_cycle(ngx_cycle_t *old_cycle)
{
//建立監聽陣列中的所有socket
if (ngx_open_listening_sockets(cycle) != NGX_OK)
{
goto failed;
}
//獲取傳送緩衝區,接收緩衝區大小,以及監聽socket
ngx_configure_listening_sockets(cycle);
}
而函式ngx_open_listening_sockets將對監聽陣列中存放的所有ip,port , 執行socket, 繫結socket, 監聽socket操作。如果失敗,則最多重複執行5次。
//建立監聽陣列中的所有socket
ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
//因此建立soceket,繫結socket, 監聽socket等操作有可能呼叫失敗。
//失敗後最多嘗試5次
for (tries = 5; tries; tries--)
{
//將對所有監聽陣列中的ip,port,開始建立socket
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++)
{
//建立套接字
s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);
//繫結套接字
bind(s, ls[i].sockaddr, ls[i].socklen);
//監聽套接字
listen(s, ls[i].backlog);
ls[i].listen = 1;
ls[i].fd = s;
}
ngx_msleep(500);
}
}
而ngx_configure_listening_sockets(ngx_cycle_t *cycle)函式只是簡單設定socket的一些選項,例如設定傳送緩衝區,接收緩衝區大小,以及監聽佇列大小等。
到此監聽事件已經分析完了,至於把監聽socket加入到epoll中,則在ngx_trylock_accept_mutex函式中完成。將監聽socket加入到epoll在前面的文章中已經分析過了,在這裡就不在分析了,可以參考master程序、work程序初始化這兩篇文章。
---------------------
作者:ApeLife
來源:CSDN
原文:https://blog.csdn.net/apelife/article/details/53647316
版權宣告:本文為博主原創文章,轉載請附上博文連結!