那谁的技术博客

感兴趣领域:高性能服务器编程,存储,算法,Linux内核
随笔 - 210, 文章 - 0, 评论 - 1183, 引用 - 0
数据加载中……

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文件.

posted on 2008-08-28 23:20 那谁 阅读(4084) 评论(2)  编辑 收藏 引用 所属分类: 网络编程服务器设计Linux/Unixlighttpd

评论

# re: lighttpd1.4.18代码分析(三)--网络IO事件处理器的使用  回复  更多评论   

分析的不错
2008-08-29 10:11 |

# re: lighttpd1.4.18代码分析(三)--网络IO事件处理器的使用[未登录]  回复  更多评论   

谢谢博主的分析,写的很到位
2008-08-29 18:50 | 邹从杰

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理