Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1469780
  • 博文数量: 150
  • 博客积分: 65
  • 博客等级: 民兵
  • 技术积分: 3415
  • 用 户 组: 普通用户
  • 注册时间: 2011-02-25 10:30
个人简介

游戏后台开发

文章分类

全部博文(150)

文章存档

2020年(1)

2019年(4)

2017年(3)

2016年(6)

2015年(4)

2014年(45)

2013年(86)

2012年(1)

分类: 服务器与存储

2013-12-01 00:59:57

事件机制尤如nginx的心脏一般,不停的运转,保证了nginx的请求响应模式得以正常工作。 
本文将剖析事件机制的原理和实现。 

nginx本身支持多种机制,如 poll, epoll, select, aio, kqueue等,这里分析epoll,因为这是nginx的杀手锏。 
初略接触时,我们大概只知道监听、请求、接受、响应这几个概念。我们沿着这个思维展开,看nginx如何设计这些结构体的。 

1、大体上设计 
不管是监听,还是请求,只要能产生fd的,都将视为连接,一个fd对应一个连接(connection)。 
每个连接都可以读(read)和写(write),这两个都视为事件(event),结构体为: 
01 struct ngx_connection_s {
02     void               *data;    // 将要关联的模型,listening, request, ... 或其它
03     ngx_event_t        *read;    //  读事件
04     ngx_event_t        *write;   //  写事件
05  
06     ngx_socket_t        fd;        //  句柄
07  
08     ngx_listening_t    *listening;   // 对应的监听
09 };
10  
11 struct ngx_event_s {
12     void            *data;      //  将要关联的模型,connection, ... 或其它
13  
14     unsigned         write:1;   //  是否可写
15      
16     unsigned         accept:1;  //   是否是accept产生的事件
17  
18     unsigned         instance:1; //   避免惊群的一个设计
19  
20     unsigned         active:1;   //   是否有效,当加入epoll_ctl时就置为1
21  
22     unsigned         ready:1;     //    epoll_wait捕获到时就置为1
23  
24  
25     unsigned         timedout:1; //   是否超时
26     unsigned         timer_set:1; //   是否置为定时器,即加入超时定时器红黑树时就置为1
27  
28  
29     ngx_event_handler_pt  handler; //   事件处理函数,核心
30  
31  
32     ngx_rbtree_node_t   timer;        //   加入红黑树时需要的辅助节点
33 };

2、监听listen 
当处理完配置文件解析(针对listen指令)时,nginx开始处理这些listen。将它们放在 ngx_cycle->listening里。 
01 struct ngx_cycle_s {
02     ...
03     ngx_array_t  listening;  // 是个数组,结构体为ngx_listening_s
04     ...
05 }
06  
07 struct ngx_listening_s {
08     ngx_socket_t        fd;        // 句柄描述符
09  
10     struct sockaddr    *sockaddr;
11     socklen_t           socklen;   
12     ...
13 };
监听是有读事件,而没有写事件的,epoll有两个模式LT和ET,监听采用LT,监听的read事件的处理函数为ngx_event_accept。 

3、接受accept 
这个产生的fd,有读和写事件,对读事件的处理函数为ngx_http_init_request。因此一个连接请求一旦发送完,就从这个函数开始执行 
这也是request开始的生命周期,这里的结构体为: 
01 struct ngx_http_request_s {
02     uint32_t                          signature;         /* "HTTP" */
03  
04     ngx_connection_t                 *connection;   // 对应的连接
05  
06     /* 这个结构体是非常庞大的,但不复杂,比如它处理了并重新保存了配置文件的上下文 */
07     void                            **ctx;
08     void                            **main_conf;
09     void                            **srv_conf;
10     void                            **loc_conf;
11  
12     /* 比如请求有关的信息的会保存到它的成员里 */
13     u_char                           *uri_start;
14     u_char                           *uri_end;
15     u_char                           *uri_ext;
16     u_char                           *args_start;
17     u_char                           *request_start;
18     u_char                           *request_end;
19     u_char                           *method_end;
20     u_char                           *schema_start;
21     u_char                           *schema_end;
22     u_char                           *host_start;
23     u_char                           *host_end;
24     u_char                           *port_start;
25     u_char                           *port_end;
26  
27     unsigned                          http_minor:16;
28     unsigned                          http_major:16;
29 };

4、神奇的超时 
因为处理了超时,整个代码的复杂度至少提升了一个档次,像libevent这种东东,它是用信号处理超时的, 
nginx作者应该认为这种处理方式不是线程安全的,所以自己实现了一个。这不是重复创造轮子,超时机制 
是应用程序的一部分逻辑,在应用程序代码里面实现无可厚非。 
超时机制用了红黑树,因为有频繁的插入,查找和删除,用红黑树的效率是非常高的。 

初始化:专门的变量ngx_event_timer_rbtree 
ngx_event_timer_init(cycle->log); 

超时检查:epoll所有事件处理之前,检查一遍哪些是超时的,将event标记为timedout,并且马上执行事件处理 
01 ngx_event_expire_timers(void)
02 {
03     ...
04      
05     for ( ;; ) {     
06  
07         node = ngx_rbtree_min(root, sentinel);
08  
09         /* node->key <= ngx_current_time,很简单巧妙的设计,怎么视为超时 */
10  
11         if ((ngx_msec_int_t) (node->key - ngx_current_msec) <= 0) {
12             ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));
13  
14             ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);
15  
16             ev->timer_set = 0; // 重置timer_set
17             ev->timedout = 1;  // 标记为超时
18  
19             ev->handler(ev);   // 马上处理,注意这里没有处理成如果超时就关闭连接,这是由handler自行处理的
20                                //  后面会再解释这个设计
21  
22             continue;
23         }
24  
25         break; // 如果没有超时的事件,结束退出
26     }
27 }

5、epoll的应用 
一个连接(或事件)它要添加到epoll里,才会被处理,不然即使它可读或可写了,也不会理会。 
01 for ( ;; ) {
02     timer = ngx_event_find_timer();
03  
04     events = epoll_wait(ep, event_list, (int) nevents, timer);
05  
06     ;更新时间
07  
08     ;超时处理
09  
10     ;正常事件处理
11 }


事件操作: 
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函数体的前面都有一段这么代码 
01 ngx_http_init_request(ngx_event_t *rev)
02 {      
03     ...
04  
05     if (rev->timedout) { 
06         ngx_http_close_connection(c);
07         return;
08     }
09  
10     ...
11 }
12  
13  
14 ngx_http_process_request_line(ngx_event_t *rev)
15 {
16     ...
17  
18     if (rev->timedout) {      
19         c->timedout = 1;
20         ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);
21         return;
22     }
23  
24     ...
25 }
所以前面提到,在超时检查时,nginx只是将event标记为timedout,而没有关闭连接,这是因为,nginx可以处理 
http, mail或不同的连接,每个连接都有自己不同的处理方式,所以这段代码无处不在,将就咯 -_- 


8、你看得懂这段代码吗? 
01 ngx_url_t                u;
02 ngx_peer_connection_t   peer;
03  
04 ngx_memzero(&u, sizeof(ngx_url_t));
05 ngx_memzero(&peer, sizeof(ngx_peer_connection_t));
06  
07 ngx_str_set(&u.url, "127.0.0.1:8080");
08  
09 ngx_parse_url(pool, &u);
10  
11 peer.sockaddr = u.addrs->sockaddr;
12 peer.socklen = u.addrs->socklen;
13 peer.name = u.addrs->name;
14 peer.get = ngx_event_get_peer;
15  
16 ngx_event_connect_peer(&peer);
17  
18 peer.connection->read->handler = ngx_mail_auth_http_read_handler;
19 peer.connection->write->handler = ngx_mail_auth_http_write_handler;
20  
21 ngx_add_timer(peer.connection->read, ahcf->timeout);
22 ngx_add_timer(peer.connection->write, ahcf->timeout);

这是截取mail部分auth_http的代码,nginx用的很广的一个就是自己创建socket连接到另一服务器, 
像fastcgi, proxy,都是这样,里面的核心就是 ngx_event_connect_peer,这主题比较深.
阅读(2731) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~