lighttpd-1.4.18分析
- 格式:docx
- 大小:102.00 KB
- 文档页数:55
lighttpd1.4.18代码分析(一)--watcher,worker模型lighttpd是目前非常流行的web服务器,很多流量非常大的网站(如youtube)使用的就是lighttpd,它的代码量不多,但是设计巧妙,效率高,功能完备(这是它将来能取代Apache的重要因素),编码风格优美, 是学习网络编程,熟悉http服务器编写的良好范例.在我初学网络编程的时候,就是看的lighttpd的源码进行学习,在其中学到了不少的技巧.我打算将这些写出来与别人分享,可能开始比较杂乱,也不会作完全的分析,因为很多部分的代码我也没有看过,写一点是一点吧.我进行阅读和分析的lighttpd版本是1.4.18.lighttpd采用的是多进程+多路复用(如select,epoll)的网络模型,它对多路复用IO操作的封装将作为下一个专题的内容,本次将讲解它所采用的多进程模型.lighttpd中的配置文件有一项server.max-worker配置的是服务器生成的工作进城数.在lighttpd中, 服务器主进程被称为watcher(监控者),而由这个主进程创建出来的子进程被称为woker(工作者),而woker的数量正是由上面提到的配置项进行配置的.watcher创建并且监控woker的代码如下所示,我觉得这是一段很巧妙的代码,在我阅读了这段代码之后,到目前为止,我所写的所有服务器采用的都是类似lighttpd的watcher-woker多进程模型,这段代码在src目录中server.c文件的main函数中:#ifdef HAVE_FORK/* start watcher and workers */num_childs = srv->srvconf.max_worker;if (num_childs > 0) {int child = 0;while (!child && !srv_shutdown && !graceful_shutdown) {if (num_childs > 0) {switch (fork()) {case -1:return -1;case 0:child = 1;break;default:num_childs--;break;}} else {int status;if (-1 != wait(&status)) {/*** one of our workers went away*/num_childs++;} else {switch (errno) {case EINTR:/*** if we receive a SIGHUP we have to close our logs ourself as we don't * have the mainloop who can help us here*/if (handle_sig_hup) {handle_sig_hup = 0;log_error_cycle(srv);/*** forward to all procs in the process-group** we also send it ourself*/if (!forwarded_sig_hup) { forwarded_sig_hup = 1;kill(0, SIGHUP);}}break;default:break;}}}}/*** for the parent this is the exit-point */if (!child) {/*** kill all children too*/if (graceful_shutdown) {kill(0, SIGINT);} elseif (srv_shutdown) {kill(0, SIGTERM);}log_error_close(srv);network_close(srv);connections_free(srv);plugins_free(srv);server_free(srv);return 0;}}#endif首先,woker的数量保存在变量num_childs中, 同时另一个变量child是一个标志位, 为0时是父进程(watcher), 为1时则是子进程(worker), 下面进入一个循环:while (!child && !srv_shutdown && !graceful_shutdown)也就是说只要是父进程而且服务器没有被关闭该循环就一直进行下去, 接着如果num_childs>0就执行fork函数创建子进程, 对于子进程而言,赋值child=1, 同时num_childs值-1, 于是子进程不满足!child这个条件退出循环继续执行下面的代码, 而父进程还在循环中, 如果num_childs=0也就是所有的子进程都创建成功了, 那么父进程就阻塞在调用wait函数中, 等待着一旦有子进程退出, 那么wait函数返回, 这样num_childs+1, 于是继续前面的调用, 再次创建出子进程.这也就是watcher和worker的来历:父进程负责创建子进程并且监控是否有子进程退出, 如果有, 那么再次创建出子进程;而子进程是worker, 是具体执行服务器操作的工作者, 在被创建完毕之后退出循环, 去做下面的事情.而如果父进程退出这个循环, 那么一定是srv_shutdown或者graceful_shutdown之一变为了非零值, 所以在循环外, 还要进行判断, 如果是父进程, 那么就是服务器程序要退出了, 最后作一些清理的工作.用伪码表示这部分代码就是:如果(是父进程而且当前没有要求终止服务器) 就一直循环下去{如果还有未创建的子进程{创建出一个子进程如果是子进程, 那么根据最上面的循环条件退出这个循环.如果是父进程, 那么将未创建的子进程数量- 1否则就是没有未创建的子进程{一直保持睡眠, 一旦发现有子进程退出父进程就苏醒, 将未创建的子进程数量+ 1;}}父进程的代码永远不会执行到这个循环体之外, 一旦发生, 就是因为要终止服务器的运行, 如果这种情况发生, 就进行最后的一些清理工作....在这之后, 各子进程分道扬镳, 各自去进行自己的工作, 互不干扰.这也是我非常喜欢多进程编程的原因, 少了多线程编程中考虑到数据同步等麻烦的事情,要考虑的事情相对而言简单的多了.关于多进程VS 多线程的话题, 不在这里多加阐述了.lighttpd1.4.18代码分析(二)--fdevents结构体解析lighttpd中采取了类似于OO中面向对象的方式封装了各种平台对网络IO的事件处理,这其中包括:OS Method Config Valueall select selectUnix poll pollLinux 2.4+ rt-signals linux-rtsigLinux 2.6+ epoll linux-sysepollSolaris /dev/poll solaris-devpollFreeBSD, ... kqueue freebsd-kqueueNetBSD kqueue kqueue所有的网络IO事件都要满足这个数据结构的要求, 这个名为fdevents的结构体可以看成是OO中虚拟基类, 而每种具体的实现则可以看成是继承并且实现了该虚拟基类中纯虚函数的派生类:typedef struct fdevents {fdevent_handler_t type;fdnode **fdarray;size_t maxfds;#ifdef USE_LINUX_SIGIOint in_sigio;int signum;sigset_t sigset;siginfo_t siginfo;bitset *sigbset;#endif#ifdef USE_LINUX_EPOLLint epoll_fd;struct epoll_event *epoll_events;#endif#ifdef USE_POLLstruct pollfd *pollfds;size_t size;size_t used;buffer_int unused;#endif#ifdef USE_SELECTfd_set select_read;fd_set select_write;fd_set select_error;fd_set select_set_read;fd_set select_set_write;fd_set select_set_error;int select_max_fd;#endif#ifdef USE_SOLARIS_DEVPOLLint devpoll_fd;struct pollfd *devpollfds;#endif#ifdef USE_FREEBSD_KQUEUEint kq_fd;struct kevent *kq_results;bitset *kq_bevents;#endif#ifdef USE_SOLARIS_PORTint port_fd;#endifint (*reset)(struct fdevents *ev);void (*free)(struct fdevents *ev);int (*event_add)(struct fdevents *ev, int fde_ndx, int fd, int events); int (*event_del)(struct fdevents *ev, int fde_ndx, int fd);int (*event_get_revent)(struct fdevents *ev, size_t ndx);int (*event_get_fd)(struct fdevents *ev, size_t ndx);int (*event_next_fdndx)(struct fdevents *ev, int ndx);int (*poll)(struct fdevents *ev, int timeout_ms);int (*fcntl_set)(struct fdevents *ev, int fd);} fdevents;该结构体中包含了一些公有的参数, 也就是类似OO中虚拟基类中的成员变量, 无论是哪个派生类继承之后都会有这部分的成员,如最前面的几个成员:fdevent_handler_t type;fdnode **fdarray;size_t maxfds;其中type是如下类型的枚举:typedef enum{FDEVENT_HANDLER_UNSET,FDEVENT_HANDLER_SELECT,FDEVENT_HANDLER_POLL,FDEVENT_HANDLER_LINUX_RTSIG,FDEVENT_HANDLER_LINUX_SYSEPOLL,FDEVENT_HANDLER_SOLARIS_DEVPOLL,FDEVENT_HANDLER_FREEBSD_KQUEUE,FDEVENT_HANDLER_SOLARIS_PORT} fdevent_handler_t;, 其实也就是各个不同IO事件类型.fdarray是一个存放fdnode *类型的数组:typedef struct _fdnode {fdevent_handler handler;void *ctx;int fd;struct _fdnode *prev, *next;} fdnode;这个结构体中, handler是一个回调函数, 在事件触发时进行回调(后面我们会讲到), ctx是一个context, 根据不同的fd进行区分.fd就不必多说了, 是socket fd, 可能是服务器监听所有的fd, 也可能是accept之后与client 相关的fd.再后面的两个参数, 将这个结构体变为了一个链表中的一个节点.maxfds, 这个成员用于存放可以处理的最大数量.除了这些公有成员外, 根据编译时的预编译宏, 该结构体还有其它的成员, 这些成员就相当于OO中具体每个派生类自己私有的成员.在这个结构体的最后, 是一组函数指针, 也就是OO中的纯虚函数, 每个派生类都要根据这些接口自己进行实现,我分别给加上了一些简单的注释:// 重置某个fdeventsint (*reset)(struct fdevents *ev);// 释放fdevents指针void (*free)(struct fdevents *ev);// 向fdevents中添加一个fd, events表示这个fd对哪些事件感兴趣int (*event_add)(struct fdevents *ev, int fde_ndx, int fd, int events);// 向fdevents中删除一个fdint (*event_del)(struct fdevents *ev, int fde_ndx, int fd);// 根据fd在fdevents中的fdarray中的index, 获取该fd目前对哪些事件感兴趣int (*event_get_revent)(struct fdevents *ev, size_t ndx);// 根据fd在fdevents中的fdarray中的index, 获取要进行处理的fdint (*event_get_fd)(struct fdevents *ev, size_t ndx);// 获取下一个需要进行处理的fd在fdarray中的indexint (*event_next_fdndx)(struct fdevents *ev, int ndx);// 轮询, timeout_ms是超时参数, 单位是微秒int (*poll)(struct fdevents *ev, int timeout_ms);// 这个接口一般都没有实现, 为NULLint (*fcntl_set)(struct fdevents *ev, int fd);在上面的函数中, 参数int events的值为以下几种:#define BV(x) (1 << x)#define FDEVENT_IN BV(0)#define FDEVENT_PRI BV(1)#define FDEVENT_OUT BV(2)#define FDEVENT_ERR BV(3)#define FDEVENT_HUP BV(4)#define FDEVENT_NVAL BV(5)从这里可以看到, 具体判断某个事件是否发生, 需要采用的掩码操作进行判断, 而不是一般的比较操作, 如:if (events & FDEVENT_IN) 而不是if (events == FDEVENT_IN)因此, 一个fd, 当前所关注的事件类型可以不止一个而是有多个.了解了这个结构体之后, 也就了解了lighttpd中每种网络IO事件必须处理的事件类型及它们对外的接口.下一节开始讲解在lighttpd中是如何使用这个事件处理器的.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->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_ed; 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;// 保存fdfdn->fd = fd;// 保存context 对server是server为socket指针, 对client是connection指针fdn->ctx = ctx;// 以fd为索引在fdarray中保存这个fdnodeev->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函数中是这个轮询的主过程:// 轮询FDif ((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);// 获取fdfd = 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文件.lighttpd1.4.18代码分析(四)--处理监听fd的流程前面介绍了lighttpd使用的watcher-worker模型, 它对IO事件处理的封装, 现在可以把这些结合起来看看这大概的流程.首先, 服务器创建监听socket, 然后在server.c中调用函数network_register_fdevents将监听socket注册到IO事件处理器中: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_ed; 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注册fd到IO事件处理器中, 对于服务器监听fd而言, 它在fdnode中的回调函数handler是函数network_server_handle_fdevent, 而ctx则是srv_socket.接着调用函数fdevent_event_add, 其中传入的第三个参数是FDEVENT_IN, 也就是当该fd 上有可读数据时触发调用, 对于所有监听的fd而言,有可读事件就意味着有新的连接到达.然后服务器创建子进程worker, 服务器父进程自己成为watcher, 自此下面的工作由子进程进行处理,每个子进程所完成的工作都是一样的.有的书上说有多个进程在等待accept连接的时候会造成所谓的惊群现象,在lighttpd的代码中,没有看到在accaept之前进行加锁操作, 这是否会造成惊群不得而知.现在, 在IO事件处理器中仅有一个fd等待触发, 就是前面注册的监听fd, 我们看看当一个连接到来的时候处理的流程, 首先看我们曾经说过的轮询fd进行处理的主循环:// 轮询FDif ((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);// 获取fdfd = 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);当一个连接到来的时候, 调用fdevent_poll返回值是1, 因为这个函数的返回值表示的是有多少网络IO事件被触发了, 接着由于n>0, 进入循环中获得被触发的fd, 回调函数, 以及ctx指针, 在这里由于是监听fd被触发, 那么返回的回调函数是前面提到的network_server_handle_fdevent,接着就要调用这个函数处理IO事件了:// 这个函数是处理server事件的函数, 与connection_handle_fdevent对应handler_t network_server_handle_fdevent(void *s, void *context, int revents) {server *srv = (server *)s;server_socket *srv_socket = (server_socket *)context;connection *con;int loops = 0;UNUSED(context);if (revents != FDEVENT_IN) {log_error_write(srv, __FILE__, __LINE__, "sdd","strange event for server socket",srv_socket->fd,revents);return HANDLER_ERROR;}/* accept()s at most 100 connections directly** we jump out after 100 to give the waiting connections a chance */// 一次最多接受100个链接for (loops = 0; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops++) { handler_t r;// 这里马上进入状态机中进行处理仅仅对应状态为CON_STATE_REQUEST_START这一段// 也就是保存连接的时间以及设置一些计数罢了connection_state_machine(srv, con);switch(r = plugins_call_handle_joblist(srv, con)) {case HANDLER_FINISHED:case HANDLER_GO_ON:break;default:log_error_write(srv, __FILE__, __LINE__, "d", r);break;}}return HANDLER_GO_ON;}我给这段代码加了一些注释, 有几个地方做一些解释:1)UNUSED(context)是一个宏, 扩展开来就是( (void)(context) ), 实际上是一段看似无用的代码, 因为没有起什么明显的作用, 是一句废话,在这个函数中, 实际上没有使用到参数context, 如果在比较严格的编译器中, 这样无用的参数会产生一条警告, 说有一个参数没有使用到, 加上了这么一句无用的语句, 就可以避免这个警告.那么, 有人就会问了, 为什么要传入这么一个无用的参数呢?回答是, 为了满足这个接口的需求,来看看回调函数的类型定义:typedef handler_t (*fdevent_handler)(void *srv, void *ctx, int revents);这个函数指针要求的第二个参数是一个ctx指针, 对于监听fd的回调函数network_server_handle_fdevent而言, 它是无用的, 但是对于处理连接fd的回调函数而言, 这个指针是有用的.2) 在函数的前面, 首先要判断传入的event事件是否是FDEVENT_IN, 也就是说, 只可能在fd有可读数据的时候才触发该函数, 其它的情况都是错误.3)函数在最后进入一个循环, 循环的最多次数是100次, 并且当connection_accept函数返回NULL时也终止循环, 也就是说, 当监听fd被触发时,服务器尽量的去接收新的连接, 最多接收100个新连接, 这样有一个好处, 假如服务器监听fd是每次触发只接收一个新的连接, 那么效率是比较低的,不如每次被触发的时候"尽力"的去接收, 一直到接收了100个新的连接或者没有可接收的连接之后才返回.接着来看看负责接收新连接的函数connection_accept做了什么:// 接收一个新的连接connection *connection_accept(server *srv, server_socket *srv_socket) {/* accept everything *//* search an empty place */int cnt;sock_addr cnt_addr;socklen_t cnt_len;/* accept it and register the fd *//*** check if we can still open a new connections** see #1216*/// 如果正在使用的连接数大于最大连接数就返回NULLif (srv->conns->used >= srv->max_conns) {return NULL;}cnt_len = sizeof(cnt_addr);if (-1 == (cnt = accept(srv_socket->fd, (struct sockaddr *) &cnt_addr, &cnt_len))) { switch (errno) {case EAGAIN:#if EWOULDBLOCK != EAGAINcase EWOULDBLOCK:#endifcase EINTR:/* we were stopped _before_ we had a connection */case ECONNABORTED: /* this is a FreeBSD thingy *//* we were stopped _after_ we had a connection */break;case EMFILE:/* out of fds */break;default:log_error_write(srv, __FILE__, __LINE__, "ssd", "accept failed:", strerror(errno), errno); }return NULL;} else {connection *con;// 当前使用的fd数量+1srv->cur_fds++;/* ok, we have the connection, register it */// 打开的connection+1(这个成员貌似没有用)srv->con_opened++;// 获取一个新的connectioncon = connections_get_new_connection(srv);// 保存接收到的fdcon->fd = cnt;// 索引为-1con->fde_ndx = -1;#if 0gettimeofday(&(con->start_tv), NULL);#endif// 注册函数指针和connection指针fdevent_register(srv->ev, con->fd, connection_handle_fdevent, con);// 状态为可以接收请求connection_set_state(srv, con, CON_STATE_REQUEST_START);// 保存接收连接的时间con->connection_start = srv->cur_ts;// 保存目标地址con->dst_addr = cnt_addr;buffer_copy_string(con->dst_addr_buf, inet_ntop_cache_get_ip(srv, &(con->dst_addr))); // 保存server_socket指针con->srv_socket = srv_socket;// 设置一下接收来的FD, 设置为非阻塞if (-1 == (fdevent_fcntl_set(srv->ev, con->fd))) {log_error_write(srv, __FILE__, __LINE__, "ss", "fcntl failed: ", strerror(errno));return NULL;}#ifdef USE_OPENSSL/* connect FD to SSL */if (srv_socket->is_ssl) {if (NULL == (con->ssl = SSL_new(srv_socket->ssl_ctx))) {log_error_write(srv, __FILE__, __LINE__, "ss", "SSL:",ERR_error_string(ERR_get_error(), NULL));return NULL;}SSL_set_accept_state(con->ssl);con->conf.is_ssl=1;if (1 != (SSL_set_fd(con->ssl, cnt))) {log_error_write(srv, __FILE__, __LINE__, "ss", "SSL:",ERR_error_string(ERR_get_error(), NULL));return NULL;}}#endifreturn con;}}抛开出错处理这部分不解释, 一旦出错, 就返回NULL指针, 这时可以终止上面那个循环接收新连接的过程,下面重点看看接收了一个新的连接之后需要做哪些事情, 在上面的代码中我加了一些简单的注释, 下面加一些更加详细些的解释:1)要将服务器已经接收的fd数量(成员cur_fds)加一, 这个数量用于判断是否可以接收新的连接的, 超过一定的数量时, 服务器就暂停接收,等一些fd释放之后才能继续接收2) 调用函数connections_get_new_connection返回一个connection指针, 用于保存新到的连接, 获得这个指针之后要保存接收这个连接的时间(成员connection_start中), 保存新到连接的地址(成员dst_addr和dst_addr_buf中), 此外还要保存一个server指针, 并且调用函数fdevent_fcntl_set将该fd设置为非阻塞的, 最后别忘了要调用fdevent_register函数将该fd注册到IO事件处理器中, 另外该fd当前的状态通过connection_set_state设置为CON_STATE_REQUEST_START, 这是后面进入状态机处理连接的基础.了解了这个函数的处理过程, 回头看看上面的循环:for (loops = 0; loops < 100 && NULL != (con = connection_accept(srv, srv_socket)); loops++) { handler_t r;// 这里马上进入状态机中进行处理仅仅对应状态为CON_STATE_REQUEST_START这一段// 也就是保存连接的时间以及设置一些计数罢了connection_state_machine(srv, con);switch(r = plugins_call_handle_joblist(srv, con)) {case HANDLER_FINISHED:case HANDLER_GO_ON:break;default:log_error_write(srv, __FILE__, __LINE__, "d", r);break;}}我们已经分析完了函数connection_accept, 当一个新的连接调用这个函数成功返回的时候, 这个循环执行函数connection_state_machine进行处理.这是一个非常关键的函数, 可以说, 我们后面讲解lighttpd的很多笔墨都将花费在这个函数上, 这也是我认为lighttpd实现中最精妙的地方之一, 在这里我们先不进行讲解, 你所需要知道的是, 在这里,connection_state_machine调用了函数fdevent_event_add, 传入的事件参数仍然是FDEVENT_IN, 也就是说, 对于新加入的fd, 它所首先关注的IO事件也是可读事件.我们大体理一理上面的流程, 省略去对watcher-worker模型的描述:创建服务器监听fd-->调用fdevent_register函数将监听fd注册到IO事件处理器中-->调用fdevent_event_add函数添加FDEVENT_IN到监听fd所关注的事件中-->当一个新的连接到来时:IO事件处理器轮询返回一个>0的值-->IO事件处理返回被触发的fd, 回调函数, ctx指针,在这里就是监听fd,回调函数则是network_server_handle_fdevent->调用监听fd注册的回调函数network_server_handle_fdevent-->network_server_handle_fdevent函数尽力接收新的连接, 除非已经接收了100个新连接, 或者没有新连接到来-->对于新到来的连接, 同样是调用fdevent_register函数将它注册到IO事件处理器中, 同样调用fdevent_event_add函数添加该fd所关注的事件是FDEVENT_IN以上, 就是lighttpd监听fd处理新连接的大体流程.我们知道, fd分为两种:一种是服务器自己创建的监听fd, 负责监听端口, 接收新到来的连接; 另一种, 就是由监听fd调用accept函数返回的连接fd, 这两种fd在处理时都会注册到IO 事件处理器中(调用fdevent_register函数),同时添加它们所关注的IO事件(可读/可写等)(调用fdevent_event_add函数).也就是说,对IO事件处理器而言, 它并不关注所处理的fd是什么类型的, 你要使用它, 那么就把你的fd以及它的回调函数注册到其中, 同时添加你所关注的IO事件是什么, 当一个fd 所关注的IO事件被触发时, IO事件处理器自动会根据你所注册的回调函数进行回调处理, 这是关键点, 如果你没有明白, 请回头看看前面提到的IO事件处理器.这些的基础就是我们前面提到IO事件处理器, 前面我们提到过, lighttpd对IO事件处理的封装很漂亮, 每个具体实现都按照接口的规范进行处理.我们在讲解时, 也没有涉及到任何一个具体实现的细节, 这也是因为lighttpd的封装很好,以至于我们只需要了解它们对外的接口不需要深入细节就可以明白其运行的原理.在本节中, 我们结合IO事件处理器, 对上面提到的第一种fd也就是监听fd的处理流程做了介绍, 在后面的内容中, 将重点讲解对连接fd的处理.lighttpd1.4.18代码分析(五)--处理超时连接本节相对简单, 讲讲lighttpd中如何处理超时的连接.方法很简单, lighttpd创建一个每隔一秒触发的定时器, 被触发后查找当前的所有连接, 看它们的时间是否已经超过了最长的生存期, 如果是就关闭连接.创建定时器的代码在server.c的main函数中:#ifdef USE_ALARMstruct itimerval interval;interval.it__sec = 1;interval.it__usec = 0;interval.it__sec = 1;interval.it__usec = 0;#endif#ifdef USE_ALARM// 定时signal(SIGALRM, signal_handler);/* setup periodic timer (1 second) */if (setitimer(ITIMER_REAL, &interval, NULL)) {log_error_write(srv, __FILE__, __LINE__, "s", "setting timer failed");return -1;}getitimer(ITIMER_REAL, &interval);#endif定时器触发的时候产生ALARM信号,此时在服务器主循环中轮询所有的连接,这段代码同样在server.c的main函数中:// 如果产生了alarm信号那么一秒钟过去了if (handle_sig_alarm) {/* a new second */#ifdef USE_ALARM/* reset notification */handle_sig_alarm = 0;#endif/* get current time */// 获得当前的时间min_ts = time(NULL);// 如果当前时间不等于server上次记录的时间if (min_ts != srv->cur_ts) {int cs = 0;connections *conns = srv->conns;handler_t r;switch(r = plugins_call_handle_trigger(srv)) {case HANDLER_GO_ON:break;case HANDLER_ERROR:log_error_write(srv, __FILE__, __LINE__, "s", "one of the triggers failed"); break;default:log_error_write(srv, __FILE__, __LINE__, "d", r);break;}/* trigger waitpid */// 更新server的当前时间srv->cur_ts = min_ts;/* cleanup stat-cache */// 每秒清空一次stat cachestat_cache_trigger_cleanup(srv);/*** check all connections for timeouts*/// 检查所有连接是否已经超时for (ndx = 0; ndx < conns->used; ndx++) {int changed = 0;connection *con;int t_diff;con = conns->ptr[ndx];if (con->state == CON_STATE_READ ||con->state == CON_STATE_READ_POST) {if (con->request_count == 1) {// 如果当前时间与read_idle_ts之差大于max_read_idle, 超时if (srv->cur_ts - con->read_idle_ts > con->conf.max_read_idle) {/* time - out */connection_set_state(srv, con, CON_STATE_ERROR);changed = 1;}} else {// 如果当前时间与read_idle_ts之差大于max_keep_alive_idle, 超时if (srv->cur_ts - con->read_idle_ts > con->conf.max_keep_alive_idle) { /* time - out */connection_set_state(srv, con, CON_STATE_ERROR);changed = 1;}}}if ((con->state == CON_STATE_WRITE) &&(con->write_request_ts != 0)) {// 如果当前时间与write_request_ts之差大于max_write_idle, 超时if (srv->cur_ts - con->write_request_ts > con->conf.max_write_idle) { /* time - out */#if 1log_error_write(srv, __FILE__, __LINE__, "sbsosds","NOTE: a request for",con->request.uri,"timed out after writing",con->bytes_written,"bytes. We waited",(int)con->conf.max_write_idle,"seconds. If this a problem increase server.max-write-idle");#endifconnection_set_state(srv, con, CON_STATE_ERROR);changed = 1;}}/* we don't like div by zero */// 如果接收连接的时间= server当前时间if (0 == (t_diff = srv->cur_ts - con->connection_start))t_diff = 1;。