lighttpd - Plugin: Overview

插件是各个Web Server的重要组成部分,很多功能通过插件完成(mini_httpd这类超级小的Server当然没有什么插件,但它的目的只是实验啊,可不是取代什么Apache这类宏伟目标。说道取代Apache,貌似Nginx是目前......  算了,还是回到lighttpd吧)。

本文介绍下lighttpd的插件部分的实现。lighttpd版本1.4.31。

1. 数据结构

Lighttpd为每个插件维护一个plugin数据结构,其中包含了插件名,版本号,以及虚函数表和一个保存dlopen(Linux下)返回的非透明句柄。其定义如下,

typedef struct {
    size_t version;

    buffer *name; /* name of the plugin */

    void *(* init)                       ();
    handler_t (* set_defaults)           (server *srv, void *p_d);
    handler_t (* cleanup)                (server *srv, void *p_d);
                                                                                       /* is called ... */
    handler_t (* handle_trigger)         (server *srv, void *p_d);                     /* once a second */
    handler_t (* handle_sighup)          (server *srv, void *p_d);                     /* at a signup */

    handler_t (* handle_uri_raw)         (server *srv, connection *con, void *p_d);    /* after uri_raw is set */
    handler_t (* handle_uri_clean)       (server *srv, connection *con, void *p_d);    /* after uri is set */
    handler_t (* handle_docroot)         (server *srv, connection *con, void *p_d);    /* getting the document-root */
    handler_t (* handle_physical)        (server *srv, connection *con, void *p_d);    /* mapping url to physical path */
    handler_t (* handle_request_done)    (server *srv, connection *con, void *p_d);    /* at the end of a request */
    handler_t (* handle_connection_close)(server *srv, connection *con, void *p_d);    /* at the end of a connection */
    handler_t (* handle_joblist)         (server *srv, connection *con, void *p_d);    /* after all events are handled */



    handler_t (* handle_subrequest_start)(server *srv, connection *con, void *p_d);

                                                                                       /* when a handler for the request
                                                * has to be found
                                                */
    handler_t (* handle_subrequest)      (server *srv, connection *con, void *p_d);    /* */
    handler_t (* connection_reset)       (server *srv, connection *con, void *p_d);    /* */
    void *data;

    /* dlopen handle */
    void *lib;
} plugin;

此外,Server的结构server,server_config里面都有一些插件相关的字段,暂时先把它们罗列在此方便一会参考。

typedef struct {
    void  *ptr;
    size_t used;
    size_t size;
} buffer_plugin;

typedef struct server {
    ...
    buffer_plugin plugins;  //插件池
    void *plugin_slots;
    ...
    server_config  srvconf;
    ...
} server;

typedef struct {
    ...
    buffer *modules_dir;   //模块路径
    array *modules;        //模块名列表
     ...
} server_config;

2. 模块加载

Plugin可以作为动态库由lighttpd主进程启动的时候“动态加载”,也可以静态编译到lighttpd中。前者灵活易于扩展,需要新增plugin时无需重新编译Web Server,只需要编译动态库,修改配置文件,并重启lighttpd(但不支持“热加载”)。当然,共享库的体积通常比静态编译要大一些(无法在多个使用者间分摊大小的情况下),加载共享库也需要一些时间。好在通常情况这两个缺点不足以使我们放弃动态库灵活,可扩展性强的优势。

了解加载函数的实现前先看几个plugin.c里的内部函数,

static plugin *plugin_init(void);
static void plugin_free(plugin *p);

static int plugins_register(server *srv, plugin *p) {
    plugin **ps;
    if (0 == srv->plugins.size) {
        srv->plugins.size = 4;
        srv->plugins.ptr  = malloc(srv->plugins.size * sizeof(*ps));
        srv->plugins.used = 0;
    } else if (srv->plugins.used == srv->plugins.size) {
        srv->plugins.size += 4;
        srv->plugins.ptr   = realloc(srv->plugins.ptr, srv->plugins.size * sizeof(*ps));
    }

    ps = srv->plugins.ptr;
    ps[srv->plugins.used++] = p;

    return 0;
}

plugin_init分配并初始化plugin结构,初始化只是清零而已,没有其他动作。plugin_free释放plugin结构,如果是动态加载,会先释放由dlopen打开的共享库。注册函数的实现也非常直接,每次以4个plugin为单位,分配plugin池,池中有空闲就设置一个。贴出它的实现是要强调“Plugin池”的概念,类似的做法在lighttpd的许多地方都会碰到。


接下来要看加载函数了,不过plugins_loads有两个版本:静态,动态。这个是编译的时候决定的。

2.1 静态加载

int plugins_load(server *srv) {
    plugin *p;
#define PLUGIN_INIT(x)\
    p = plugin_init(); \
    if (x ## _plugin_init(p)) { \
        log_error_write(srv, __FILE__, __LINE__, "ss", #x, "plugin init failed" ); \
        plugin_free(p); \
        return -1;\
    }\ 
    plugins_register(srv, p);

#include "plugin-static.h"

    return 0;
}

这里定义了一个宏PLUGIN_INIT,每次使用这个宏,就可以通过plugin_init分配一个新的plugin结构,并调用xxx_plugin_init()函数,最后在用plugin_register将新的plugin结构注册到server中。但问题是,只有宏定义啊,调用呢?!。据本人猜测,应该在plugin-static.h中,使用./configure的默认配置的话,代码树中并没有此文件。不过本人做了个实验,重新使用"--enable-static[=PKGS]"、"--with-PACKAGE[=ARG]"重新配置、编译了一下Lighttpd,居然还是没有plugin-static.h啊,猜测失败?谁知道的话麻烦搞告诉我下。


好在我们主要关注的是动态版本的plugins_load。

2.2 动态加载

动态加载是默认的情况。对于配置文件中使用"server.modules = (...)"所列出的每个模块进行加载。函数原型和静态版本相同,

int plugins_load(server *srv) {
...
for (i = 0; i < srv->srvconf.modules->used; i++) { ... }

加载过程如下,

  • 使用modules_dir,"/",模块名和".so"拼装成完整的路径名。
        buffer_copy_string_buffer(srv->tmp_buf, srv->srvconf.modules_dir);

        buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN("/"));
        buffer_append_string(srv->tmp_buf, modules);
        buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN(".so"));
  • 使用plugin_init分配并初始化一个新的plugin结构
        p = plugin_init();
  • 调用dlopen打开共享库,并将非透明的handle保存到plugin->lib
        if (NULL == (p->lib = dlopen(srv->tmp_buf->ptr, RTLD_NOW|RTLD_GLOBAL))) {
            ...
        }
  • 使用模块名,“_plugin_init”组装成模块初始化函数,并用dlsym取出其地址,并调用它
        buffer_reset(srv->tmp_buf);
        buffer_copy_string(srv->tmp_buf, modules);
        buffer_append_string_len(srv->tmp_buf, CONST_STR_LEN("_plugin_init"));
        ...
        init = (int (*)(plugin *))(intptr_t)dlsym(p->lib, srv->tmp_buf->ptr);
        ...
        if ((*init)(p)) {
            ...
        }
  • 调用plugins_register将模块注册到server
        plugins_register(srv, p);

3. Server和Plugin间的接口

3.1 server,plugin结构关系图

要知道plugin如何工作,先要理清下面几个数据结构的关系,server, plugin, plugin_t, server->plugin_slots, server->plugins。它们的关系如下图所示,slot中每个元素代表了一个VFT function,而每个function有一组plugin与之关联。这意味着调用VFT function的时候,要把每个plugin的对应function都调用一遍(实际上未必会都都调用,要根据某个plugin Function的返回值判断是否继续,这个稍后讨论)。

 

3.2 Plugin API: plugins_call_xxx 函数

lighttpd的Plugin模块对外(Server)的接口是plugins_call_xxx函数。这些函数则是使用PLUGIN_TO_SLOT宏(如果愿意可以称为模板,虽然C里面没有模板)来生成的。PLUGIN_TO_SLOT是一个非常重要,而又奇特的宏:1. 名字上更本看不出它是干嘛的,2. 它被多次重定义,并用来或者生成函数、或者执行代码。

plugins_call_xxx函数分成3类,其实只是原型不同而已。

#define SERVER_FUNC(x) \
        static handler_t x(server *srv, void *p_d)
       
#define CONNECTION_FUNC(x) \
        static handler_t x(server *srv, connection *con, void *p_d)
        
#define INIT_FUNC(x) \
        static void *x()

我们暂时忽略SERVER_FUNC,CONNECTION_FUNC和INIT_FUNC的用处。看看如何用PLUGIN_TO_SLOT生成这些API。

PLUGIN_TO_SLOT的第一次定义,以及用它生成的Plugin API:

#define PLUGIN_TO_SLOT(x, y) \
    handler_t plugins_call_##y(server *srv, connection *con) {\
        plugin **slot;\
        size_t j;\
                if (!srv->plugin_slots) return HANDLER_GO_ON;\
                slot = ((plugin ***)(srv->plugin_slots))[x];\
        if (!slot) return HANDLER_GO_ON;\
        for (j = 0; j < srv->plugins.used && slot[j]; j++) { \
            plugin *p = slot[j];\
            handler_t r;\
            switch(r = p->y(srv, con, p->data)) {\
            case HANDLER_GO_ON:\
                break;\
            case HANDLER_FINISHED:\
            case HANDLER_COMEBACK:\
            case HANDLER_WAIT_FOR_EVENT:\
            case HANDLER_WAIT_FOR_FD:\
            case HANDLER_ERROR:\
                return r;\
            default:\
                log_error_write(srv, __FILE__, __LINE__, "sbs", #x, p->name, "unknown state");\
                return HANDLER_ERROR;\
            }\
        }\
        return HANDLER_GO_ON;\
    }

PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_CLEAN, handle_uri_clean)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_RAW, handle_uri_raw)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_REQUEST_DONE, handle_request_done)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_CONNECTION_CLOSE, handle_connection_close)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_SUBREQUEST, handle_subrequest)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_SUBREQUEST_START, handle_subrequest_start)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_JOBLIST, handle_joblist)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_DOCROOT, handle_docroot)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_PHYSICAL, handle_physical)
PLUGIN_TO_SLOT(PLUGIN_FUNC_CONNECTION_RESET, connection_reset)

于是就有了,

handler_t plugins_call_handle_uri_clean(server *srv, connection *con) {\
    ...
}
...

这些生成函数只是函数名不同,实现都是一样的。从server的plugin_slots中找到函数(x作为数组下标)对应的slot;然后对该slot里面的每个plugin调用此函数。当然要查看调用的返回值。根据返回值判定下一步的动作,例如,HANDLER_GO_ON的情况下会继续调用其他plugin的函数。

PLUGIN_TO_SLOT的第二次定义,以及用它生成的Plugin API:

#undef PLUGIN_TO_SLOT

#define PLUGIN_TO_SLOT(x, y) \
    handler_t plugins_call_##y(server *srv) {\
    ...
    }
...
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_TRIGGER, handle_trigger)
PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_SIGHUP, handle_sighup)
PLUGIN_TO_SLOT(PLUGIN_FUNC_CLEANUP, cleanup)
PLUGIN_TO_SLOT(PLUGIN_FUNC_SET_DEFAULTS, set_defaults)

#undef PLUGIN_TO_SLOT

PLUGIN_TO_SLOT的第三次定义并不是用来生成函数的。这个稍后我们会看到。

对照plugin的VFT,我们发现还有一个函数没有对应的plugins_call_xxx函数,即init。它和其他函数原型不同又只此一个,故单独定义。

handler_t plugins_call_init(server *srv) {
    ...
}

然后是它的实现,哦!怎么又是PLUGIN_TO_SLOT?(第三次定义)

#define PLUGIN_TO_SLOT(x, y) \
    if (p->y) { \
        plugin **slot = ((plugin ***)(srv->plugin_slots))[x]; \
        if (!slot) { \
            slot = calloc(srv->plugins.used, sizeof(*slot));\
            ((plugin ***)(srv->plugin_slots))[x] = slot; \
        } \
        for (j = 0; j < srv->plugins.used; j++) { \
            if (slot[j]) continue;\
            slot[j] = p;\
            break;\
        }\
    }

有了它,就可以初始化plugin_slots了,

    ps = srv->plugins.ptr;
    ...
    srv->plugin_slots = calloc(PLUGIN_FUNC_SIZEOF, sizeof(ps));

    for (i = 0; i < srv->plugins.used; i++) {
        ...
        plugin *p = ps[i];

        PLUGIN_TO_SLOT(PLUGIN_FUNC_HANDLE_URI_CLEAN, handle_uri_clean);
        ...
        PLUGIN_TO_SLOT(PLUGIN_FUNC_SET_DEFAULTS, set_defaults);
#undef PLUGIN_TO_SLOT

        if (p->init) {
            if (NULL == (p->data = p->init())) {
            ...
            }
            ...
            ((plugin_data *)(p->data))->id = i + 1;

            if (p->version != LIGHTTPD_VERSION_ID) {
            ...
            }
        } else {
            p->data = NULL;
        }
    }

具体的就不介绍了,最终的结果就是上面那张图。除了初始化slot外,plugins_call_init还需要,工作是调用每个plugin的init函数。

3.3 Plugins API的返回值

Plugin API返回值是handler_t(这名字起的。。),调用者根据它的不同返回执行不同操作。但是这些返回值的名字只能算作Plugin API 对调用这的“建议”,并非所有调用做相同的处理,有的甚至不检查这些返回值。

typedef enum { HANDLER_UNSET,
        HANDLER_GO_ON,
        HANDLER_FINISHED,
        HANDLER_COMEBACK,
        HANDLER_WAIT_FOR_EVENT,
        HANDLER_ERROR,
        HANDLER_WAIT_FOR_FD
} handler_t;

4. Plugins如何工作

各个plugin所实现的虚函数(callbacks)是在它所对应的plugins_call_xxx函数中被调用的,那这些函数又是在什么时候被调用呢?这个是整个plugin工作的关键。

4.1 lugins_load

在Lighttpd server初始化阶段(main()函数)中被调用。

4.2 plugins_call_init

在Lighttpd server初始化阶段(main()函数)中被调用。调用各个Plugin虚函数的init实例。

4.3 plugins_call_set_defaults

在Lighttpd server初始化阶段(main()函数)中被调用,调用各个Plugin虚函数的实例,将server的配置读到各个Plugin私有结构中。

4.4 plugins_call_cleanup

在plugins_free中被调用,后者在server正常或者异常退出的时候被调用。

4.5 plugins_call_trigger

在worker进程的SIGALRM处理时被调用,也就是周期性每秒被调用一次。

4.6 plugins_call_sighup

在worker进程的handle_sig_hup阶段处理,SIGHUP在watcher进程收到SIGHUP后,向所有进程组中的进程转发。

4.7 plugins_call_connection_close

在connection 状态机中,的RESPONSE_END状态下,如果不是KEEP-Alive,会关闭连接,在此之前,先调用该函数。
在connection 状态机中,的ERROR状态下,如果非DIRECT模式,则调用此函数,并在稍后关闭连接。

4.8 plugins_call_connection_reset

在connection_reset()中调用,后者在

    1. 获取新的空conn (connections_get_new_connection)
    2. 释放连接(connections_free)
    3. 各种原因(正常、或异常)接关闭之后

等处多处被调用。

4.9 plugins_call_handle_uri_raw / plugins_call_handle_uri_clean

要理解调用这两个函数的时机,先要理解什么是raw和clean的uri。

typedef struct {
    buffer *scheme; /* scheme without colon or slashes ( "http" or "https" ) */

    /* authority with optional portnumber ("site.name" or "site.name:8080" ) 
     * NOTE: without "username:password@" */
    buffer *authority;

    /* path including leading slash ("/" or "/index.html") 
     * - urldecoded, and sanitized  ( buffer_path_simplify() && buffer_urldecode_path() ) */
    buffer *path;
    buffer *path_raw; /* raw path, as sent from client. no urldecoding or path simplifying */
    buffer *query; /* querystring ( everything after "?", ie: in "/index.php?foo=1", query is "foo=1" ) */
} request_uri;

Server从request URL中提取原始的uri部分,包括scheme,authority(即hostname),path,query(‘?’之后,'#'之前),以及fragment(‘#’之后的部分)。要注意的是path_raw和path的区别,path_raw是指尚未经过decoding、simplifying的“原始的path”,例如,包含"%20"之类的转移,"../"之类妄想逃离chroot范围的字段尚未清除的情况。

看看http_response_prepare是如何处理URL的,

     handler_t http_response_prepare(server *srv, connection *con) {
        ...
        if (con->conf.is_ssl) {
            buffer_copy_string_len(con->uri.scheme, CONST_STR_LEN("https"));
        } else {
            buffer_copy_string_len(con->uri.scheme, CONST_STR_LEN("http"));
        }  
        buffer_copy_string_buffer(con->uri.authority, con->request.http_host);
        buffer_to_lower(con->uri.authority);
        ...
        /** their might be a fragment which has to be cut away */
        if (NULL != (qstr = strchr(con->request.uri->ptr, '#'))) {
            con->request.uri->used = qstr - con->request.uri->ptr;
            con->request.uri->ptr[con->request.uri->used++] = '\0';
        }

        /** extract query string from request.uri */
        if (NULL != (qstr = strchr(con->request.uri->ptr, '?'))) {
            buffer_copy_string    (con->uri.query, qstr + 1);
            buffer_copy_string_len(con->uri.path_raw, con->request.uri->ptr, qstr - con->request.uri->ptr);
        } else {
            buffer_reset     (con->uri.query);
            buffer_copy_string_buffer(con->uri.path_raw, con->request.uri);
        }
     ...
     }

这一步部分只是完成“提取”工作,path_row, query等还都是“原始”的,此时调用便是调用plugins_call_handle_uri_raw的时机。

然后继续对uri进程处理decode url-encoding,以及simplifying,处理完之后uri.path被设置。

            buffer_copy_string_buffer(srv->tmp_buf, con->uri.path_raw);
            buffer_urldecode_path(srv->tmp_buf);
            buffer_path_simplify(con->uri.path, srv->tmp_buf);

之后就可以调用plugins_call_handle_uri_clean了。

4.10 plugins_call_docroot

还是http_response_prepare中,话说处理完uri,提取并转码了path而得到con->uri.path之后。需要吧逻辑地址转换为物理地址。先记录下doc_root,和rel_path到physical结构中。之后,调用plugins_call_docroot。而此plugin可能会设置con->server_name,如果没有设置,使用默认值,

        buffer_copy_string_buffer(con->physical.doc_root, con->conf.document_root);
        buffer_copy_string_buffer(con->physical.rel_path, con->uri.path);

4.11 plugins_call_request_done

connection 状态机RESPONSE_END的时候调用。另一个地方是ERROR状态下,如果http_status已经决定的话。

4.12 plugins_call_joblist

worker进程的主循环的中,会对每个处于joblist的con调用,state_machine,以及plugins_call_joblist。此外network_server_handle_fdevent中,每accept一个新的conn,会调用state_machine以及plugins_call_joblist。

4.13 plugins_call_subrequest_start

当http_response_prepare已经吧physical路径设置完后,会做一些检查,文件是否存在、path是不是目录,如果是目录,需要重定向到index文件。而整个检查基于cache系统,即先从cache中查看。如果没有再查看实际的文件。

然后就会调用plugins_call_subrequest_start,简而言之,在设置、检查完physical path后调用。

4.14 plugins_call_subrequest

在http_response_prepare最后调用,如果之前没有因有些原因出去的话。

posted @ 2012-09-16 19:22  beacer  阅读(1561)  评论(0编辑  收藏  举报