1. 程式人生 > >nginx監聽事件流程

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 
版權宣告:本文為博主原創文章,轉載請附上博文連結!