分类 wifidog分析 下的文章

解决WiFiDog在高版本内核下不能使用问题

修改wifidog中的utl.c文件, 修改get_iface_ip 函数
if ((sockd = socket (AF_INET, SOCK_PACKET, htons(0x8086))) < 0) {
这句建议修改为 sockfd = socket(AF_INET,SOCK_DGRAM,0);
原因: 新kernel对于原模式过时

本文章由:http://www.wifidog.pro/2016/04/06/%E8%A7%A3%E5%86%B3WiFiDog%E5%9C%A8%E9%AB%98%E7%89%88%E6%9C%AC%E5%86%85%E6%A0%B8%E4%B8%8B%E4%B8%8D%E8%83%BD%E4%BD%BF%E7%94%A8%E9%97%AE%E9%A2%98.html整理编辑,转载请注明出处

wifidog源码分析Lighttpd1.4.20源码分析之fdevent系统(4) -----连接socket的处理与超时处理

前面讲了lighttpd是怎样使用fdevent系统的,以及监听socket的处理过程。这一篇我们来看一看lighttpd是怎样处理连接socket的。
首先,我们来看看lighttpd是怎样建立和客户端的连接的。前面在讲监听socket的处理过程中其实已经讲解了连接的建立过程。lighttpd监测监听socket的IO事件,如果有可读事件发生,那么表示有新的连接请求,然后调用network.c/network_server_handle_fdevent()来处理连接请求。network_server_handle_fdevent()函数调用connections.c/connection_accept() 接受客户端的请求,建立连接。在建立连接的同时,就得到了连接socket的fd,也就是accept函数的返回值。
建立连接之后,这个连接对应的状态机的状态被设置为CON_STATE_REQUEST_START,就是开始读取客户端发过来的request信息。在从connection_accept函数跳回network_server_handle_fdevent()函数的for循环中后,程序紧接着就调用了一次connection_state_machine()函数,这个函数是根据当前连接的状态机的状态设置状态机的下一个状态,CON_STATE_REQUEST_START的下一个状态是CON_STATE_READ,这个状态表示连接正在读取客户端发送的数据。当连接的状态机被设置成CON_STATE_READ后,在connection_state_machine()函数的最后,有这样一个switch语句:

switch (con->state)
    {
    case CON_STATE_READ_POST:
    case CON_STATE_READ:
    case CON_STATE_CLOSE:
        fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_IN);
        break;
    case CON_STATE_WRITE:
        /*
         * request write-fdevent only if we really need it
         * - if we have data to write
         * - if the socket is not writable yet
         */
        if (!chunkqueue_is_empty(con->write_queue) && (con->is_writable == 0) &&(con->traffic_limit_reached == 0))
        {
            fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_OUT);
        }
        else
        {
            fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
        }
        break;
    default:
        fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
        break;
    }

  上面这个switch语句将状态处在CON_STATE_READ_POST,CON_STATE_READ和CON_STATE_CLOSE的连接对应的连接socket fd加入到fdevent系统中,并监听可读事件。将处CON_STATE_WRITE状态且有数据要写的连接对应的socket fd加入到fdevent系统中,并监听可写事件。其他状态的连接则把对应的fd从fdevent系统中删除,因为这些连接不会有IO事件发生。
  这样,连接socket fd就被加入到了fdevent系统中。下面就是等待IO事件的发生。程序在前面已经提到过,如下:

if ((n = fdevent_poll(srv->ev, 1000)) > 0)
        {
            int revents;
            int fd_ndx;
            fd_ndx = -1;
            do
            {
                fdevent_handler handler;
                void *context;
                handler_t r;
                fd_ndx = fdevent_event_next_fdndx(srv->ev, fd_ndx);
                revents = fdevent_event_get_revent(srv->ev, fd_ndx);
                fd = fdevent_event_get_fd(srv->ev, fd_ndx);
                handler = fdevent_get_handler(srv->ev, fd);
                context = fdevent_get_context(srv->ev, fd);
                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系统,它不关心自己处理的fd是连接fd还是监听fd,它所做的就是对于发生了这个fd所希望的IO事件以后,调用这个fd对应的处理函数处理IO事件。连接fd对应的处理函数是connections.c/connection_handle_fdevent()函数。函数的代码如下:

handler_t connection_handle_fdevent(void *s, void *context,int revents)
{
    server *srv = (server *) s;
    connection *con = context;
    //把这个连接加到作业队列中。
     joblist_append(srv, con);
    if (revents & FDEVENT_IN)
    {
        con->is_readable = 1;
    }
    if (revents & FDEVENT_OUT)
    {
        con->is_writable = 1;
        /*
         * we don't need the event twice
         */
    }
    if (revents & ~(FDEVENT_IN | FDEVENT_OUT))
    {
        /*
         * looks like an error 即可读又可写,可能是一个错误。
         */
        /*
         * FIXME: revents = 0x19 still means that we should read from the queue
         */
        if (revents & FDEVENT_HUP)
        {
            if (con->state == CON_STATE_CLOSE)
            {
                con->close_timeout_ts = 0;
            }
            else
            {
                /*
                 * sigio reports the wrong event here there was no HUP at all
                 */
                connection_set_state(srv, con, CON_STATE_ERROR);
            }
        }
        else if (revents & FDEVENT_ERR)
        {
            connection_set_state(srv, con, CON_STATE_ERROR);
        }
        else
        {
            log_error_write(srv, __FILE__, __LINE__, "sd","connection closed: poll() -> ???", revents);
        }
    }
    if (con->state == CON_STATE_READ|| con->state == CON_STATE_READ_POST)
    {
        connection_handle_read_state(srv, con);
 //继续读取数据,直到数据读取完毕
     }
 // 数据的写回并没有放给状态机去处理。
     if (con->state == CON_STATE_WRITE&& !chunkqueue_is_empty(con->write_queue) && con->is_writable)
    {
        if (-1 == connection_handle_write(srv, con))
        {
            connection_set_state(srv, con, CON_STATE_ERROR);
            log_error_write(srv, __FILE__, __LINE__, "ds", con->fd,"handle write failed.");
        }
        else if (con->state == CON_STATE_WRITE)
        {
            //写数据出错,记录当前时间,用来判断连接超时。
             con->write_request_ts = srv->cur_ts;
        }
    }
    if (con->state == CON_STATE_CLOSE)
    {
        /*
         * flush the read buffers 清空缓冲区中的数据。
         */
        int b;
        //获取缓冲区中数据的字节数
         if (ioctl(con->fd, FIONREAD, &b))
        {
            log_error_write(srv, __FILE__, __LINE__, "ss","ioctl() failed", strerror(errno));
        }
        if (b > 0)
        {
            char buf[1024];
            log_error_write(srv, __FILE__, __LINE__, "sdd","CLOSE-read()", con->fd, b);
            //将缓冲区中的数据读取后并丢弃,此时连接已经关闭,数据是无用数据。
             read(con->fd, buf, sizeof(buf));
        }
        else
        {
            /*
             * nothing to read 缓冲区中没有数据。复位连接关闭超时计时。
             */
            con->close_timeout_ts = 0;
        }
    }
    return HANDLER_FINISHED;
}

  可以看到,connection_handle_fdevent()函数根据当前连接fd所发生的IO事件,对connection结构体中的标记变量赋值,如is_writable,is_readable等,并做一些时间的记录。这些事件所对应的真正的IO处理则交给状态机处理。状态机根据这些标记变量进行相应的动作处理。
  这样,对于fdevent系统对于一次连接fd的IO事件就处理结束了。当然,真正的处理工作是由状态机来完成。下面的图简要的描述了fdevent系统对连接fd和监听fd的处理:
1.jpg

  下面我们来看一看连接超时的处理。连接超时有三种:读数据超时,写数据超时和关闭超时。处理超时的代码在server.c中的main函数woker进程开始部分:

/**
         * 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);
            /**
             * 这里判断和服务器记录的当前时间是否相同。
             * 相同,则表示服务器还在这一秒中,继续处理请求等。
             * 如果不相同,则进入了一个新的周期(当然周期是一秒)。这就要做一些触发和检查以及清理的动作。
             * 如插件的触发连接的超时清理状态缓存等。
             * 其中,最主要的工作是检查连接的超时。
             */
            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
                 */
                srv->cur_ts = min_ts;
                /*
                 * cleanup stat-cache 清理状态缓存。每秒钟清理一次。
                 */
                stat_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) //连接处理一个请求
                         {
                            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
                        {
                            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))
                    {
                        if (srv->cur_ts - con->write_request_ts> con->conf.max_write_idle)
                        {
                            /*
                             * time - out
                             */
#if 1
                            log_error_write(srv, __FILE__, __LINE__,"sbsosds", "NOTE: a request for",
                                            con->request.uri, "timed outafter writing", con->bytes_written, "bytes. We waited",
                                        (int) con->conf. max_write_idle,
                                            "seconds. If this a problemincrease server.max-write-idle");
#endif
                            connection_set_state(srv, con, CON_STATE_ERROR);
                            changed = 1;
                        }
                    }

                    /*
                     * we don't like div by zero 防止除0。。。
                     */
                    if (0 ==(t_diff = srv->cur_ts - con->connection_start))
                            t_diff = 1;

                    /**
                     * 下面的if语句不是用来判断连接是否超时。
                     * lighttpd对每个连接设置了一个kbytes_per_second,这个变量设定每个连接在一秒钟内多能传输的最大数据量。
                     * 如果传送的数据大于这个值,那么这个连接将停止传输数据,被追加到作业队列中等待下一次处理。
                     * 作者这样做估计是为了平衡各个连接之间的数据传输。
                     */
                    if (con->traffic_limit_reached && (con->conf.kbytes_per_second == 0|| ((con->bytes_written / t_diff)< con->conf.kbytes_per_second * 1024)))
                    {
                        /*
                         * enable connection again
                         */
                        con->traffic_limit_reached = 0;
                        changed = 1;
                    }

                    if (changed)
                    {
                        connection_state_machine(srv, con);
                    }
                    con->bytes_written_cur_second = 0;
                    *(con->conf.global_bytes_per_second_cnt_ptr) = 0;
                }//end of for( ndx = 0; ...
                if (cs == 1)
                    fprintf(stderr, "\n");
            }//end of if (min_ts != srv->cur_ts)...
        }//end of if (handle_sig_alarm)...

在这个If语句中,作者的本意是通过alarm信号来判断时间是否到一秒种。handle_sig_alarm就是标记是否已经过了一秒钟。在server.c的信号处理函数sigaction_handler()中可以看到:

case SIGALRM:     //超时信号
         handle_sig_alarm = 1;
         break;

当收到SIGALRM信号时,标记handle_sig_alarm为1。
下面的代码是启动计时器。两段代码都被宏包围。说明需要定义宏USE_ALARM才启动计时器。

#ifdef USE_ALARM
    struct itimerval interval;
    interval.it_interval.tv_sec = 1;
    interval.it_interval.tv_usec = 0;
    interval.it_value.tv_sec = 1;
    interval.it_value.tv_usec = 0;
#endif
#ifdef USE_ALARM
    signal(SIGALRM, signal_handler);
    if (setitimer(ITIMER_REAL, &interval, NULL))
    {
        log_error_write(srv, __FILE__, __LINE__, "s", "setting timer failed");
        return -1;
    }
    getitimer(ITIMER_REAL, &interval);
#endif

下面寻找宏USE_ALARM的定义。仍然在server.c文件中:

/*
 * IRIX doesn't like the alarm based time() optimization
 */
/*
 * #define USE_ALARM
 */

  不过,这个唯一的定义被注释掉了。。。
  那么,也就是说,作者并没有使用计时器产生SIGALRM信号来判断时间是否过了一秒。其实,上面处理连接超时的代码中,作者通过判断当前时间和服务器记录的当前时间来判断时间是否过了一秒。如果两个时间不一样,那么时间就过了一秒。不使用SIGALRM信号,可以减少很多信号处理,降低程序的复杂度。没有使用SIGALRM信号,那么handle_sig_alarm就一直是1。子进程每循环一次都要比较服务器记录的时间和当前时间。
  下面继续看超时处理。在上面的处理程序中,lighttpd通过比较read_idle_ts,write_request_ts和当前时间的差值来判断连接是否读超时或写超时。如果这两个差值分别大于max_read_idle和max_write_idle则表示超时。如果一个连接正在处理多个请求时,读超时是和max_keep_alive_idle比较。这些上限值在配置中设置。
  那么,read_idle_ts和write_request_ts又是记录的什么呢?
  对于read_idle_ts,在连接进入CON_STATE_REQUEST_START状态时,记录了当前时间。如果连接长时间没有去读取request请求,则也表示连接超时。当连接开始读数据时,read_idle_ts记录开始读数据的时间。这个不多说了。
  对于write_request_ts,在处理CON_STATE_WRITE状态时,有对其赋值的语句。在connection_handle_fdevent函数中也有。其实,都是在调用connection_handle_write函数出错并且连接处在CON_STATE_WRITE状态时,记录当前时间。
  通过这两个变量可以看出,lighttpd对读和写的超时处理是不一样的。对于读,设定了最长时间,不管读多少数据,一旦时间超了就算超时。而对于写,只有在写出错的时候才开始计算超时。如果没有出错,那么写数据花再多的时间也不算超时。这就有一个问题了,如果客户端上传的数据很多呢?这样没上传完就有可能被判断为超时。其实,lighttpd做为一个web服务器,其假设上传的数据都是有限的。在绝大多数情况下,上传数据都是很小的,也就是http头等,而下载的数据往往很多。因此,这样处理可以提高效率。如果需要上传大量数据,可以修改配置中的超时限制。(PS:这点不太确定,望高手讲解。)
  lighttpd每过一秒钟就要轮询连接,检查是否超时。如果连接很多时,这将浪费大量的时间。虽然这样很低效,但是处理简单,程序复杂度低。在真正的使用中,效率也没有想像中的那么差。
  至此,lighttpd的fdevent系统就介绍完毕了。从下一篇开始,我们将走进lighttpd的状态机。

本文章由 http://www.wifidog.pro/2015/04/21/wifidog%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90Lighttpd-socket%E7%9A%84%E5%A4%84%E7%90%86.html 整理编辑,转载请注明出处

wifidog源码分析Lighttpd1.4.20源码分析之fdevent系统(3) -----使用(2)

接着上文介绍的函数fdevent_linux_sysepoll_event_add 讲解,首先看函数的第三个参数events,他是一个整型,其没以为对应一种IO事件。
上面fdevent_event_add()函数的额第三个参数是FDEVENT_IN,这是一个宏

/*
 * 用于标记文件描述符的状态
 */
 #define FDEVENT_IN     BV(0)     //文件描述符是否可写
 #define FDEVENT_PRI    BV(1)      //不阻塞的可读高优先级的数据 poll
 #define FDEVENT_OUT    BV(2)     //文件描述符是否可读
#define FDEVENT_ERR    BV(3)     //文件描述符是否出错
#define FDEVENT_HUP    BV(4)     //已挂断 poll
#define FDEVENT_NVAL   BV(5)     //描述符不引用一打开文件 poll

其中BV也是一个宏,定义在settings.c文件中:

#define BV(x) (1 << x)

其作用就是将一个整数变量第x位置1,其余为0。
那么上面FDEVENT_XX的宏定义就是定义了一系列者养的整数,通过这些宏的或操作,可以在一个整数中用不同的位表示不同的事件。这些宏和struct epoll_event结构体中的events变量的对应的宏定义对应(有点绕。。。)。说白了就是和epoll.h中的枚举EPOLL_EVENTS对应。
这个函数的主要工作就是设置一些值然后调用epoll_ctl函数。虽然函数的名称是event_add,但是如果第二个参数fde_ndx不等于-1(那肯定就等于后面的参数fd),那这个时候可以肯定fd已经在epoll的监测中了,这时候不再是将fd增加到epoll中,而是修改其要监听的IO事件,就是最后一个参数表示的事件。fdevent_linux_sysepoll_event_add()增加成功后返回fd,在fdevent_event_add中,将这个返回值赋给fde_ndx,所以,fde_ndx==fd。
至此,监听fd的注册流程已经全部结束了,由于当有连接请求是,监听fd的表现是有数据课读,因此,只监听其FDEVENT_IN事件。注册之后,监听fd就开始等待连接请求。
接着,进程执行到了下面的语句:

//启动事件轮询。底层使用的是IO多路转接。

if ((n = fdevent_poll(srv->ev, 1000)) > 0)
{
            /*
             * nn是事件的数量(服务请求啦,文件读写啦什么的。。。)
             */
            int revents;
            int fd_ndx = -1;
            /**
             * 这个循环中逐个的处理已经准备好的请求,知道所有的请求处理结束。
             */
            do
            {
                fdevent_handler handler;
                void *context;
                handler_t r;

                fd_ndx = fdevent_event_next_fdndx(srv->ev, fd_ndx);
                revents = fdevent_event_get_revent(srv->ev, fd_ndx);
                fd = fdevent_event_get_fd(srv->ev, fd_ndx);
                handler = fdevent_get_handler(srv->ev, fd);
                context = fdevent_get_context(srv->ev, fd);
                /*
                 * connection_handle_fdevent needs a joblist_append
                 */
                /**
                 * 这里,调用请求的处理函数handler处理请求!
                 */
                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:
                    SEGFAULT();
                    break;
                default:
                    log_error_write(srv, __FILE__, __LINE__, "d", r);
                    break;
                }
            }while (--n > 0);
        }
        else if (n < 0 && errno != EINTR)
        {
            log_error_write(srv, __FILE__, __LINE__, "ss","fdevent_poll failed:", strerror(errno));
        }
}

  这段语句是worker子进程的工作重心所在。首先调用fdevent_poll()函数等待IO事件发生,如果没有IO事件,程序会阻塞在这个函数中。如果有fd发生了IO事件,则从fdevent_poll函数中返回,返回值是发生了IO事件的fd的数量。接着,程序进入do-while循环,循环中对每个fd,调用一些列fdevent系统的接口函数,最后调用event_handler处理IO事件。
  fdevent_poll()函数调用fdevents结构体中的poll,最终调用的是epoll_wait()函数。epoll_wait()函数将发生了IO事件的fd对应的epoll_evet结构体实例的存储在fdevents结构体的epoll_events数组成员中。fdevent_event_next_fdndx函数返回epoll_events数组中下一个元素的下标,fdevent_event_get_revent函数调用ev->event_get_revent()获得fd发生的IO事件,最终调用的是:

static int fdevent_linux_sysepoll_event_get_revent(fdevents * ev, size_t ndx)
{
    int events = 0, e;
    e = ev->epoll_events[ndx].events;
    if (e & EPOLLIN)
        events |= FDEVENT_IN;
    if (e & EPOLLOUT)
        events |= FDEVENT_OUT;
    if (e & EPOLLERR)
        events |= FDEVENT_ERR;
    if (e & EPOLLHUP)
        events |= FDEVENT_HUP;
    if (e & EPOLLPRI) //有紧急数据到达(带外数据)
        events |= FDEVENT_PRI;
    return e;
}

  这个函数就做了一个转换。fdevent_get_handler和fdevent_get_context返回fd对应的fdnode中的handler和ctx。这几个函数都简单,这里不再列出源代码。
  最后,在switch语句中调用fd对应的handler函数处理事件。对于监听fd,调用的函数为:

/**
 * 这个是监听socket的IO事件处理函数。
 * 只要的工作就是建立和客户端的socket连接。只处理读事件。在处理过程中,
 * 每次调用这个函数都试图一次建立100个连接,这样可以提高效率。
 */
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);
    /*
     * 只有fd事件是FDEVENT_IN时,才进行事件处理。
     */
    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
     *一次监听fd的IO事件,表示有客户端请求连接,对其的处理就是建立连接。建立连接后并不急着退出函数,
     * 而是继续尝试建立新连接,直到已经建立了100次连接。这样可以提高效率。
     */
    for (loops = 0; loops < 100 && NULL != (con =connection_accept(srv, srv_socket)); loops++)
    {
        handler_t r;
        //根据当前状态,改变con的状态机,并做出相应的动作。
        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;
}

  监听fd有IO事件,表示有客户端请求连接,对其的处理就是建立连接。在这个函数中,建立连接后并不急着退出,而是继续尝试建立新连接,直到已经建立了100次连接。这样可以提高效率。connection_accept()函数接受连接请求并返回一个connection结构体指针。接着对这个连接启动状态机(状态机可是很有意思的。。。)。然后把连接加到作业队列中。
  这里有一个问题,如果连接迟迟达不到100个,那么程序就会阻塞在这个函数中,这样对于已经建立的连接也就没法进行处理。这怎么办?读者不要忘了,在将监听fd注册到fdevent系统时,其被设置成了非阻塞的,因此,如果在调用accept()函数时没有连接请求,那么accept()函数会直接出错返回,这样connection_accept就返回一个NULL,退出了for循环。
到这,fdevent系统对于监听fd的处理就完成了一个轮回。处理完IO事件以后fd接着在epoll中等待下一次事件。
  下一篇中回介绍一些fdevent系统对连接fd(accept函数的返回值)的处理,以及超时连接的处理。

本文章由 http://www.wifidog.pro/2015/04/21/wifidog%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90lighttpd%E4%BD%BF%E7%94%A8-2.html 整理编辑,转载请注明出处