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最后调用,如果之前没有因有些原因出去的话。