本文主要是介绍lighttpd - Plugin: Overview,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
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_SLOTif (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最后调用,如果之前没有因有些原因出去的话。
这篇关于lighttpd - Plugin: Overview的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!