分享

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

 WindySky 2017-09-27

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

一、连接事件管理

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

  1. //ngx_event_core_module模块的init_process方法。在函数ngx_worker_process_init中被调用  
  2. static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle)  
  3. {  
  4.     //对于每一个监听端口,从连接池中取出一个连接对象(也将从读时间,写事件池取出对象,  
  5.     //使得连接,读、写保持一一对应关系),负责监听来自客户端的连接  
  6.     ls = cycle->listening.elts;  
  7.     for (i = 0; i < cycle->listening.nelts; i++)   
  8.     {  
  9.         c = ngx_get_connection(ls[i].fd, cycle->log);  
  10.         //建立连接对象与监听对象的关系  
  11.         c->listening = &ls[i];  
  12.         //建立监听对象与连接对象的关系  
  13.         ls[i].connection = c;  
  14.         rev = c->read;  
  15.   
  16.         //设置连接回调,当有客户端连接时,将触发回调  
  17.         rev->handler = ngx_event_accept;  
  18.           
  19.         //如果work进程之间没有使用枷锁,则把读事件加入epoll中  
  20.         //此时写事件的回调为NULL,因为在ngx_get_connection函数中会把整个结构进行清0操作  
  21.         if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR)   
  22.         {  
  23.             return NGX_ERROR;  
  24.         }  
  25. }  

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

  1. //客户端请求连接回调  
  2. void ngx_event_accept(ngx_event_t *ev)  
  3. {  
  4.     do   
  5.     {  
  6.         //接收客户端连接  
  7.         s = accept(lc->fd, (struct sockaddr *) sa, &socklen);  
  8.   
  9.         //work进程之间赋值均衡,但一个work进程超过每一个进程的最大连接数的7/8时,  
  10.         //则该work进程不在监听来自客户端的连接请求。但已经建立tcp连接的客户端不收影响,正常进行数据读写  
  11.         ngx_accept_disabled = ngx_cycle->connection_n / 8  
  12.                               - ngx_cycle->free_connection_n;  
  13.   
  14.         //获取一个空闲连接对象(同时也获取到读,写事件)  
  15.         c = ngx_get_connection(s, ev->log);  
  16.   
  17.         //给新连接对象赋值  
  18.         c->pool = ngx_create_pool(ls->pool_size, ev->log);  
  19.         c->sockaddr = ngx_palloc(c->pool, socklen);  
  20.         ngx_memcpy(c->sockaddr, sa, socklen);  
  21.   
  22.         //设置从内核读取数据,写入数据的的公共方法。这些方法实际上就是ngx_os_io结构的各个成员  
  23.         //这些方法为什么不设置在事件对象上,而是设置在连接对象。因为这对读写事件而言,这些方法是公共的。  
  24.         //连接对象里面包含了读写事件对象的引用关系,如果设置在相应的读事件,或者写事件上,则每个事件都需要设置一次  
  25.         //而在连接对象上只需要设置一次  
  26.         c->recv = ngx_recv;  
  27.         c->send = ngx_send;  
  28.         c->recv_chain = ngx_recv_chain;  
  29.         c->send_chain = ngx_send_chain;  
  30.           
  31.         //连接对象里面的监听指针指向ls,但ls并没有把连接指向当前已经调用accept的这个连接,  
  32.         //而是指向监听连接对象  
  33.         c->listening = ls;  
  34.   
  35.         //调用监听对象的方法, 将读事件写入到epoll  
  36.         //考虑下为什么要把这个回调设置在监听对象上,而不是连接对象上。  
  37.         //因为如果有5个客户端连接上同一个监听socket, 则会创建5个连接对象。而每一个连接对象都需要设置  
  38.         //这个回调,占用4字节指针空间,浪费内存资源。而如果回调设置在监听对象上,则只需要设置一次回调就可以了。  
  39.         ls->handler(c);      //ngx_http_init_connection  
  40.   
  41.     } while (ev->available);  
  42. }  

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

ngx_http_init_listening

   ---> ngx_http_add_listening

           --->

  1. //创建一个ngx_listening_t对象,并给对象的成员赋值。例如设置监听回调  
  2. ngx_listening_t * ngx_http_add_listening(ngx_conf_t *cf, ngx_http_conf_addr_t *addr)  
  3. {  
  4.     //创建一个ngx_listening_t对象  
  5.     ls = ngx_create_listening(cf, &addr->opt.u.sockaddr, addr->opt.socklen);  
  6.       
  7.     //监听回调  
  8.     ls->handler = ngx_http_init_connection;  
  9. }  
        ngx_http_init_connection是用来为新建立的客户端连接注册读事件回调ngx_http_init_request、写事件回调ngx_http_empty_handler、同时将注册读事件的超时事件到红黑树实现的定时器中。最终将读事件放入到epoll中。这些操作执行之后,就可以接收来自客户端的数据了。

  1. //在收到客户端连接时,ngx_event_accept函数中会调用ngx_listening_t的handler,也就是本函数  
  2. //功能:注册客户端的读写事件回调  
  3. void ngx_http_init_connection(ngx_connection_t *c)  
  4. {  
  5.     rev = c->read;  
  6.     //读事件回调  
  7.     rev->handler = ngx_http_init_request;                  
  8.   
  9.     //该写回调没有做任何事件,因为这个阶段还不需要向客户端写入任何数据  
  10.     c->write->handler = ngx_http_empty_handler;             
  11.   
  12.     //将读事件插入到红黑树中,用于管理超时事件,post_accept_timeout超时事件  
  13.     //为nginx.conf中的client_header_timeout选项  
  14.     ngx_add_timer(rev, c->listening->post_accept_timeout);  
  15.   
  16.     //将读事件注册到epoll中,此时并没有把写事件注册到epoll中,因为现在还不需要向客户端发送任何数据,所以写事件并不需要注册  
  17.     ngx_handle_read_event(rev, 0);  
  18. }  
        nginx服务器处理完客户端的连接请求后,又回到了work进程的事件循环中。监听新建立的对象,等待客户端发来的数据,与客户端进行数据交互。

  1. //work进程的事件循环  
  2. static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)  
  3. {  
  4.     for ( ;; )   
  5.     {  
  6.         ngx_process_events_and_timers(cycle);  
  7.     }  
  8. }  
二、超时事件管理

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

  1. //在收到客户端连接时,ngx_event_accept函数中会调用ngx_listening_t的handler,也就是本函数  
  2. //功能:注册客户端的读写事件回调  
  3. void ngx_http_init_connection(ngx_connection_t *c)  
  4. {  
  5.     rev = c->read;  
  6.     //读事件回调  
  7.     rev->handler = ngx_http_init_request;                  
  8.   
  9.     //该写回调没有做任何事件,因为这个阶段还不需要向客户端写入任何数据  
  10.     c->write->handler = ngx_http_empty_handler;             
  11.   
  12.     //将读事件插入到红黑树中,用于管理超时事件,post_accept_timeout超时事件  
  13.     //为nginx.conf中的client_header_timeout选项  
  14.     ngx_add_timer(rev, c->listening->post_accept_timeout);  
  15.   
  16.     //将读事件注册到epoll中  
  17.     ngx_handle_read_event(rev, 0);  
  18. }  

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

  1. //将定时事件添加到红黑树中,timer为超时时间  
  2. static ngx_inline void ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer)  
  3. {  
  4.     ngx_msec_t      key;  
  5.     ngx_msec_int_t  diff;  
  6.   
  7.     key = ngx_current_msec + timer;  
  8.   
  9.     //已经将事件插入到红黑树种,则先删除之前的事件  
  10.     if (ev->timer_set)   
  11.     {  
  12.         ngx_del_timer(ev);  
  13.     }  
  14.   
  15.     //设置定时器的唯一id,也就是时间  
  16.     ev->timer.key = key;  
  17.   
  18.   
  19.     //插入到红黑树种  
  20.     ngx_rbtree_insert(&ngx_event_timer_rbtree, &ev->timer);  
  21.   
  22.   
  23.     //表示事件已经存在红黑树中了  
  24.     ev->timer_set = 1;  
  25. }  

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

  1. //work进程事件循环  
  2. void ngx_process_events_and_timers(ngx_cycle_t *cycle)  
  3. {  
  4.     //在红黑树中查找所有事件的最小超时事件,返回值timer就是所有事件的最小超时时间  
  5.     timer = ngx_event_find_timer();  
  6.   
  7.     //调用epoll_wait等待事件  
  8.     (void) ngx_process_events(cycle, timer, flags);  
  9.       
  10.     //epoll_wait返回后,处理所有超时事件  
  11.     ngx_event_expire_timers();  
  12. }  

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

  1. //返回红黑树中最小事件的超时事件;  
  2. //返回值:>0 表示还剩多长事件超时  
  3. //       <=0 表示事件已经超时  
  4. ngx_msec_t ngx_event_find_timer(void)  
  5. {  
  6.     ngx_msec_int_t      timer;  
  7.     ngx_rbtree_node_t  *node, *root, *sentinel;  
  8.   
  9.     //红黑树为空,则返回-1表示事件已经超时  
  10.     if (ngx_event_timer_rbtree.root == &ngx_event_timer_sentinel)   
  11.     {  
  12.         return NGX_TIMER_INFINITE;  
  13.     }  
  14.     root = ngx_event_timer_rbtree.root;  
  15.     sentinel = ngx_event_timer_rbtree.sentinel;  
  16.     //查找左字数  
  17.     node = ngx_rbtree_min(root, sentinel);  
  18.   
  19.     //计算剩余超时时间  
  20.     timer = (ngx_msec_int_t) node->key - (ngx_msec_int_t) ngx_current_msec;  
  21.   
  22.     return (ngx_msec_t) (timer > 0 ? timer : 0);  
  23. }  
        而epoll_wait调用返回后,如果有事件超时了,那如何处理这些超时事件呢?ngx_event_expire_timers内部会遍历红黑树,查找所有已经超时的事件,并调用这些超时事件的处理回调。需要注意的是,函数也会从红黑树中删除这个超时事件,因此如果还需要管理这个超时事件,则需要重新把事件添加到红黑树实现的定时器中。
  1. //调用红黑树中所有已经超时的事件回调,并把已经超时的事件从红黑树中删除  
  2. void ngx_event_expire_timers(void)  
  3. {  
  4.     ngx_event_t        *ev;  
  5.     ngx_rbtree_node_t  *node, *root, *sentinel;  
  6.   
  7.     sentinel = ngx_event_timer_rbtree.sentinel;  
  8.     //遍历红黑树,查找超时事件  
  9.     for ( ;; )   
  10.     {  
  11.         root = ngx_event_timer_rbtree.root;  
  12.         //红黑树为空则返回  
  13.         if (root == sentinel)  
  14.         {  
  15.             return;  
  16.         }  
  17.   
  18.         //取出红黑树中时间最小的节点  
  19.         node = ngx_rbtree_min(root, sentinel);  
  20.   
  21.         /* node->key <= ngx_current_time */  
  22.         //发生超时  
  23.         if ((ngx_msec_int_t) node->key - (ngx_msec_int_t) ngx_current_msec <= 0)  
  24.         {  
  25.             ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));  
  26.   
  27.             //从红黑树中删除这个已经超时的定时器事件  
  28.             ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);  
  29.   
  30.             //表示事件已经不存在定时器中了  
  31.             ev->timer_set = 0;  
  32.   
  33.             //标示事件已经超时  
  34.             ev->timedout = 1;  
  35.   
  36.             //调用事件回调  
  37.             ev->handler(ev);  
  38.   
  39.             //直接处理下一个超时事件,前一个超时事件已经从红黑树中删除了  
  40.             continue;  
  41.         }  
  42.   
  43.         //没有事件超时则直接退出,因此最小时间都没有超时,那红黑树中其它时间也肯定没有超时  
  44.         break;  
  45.     }  
  46. }  
        到此,超时事件的管理也分析完成了。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多