nginx事件模块之客户端连接与超时管理

        上一篇文章分析了nginx是如何管理监听事件,并把监听事件注册到epoll事件管理器中。接下来在这基础上分析当有客户端连接请求到来时,nginx是如何与客户端建立tcp连接,以及连接建立后又是如何管理超时事件。

一、连接事件管理

        在函数ngx_event_process_init中,会设置读事件的回调为ngx_event_accept。 这样设置后,在nginx服务器监听到来自客户端的连接请求后,该回调会被触发,用来与客户端建立tcp连接。连接建立后,就可以正常与客户端进行数据交互。

//ngx_event_core_module模块的init_process方法。在函数ngx_worker_process_init中被调用
static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle)
{
	//对于每一个监听端口,从连接池中取出一个连接对象(也将从读时间,写事件池取出对象,
	//使得连接,读、写保持一一对应关系),负责监听来自客户端的连接
    ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) 
	{
        c = ngx_get_connection(ls[i].fd, cycle->log);
        //建立连接对象与监听对象的关系
		c->listening = &ls[i];
		//建立监听对象与连接对象的关系
        ls[i].connection = c;
        rev = c->read;

		//设置连接回调,当有客户端连接时,将触发回调
        rev->handler = ngx_event_accept;
		
		//如果work进程之间没有使用枷锁,则把读事件加入epoll中
		//此时写事件的回调为NULL,因为在ngx_get_connection函数中会把整个结构进行清0操作
		if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) 
		{
			return NGX_ERROR;
		}
}

        ngx_event_accept用来接收来自客户端的连接请求。tcp建立后,从连接池中获取一个新连接对象(同时获取到读、写事件), 并把读事件加入到epoll中。这个新连接对象与监听对象作用是不同的。 监听对象注意用来监听来自客户端的连接,是还没有与客户端建立连接前被调用。而这个新连接对象是在tcp连接后被调用,用来与客户端进行数据读写。

//客户端请求连接回调
void ngx_event_accept(ngx_event_t *ev)
{
    do 
	{
		//接收客户端连接
        s = accept(lc->fd, (struct sockaddr *) sa, &socklen);

		//work进程之间赋值均衡,但一个work进程超过每一个进程的最大连接数的7/8时,
		//则该work进程不在监听来自客户端的连接请求。但已经建立tcp连接的客户端不收影响,正常进行数据读写
        ngx_accept_disabled = ngx_cycle->connection_n / 8
                              - ngx_cycle->free_connection_n;

		//获取一个空闲连接对象(同时也获取到读,写事件)
        c = ngx_get_connection(s, ev->log);

		//给新连接对象赋值
        c->pool = ngx_create_pool(ls->pool_size, ev->log);
        c->sockaddr = ngx_palloc(c->pool, socklen);
        ngx_memcpy(c->sockaddr, sa, socklen);

		//设置从内核读取数据,写入数据的的公共方法。这些方法实际上就是ngx_os_io结构的各个成员
		//这些方法为什么不设置在事件对象上,而是设置在连接对象。因为这对读写事件而言,这些方法是公共的。
		//连接对象里面包含了读写事件对象的引用关系,如果设置在相应的读事件,或者写事件上,则每个事件都需要设置一次
		//而在连接对象上只需要设置一次
        c->recv = ngx_recv;
        c->send = ngx_send;
        c->recv_chain = ngx_recv_chain;
        c->send_chain = ngx_send_chain;
		
		//连接对象里面的监听指针指向ls,但ls并没有把连接指向当前已经调用accept的这个连接,
		//而是指向监听连接对象
        c->listening = ls;

		//调用监听对象的方法, 将读事件写入到epoll
		//考虑下为什么要把这个回调设置在监听对象上,而不是连接对象上。
		//因为如果有5个客户端连接上同一个监听socket, 则会创建5个连接对象。而每一个连接对象都需要设置
		//这个回调,占用4字节指针空间,浪费内存资源。而如果回调设置在监听对象上,则只需要设置一次回调就可以了。
        ls->handler(c);		//ngx_http_init_connection

    } while (ev->available);
}

        在函数中会调用ngx_listening_s对象的handler方法。这个方法其实就是ngx_http_init_connection,在ngx_http_add_listening函数中设置。

ngx_http_init_listening

   ---> ngx_http_add_listening

           --->

//创建一个ngx_listening_t对象,并给对象的成员赋值。例如设置监听回调
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->handler = ngx_http_init_connection;
}

        ngx_http_init_connection是tcp建立连接后,http请求初始化前的一个阶段,用来为新建立的客户端连接注册读事件回调ngx_http_init_request、写事件回调ngx_http_empty_handler、同时将注册读事件的超时事件到红黑树实现的定时器中。最终将读事件放入到epoll中。这些操作执行之后,就可以接收来自客户端的数据了。

//在收到客户端连接时,ngx_event_accept函数中会调用ngx_listening_t的handler,也就是本函数
//功能:注册客户端的读写事件回调
void ngx_http_init_connection(ngx_connection_t *c)
{
    rev = c->read;
	//读事件回调
    rev->handler = ngx_http_init_request;				

	//该写回调没有做任何事件,因为这个阶段还不需要向客户端写入任何数据
    c->write->handler = ngx_http_empty_handler;			

	//将读事件插入到红黑树中,用于管理超时事件,post_accept_timeout超时事件
	//为nginx.conf中的client_header_timeout选项
    ngx_add_timer(rev, c->listening->post_accept_timeout);

	//将读事件注册到epoll中,此时并没有把写事件注册到epoll中,因为现在还不需要向客户端发送任何数据,所以写事件并不需要注册
    ngx_handle_read_event(rev, 0);
}

        nginx服务器处理完客户端的连接请求后,又回到了work进程的事件循环中。监听新建立的对象,等待客户端发来的数据,与客户端进行数据交互。

//work进程的事件循环
static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    for ( ;; ) 
	{
        ngx_process_events_and_timers(cycle);
    }
}

二、超时事件管理

        nginx服务器在监听到来自客户端的连接请求后,会与客户端建立一个tcp连接,并为这个新连接注册读写事件,并将读事件的超时事件加入到红黑树实现的定时器中。
从上面的ngx_http_init_connection函数中就可以看出这些操作,并把读事件添加到了红黑树实现的定时器中。

//在收到客户端连接时,ngx_event_accept函数中会调用ngx_listening_t的handler,也就是本函数
//功能:注册客户端的读写事件回调
void ngx_http_init_connection(ngx_connection_t *c)
{
    rev = c->read;
	//读事件回调
    rev->handler = ngx_http_init_request;				

	//该写回调没有做任何事件,因为这个阶段还不需要向客户端写入任何数据
    c->write->handler = ngx_http_empty_handler;			

	//将读事件插入到红黑树中,用于管理超时事件,post_accept_timeout超时事件
	//为nginx.conf中的client_header_timeout选项
    ngx_add_timer(rev, c->listening->post_accept_timeout);

	//将读事件注册到epoll中
    ngx_handle_read_event(rev, 0);
}

    ngx_event_add_timer负责将事件注册到红黑树实现的定时器中。红黑树中的所有超时事件节点都是通过ngx_event_s对象的timer成员给串接起来。而定时器中每一个超时事件节点的key就是超时时间,记录该事件的超时时间。

//将定时事件添加到红黑树中,timer为超时时间
static ngx_inline void ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer)
{
    ngx_msec_t      key;
    ngx_msec_int_t  diff;

    key = ngx_current_msec + timer;

	//已经将事件插入到红黑树种,则先删除之前的事件
    if (ev->timer_set) 
	{
        ngx_del_timer(ev);
    }

	//设置定时器的唯一id,也就是时间
    ev->timer.key = key;


	//插入到红黑树种
    ngx_rbtree_insert(&ngx_event_timer_rbtree, &ev->timer);


	//表示事件已经存在红黑树中了
    ev->timer_set = 1;
}

        将读事件加入到红黑树定时器后,接下来work进程进入事件循环,阻塞在epoll_wait调用。那epoll_wait什么时候返回呢? 在接收到客户端的数据后,或者每个事件的定时时间到后,可以从epoll_wait返回。接下来看下如何设置epoll_wait的超时时间,使得定时时间到后,能及时从epoll_wait返回。

//work进程事件循环
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
	//在红黑树中查找所有事件的最小超时事件,返回值timer就是所有事件的最小超时时间
	timer = ngx_event_find_timer();

	//调用epoll_wait等待事件
    (void) ngx_process_events(cycle, timer, flags);
	
	//epoll_wait返回后,处理所有超时事件
	ngx_event_expire_timers();
}

        红黑树是一颗二叉排序树,因此最小超时时间实际上就是左子树的最小值。因此可以看到ngx_event_find_timer函数的实现,就是在左子树种查找最小值。如不清楚红黑树的实现,则可以查看july大神的博客http://www.cnblogs.com/v-July-v/archive/2010/12/29/1983707.html

//返回红黑树中最小事件的超时事件;
//返回值:>0 表示还剩多长事件超时
//		 <=0 表示事件已经超时
ngx_msec_t ngx_event_find_timer(void)
{
    ngx_msec_int_t      timer;
    ngx_rbtree_node_t  *node, *root, *sentinel;

	//红黑树为空,则返回-1表示事件已经超时
    if (ngx_event_timer_rbtree.root == &ngx_event_timer_sentinel) 
	{
        return NGX_TIMER_INFINITE;
    }
    root = ngx_event_timer_rbtree.root;
    sentinel = ngx_event_timer_rbtree.sentinel;
	//查找左字数
    node = ngx_rbtree_min(root, sentinel);

	//计算剩余超时时间
    timer = (ngx_msec_int_t) node->key - (ngx_msec_int_t) ngx_current_msec;

    return (ngx_msec_t) (timer > 0 ? timer : 0);
}

        而epoll_wait调用返回后,如果有事件超时了,那如何处理这些超时事件呢?ngx_event_expire_timers内部会遍历红黑树,查找所有已经超时的事件,并调用这些超时事件的处理回调。需要注意的是,函数也会从红黑树中删除这个超时事件,因此如果还需要管理这个超时事件,则需要重新把事件添加到红黑树实现的定时器中。

//调用红黑树中所有已经超时的事件回调,并把已经超时的事件从红黑树中删除
void ngx_event_expire_timers(void)
{
    ngx_event_t        *ev;
    ngx_rbtree_node_t  *node, *root, *sentinel;

    sentinel = ngx_event_timer_rbtree.sentinel;
	//遍历红黑树,查找超时事件
    for ( ;; ) 
	{
        root = ngx_event_timer_rbtree.root;
		//红黑树为空则返回
        if (root == sentinel)
		{
            return;
        }

		//取出红黑树中时间最小的节点
        node = ngx_rbtree_min(root, sentinel);

        /* node->key <= ngx_current_time */
		//发生超时
        if ((ngx_msec_int_t) node->key - (ngx_msec_int_t) ngx_current_msec <= 0)
        {
            ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));

			//从红黑树中删除这个已经超时的定时器事件
            ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);

			//表示事件已经不存在定时器中了
            ev->timer_set = 0;

			//标示事件已经超时
            ev->timedout = 1;

			//调用事件回调
            ev->handler(ev);

			//直接处理下一个超时事件,前一个超时事件已经从红黑树中删除了
            continue;
        }

		//没有事件超时则直接退出,因此最小时间都没有超时,那红黑树中其它时间也肯定没有超时
        break;
    }
}

        到此,超时事件的管理也分析完成了。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值