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

        需要注意的是,这个ports数组保存在main级别ngx_http_core_module模块结构中的ports中。

ngx_http_conf_port_t是监听端口结构,所有监听端口保存到ports数组中;而ngx_http_conf_addr_t记录ip结构,所有监听同一个端口的ip地址列表保存在addrs中;ngx_http_core_srv_conf_t记录server块结构,所有监听同一ip:port的server块保存到servers中。

        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函数会根据上面配置解析得到的ip:port对结构,为每个监听的ip:port对都创建一个ngx_listening_t对象,并为这个对象的成员赋值。同时将创建后的这个对象存放到cf->cycle->listening这个监听数组中保存。正常情况下每一个listen监听的ip:port对都会创建一个ngx_listening_t对象。例如会为listen 127.0.0.1:80创建一个ngx_listening_t对象,然而当有多个ip地址监听同一个端口,且某个server块配置了listen *:80,在这种场景下,只会为这些ip:port对创建一个ngx_listening_t对象,监听本地端口80的所有ip地址,相当于将多个ip:port对合并成一个ngx_listening_t对象。

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进程初始化这两篇文章。

五、查找监听ip:port的过程

        每个监听的ip:port对都会关联一个监听对象ngx_listening_s。在ngx_http_init_request函数中,当在某个端口上收到了tcp连接,接着根据连接的服务器ip地址查找到监听这个端口的ip结构。而每个ip结构里面都各自维护了server_name哈希表,后续就可以根据host查找到具体的server块。

    port = c->listening->servers;
    if (port->naddrs > 1) 
	{
		//server块A配置listen 127.0.0.1:80;   server块B配置listen *:80, 在这种场景下,会将这两条listen合并为一条
		//只会创建一个监听对象,监听本机任意地址,端口是80。此时这个监听对象就包含了2个地址列表
		sin = (struct sockaddr_in *) c->local_sockaddr;
		addr = port->addrs;

		/* the last address is "*" */

		for (i = 0; i < port->naddrs - 1; i++) 
		{
			if (addr[i].addr == sin->sin_addr.s_addr) 
			{
				break;
			}
		}
		addr_conf = &addr[i].conf;
    }
	else 
	{
		//正常的监听对象,只会有一个ip地址
		addr = port->addrs;
		addr_conf = &addr[0].conf;
    }

	//查找到虚拟主机位置,里面有一个哈希表,存放了监听同一个socket的所有域名。
	//后续在解析http头部时会根据http请求头部的host字段,在这个哈希表中查找真正的server块,
	//而不是采用下面这个默认server块的配置信息
    r->virtual_names = addr_conf->virtual_names;

    /* the default server configuration for the address:port */
	//当前这个请求是使用nginx.conf配置中的哪一个默认server块来处理请求的
	//后面在解析http头部时会查找哈希表从而获取正在server块,这里只是给一个默认的server块
    cscf = addr_conf->default_server;

	//设置这个请求对应的main,srv,loc级别的配置项,后续会被修改
    r->main_conf = cscf->ctx->main_conf;
    r->srv_conf = cscf->ctx->srv_conf;
    r->loc_conf = cscf->ctx->loc_conf;

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值