3.5 定义自己的HTTP模块
上文中我们了解了定义HTTP模块时需要定义哪些成员以及实现哪些方法,但在定义HTTP模块前,首先需要确定自定义的模块应当在什么样的场景下开始处理用户请求,也就是说,先要弄清楚我们的模块是如何介入到Nginx处理用户请求的流程中的。从2.4节中的HTTP配置项意义可知,一个HTTP请求会被许多个配置项控制,实际上这是因为一个HTTP请求可以被许多个HTTP模块同时处理。这样一来,肯定会有一个先后问题,也就是说,谁先处理请求谁的“权力”就更大。例如,ngx_http_access_module模块的deny选项一旦得到满足后,Nginx就会决定拒绝来自某个IP的请求,后面的诸如root这种访问静态文件的处理方式是得不到执行的。另外,由于同一个配置项可以从属于许多个server、location配置块,那么这个配置项将会针对不同的请求起作用。因此,现在面临的问题是,我们希望自己的模块在哪个时刻开始处理请求?是希望自己的模块对到达Nginx的所有请求都起作用,还是希望只对某一类请求(如URI匹配了location后表达式的请求)起作用?
Nginx的HTTP框架定义了非常多的用法,我们有很大的自由来定义自己的模块如何介入HTTP请求的处理,但本章只想说明最简单、最常见的HTTP模块应当如何编写,因此,我们这样定义第一个HTTP模块介入Nginx的方式:
1)不希望模块对所有的HTTP请求起作用。
2)在nginx.conf文件中的http{}、server{}或者location{}块内定义mytest配置项,如果一个用户请求通过主机域名、URI等匹配上了相应的配置块,而这个配置块下又具有mytest配置项,那么希望mytest模块开始处理请求。
在这种介入方式下,模块处理请求的顺序是固定的,即必须在HTTP框架定义的NGX_HTTP_CONTENT_PHASE阶段开始处理请求,具体内容下文详述。
下面开始按照这种方式定义mytest模块。首先,定义mytest配置项的处理。从上文中关于ngx_command_t结构的说明来看,只需要定义一个ngx_command_t数组,并设置在出现mytest配置后的解析方法由ngx_http_mytest“担当”,如下所示:
static ngx_command_t ngx_http_mytest_commands[] = {
{ ngx_string("mytest"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS,
ngx_http_mytest,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
ngx_null_command
};
其中,ngx_http_mytest是ngx_command_t结构体中的set成员(完整定义为char (set)(ngx_conf_t cf, ngx_command_t cmd, void *conf);),当在某个配置块中出现mytest配置项时,Nginx将会调用ngx_http_mytest方法。下面看一下如何实现ngx_http_mytest方法。
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
/首先找到mytest配置项所属的配置块,clcf看上去像是location块内的数据结构,其实不然,它可以是main、srv或者loc级别配置项,也就是说,在每个http{}和server{}内也都有一个ngx_http_core_loc_conf_t结构体/
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
/HTTP框架在处理用户请求进行到NGX_HTTP_CONTENT_PHASE阶段时,如果请求的主机域名、URI与mytest配置项所在的配置块相匹配,就将调用我们实现的ngx_http_mytest_handler方法处理这个请求/
clcf->handler = ngx_http_mytest_handler;
return NGX_CONF_OK;
}
当Nginx接收完HTTP请求的头部信息时,就会调用HTTP框架处理请求,另外在11.6节描述的NGX_HTTP_CONTENT_PHASE阶段将有可能调用mytest模块处理请求。在ngx_http_mytest方法中,我们定义了请求的处理方法为ngx_http_mytest_handler,举个例子来说,如果用户的请求URI是/test/example,而在配置文件中有这样的location块:
Location /test {
mytest;
}
那么,HTTP框架在NGX_HTTP_CONTENT_PHASE阶段就会调用到我们实现的ngx_http_mytest_handler方法来处理这个用户请求。事实上,HTTP框架共定义了11个阶段(第三方HTTP模块只能介入其中的7个阶段处理请求,详见10.6节),本章只关注NGX_HTTP_CONTENT_PHASE处理阶段,多数HTTP模块都在此阶段实现相关功能。下面简单说明一下这11个阶段。
typedef enum {
//在接收到完整的HTTP头部后处理的HTTP阶段
NGX_HTTP_POST_READ_PHASE = 0,
/在还没有查询到URI匹配的location前,这时rewrite重写URL也作为一个独立的HTTP阶段/
NGX_HTTP_SERVER_REWRITE_PHASE,
/根据URI寻找匹配的location,这个阶段通常由ngx_http_core_module模块实现,不建议其他HTTP模块重新定义这一阶段的行为/
NGX_HTTP_FIND_CONFIG_PHASE,
/在NGX_HTTP_FIND_CONFIG_PHASE阶段之后重写URL的意义与NGX_HTTP_SERVER_REWRITE_PHASE阶段显然是不同的,因为这两者会导致查找到不同的location块(location是与URI进行匹配的)/
NGX_HTTP_REWRITE_PHASE,
/这一阶段是用于在rewrite重写URL后重新跳到NGX_HTTP_FIND_CONFIG_PHASE阶段,找到与新的URI匹配的location。所以,这一阶段是无法由第三方HTTP模块处理的,而仅由ngx_http_core_module模块使用/
NGX_HTTP_POST_REWRITE_PHASE,
//处理NGX_HTTP_ACCESS_PHASE阶段前,HTTP模块可以介入的处理阶段
NGX_HTTP_PREACCESS_PHASE,
/*这个阶段用于让HTTP模块判断是否允许这个请求访问Nginx服务器
NGX_HTTP_ACCESS_PHASE,
/当NGX_HTTP_ACCESS_PHASE阶段中HTTP模块的handler处理方法返回不允许访问的错误码时(实际是NGX_HTTP_FORBIDDEN或者NGX_HTTP_UNAUTHORIZED),这个阶段将负责构造拒绝服务的用户响应。所以,这个阶段实际上用于给NGX_HTTP_ACCESS_PHASE阶段收尾/
NGX_HTTP_POST_ACCESS_PHASE,
/这个阶段完全是为了try_files配置项而设立的。当HTTP请求访问静态文件资源时,try_files配置项可以使这个请求顺序地访问多个静态文件资源,如果某一次访问失败,则继续访问try_files中指定的下一个静态资源。另外,这个功能完全是在NGX_HTTP_TRY_FILES_PHASE阶段中实现的/
NGX_HTTP_TRY_FILES_PHASE,
//用于处理HTTP请求内容的阶段,这是大部分HTTP模块最喜欢介入的阶段
NGX_HTTP_CONTENT_PHASE,
/处理完请求后记录日志的阶段。例如,ngx_http_log_module模块就在这个阶段中加入了一个handler处理方法,使得每个HTTP请求处理完毕后会记录access_log日志/
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
当然,用户可以在以上11个阶段中任意选择一个阶段让mytest模块介入,但这需要学习完第10章、第11章的内容,完全熟悉了HTTP框架的处理流程后才可以做到。
暂且不管如何实现处理请求的ngx_http_mytest_handler方法,如果没有什么工作是必须在HTTP框架初始化时完成的,那就不必实现ngx_http_module_t的8个回调方法,可以像下面这样定义ngx_http_module_t接口。
static ngx_http_module_t ngx_http_mytest_module_ctx = {
NULL, /* preconfiguration */
NULL, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
NULL, /* create location configuration */
NULL /* merge location configuration */
};
最后,定义mytest模块:
ngx_module_t ngx_http_mytest_module = {
NGX_MODULE_V1,
&ngx_http_mytest_module_ctx, /* module context */
ngx_http_mytest_commands, /* module directives */
NGX_HTTP_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
这样,mytest模块在编译时将会被加入到ngx_modules全局数组中。Nginx在启动时,会调用所有模块的初始化回调方法,当然,这个例子中我们没有实现它们(也没有实现HTTP框架初始化时会调用的ngx_http_module_t中的8个方法)。