lighttpd1.4.18代码分析(三)--网络IO事件处理器的使用

lighttpd1.4.18代码分析(三)--网络IO事件处理器的使用

本节是第二节 lighttpd1.4.18代码分析(二)--fdevents结构体解析的延续,在阅读本节内容之前,请先阅读上一节内容.

上一节已经对lighttpd中的fdevent结构体进行了分析,前面提过,fdevent结构体是网络IO事件处理器的"虚拟基类",提供了网络IO事件处理器的公共成员,私有成员以及对外接口,这一节将对这个事件处理器的实现和使用进行解析.与这些相关的文件有这些:fdevent.h提供了fdevent结构体的定义, 在这个头文件中声明的函数可以看作是fdevent这个结构体对外暴露的接口, 也就是OO中所谓的类public函数, fdevent.c则是这些函数的实现,而以fdevent_为开头的几个C文件则是不同的网络IO模型的实现,比如fdevent_select.c文件是select模型的实现.我不打算对各种类型的网络IO模型做详细的介绍,事实上,所有这里用到的网络IO模型,我只用过select和epoll,所以我打算以select模型为例展开这里的讨论,因为select是相对而言用的最多也是大多数人在学习多路复用IO的时候学到的第一个模型,即使在epoll横行的今天,select模型仍然有着它的一席之地.

1)初始化
如何配置使用的是哪种网络IO模型?在配置文件中有一项server.event-handler就是配置需要使用的网络IO的,比如server.event-handler="select"就是选择select, 其它的配置字符串参见前一节最开始提到的那些类型.服务器在初始化的时候读取该配置项, 将网络IO事件类型存放在结构体server的成员event_handler中.
接着, 在server.c的main函数中服务器调用fdevent_init(size_t maxfds, fdevent_handler_t type)初始化一个fdevents指针, 返回的结果存放在server结构体中的ev成员中.
在这个函数中, 根据type参数进行初始化, 生成具体各种不同类型的fdevents指针, 这些初始化的函数都是以init为后缀的, 而所有具体实现的文件名为
fdevent_***.c(如fdevent_select.c是select模型的实现), 对外暴露的仅仅是那个以init为后缀的函数, 而上面那些函数接口的实现全都是这些文件中
静态函数, 很好的限制了它们的使用范围, 做到了信息隐藏, 这些函数可以看作是类中的私有函数, 以select模型为例:
对外暴露的初始化函数是fdevent_select_init, 它在fdevent.h中声明, 也就是说这个函数是对外暴露的, 而这个函数在fdevent_select.c被定义:
int  fdevent_select_init(fdevents  * ev) {
    ev
-> type  =  FDEVENT_HANDLER_SELECT;
#define  SET(x) \
    ev
-> =  fdevent_select_##x;

    SET(reset);
    SET(poll);

    SET(event_del);
    SET(event_add);

    SET(event_next_fdndx);
    SET(event_get_fd);
    SET(event_get_revent);

    
return   0 ;
}
查看fdevent_secelt.c文件,可以看到,名为fdevent_select_***的函数都是这个文件的静态函数, 再从面向对象的观点出发,这些函数属于采用select模型实现的fdevent的"私有函数", 如此做法, 很好的满足了所谓的"信息隐藏".

2) 使用
在服务器创建一个socket fd并且进行监听后, 要将该fd注册到fdevent中, 这样才能使用使用这个事件处理机制.
在server.c文件的main函数中, 调用network_register_fdevents函数将所有监听的fd注册到事件处理器中:
int  network_register_fdevents(server  * srv) {
    size_t i;

    
if  ( - 1   ==  fdevent_reset(srv -> ev)) {
        
return   - 1 ;
    }

    
/*  register fdevents after reset  */
    
for  (i  =   0 ; i  <  srv -> srv_sockets.used; i ++ ) {
        server_socket 
* srv_socket  =  srv -> srv_sockets.ptr[i];

        fdevent_register(srv
-> ev, srv_socket -> fd, network_server_handle_fdevent, srv_socket);
        fdevent_event_add(srv
-> ev,  & (srv_socket -> fde_ndx), srv_socket -> fd, FDEVENT_IN);
    }
    
return   0 ;
}


关键是在循环体中的两个函数, fdevent_register的第三个参数是一个回调函数, 就是fdevents的成员fdarray中每个fdnode的成员handler:
int  fdevent_register(fdevents  * ev,  int  fd, fdevent_handler handler,  void   * ctx) {
    fdnode 
* fdn;

    
//  分配一个fdnode指针
    fdn  =  fdnode_init();
   
    
//  保存回调函数
    fdn -> handler  =  handler;
    
//  保存fd
    fdn -> fd       =  fd;
    
//  保存context 对server是server为socket指针, 对client是connection指针
    fdn -> ctx      =  ctx;

    
//  以fd为索引在fdarray中保存这个fdnode
    ev -> fdarray[fd]  =  fdn;

    
return   0 ;
}

这里有一个小技巧, 函数中的倒数第二行, 以fd为索引保存fdnode, 因为这里的fdarray是一个数组, 因此这个方法可以以O(1)的速度找到与该fd相关的fdnode指针.但是, 因为0,1,2这三个fd已经提前预留给了标准输入输出错误这三个IO, 所以采用这样的算法将会至少浪费三个fdnode指针.

现在, 可以对fdnode结构体中两个成员进一步进行解析了:
    fdevent_handler handler;
    void *ctx;
其中, 如果该fd是服务器监听客户端连接的fd, 那么handler = network_server_handle_fdevent(在network.c文件中), ctx保存的就是server指针;
如果该fd是accapt客户端连接之后的fd, 那么handler = connection_handle_fdevent(在connections.c文件中), ctx保存的就是connection指针.

回过头来看,在将服务器监听fd注册到网络IO事件处理器中之后, 这个处理器就要开始循环处理了, 在server.c中的main.c函数中是这个轮询的主过程:
         //  轮询FD
         if  ((n  =  fdevent_poll(srv -> ev,  1000 ))  >   0 ) {
            
/*  n is the number of events  */
            
int  revents;
            
int  fd_ndx;

            fd_ndx 
=   - 1 ;
            
do  {
                fdevent_handler handler;
                
void   * context;
                handler_t r;

                
//  获得处理这些事件的函数指针 fd等

                
//  获得下一个fd在fdarray中的索引
                fd_ndx   =  fdevent_event_next_fdndx (srv -> ev, fd_ndx);
                
//  获得这个fd要处理的事件类型
                revents  =  fdevent_event_get_revent (srv -> ev, fd_ndx);
                
//  获取fd
                fd       =  fdevent_event_get_fd     (srv -> ev, fd_ndx);
                
//  获取回调函数
                handler  =  fdevent_get_handler(srv -> ev, fd);
                
//  获取处理相关的context(对server是server_socket指针, 对client是connection指针)
                context  =  fdevent_get_context(srv -> ev, fd);

                
/*  connection_handle_fdevent needs a joblist_append  */
                
//  进行处理
                 switch  (r  =  ( * handler)(srv, context, revents)) {
                
case  HANDLER_FINISHED:
                
case  HANDLER_GO_ON:
                
case  HANDLER_WAIT_FOR_EVENT:
                
case  HANDLER_WAIT_FOR_FD:
                    
break ;
                
case  HANDLER_ERROR:
                    
/*  should never happen  */
                    SEGFAULT();
                    
break ;
                
default :
                    log_error_write(srv, __FILE__, __LINE__, 
" d " , r);
                    
break ;
                }
            } 
while  ( -- >   0 );
简单的说, 这个过程就是:首先调用poll函数指针获取相关网络IO被触发的事件数, 保存在整型变量n中, 然后根据这个n值进行以下循环, 每次处理完n值减一, 为0之后退出, 这个循环的大致过程是: 首先获取下一个被触发的网络事件在fdnode数组中的索引, 接着根据该索引获取相关的事件类型, fd, 回调函数, contex, ,接着根据这些调用回调函数(也就是我们上面提到的函数
network_server_handle_fdevent和 connection_handle_fdevent ),请注意, 在本节的最开始部分曾经提到过fdevent.h中声明的函数都是对外暴露的fdevent结构体"public函数", 在上面这个轮询的过程中使用的正是这些"public函数", 在这些"public函数"中再根据曾经初始化的函数指针进行调用, 实现了OO中所谓的"多态".

以上就是通过fdevent结构体实现的网络IO处理器模型, 在这里体现如何使用C实现OO面向对象编程的种种常用技巧,不放在本节最后做一个总结:
1) fdevent结构体是一个虚拟基类, 其中的函数指针就是虚拟基类中的纯虚函数, 由具体实现去初始化之.fdevent结构体中的对象为所有派生类的公共成员, 而用各个预编译宏包围的成员则是各个派生类的私有成员.

2) 在fdevent.h中声明的函数可以理解为虚拟基类对外暴露的接口, 也就是public函数.

3) 各个具体的实现分别是各个实现C文件中的静态函数, 也就是派生类的private函数.

如果阅读到这里仍然对lighttpd中网络IO处理器模型有疑问, 可以具体参看前面提到的fdevent.h/c文件, 以及以fdevent_为前缀的c文件.

你可能感兴趣的:(lighttpd1.4.18代码分析(三)--网络IO事件处理器的使用)