本节是第二节
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->x = 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 (--n > 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文件.