事件机制尤如nginx的心脏一般,不停的运转,保证了nginx的请求响应模式得以正常工作。 本文将剖析事件机制的原理和实现。 nginx本身支持多种机制,如 poll, epoll, select, aio, kqueue等,这里分析epoll,因为这是nginx的杀手锏。 初略接触时,我们大概只知道监听、请求、接受、响应这几个概念。我们沿着这个思维展开,看nginx如何设计这些结构体的。 1、大体上设计 不管是监听,还是请求,只要能产生fd的,都将视为连接,一个fd对应一个连接(connection)。 每个连接都可以读(read)和写(write),这两个都视为事件(event),结构体为:

struct         ngx_connection_s {        
                  void                       *data;             // 将要关联的模型,listening, request, ... 或其它        
                  ngx_event_t        *read;             //  读事件        
                  ngx_event_t        *write;            //  写事件        
                  
                  ngx_socket_t        fd;                 //  句柄        
                  
                  ngx_listening_t    *listening;            // 对应的监听        
         };        
                  
         struct         ngx_event_s {        
                  void                    *data;               //  将要关联的模型,connection, ... 或其它        
                  
                  unsigned         write:1;            //  是否可写        
                  
                  unsigned         accept:1;           //   是否是accept产生的事件        
                  
                  unsigned         instance:1;          //   避免惊群的一个设计        
                  
                  unsigned         active:1;            //   是否有效,当加入epoll_ctl时就置为1        
                  
                  unsigned         ready:1;              //    epoll_wait捕获到时就置为1        
                  
                  
                  unsigned         timedout:1;          //   是否超时        
                  unsigned         timer_set:1;          //   是否置为定时器,即加入超时定时器红黑树时就置为1        
                  
                  
                  ngx_event_handler_pt  handler;          //   事件处理函数,核心        
                  
                  
                  ngx_rbtree_node_t   timer;                 //   加入红黑树时需要的辅助节点        
         };

2、监听listen

当处理完配置文件解析(针对listen指令)时,nginx开始处理这些listen。将它们放在 ngx_cycle->listening里。

struct         ngx_cycle_s {        
                  ...        
                  ngx_array_t  listening;           // 是个数组,结构体为ngx_listening_s        
                  ...        
         }        
                  
         struct         ngx_listening_s {        
                  ngx_socket_t        fd;                 // 句柄描述符        
                  
                  struct         sockaddr    *sockaddr;        
                  socklen_t           socklen;            
                  ...        
         };

监听是有读事件,而没有写事件的,epoll有两个模式LT和ET,监听采用LT,监听的read事件的处理函数为ngx_event_accept。

3、接受accept

这个产生的fd,有读和写事件,对读事件的处理函数为ngx_http_init_request。因此一个连接请求一旦发送完,就从这个函数开始执行

这也是request开始的生命周期,这里的结构体为:

struct         ngx_http_request_s {        
                  uint32_t                          signature;                  /* "HTTP" */        
                  
                  ngx_connection_t                 *connection;            // 对应的连接        
                  
                  /* 这个结构体是非常庞大的,但不复杂,比如它处理了并重新保存了配置文件的上下文 */        
                  void                                    **ctx;        
                  void                                    **main_conf;        
                  void                                    **srv_conf;        
                  void                                    **loc_conf;        
                  
                  /* 比如请求有关的信息的会保存到它的成员里 */        
                  u_char                           *uri_start;        
                  u_char                           *uri_end;        
                  u_char                           *uri_ext;        
                  u_char                           *args_start;        
                  u_char                           *request_start;        
                  u_char                           *request_end;        
                  u_char                           *method_end;        
                  u_char                           *schema_start;        
                  u_char                           *schema_end;        
                  u_char                           *host_start;        
                  u_char                           *host_end;        
                  u_char                           *port_start;        
                  u_char                           *port_end;        
                  
                  unsigned                          http_minor:16;        
                  unsigned                          http_major:16;        
         };

4、神奇的超时

因为处理了超时,整个代码的复杂度至少提升了一个档次,像libevent这种东东,它是用信号处理超时的,

nginx作者应该认为这种处理方式不是线程安全的,所以自己实现了一个。这不是重复创造轮子,超时机制

是应用程序的一部分逻辑,在应用程序代码里面实现无可厚非。

超时机制用了红黑树,因为有频繁的插入,查找和删除,用红黑树的效率是非常高的。

初始化:专门的变量ngx_event_timer_rbtree

ngx_event_timer_init(cycle->log);

超时检查:epoll所有事件处理之前,检查一遍哪些是超时的,将event标记为timedout,并且马上执行事件处理

ngx_event_expire_timers(         void         )        
         {        
                  ...        
                  
                  for         ( ;; ) {              
                  
                  node = ngx_rbtree_min(root, sentinel);        
                  
                  /* node->key <= ngx_current_time,很简单巧妙的设计,怎么视为超时 */        
                  
                  if         ((ngx_msec_int_t) (node->key - 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;          // 重置timer_set        
                  ev->timedout = 1;           // 标记为超时        
                  
                  ev->handler(ev);            // 马上处理,注意这里没有处理成如果超时就关闭连接,这是由handler自行处理的        
                  //  后面会再解释这个设计        
                  
                  continue         ;        
                  }        
                  
                  break         ;          // 如果没有超时的事件,结束退出        
                  }        
         }

5、epoll的应用

一个连接(或事件)它要添加到epoll里,才会被处理,不然即使它可读或可写了,也不会理会。

for          ( ;; ) {        
                  timer = ngx_event_find_timer();        
                  
                  events = epoll_wait(ep, event_list, (         int         ) nevents, timer);        
                  
                  ;更新时间        
                  
                  ;超时处理        
                  
                  ;正常事件处理        
         }

事件操作:

ngx_epoll_add_event 

 ngx_epoll_del_event

6、梳理

如果我们自己写业务逻辑,如何处理一个事件呢?

假设fd已经有了,可能是你通过socket函数产生的。

获取连接:

c = ngx_get_connection(fd);

处理read,write:

c->read->handler = ngx_http_init_request; 

 c->write->handler = ngx_http_empty_handler;

定时器处理:定时器是针对事件的

ngx_add_timer(c->read, c->listening->post_accept_timeout); 

 ngx_add_timer(c->write, ...);

注册事件:即加入epoll,这里一般采用ET模式。

ngx_handle_read_event(c->read, 0); 

 ngx_handle_read_event(c->write, 0);

上面的例子将read,write都处理了,实际情况不一定得这样,看你要不要处理读或写事件,哪个需要,启用哪个。

7、不能忘却的timedout。

如果你用心,你会发现,所有的event的handler函数体的前面都有一段这么代码

ngx_http_init_request(ngx_event_t *rev)        
         {               
                  ...        
                  
                  if         (rev->timedout) {          
                  ngx_http_close_connection(c);        
                  return         ;        
                  }        
                  
                  ...        
         }        
                  
                  
         ngx_http_process_request_line(ngx_event_t *rev)        
         {        
                  ...        
                  
                  if         (rev->timedout) {               
                  c->timedout = 1;        
                  ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);        
                  return         ;        
                  }        
                  
                  ...        
         }

所以前面提到,在超时检查时,nginx只是将event标记为timedout,而没有关闭连接,这是因为,nginx可以处理

http, mail或不同的连接,每个连接都有自己不同的处理方式,所以这段代码无处不在,将就咯 -_-

8、你看得懂这段代码吗?

ngx_url_t                u;        
         ngx_peer_connection_t   peer;        
                  
         ngx_memzero(&u,          sizeof         (ngx_url_t));        
         ngx_memzero(&peer,          sizeof         (ngx_peer_connection_t));        
                  
         ngx_str_set(&u.url,          "127.0.0.1:8080"         );        
                  
         ngx_parse_url(pool, &u);        
                  
         peer.sockaddr = u.addrs->sockaddr;        
         peer.socklen = u.addrs->socklen;        
          = u.addrs->name;        
         peer.get = ngx_event_get_peer;        
                  
         ngx_event_connect_peer(&peer);        
                  
         peer.connection->read->handler = ngx_mail_auth_http_read_handler;        
         peer.connection->write->handler = ngx_mail_auth_http_write_handler;        
                  
         ngx_add_timer(peer.connection->read, ahcf->timeout);        
         ngx_add_timer(peer.connection->write, ahcf->timeout);

这是截取mail部分auth_http的代码,nginx用的很广的一个就是自己创建socket连接到另一服务器,

像fastcgi, proxy,都是这样,里面的核心就是 ngx_event_connect_peer,这主题比较深,留以后专门分析,抛下砖头先。

ps:写这些文章时,有些代码都是凭经验敲的,有错误之处请指正。能谈到设计上的层次就足矣。