那谁的技术博客

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

lighttpd1.4.18代码分析(六)--处理连接fd的流程

现在开始讲解lighttpd如何处理连接fd.

第四节,已经讲解了如何处理服务器负责监听连接的fd,处理连接fd的流程在前半部分与监听fd大体相同:在通过监听fd接收一个新的连接之后,服务器将这个fd加入到事件处理器中, 设置它所感兴趣的IO事件类型, 当IO状态发生变化时, 事件处理器获取被触发的fd,回调函数,事件等,然后通过已经注册的回调函数进行处理.

这里的区别就在于回调函数的不同上.对于监听fd而言, 该回调函数是network.c文件中的network_server_handle_fdevent函数,这个在第四节中已经做了分析;与之对应的,连接fd的回调函数是connections.c文件中的connection_handle_fdevent函数.

// 这个函数是处理接受链接的函数, 与network_server_handle_fdevent对应
handler_t connection_handle_fdevent(void *s, void *context, int revents) {
    server     
*srv = (server *)s;
    connection 
*con = context;

    
// 添加到server的joblist中
    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
                 
*/
#ifdef USE_LINUX_SIGIO
                
if (srv->ev->in_sigio == 1) {
                    log_error_write(srv, __FILE__, __LINE__, 
"sd",
                        
"connection closed: poll() -> HUP", con->fd);
                } 
else {
                    connection_set_state(srv, con, CON_STATE_ERROR);
                }
#else
                connection_set_state(srv, con, CON_STATE_ERROR);
#endif

            }
        } 
else if (revents & FDEVENT_ERR) {
#ifndef USE_LINUX_SIGIO
            log_error_write(srv, __FILE__, __LINE__, 
"sd",
                    
"connection closed: poll() -> ERR", con->fd);
#endif
            connection_set_state(srv, con, CON_STATE_ERROR);
        } 
else {
            log_error_write(srv, __FILE__, __LINE__, 
"sd",
                    
"connection closed: poll() -> ???", revents);
        }
    }

    
// 如果连接的状态是READ, 那么处理read状态
    if (con->state == CON_STATE_READ ||
        con
->state == CON_STATE_READ_POST) {
        connection_handle_read_state(srv, con);
    }

    
// 如果连接状态是WRITE并却写缓冲区队列中不为空 并且该连接是可写的, 就去处理写状态
    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));
        }

        
// 如果还有数据, 打印错误log, 并且读入数据
        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,也就是保存连接相关数据的结构体中, 有一个叫state的成员, 顾名思义, 这个成员保存的是一个连接的状态,这里所说的状态与前面提到的IO状态是不同, IO状态是用于表示一个fd可读/可写/出错等, 而这个状态更多的是与协议相关的部分.它是一个枚举类型:
/* the order of the items should be the same as they are processed
 * read before write as we use this later 
*/
typedef 
enum {
    CON_STATE_CONNECT,            
// 连接
    CON_STATE_REQUEST_START,    // 开始获取请求
    CON_STATE_READ,                // 处理读
    CON_STATE_REQUEST_END,        // 请求结束
    CON_STATE_READ_POST,        // 处理读,但是是POST过来的数据
    CON_STATE_HANDLE_REQUEST,    // 处理请求
    CON_STATE_RESPONSE_START,    // 开始回复
    CON_STATE_WRITE,            // 处理写
    CON_STATE_RESPONSE_END,        // 回复结束
    CON_STATE_ERROR,            // 出错
    CON_STATE_CLOSE                // 连接关闭
} connection_state_t;

为什么需要这些状态?因为lighttpd中采用了所谓"状态机"去处理连接,而这些状态就是状态机中的各种不同状态.
在lighttpd的官方文档中, 对其使用的状态机有一篇文档,在这里:
http://redmine.lighttpd.net/wiki/lighttpd/Docs:InternalHTTPStates

我觉得里面的这幅图非常的直观,学习过编译原理的人一看就可以知道这是状态机的转换图:


现在回到本章的主题中, lighttpd如何处理连接fd.
前面给出的处理连接fd的回调函数,最开始地方有一段代码:
    joblist_append(srv, con);
这个函数将一个连接connection结构体放入到joblist中, 后面的部分根据不同的情况设置connection中的state字段,调用的是connection_set_state函数.

现在回到server.c函数中, 第四节中已经结合处理监听fd的流程讲解了这段函数:
        // 轮询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);
        } 
else if (n < 0 && errno != EINTR) {
            log_error_write(srv, __FILE__, __LINE__, 
"ss",
                    
"fdevent_poll failed:",
                    strerror(errno));
        }

在server.c文件中, 紧跟着这段代码的是:
        // 处理joblist中的连接
        for (ndx = 0; ndx < srv->joblist->used; ndx++) {
            connection 
*con = srv->joblist->ptr[ndx];
            handler_t r;

            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;
            }

            con
->in_joblist = 0;
        }
简单的说, 这段代码是一个循环, 从joblist中依次取出已经放在这里的connection指针, 再调用connection_state_machine函数进行处理.

connection_state_machine函数是一个非常重要的函数, 它就是处理连接fd的状态机, 在后面将详细分析这个函数.

现在回顾一下lighttpd处理连接fd的大体框架:前面的流程与处理监听fd大体相同,在与处理连接fd相关的回调函数中, 首先将需要处理的fd相关的connection加入到joblist中, 设置它的state, 后面再轮询joblist, 进入状态机进行处理.

这个过程可能值得商榷, 比如为什么在回调函数中需要将一个connection指针加入到joblist中, 后面再一个循环轮询joblist中的connection,这样不是显得效率低下吗?我们需要注意的是,前面提到的IO事件状态和connection中的成员state是不同的!第一个轮询过程(IO事件处理器的轮询)是根据哪些fd的IO发生了变化被触发而去调用回调函数, 而后面的循环(joblist的轮询)中的connection则不一定都是IO发生变化的!

打一个比方, 一个连接到来, 此时我们把它放入到事件处理器中, 当它可读时被触发, 也就是在第一个轮询中被触发;如果在处理的时候出了问题, 不能继续, 此时我们只需要保存它当前的state字段, 在下一次操作中, 由于它的IO没有发生变化, 那么将不会在第一个轮询也就是IO事件处理器中被处理, 而只会在轮询joblist时被处理, 只需要它的state字段是正确的, 放到状态机处理函数中就可以继续下去.


posted on 2008-09-19 10:46 那谁 阅读(3697) 评论(1)  编辑 收藏 引用 所属分类: 网络编程服务器设计Linux/Unixlighttpd

评论

# re: lighttpd1.4.18代码分析(六)--处理连接fd的流程  回复  更多评论   

“在下一次操作中, 由于它的IO没有发生变化, 那么将不会在第一个轮询也就是IO事件处理器中被处理, 而只会在轮询joblist时被处理”

这句容易误导读者,应改为"轮询完毕(io操作完成,可能设置status)后的joblist循环里继续处理status

2010-03-26 15:55 | sunceenjoy

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