lighttpd-1.4.39 : state machine

http://www.cnblogs.com/kernel_hcy/archive/2010/03/24/1694203.html

之前已经分析过,服务器首先会创建监听套接字,然后设置监听套接字对应的处理函数handler为network_server_handle_fdevent()
当有新的连接到来,该handler会accept此连接,并返回一个新的socket fd,称它为已连接套接字infd。并设置infd的处理器handler为connection_handle_fdeventconnection_handle_fdevent()将当前连接加入到joblist中,并根据当前连接fd所发生的IO事件,对connection结构体中的标记变量赋值,如is_writable,is_readable等,并做一些时间的记录。这些事件所对应的真正的IO处理则交给状态机处理。状态机根据这些标记变量进行相应的动作处理。

状态机可以说是lighttpd最核心的部分。lighttpd将一个连接在不同的时刻分成不同的状态,状态机则根据连接当前的状态,决定要对连接进行的处理以及下一步要进入的状态。下面这幅图描述了lighttpd的状态机:
这里写图片描述

图中的各个状态对应于下面的一个枚举类型:

typedef enum
{
     CON_STATE_CONNECT,          //connect 连接开始
     CON_STATE_REQUEST_START,    //reqstart 开始读取请求
     CON_STATE_READ,             //read 读取并解析请求
     CON_STATE_REQUEST_END,      //reqend 读取请求结束
     CON_STATE_READ_POST,        //readpost 读取post数据
     CON_STATE_HANDLE_REQUEST,   //handelreq 处理请求
     CON_STATE_RESPONSE_START,   //respstart 开始回复
     CON_STATE_WRITE,            //write 回复写数据
     CON_STATE_RESPONSE_END,     //respend 回复结束
     CON_STATE_ERROR,            //error 出错
     CON_STATE_CLOSE             //close 连接关闭
} connection_state_t;

在每个连接中都会保存这样一个枚举类型变量,用以表示当前连接的状态。connection结构体的第一个成员就是这个变量。(一个connection代表一个连接)
在连接建立以后,在connections.c/connection_accpet()函数中,lighttpd会调用connection_set_state()函数,将新建立的连接的状态设置为CON_STATE_REQUEST_START。在这个状态中,lighttpd记录连接建立的时间等信息。
下面先来说一说整个状态机的核心函数:connections.c/connection_state_machine()函数。函数很长,看着比较吓人。其实,这里我们主要关心的是函数的主体部分:while循环和其中的那个大switch语句,删减之后如下:

int connection_state_machine(server * srv, connection * con)
{
    int done = 0, r;
    while (done == 0)
    {
        size_t ostate = con -> state;
        int b;
        //这个大switch语句根据当前状态机的状态进行相应的处理和状态转换。
        switch (con->state)
        {
        case CON_STATE_REQUEST_START:    /* transient */
        case CON_STATE_REQUEST_END:    /* transient */
        case CON_STATE_HANDLE_REQUEST:
        case CON_STATE_RESPONSE_START:
        case CON_STATE_RESPONSE_END:    /* transient */
        case CON_STATE_CONNECT:
        case CON_STATE_CLOSE:
        case CON_STATE_READ_POST:
        case CON_STATE_READ:
        case CON_STATE_WRITE:
        case CON_STATE_ERROR:    /* transient */
        default:
            break;
        }//end of switch(con -> state) ...
        if (done == -1)
        {
            done = 0;
        }
        else if (ostate == con->state)
        {
            done = 1;
        }
    }
    return 0;
}

程序进入这个函数以后,首先根据当前的状态进入对应的switch分支执行相应的动作。然后,根据情况,进入下一个状态。跳出switch语句之后,如果连接的状态没有改变,说明连接读写数据还没有结束,但是需要等待IO事件,这时,跳出循环,等待IO事件。对于done==-1的情况,是在CON_STATE_HANDLE_REQUEST状态中的问题,后面再讨论。如果在处理的过程中没有出现需要等待IO事件的情况,那么在while循环中,连接将被处理完毕并关闭。
接着前面的话题,在建立新的连接以后,程序回到network.c/network_server_handle_fdevent()函数中的for循环在中后,lighttpd对这个新建立的连接调用了一次connection_state_machine()函数。如果这个连接没有出现需要等待IO事件的情况,那么在这次调用中,这个连接请求就被处理完毕。但是实际上,在连接第一次进入CON_STATE_READ状态时,几乎是什么都没做,保持这个状态,然后跳出了while循环。在循环后面,还有一段代码:

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

这段代码前面已经介绍过,这个连接的连接fd被加入到fdevent系统中,等待IO事件。当有数据可读的时候,在main函数中,lighttpd调用这个fd对应的handle函数,这里就是connection_handle_fdevent()函数。这个函数一开始将连接加入到了joblist(作业队列)中。前面已经说过,这个函数仅仅做了一些标记工作。程序回到main函数中时,执行了下面的代码:

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;//标记con已经不在队列中。
        }

这段代码就是对joblist中的所有连接,依次对其调用connection_state_machine()函数。在这次调用中,连接开始真正的读取数据。lighttpd调用connection_handle_read_state()函数读取数据。在这个函数中,如果数据读取完毕或出错,那么连接进入相应的状态,如果数据没有读取完毕那么连接的状态不变。(PS:在connection_handle_read_state()读取的数据其实就是HTTP头,在这个函数中根据格式HTTP头的格式判断HTTP头是否已经读取完毕,包括POST数据。)上面说到,在connection_state_machile()函数的while循环中,如果连接的状态没有改变,那么将跳出循环。继续等待读取数据。
读取完数据,连接进入CON_STATE_REQUEST_END。在这个状态中lighttpd对HTTP头进行解析。根据解析的结果判断是否有POST数据。如果有,则进入CON_STATE_READ_POST状态。这个状态的处理和CON_STATE_READ一样。如果没有POST数据,则进入CON_STATE_HANDLE_REQUEST状态。在这个状态中lighttpd做了整个连接最核心的工作:处理连接请求并准备response数据。

处理完之后,连接进入CON_STATE_RESPONSE_START。在这个状态中,主要工作是准备response头。准备好后,连接进入CON_STATE_WRITE状态。显然,这个状态是向客户端回写数据。第一次进入WRITE状态什么都不做,跳出循环后将连接fd加入fdevent系统中并监听写事件(此时仅仅是修改要监听的事件)。当有写事件发生时,和读事件一样调用connection_handle_fdevent函数做标记并把连接加入joblist中。经过若干次后,数据写完。连接进入CON_STATE_RESPONSE_END状态,进行一些清理工作,判断是否要keeplive,如果是则连接进入CON_STATE_REQUEST_START状态,否则进入CON_STATE_CLOSE。进入CLOSE后,等待客户端挂断,执行关闭操作。这里顺便说一下,在将fd加到fdevent中时,默认对每个fd都监听错误和挂断事件。
连接关闭后,connection结构体并没有删除,而是留在了server结构体的connecions成员中。以便以后再用。
关于joblist有一个问题。在每次将连接加入的joblist中时,通过connection结构体中的in_joblist判断是否连接已经在joblist中。但是,在joblist_append函数中,并没有对in_joblist进行赋值,在程序的运行过程中,in_joblist始终是0.也就是说,每次调用joblist_append都会将连接加入joblist中,不论连接是否已经加入。还有,当连接已经处理完毕后,程序也没有将对应的connection结构体指针从joblist中删除,虽然这样不影响程序运行,因为断开后,对应的connection结构体的状态被设置成CON_STATE_CONNECT,这个状态仅仅是清理了一下chunkqueue。但这将导致joblist不断增大,造成轻微的内存泄漏。在最新版(1.4.26)中,这个问题依然没有修改。
就先说到这。后面将详细介绍各个状态的处理。



http://bbs.chinaunix.net/thread-1251434-6-1.html

  • CON_STATE_REQUEST_START状态
    这个状态出现在刚刚通过accept函数接收一个新的连接时,此时需要保存一些数据:
       case CON_STATE_REQUEST_START: /* transient, 开始接收请求 */
            if (srv->srvconf.log_state_handling) {
                log_error_write(srv, __FILE__, __LINE__, "sds",
                        "state for fd", con->fd, connection_get_state(con->state));
            }

            // 保存时间
            con->request_start = srv->cur_ts;
            con->read_idle_ts = srv->cur_ts;

            // 该连接的请求次数
            con->request_count++;
            // 每次循环处理的请求次数
            con->loops_per_request = 0;

            // 状态改为可读, 也就是可以接收数据
            connection_set_state(srv, con, CON_STATE_READ);

            /* patch con->conf.is_ssl if the connection is a ssl-socket already */

#ifdef USE_OPENSSL
            con->conf.is_ssl = srv_sock->is_ssl;
#endif

            break;

需要注意的是这里将connetion中的两个字段保存为当前时间,request_start和read_idle_ts, 前者存放的是接收连接的时间, 后者用于超时判断, 在第5节讲解lighttpd中如何处理超时的时候提到过,lighttpd设置了一个每一秒一次的定时器, 每次定时器到时就依次轮询所有的连接, 判断是否超时, 而判断的依据就是拿当前的时间 - connection的read_idle_ts字段, 看看是否超时:

// 如果当前时间与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;
}

这些该保存的数据都保存完毕之后, 状态机进入下一个状态,CON_STATE_READ, 也就是开始接收数

http://bbs.chinaunix.net/thread-1251434-7-1.html

  • CON_STATE_READ状态
    首先看看connection_state_machine函数部分的代码:
         // 读             
        case CON_STATE_READ_POST:
        case CON_STATE_READ:
            if (srv->srvconf.log_state_handling) {
                log_error_write(srv, __FILE__, __LINE__, "sds",
                        "state for fd", con->fd, connection_get_state(con->state));
            }

            connection_handle_read_state(srv, con);
            break;

可以看到,CON_STATE_READ_POST状态和CON_STATE_READ状态调用的同一段代码,不过我们今天讲解的CON_STATE_READ状态,CON_STATE_READ_POST状态在后面讲解.

上面的代码调用了connection_handle_read_state函数,进入这个函数内部,分为几个部分进行分析:

if (con->is_readable) {
        con->read_idle_ts = srv->cur_ts;

        // -1:出错 -2:对方关闭连接 0:成?
        switch(connection_handle_read(srv, con)) {
        case -1:
            return -1;
        case -2:
            is_closed = 1;
            break;
        default:
            break;
        }
    }

这里调用函数connection_handle_read, 来看看这个函数的实现:

// -1:出错 -2:对方关闭连接 0:成功
static int connection_handle_read(server *srv, connection *con) {
    int len;
    buffer *b;
    int toread;

    if (con->conf.is_ssl) {
        return connection_handle_read_ssl(srv, con);
    }

#if defined(__WIN32)
    b = chunkqueue_get_append_buffer(con->read_queue);
    buffer_prepare_copy(b, 4 * 1024);
    len = recv(con->fd, b->ptr, b->size - 1, 0);
#else
    // 获取有多少数据可读
    if (ioctl(con->fd, FIONREAD, &toread)) {
        log_error_write(srv, __FILE__, __LINE__, "sd",
                "unexpected end-of-file:",
                con->fd);
        return -1;
    }
    // 根据数据量准备缓冲区
    b = chunkqueue_get_append_buffer(con->read_queue);
    buffer_prepare_copy(b, toread + 1);
    // 读数据
    len = read(con->fd, b->ptr, b->size - 1);
#endif

    if (len < 0) {
        con->is_readable = 0;

        // Non-blocking  I/O has been selected using O_NONBLOCK and no data
        // was immediately available for reading.
        if (errno == EAGAIN) 
            return 0;
        if (errno == EINTR) {
            /* we have been interrupted before we could read */
            con->is_readable = 1;
            return 0;
        }

        if (errno != ECONNRESET) {
            /* expected for keep-alive */
            log_error_write(srv, __FILE__, __LINE__, "ssd", "connection closed - read failed: ", strerror(errno), errno);
        }

        connection_set_state(srv, con, CON_STATE_ERROR);

        return -1;
    } else if (len == 0) {
        // 当读入数据 = 0时 表示对端关闭了连接
        con->is_readable = 0;
        /* the other end close the connection -> KEEP-ALIVE */

        /* pipelining */

        return -2;
    } else if ((size_t)len < b->size - 1) {
        /* we got less then expected, wait for the next fd-event */

        con->is_readable = 0;
    }

    // 记录读入的数据量 未使用的数据第一个字节为0
    b->used = len;
    b->ptr[b->used++] = '\0';

    con->bytes_read += len;
#if 0
    dump_packet(b->ptr, len);
#endif

    return 0;
}

简单的说, 该函数首先调用ioctl获取fd对应的缓冲区中有多少可读的数据, 然后调用chunkqueue_get_append_buffer和buffer_prepare_copy函数准备好所需的缓冲区, 准备好缓冲区之后, 调用read函数读取缓冲区中的数据.对read函数的调用结果进行区分, 小于0表示出错, 返回-1;等于0表示关闭了连接, 返回-2;假如读取的数据长度比预期的小, 那么就等待下一次继续读数据, 最后将已经读入的数据缓冲区最后一个字节置’\0’,返回0.

继续回到函数connection_handle_read_state中, 看接下来的代码:

// 这一段循环代码用于更新read chunk队列,没有使用的chunk都归入未使用chunk链中
    /* the last chunk might be empty */
    for (c = cq->first; c;) {
        if (cq->first == c && c->mem->used == 0) {
            // 如果第一个chunk是空的并且没有使用过
            /* the first node is empty */
            /*  and it is empty, move it to unused */

            // 则chunk队列的第一个chunk为下一个chunk
            cq->first = c->next;
            // 第一个chunk为NULL, 那么最后一个chunk为NULL
            if (cq->first == NULL) 
                cq->last = NULL;

            // 更新chunk队列中对于未使用chunk的记录
            c->next = cq->unused;
            cq->unused = c;
            cq->unused_chunks++;

            // 重新指向第一个chunk
            c = cq->first;
        } else if (c->next && c->next->mem->used == 0) {
            chunk *fc;
            // 如果下一个chunk存在而且未使用过
            /* next node is the last one */
            /*  and it is empty, move it to unused */

            // 将这个chunk从队列中分离出去, 同时fc指向这个未使用的chunk
            fc = c->next;
            c->next = fc->next;

            // 将这个未使用的chunk(fc所指)保存到未使用chunk链中
            fc->next = cq->unused;
            cq->unused = fc;
            cq->unused_chunks++;

            /* the last node was empty */
            // 如果c的下一个chunk是空的, 那么chunk队列的最后一个chunk就是c了
            if (c->next == NULL) {
                cq->last = c;
            }

            // 继续往下走
            c = c->next;
        } else {
            // 继续往下走
            c = c->next;
        }
    }

每个connection结构体中, 有一个read_queue成员, 该成员是chunkqueue类型的, 一个connection读入的数据都会保存在这个成员中, 由于一直没有详细介绍chunkqueue结构体及其使用, 这里不对上面的过程进行详细的分析, 只需要知道chunkqueue结构体内部使用的是链表保存数据, 上面这段代码遍历这个链表, 将未使用的部分抽取下来放到未使用chunkqueue中.

继续看下面的代码, 下面的代码根据状态是CON_STATE_READ还是CON_STATE_READ_POST进行了区分, 同样的,目前仅关注CON_STATE_READ状态部分:

  case CON_STATE_READ:    // 如果是可读状态
        /* if there is a \r\n\r\n in the chunkqueue
         *
         * scan the chunk-queue twice
         * 1. to find the \r\n\r\n
         * 2. to copy the header-packet
         *
         */

        last_chunk = NULL;
        last_offset = 0;

        // 遍历read chunk队列
        for (c = cq->first; !last_chunk && c; c = c->next) {
            buffer b;
            size_t i;

            b.ptr = c->mem->ptr + c->offset;
            b.used = c->mem->used - c->offset;

            // 遍历当前chunk中的每一个字符
            for (i = 0; !last_chunk && i < b.used; i++) {
                char ch = b.ptr[i];
                size_t have_chars = 0;

                // 判断当前字符
                switch (ch) {
                case '\r':        // 如果当前字符是'\r'
                    /* we have to do a 4 char lookup */
                    // 该chunk还剩余多少个字符
                    have_chars = b.used - i - 1;

                    if (have_chars >= 4) {
                        // 如果当前剩余字符大于等于4, 判断紧跟着的4个字符是不是"\r\n\r\n", 如果是就退出循环
                        /* all chars are in this buffer */

                        if (0 == strncmp(b.ptr + i, "\r\n\r\n", 4)) {
                            /* found */
                            last_chunk = c;
                            last_offset = i + 4;

                            break;
                        }
                    } else {
                        // 否则就去查看下一个chunk, 看看是不是和这个chunk一起形成了"\r\n\r\n"
                        chunk *lookahead_chunk = c->next;
                        size_t missing_chars;
                        /* looks like the following chars are not in the same chunk */

                        missing_chars = 4 - have_chars;

                        if (lookahead_chunk && lookahead_chunk->type == MEM_CHUNK) {
                            /* is the chunk long enough to contain the other chars ? */

                            if (lookahead_chunk->mem->used > missing_chars) {
                                if (0 == strncmp(b.ptr + i, "\r\n\r\n", have_chars) &&
                                    0 == strncmp(lookahead_chunk->mem->ptr, "\r\n\r\n" + have_chars, missing_chars)) {

                                    last_chunk = lookahead_chunk;
                                    last_offset = missing_chars;

                                    break;
                                }
                            } else {
                                /* a splited \r \n */
                                break;
                            }
                        }
                    }

                    break;
                }
            }
        }

这段代码用于在读入数据中查找”\r\n\r\n”,熟悉http协议的人知道, 这代表着一个http请求的结束,也就是说, 上面的代码用于判断是否已经接受了一个完整的http请求.但是有一个细节部分需要注意, 前面说过chunkqueue内部是使用一个链表来存放数据,比方说这个链表中有两个节点, 一个节点存放一字节的数据, 一个节点存放了十字节的数据,这时候可能会出现这样的情况:假如在一个节点存放的数据中找到了字符’\r’,而该节点剩下的数据不足以存放”\r\n\r \n”字符串剩余的字符, 也就是说, 不足4个字节, 那么查找”\r\n\r\n”的过程就要延续到下一个节点继续进行查找.比如在一个节点中最后部分找到了”\r”, 那么就要在下一个节点的数据起始位置中查找”\n\r\n”.

继续看下面的代码:

       /* found */
        // 读取到了请求的结尾, 现在将请求字符串放到request字段中
        if (last_chunk) {
            buffer_reset(con->request.request);

            for (c = cq->first; c; c = c->next) {
                buffer b;

                b.ptr = c->mem->ptr + c->offset;
                b.used = c->mem->used - c->offset;

                if (c == last_chunk) {
                    b.used = last_offset + 1;
                }

                buffer_append_string_buffer(con->request.request, &b);

                if (c == last_chunk) {
                    c->offset += last_offset;

                    break;
                } else {
                    /* the whole packet was copied */
                    c->offset = c->mem->used - 1;
                }
            }

            // 设置状态为读取请求结束
            connection_set_state(srv, con, CON_STATE_REQUEST_END);
        } else if (chunkqueue_length(cq) > 64 * 1024) {
            // 读入的数据太多, 出错
            log_error_write(srv, __FILE__, __LINE__, "s", "oversized request-header -> sending Status 414");

            con->http_status = 414; /* Request-URI too large */
            con->keep_alive = 0;
            connection_set_state(srv, con, CON_STATE_HANDLE_REQUEST);
        }
        break;

如果前面查找到了”\r\n\r\n”, 那么函数就进入这个部分.这部分代码做的事情就是复制http请求头到connection结构体中request成员中.需要注意的是如果没有查找到”\r\n\r\n”,并且缓冲区数据长度大于64*1024, 也就是64K字节, 那么就返回414错误, 也就是说, 对于lighttpd而言, 一般的http请求不能超过64K字节.

这个过程就分析到这里,简单的总结一下:首先从缓冲区中读取数据, 然后查找”\r\n\r\n”字符串, 判断是否已经读取了完整的http请求, 如果是的话就复制下来, 最后进入CON_STATE_REQUEST_END状态

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值