标签 wifidog原理 下的文章

wifidog自带HTTP 服务器 Lighttpd1.4.20源码分析之array.c(h) -----通用数组(1)

Lighttpd提供了一个通用数组,这个数组与程序的其他部分练习较少,因此可以单独进行分析。
首先要说一下Lighttpd中的定义的一些数据结构。
在array.h中有下面的定义:

Code
typedef enum { 
        TYPE_UNSET,         /* 数据的类型未设置,
                               这几种数据类型使用了面向对象的设计思想,
                               这个类型相当于父类型,继承关系见后面 
                             */
        TYPE_STRING,         /* 字符串类型 */
        TYPE_COUNT,         /* COUNT类型 */
        TYPE_ARRAY,         /* 数组类型 */
        TYPE_INTEGER,     /* 整数类型 */
        TYPE_FASTCGI,     /* FASTCGI类型 */
        TYPE_CONFIG         /* CONFIG类型 */
} data_type_t;

这是一个枚举类型,定义了各个数据类型的标志。从中可以看出程序中所定义使用的数据类型的种类和个数。
Lighttpd在定义数据类型的时候使用了面向对象的思想,因此,程序具有很好的扩展性和适应性。这些类型中,最重要的是UNSET类型,这个类型在所有的数据类型中,起到了父类型的作用。在array.h中,UNSET类型的定义如下:

Code
#define DATA_UNSET \
    data_type_t type; \
    buffer *key; \
    int is_index_key; /* 1 if key is a array index */ \
    struct data_unset *(*copy)(const struct data_unset *src); \
    void (* free)(struct data_unset *p); \
    void (* reset)(struct data_unset *p); \
        int (*insert_dup)(struct data_unset *dst, struct data_unset *src); \
    void (*print)(const struct data_unset *p, int depth)

typedef struct data_unset {
    DATA_UNSET;
} data_unset;

其中,UNSET类型数据的定义中,数据的实际定义部分使用宏DATA_UNSET,这样可以方便其他类型在定义中直接引用DATA_UNSET宏来模拟继承。在宏DATA_UNSET中,定义了下面五个函数指针:

Code
struct data_unset *(*copy)(const struct data_unset *src); 
void (* free)(struct data_unset *p); 
void (* reset)(struct data_unset *p); 
int (*insert_dup)(struct data_unset *dst, struct data_unset *src); 
void (*print)(const struct data_unset *p, int depth)

这五个函数指针相当于UNSET的成员函数,其他类型可以通过对这五个指针赋值来实现成员函数的重写(Overwrite)。每种类型都配有自己特有的初始化函数,形式为:data_XXXXX *data_XXXXX_init(void)。在这些初始化函数中,对上面这五个函数指针进行赋值,当然,赋值的函数都应先定义好。
这几种类型的继承关系图如下:(类图)

wifidog libhttpd源码分析.jpg

下面分析一下STRING类型的初始化函数data_string * data_string_init(void):

Code
data_string *data_string_init(void)
{
    data_string *ds;
    /*
分配内存空间。
这里用的是calloc函数,分配的空间会自动清零。
*/
    ds = calloc(1, sizeof(*ds));
    assert(ds);
    /*
初始化各个数据成员,
这里调用buffer_init函数,主要就是分配内存空间
*/
    ds->key = buffer_init();
    ds->value = buffer_init();
    /*确定成员函数的具体调用函数,对函数指针赋值*/
    ds->copy = data_string_copy;
    ds->free = data_string_free;
    ds->reset = data_string_reset;
    ds->insert_dup = data_string_insert_dup;
    ds->print = data_string_print;
    ds->type = TYPE_STRING;
    return ds;
}

其他类型的init函数,以及其他函数都不难,读者可自行查看代码。
至于各个类型的用处以及各个类型中个成员变量的含义,暂且不用关心,只要知道这七个类型之间的关系即可,除了UNSET类型,其他类型的操作函数的实现都在文件data_XXXXX.c中。这些函数的实现都很简单,不在一一介绍,读者可自己看看。这七个类型构成了通用数组所要处理的类型,其中,在数组的定义和实现中只使用UNSET类型,利用上面的定义,通用数组可以不用关心数组中存储的到底是哪种具体的类型,只需将其按照UNSET类型来处理就可以了。这就实现了通用数组。
下面这个定义是通用数组的核心定义,也就是定义了数组。。。

Code
typedef struct 
{
    /* UNSET类型的指针型数组,存放数组中的元素 */
    data_unset **data;
    /* 存放着排好序的各个元素的下标的数组 */
    size_t *sorted;
    size_t used;    /* data中使用了的长度,也就是数组中元素个数 */

        /* data的大小。data的大小会根据数据的多少变化,会为以后的数据预先分
           配空间 */
    size_t size;    

        size_t unique_ndx;        /*  */
        /* 比used大的最小的2的倍数。也就是离used最近的且比used大的2的倍
              数 ,用于在数组中利用二分法查找元素*/
    size_t next_power_of_2;
        /* data is weakref, don't bother the data */
        /* data就是一个指针,不用关系其所指向的内容 */
    int is_weakref;                
} array;

各个变量的含义见上。
array.h中还有一个定义:

typedef struct {
  DATA_UNSET;
  array *value;
} data_array;

本文章由 http://www.wifidog.pro/2015/04/13/wifidog-lighttpd%E5%88%86%E6%9E%90%E9%80%9A%E7%94%A8%E6%95%B0%E7%BB%84-1.html 整理编辑,转载请注明出处

wifidog认证自带http服务器Lighttpd1.4.20源码分析之状态机(3)返回response

好久没顾这个了,最近比较清闲,重新拾掇一下,有始有终。
  回到正题,前一篇介绍完了请求的处理,先面lighttpd将会把处理的结果返回给客户端。状态机进入CON_STATE_RESPONST_START。在这个状态中,服务器主要的工作在函数connection_handle_write_prepare。这个函数不算复杂,主要是根据客户端请求的method来设置response的headers,其实就是设置“Content-Length”的值。下面是函数代码,做了一些删减:

static int connection_handle_write_prepare(server * srv, connection * con)
{
   if (con->mode == DIRECT)
   {
       /*
        * static files
        */
       switch (con->request.http_method)
       {
       case HTTP_METHOD_GET:
       case HTTP_METHOD_POST:
       case HTTP_METHOD_HEAD:
       case HTTP_METHOD_PUT:
       case HTTP_METHOD_MKCOL:
       case HTTP_METHOD_DELETE:
       case HTTP_METHOD_COPY:
       case HTTP_METHOD_MOVE:
       case HTTP_METHOD_PROPFIND:
       case HTTP_METHOD_PROPPATCH:
       case HTTP_METHOD_LOCK:
       case HTTP_METHOD_UNLOCK:
           break;
       case HTTP_METHOD_OPTIONS:
           /*
            * 400 is coming from the request-parser BEFORE uri.path is set
            * 403 is from the response handler when noone else catched it
            * */
           if ((!con->http_status || con->http_status == 200)
               && con->uri.path->used && con->uri.path->ptr[0] != '*')
           {
               response_header_insert(srv, con, CONST_STR_LEN("Allow"),
                  CONST_STR_LEN("OPTIONS, GET, HEAD, POST"));

               con->response.transfer_encoding &= ~HTTP_TRANSFER_ENCODING_CHUNKED;
               con->parsed_response &= ~HTTP_CONTENT_LENGTH;

               con->http_status = 200;
               con->file_finished = 1;

               chunkqueue_reset(con->write_queue);
           }
           break;
       default:
           switch (con->http_status)
           {
           case 400:            /* bad request */
           case 414:            /* overload request header */
           case 505:            /* unknown protocol */
           case 207:            /* this was webdav */
               break;
           default:
               con->http_status = 501;
               break;
           }
           break;
       }
   }

   if (con->http_status == 0)
   {
       con->http_status = 403;
   }

   switch (con->http_status)
   {
   case 204:                    /* class: header only */
   case 205:
   case 304:
       /*
        * disable chunked encoding again as we have no body
        */
       con->response.transfer_encoding &= ~HTTP_TRANSFER_ENCODING_CHUNKED;
       con->parsed_response &= ~HTTP_CONTENT_LENGTH;
       chunkqueue_reset(con->write_queue);

       con->file_finished = 1;
       break;
   default:                    /* class: header + body */
       if (con->mode != DIRECT)
           break;

       /*
        * only custom body for 4xx and 5xx
        */
       if (con->http_status < 400 || con->http_status >= 600)
           break;

       con->file_finished = 0;
       buffer_reset(con->physical.path);

       /*
        * try to send static errorfile
        */
       if (!buffer_is_empty(con->conf.errorfile_prefix))
       {
        //设置对应的错误提示文件
       }

       if (!con->file_finished)
       {
           //没有对应的错误提示文件,设置默认错误提示。
       }
       break;
   }

   if (con->file_finished)
   {
       /*
        * we have all the content and chunked encoding is not used, set a
        * content-length
        */
       if ((!(con->parsed_response & HTTP_CONTENT_LENGTH)) &&
           (con->response.transfer_encoding & HTTP_TRANSFER_ENCODING_CHUNKED) == 0)
       {
           off_t qlen = chunkqueue_length(con->write_queue);
           /**
            * The Content-Length header only can be sent if we have content:
            * - HEAD doesn't have a content-body (but have a content-length)
            * - 1xx, 204 and 304 don't have a content-body (RFC 2616 Section 4.3)
            *
            * Otherwise generate a Content-Length header as chunked encoding is not
            * available
            */
           if ((con->http_status >= 100 && con->http_status < 200) ||
               con->http_status == 204 || con->http_status == 304)
           {
               data_string *ds;
               /*
                * no Content-Body, no Content-Length
                */
               if (NULL != (ds = (data_string *) array_get_element(con->response.headers, "Content-Length")))
               {
                   buffer_reset(ds->value);    /* Headers with empty values
                                                * are ignored for output */
               }
           }
           else if (qlen > 0 || con->request.http_method != HTTP_METHOD_HEAD)
           {
               /*
                * qlen = 0 is important for Redirects (301, ...) as they MAY
                * have a content. Browsers are waiting for a Content otherwise
                */
               buffer_copy_off_t(srv->tmp_buf, qlen);

               response_header_overwrite(srv, con, CONST_STR_LEN("Content-Length"), CONST_BUF_LEN(srv->tmp_buf));
           }
       }
   }
   else
   {
       /**
        * the file isn't finished yet, but we have all headers
        *
        * to get keep-alive we either need:
        * - Content-Length: ... (HTTP/1.0 and HTTP/1.0) or
        * - Transfer-Encoding: chunked (HTTP/1.1)
        */

       if (((con->parsed_response & HTTP_CONTENT_LENGTH) == 0) &&
           ((con->response.transfer_encoding & HTTP_TRANSFER_ENCODING_CHUNKED) == 0))
       {
           con->keep_alive = 0;
       }

       /**
        * if the backend sent a Connection: close, follow the wish
        *
        * NOTE: if the backend sent Connection: Keep-Alive, but no Content-Length, we
        * will close the connection. That's fine. We can always decide the close
        * the connection
        *
        * FIXME: to be nice we should remove the Connection: ...
        */
       if (con->parsed_response & HTTP_CONNECTION)
       {
           /*
            * a subrequest disable keep-alive although the client wanted it
            */
           if (con->keep_alive && !con->response.keep_alive)
           {
               con->keep_alive = 0;
           }
       }
   }

   if (con->request.http_method == HTTP_METHOD_HEAD)
   {
       /**
        * a HEAD request has the same as a GET
        * without the content
        */
       con->file_finished = 1;

       chunkqueue_reset(con->write_queue);
       con->response.transfer_encoding &= ~HTTP_TRANSFER_ENCODING_CHUNKED;
   }

   http_response_write_header(srv, con);

   return 0;
}

首先,该函数判断连接的模式(mode)是否是DIRECT,如果是,说明连接没有经过插件处理,是由服务器自身处理的。这里判断连接的请求method,如果是OPTION,则设置Allow的值。同时清空write_queue,因为没有数据需要返回。 接着,在下面这个switch语句中,比较http_status的值,如果为204,205,304,说明服务器不需要给客户端返回文件,仅仅返回 response中headers及其之前的部分。这里和前面处理OPTION方法都设置con->file_finished为1。 file_finished用来标记客户端请求的静态文件是否已经发送完了(这个file_finished的含义比较模糊,目前还没搞清楚是表示文件发 送完毕,还是要发送的文件设置完毕可以发送。。。也有可能是个bug。。。如果各位读者有什么高见,还望不吝赐教!)。这两处都不需要给客户端发送文件, 因此将其设置为1,发送程序将直接跳过文件的发送。switch的default分支处理4xx错误,返回相应的错误提示文件。
  出了switch语句之后,接着是一个if判断file_finished的值。如果值为1,说明不需要给客户端返回文件数据。对于1xx,204和 304状态,将Content-Length设置为空值。如果method是HEAD,那么服务器可能需要返回一些数据,这时候要设置对应的 Content-Length。如果file_finished的值为0,那么要设置一下keep_alive的值。在 connection_handle_write_prepare函数的最后,调用http_response_write_header将 headers写入write_queue,等待返回给客户端。如果一切顺利,那么状态机进入CON_STATE_WRITE。
  由于数据可能不会一次写完,尤其是发送大文件的时候。因此,在CON_STATE_WRITE状态中首先判断write_queue时候为空,也就是有没 有数据还没有发送。同时判断连接是否可写。如果有数据且可写,那么调用connection_handle_write发送数据。如果没有数据可写或者连 接不可写,那么会退出switch(con->state)这个语句。同时,由于状态机状态没有发生改变,switch后面的if语句使得服务器退 出了大while循环,进入循环后面的小switch(con->state)语句。这个switch语句在前面已经介绍过。在这里,进入 CON_STATE_WRITE分支,如果有数据可写且连接可写且没有达到流量限制,那么在fdevent中注册这个连接,否则,删除这个连接。
  下面我们进入connection_handle_write函数,看看有数据可写且连接可写的情况。connection_handle_write函 数会首先在switch语句中调用network_write_chunkqueue函数。顾名思义,这个函数就是将write_queue中的数据写回 给客户端的。函数network_write_chunkqueue首先判断当前连接的流量是否超过了限制,如果是,则不发送任何数据,直接将连接加到作 业列表(joblist)中,让其他连接发送数据。如果没有超限,那么首先设置TCP_CORK选项。这个选项可以将多个write调用的数据一起发送, 提高发送效率。有关TCP_CORK的内容读者可自行google之。接下来就是调用srv->network_backend_wirte()函 数进行真正的写数据了。这个函数的真正定义有多个,在network_*.c文件中。服务器在network.c的network_init函数中会根据 当前的运行环境设置不同的值。为了提高服务器的效率,不同的OS会提供一些不同的方法,提高发送文件的速度。通过传统的先read在write的方法,需 要4次数据拷贝(从磁盘到内核缓冲区,从内核缓冲区到用户缓冲区,从用户缓冲区到网络接口的内核缓冲区,最后,从网络接口的内核缓冲区到网络设备缓冲 区。),OS会提供一些特定的方法来减少拷贝的次数,具体读者可以google“直接IO”,有不少介绍这个的文章。为了充分利用这些方法来提高服务器的 性能,lighttpd在不同的OS上都会去使用这些特定的接口,因此就需要多个network_backend_wirte函数的实现。这些实现基本上 大同小异,因此这里只介绍network_write.c中的实现。函数的主体是个大循环,遍历所有的chunk。如果chunk的类型是 MEM_CHUNK,那么这个chunk中的数据是在内存中的,直接调用write或者windows下的send函数发送数据。如果是 FILE_CHUNK类型,说明这个chunk表示的是一个文件,那么如果运行环境有mmap函数,就使用mmap映射文件并发送,否则就read在 write。如果这个chunk发送完了,那么继续发送下一个chunk。如果没有发送完(chunk_finished=0),那么退出循环,接着也就 退出了这个函数。服务器这时返回到network_write_chunkqueue中,下面做一些统计工作,再一次检查该连接的流量是否超限。最后服务 器返回到connection_handle_write中。如果network_write_chunkqueue返回-1,表示服务器出错。状态机进 入CON_STATE_ERROR。如果返回-2,则客户端关闭连接,状态机也进入CON_STATE_ERROR。返回0表示发送完毕,进入下一个状 态。返回1说明数据没有发送完,标记is_wirtable为0。
  从connection_handler_write函数返回后,如果数据没有发送完毕,那么状态机还在
CON_STATE_WRITE状态,接着连接被加到fdevent系统中,等待下一次数据发送。重复上述过程知道发送完毕或出错。
  如果数据发送完毕,状态机进入CON_STATE_RESPONSE_END状态。在这个状态中,服务器首先调用 plugins_call_handle_request_done通知所有插件连接处理完毕。然后判断是否保持连接,如果保持,将状态机设置为 CON_STATE_REQUEST_START。如果不保持,那么先调用plugins_call_handle_connection_close通 知所有插件连接关闭。然后关闭连接。最后,重置con,清除前一次请求的数据。
  至此,请求处理结束。总的来说,返回response的过程还算直接,没有多少难懂的地方。比较不好懂的地方都是关于http协议中一些情况的细节处理,读者可以参照rfc理解。
下面一片文章将会分析一些错误处理过程。

本文章由 http://www.wifidog.pro/2015/04/10/wifidog%E8%AE%A4%E8%AF%81lighttpd-1.html 整理编辑,转载请注明出处

wifidog认证自带http服务器Lighttpd1.4.20源码分析之状态机(2)---通过状态机看连接处理1

前面大概的介绍了一下lighttpd的状态机。在这篇中,将通过状态机,看看lighttpd到底是怎样处理连接请求的。

在本篇中,我们只介绍lighttpd的最基本功能──处理静态页面。lighttpd处理静态页面要使用mod_staticfile.c插件。从名字中也可以看出是用来处理静态文件的。另外这个插件在配置文件中没有配置,是lighttpd默认会加载的。

首先还是把状态机放这,以便查阅。
wifidog lighttpd状态机制.png

首先,连接建立以后,连接进入CON_STATE_REQUEST_START状态。在这个状态中,服务器仅仅是记录了做一些标记,如记录连接开始的时间,读操作发呆的时间等。接着,连接就进入了CON_STATE_READ状态。

在READ状态中,服务器从连接读取HTTP头并存放在con->requeset.request。在读取过程中,可能一次调用没有读取全部的数据,连接的状态继续停留在READ,然后,继续等待剩下的数据可读。由于这个连接同时也在作业队列中,在对作业队列进行处理的时候,依然会调用connecion_handle_read_state函数进行处理,不过,函数中通过con->is_readable来判断是否有数据可读,如果没有,则只是处理一下以前已经读取的数据。数据读取完毕之后,连接进入CON_STATE_REQUEST_END状态。

在REQUEST_END状态中,主要就是调用了http_request_parse函数。从函数名字就可以看出来是在解析request请求。request的请求格式如下:

HTTP Request Message的格式:
wifidog lighttpd request格式.png

不熟悉的可以看看RFC2616。

函数首先解析Request line,解析出来的结果分贝存放在:con->request.http_method, con->request.http_version和con->request.uri

前两个变量都是枚举类型,后一个是个buffer。程序的第一个for循环解析完request line后,进入第二个for循环,这个循环很长,但做的工作很简单。就是分析header lines。在找到一个header field name以后,就和所有已经定义的field name比较,看看是哪个。确定之后,就将field name和value保存到con->request.headers中。request.headers是一个array类型变量,存放的是data_string类型数据。其中,data_string的key是filed name,value就是field的成员。解析完之后做一些检查工作。最后,判断此次连接是否有POST数据,如果有则读取POST数据,否则进入HANDLE_REQUEST。

在阅读这个函数的时候,读者可参考RFC2616中的规定。

在这里,我们假设有POST数据要读。这时候,连接进入READ_POST状态。其实,READ_POST状态的处理和READ状态一样,在connection_state_mechine函数中,可以看到这两个switch分支一样。主要区别是在connection_handle_read_state函数中。connection_handle_read_state函数的前半部分读取数据,大家都一样,在后面处理数据的时候是分开的。对于读取POST数据,由于数据可能很大,这时候可能会用到临时文件来存储。在程序中,作者对于小于64k的数据,直接存储在buffer中,如果大于64k则存储在临时文件中。在向临时文件写数据的时候,每个临时文件只写1M的数据。数据大于1M就在打开一个临时文件。POST数据保存在con->requeset_content_queue,这是一个chunkqueue类型的成员变量,它是chunk结构体的链表,定义如下:

typedef struct
{

        chunk *first;
        chunk *last;
        /**
        * 这个unused的chunk是一个栈的形式。
        * 这个指针指向栈顶,而chunk中的next指针则将栈连 接起来。
        */
        chunk *unused;
        size_t unused_chunks;
        array *tempdirs;
        off_t bytes_in, bytes_out;
} chunkqueue;

unused成员也是一个链表,不过这个链表是栈的形式,unused指向栈顶。这个栈用来存储不用的chunk结构体,如果需要chunk,则先从这个栈中查找有无空闲的。也就是看栈顶是否是NULL。如果chunk不使用了,可以加到栈顶。这样可以减少内存分配的时间,提高程序的效率。这时一个很通用的小技巧。unused_chunks标记栈中有多少数据。

first和last分别指向链表的开头和结尾。

chunk的定义如下:

typedef struct chunk
{

        enum { UNUSED_CHUNK, MEM_CHUNK, FILE_CHUNK } type;
        /* 内存中的存储块或预读缓存 */
        buffer *mem;
        /* either the storage of the mem-chunk or the read-ahead buffer */
        struct
        {
                /*
                * filechunk 文件块
                */

                buffer *name;/* name of the file */
                off_t start;/* starting offset in the file */
                off_t length;/* octets to send from the starting offset */

                int fd;
                struct
                {
                        char *start;/* the start pointer of the mmap'ed area */
                        size_t length;/* size of the mmap'ed area */
                        off_t offset;     /* start is <n> octet away from the start of the file */
                } mmap;
                int is_temp;/* file is temporary and will be
                deleted if on cleanup */

        } file;
        off_t offset;/* octets sent from this chunk the size of the
                                * chunk is either -mem-chunk: mem->used - 1 file-chunk: file.length */

        struct chunk *next;

} chunk;

从名字可以看出,chunk用来表述一块存储空间。这个存储空间可能在内存中,也可能在文件中。mem成员指向内存中的存储空间。而file结构体则表示在文件中的存储空间。tpye成员标记这个块是内存的还是文件的。对于在内存中的存储空间,实际就是一个buffer。这个没什么说的。对于文件中的存储空间,程序首先会使用mmap函数将文件映射到内存中,mmap结构体的start成员保存映射到内存中的地址。然后,对于文件的操作就可以像使用内存一样。

关于这两个结构体的操作函数读者课自行查看。

当POST数据读取完毕之后,程序进入CON_STATE_REQUEST_END状态。在这个状态中,主要调用了是http_response_prepare函数,然后根据这个函数的返回值进行相应的处理。http_response_prepare函数定义在response.c文件中。粗略的浏览一遍这个函数,你会发现函数中调用了很多plugins_call_handle_xxxx函数。其实插件系统的接口函数主要是在这个函数中调用,这个函数也是服务器和插件系统交互的地方。函数定义如下:

handler_t http_response_prepare(server * srv, connection * con)
{
    handler_t r;
    if (con->mode == DIRECT && con->physical.path->used == 0)
    {
        char *qstr;
        /**
         * Name according to RFC 2396*
         * (scheme)://(authority)(path)?(query)#fragment 
         * fragment: 用于页面内部的跳转。通常不属于uri的一部分。
         * 服务器没有你要对其进行解析,后面的解析中,直接丢弃fragment。
         */
        /* 对HTTP头中的uri进行解析,分析出各个部分的内容。 */
        switch (r = plugins_call_handle_uri_raw(srv, con))
        {
        case HANDLER_GO_ON:
            break;
        case HANDLER_FINISHED:
        case HANDLER_COMEBACK:
        case HANDLER_WAIT_FOR_EVENT:
        case HANDLER_ERROR:
            return r;
        default:
            log_error_write(srv, __FILE__, __LINE__, "sd", "handle_uri_raw: unknown return value", r);
            break;
        }
        /*do we have to downgrade to 1.0 ? */
        if (!con->conf.allow_http11)
        {
            con->request.http_version = HTTP_VERSION_1_0;
        }
        switch (r = plugins_call_handle_uri_clean(srv, con))
        {
        case HANDLER_GO_ON:
            break;
        case HANDLER_FINISHED:
        case HANDLER_COMEBACK:
        case HANDLER_WAIT_FOR_EVENT:
        case HANDLER_ERROR:
            return r;
        default:
            log_error_write(srv, __FILE__, __LINE__, "");
            break;
        }
        if (con->request.http_method == HTTP_METHOD_OPTIONS && con->uri.path->ptr[0] == '*'
                             && con->uri.path_raw->ptr[1] == '\0')
        {
            /*将key=val加到response的head中。*/
            response_header_insert(srv, con, CONST_STR_LEN("Allow"),CONST_STR_LEN("OPTIONS, GET, HEAD, POST"));
            con->http_status = 200;
            con->file_finished = 1;
            return HANDLER_FINISHED;
        }
        /**
         *将请求地址转换成服务器的物理地址,也就是文件路径。
         */
        buffer_copy_string_buffer(con->physical.doc_root, con->conf.document_root);
        buffer_copy_string_buffer(con->physical.rel_path, con->uri.path);
        switch (r = plugins_call_handle_docroot(srv, con))
        {
        case HANDLER_GO_ON:
            break;
        case HANDLER_FINISHED:
        case HANDLER_COMEBACK:
        case HANDLER_WAIT_FOR_EVENT:
        case HANDLER_ERROR:
            return r;
        default:
            log_error_write(srv, __FILE__, __LINE__, "");
            break;
        }
        /**
         * create physical filename
         * -> physical.path = docroot + rel_path
         */
        buffer_copy_string_buffer(con->physical.path, con->physical.doc_root);
        BUFFER_APPEND_SLASH(con->physical.path);
        buffer_copy_string_buffer(con->physical.basedir, con->physical.path);
        if (con->physical.rel_path->used && con->physical.rel_path->ptr[0] == '/')
        {
            buffer_append_string_len(con->physical.path,
                                     con->physical.rel_path->ptr + 1,
                                     con->physical.rel_path->used - 2);
        } 
        else
        {
            buffer_append_string_buffer(con->physical.path, con->physical.rel_path);
        }
        switch (r = plugins_call_handle_physical(srv, con))
        {
        case HANDLER_GO_ON:
            break;
        case HANDLER_FINISHED:
        case HANDLER_COMEBACK:
        case HANDLER_WAIT_FOR_EVENT:
        case HANDLER_ERROR:
            return r;
        default:
            log_error_write(srv, __FILE__, __LINE__, "");
            break;
        }
    /*
     * Noone catched away the file from normal path of execution yet (like mod_access)
     * Go on and check of the file exists at all
     */
    if (con->mode == DIRECT)
    {
        char *slash = NULL;
        char *pathinfo = NULL;
        int found = 0;
        stat_cache_entry *sce = NULL;
        if (HANDLER_ERROR != stat_cache_get_entry(srv, con, con->physical.path, &sce))
        {
            /*file exists */
        } 
        else
        {
            /*not found, perhaps PATHINFO*/
            /*we have a PATHINFO */

        }
        switch (r = plugins_call_handle_subrequest_start(srv, con))
        {
        case HANDLER_GO_ON:
            /*request was not handled */
            break;
        case HANDLER_FINISHED:
        default:
            /* something strange happend */
            return r;
        }
        /*if we are still here, no one wanted the file, status 403 is ok I think*/
        if (con->mode == DIRECT && con->http_status == 0)
        {
            switch (con->request.http_method)
            {
            case HTTP_METHOD_OPTIONS:
                con->http_status = 200;
                break;
            default:
                con->http_status = 403;
            }
            return HANDLER_FINISHED;
        }
    }
    switch (r = plugins_call_handle_subrequest(srv, con))
    {
    case HANDLER_GO_ON:
        /*request was not handled, looks like we are done */
        return HANDLER_FINISHED;
    case HANDLER_FINISHED:
        /*request is finished */
    default:
        /*something strange happend  */
        return r;
    }
    /*can't happen  */
    return HANDLER_COMEBACK;
}

上面的代码删除了一些代码,重点突出插件的调用。完整代码读者可自行查阅。下面我们开始分析。

首先我们先来看看HTTP协议,前面提到的HTTP Request Messag格式可以看出,在整个HTTP requset head中,只有url和methods可以用来确定请求所对应的插件。比如,对于cgi请求,url中的文件的扩展名肯定是配置文件中定义的,不会是.html。在http_response_prepare函数中,通过对url的解析,逐步的调用插件来处理。对url解析的结果存放在con->uri中。uri的定义如下:

typedef struct
{
        buffer *scheme; //http , https and so on
         buffer *authority; //user:password
         buffer *path; //www.xxx.com/xxx/xxxx.html
         buffer *path_raw; //www.xxx.com
         buffer *query; //key1=data1&key2=data2
 } request_uri;

uri的定义为:(scheme)://(authority)(path)?(query)#fragment。上面的结构体和这个定义对应。举个例子:

http://user:passwd@www.google.com/pages/index.html?key1=data1&key2=data2#frag

解析之后:

scheme = http

authority = user:passwd

path = www.google.com/pages/index.html

path_raw = 未进行解码的path

query = key1=data1&key2=date2

对于path_raw要说明一下。在浏览器向服务器发送url请求的时候,会对其中的保留字符和不安全字符进行编码(具体参见RFC2396),最长见的就是对汉字。编码的形式是% HEX HEX,一个%加两个十六进制的数字。服务器在接受到请求之后,要对这些编码过的字符进行解码。path_raw中保存的就是还没有解码的url,path保存的是已经解码过的url。

前面的程序已经解析了HTTP头,HTTP头中的request line中的uri被存储在con->request.uri中。在函数的一开始,对这个uri地址进行解析。对于uri中的query和fragment部分,服务器不需要进行处理,因此将这些部分提取后存储到con -> uri中,对fragment的处理时直接抛弃,因为fragment是浏览器使用的。当解析出url中的path之后,服务器调用插件的plugins_call_handle_uri_raw函数,在这个函数中,插件根据分析出的为解码的url path进行处理。如果没有插件进行处理,服务器接着调用哪个插件的plugins_call_handle_uri_clean函数,这个函数自然就是根据解码过的url path进行处理。这两个函数最典型的应用就是proxy服务器。proxy服务器根据解析出来的url地址直接将请求转发,不需要对请求进行处理。

当请求仍然没有被处理时,说明这个请求必须在要被处理。首先服务器调用插件的plugins_call_handle_docroot函数对处理请求时的根目录进行设置。对于不同种类的资源,可以设置不同的根目录,提供一个虚拟服务器。设置根目录也可以简化请求的url地址。

接着,服务器根据根目录和请求的url地址,拼接处资源在本机上对应的物理地址。比如,doc root = /abc/root, url path = /doc/index.html,得到的物理地址就是/abc/root/doc/index.html。然后服务器调用插件的plugins_call_handle_physical函数。这个函数根据得到的物理地址进行相应的处理。

接着,服务器调用插件的plugins_call_handle_subrequest_start函数和plugins_call_handle_subrequest函数进行最后的处理。

下面总结一下插件接口的作用:

1、 plugins_call_handle_uri_raw

在得到http头中,request line的url地址直接调用,此时的url地址没有解码。url地址中不包含query。

2、plugins_call_handle_uri_clean

对url地址进行解码之后调用。

以上两个函数典型的应用时proxy服务器。直接将请求转发。

3、plugins_call_handle_docroot

设置处理请求时的根目录。

4、plugins_call_handle_physical

得到请求资源在服务器上的物理地址,根据这个物理地址做相应的处理。

5、plugins_call_handle_subrequest_start

子请求处理开始。

6、plugins_call_handle_subrequest

处理子请求。

最后,连接进入CON_STATE_RESPONSE_START状态。进入这个状态之后,服务器根据处理得到的记过准备给客户端的response。包括准备response头和写数据。这在后面的文章中继续讨论。

本文章由 http://www.wifidog.pro/2015/04/10/wifidog%E8%AE%A4%E8%AF%81lighttpd.html 整理编辑,转载请注明出处

wifidog认证自带http服务器Lighttpd1.4.20源码分析之状态机(1)---状态机总览

前面讲了lighttpd的fdevent系统,从这一篇开始,我们将进入lighttpd的状态机。状态机可以说是lighttpd最核心的部分。lighttpd将一个连接在不同的时刻分成不同的状态,状态机则根据连接当前的状态,决定要对连接进行的处理以及下一步要进入的状态。下面这幅图描述了lighttpd的状态机:
1.png

(在lighttpd源码文件夹中的doc目录中有个state.dot文件,通过dot命令可以生成上面的图:dot -Tpng state.dot -o state.png。)
图中的各个状态对应于下面的一个枚举类型:

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结构体的第一个成员就是这个变量。
在连接建立以后,在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://www.wifidog.pro/2015/04/10/wifidog%E8%AE%A4%E8%AF%81http.html 整理编辑,转载请注明出处