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 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/ai2000ai/article/details/84107013