您的位置:首页 > 运维架构 > Nginx

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

2016-12-27 07:26 267 查看
        上一篇文章分析了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是用来为新建立的客户端连接注册读事件回调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;
}
}
        到此,超时事件的管理也分析完成了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息