本文将学些开发一个简单的HTTP过滤模块,它能够对Content-Type为text/plain的包体前加上前缀字符串prefix.
<一> 过滤模块的调用顺序
过滤模块可以叠加,也就是说一个请求会被所有的HTTP过滤模块依次处理。
过滤模块的调用时有顺序的,它的顺序在编译的时候就决定了。控制编译的脚本位于auto/modules中,当你编译完Nginx以后,可以再objs目录下面看到一个ngx_modules.c的文件,打开这个文件,有类似的代码:
ngx_module_t *ngx_modules[] = {
...
&ngx_http_write_filter_module,
&ngx_http_header_filter_module,
&ngx_http_chunked_filter_module,
&ngx_http_range_header_filter_module,
&ngx_http_gzip_filter_module,
&ngx_http_postpone_filter_module,
&ngx_http_ssi_filter_module,
&ngx_http_charset_filter_module,
&ngx_http_userid_filter_module,
&ngx_http_headers_filter_module,
&ngx_http_copy_filter_module,
&ngx_http_range_body_filter_module,
&ngx_http_not_modified_filter_module,
NULL
};
从write_filter到not_modified_filter,模块的执行顺序是反向的。也就是说最早执行的是not_modified_filter,然后各个模块以此执行。所有第三方的模块只能加入到copy_filter和headers_filter模块之间执行。
在编译Nginx源码时,已经定义了一个由所有HTTP过滤模块组成的单链表,这个单链表是这样的:
链表的每个元素都是一个C源代码文件,这个C源代码文件中有两个指针,分别指向下一个过滤模块(文件)的过滤头部和包体的方法(可理解为链表中的next指针)
过滤模块单链表示意图:
这两个指针的声明如下:
/*过滤模块处理HTTP头部的函数指针类型定义,它携带一个参数:请求*/
typedef ngx_int_t (*ngx_http_output_header_filter_pt)(ngx_http_request_t *r);
/*过滤模块处理HTTP包体的函数指针类型定义,它携带两个参数:请求、要发送的包体*/
typedef ngx_int_t (*ngx_http_output_body_filter_pt) (ngx_http_request_t *r, ngx_chain_t *chain);
在我们定义的第三方模块中则有如下声明:
static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
那么怎么将这个源文件(节点),插入到HTTP过滤模块组成的单链表中去呢?Nginx采用头插法的办法,所有的新节点都插入在链表的开头:
//插入到头部处理方法链表的首部
ngx_http_next_header_filter=ngx_http_top_header_filter;
ngx_http_top_header_filter=ngx_http_myfilter_header_filter;
//插入到包体处理方法链表的首部
ngx_http_next_body_filter=ngx_http_top_body_filter;
ngx_http_top_body_filter=ngx_http_myfilter_body_filter;
其中,两个top指针声明如下:
extern ngx_http_output_header_filter_pt ngx_http_next_header_filter;
extern ngx_http_output_body_filter_pt ngx_http_next_body_filter;
由于是头插法,这样就解释了,越早插入链表的过滤模块,就会越晚执行。
<二> 开发一个简单的过滤模块
要开发一个简单的过滤模块,它的功能是对Content-Type为text/plain的响应添加一个前缀,类似于开发一个HTTP模块,它应该遵循如下步骤:
1.确定源代码文件名称,源代码所在目录创建config脚本文件,config文件的编写方式跟HTTP模块开发基本一致,不同的是需要将HTTP_MODULES改成HTTP_FILTER_MODULES.
2.定义过滤模块。实例化ngx_module_t类型模块结构,因为HTTP过滤模块也是HTTP模块,所以其中的type成员也是NGX_HTTP_MODULE.
3.处理感兴趣的配置项,通过设置ngx_module_t中的ngx_command_t数组来处理感兴趣的配置项。
4.实现初始化方法。初始化方法就是把本模块中处理HTTP头部的ngx_http_output_header_filter_pt方法和处理HTTP包体的ngx_http_output_body_filter_pt方法插入到过滤模块链表的首部。
5.实现4.中提到两个处理头部和包体的方法。
接下来按照上述步骤依次来实现:
2.1 确定源代码文件目录,编写config文件
config文件如下:
ngx_addon_name=ngx_http_myfilter_module
HTTP_FILTER_MODULES="$HTTP_FILTER_MODULES ngx_http_myfilter_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_myfilter_module.c"
2.2 定义过滤模块,实例化ngx_module_t
/*定义过滤模块,ngx_module_t结构体实例化*/
ngx_module_t ngx_http_myfilter_module = {
NGX_MODULE_V1, /*Macro*/
&ngx_http_myfilter_module_ctx, /*module context*/
ngx_http_myfilter_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 /*Macro*/
};
2.3 处理感兴趣的配置项
/*处理感兴趣的配置项*/
static ngx_command_t ngx_http_myfilter_commands[] = {
{
ngx_string("add_prefix"), //配置项名称
NGX_HTTP_MAIN_CONF| NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG, //配置项只能携带一个参数并且是on或者off
ngx_conf_set_flag_slot, //使用nginx自带方法,参数on/off
NGX_HTTP_LOC_CONF_OFFSET, //使用create_loc_conf方法产生的结构体来存储
//解析出来的配置项参数
offsetof(ngx_http_myfilter_conf_t, enable),//on/off
NULL
},
ngx_null_command
};
其中定义结构体ngx_http_myfilter_conf_t来保存配置项参数
typedef struct {
ngx_flag_t enable;
} ngx_http_myfilter_conf_t;
2.4 实现初始化方法
头插入法将本过滤模块插入到单链表的首部:
/*初始化方法*/
static ngx_int_t ngx_http_myfilter_init(ngx_conf_t *cf) {
//插入到头部处理方法链表的首部
ngx_http_next_header_filter=ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_myfilter_header_filter;
//插入到包体处理方法链表的首部
ngx_http_next_body_filter=ngx_http_top_body_filter;
ngx_http_top_body_filter=ngx_http_myfilter_body_filter;
}
2.5 实现头部和包体过滤方法
2.5.1 函数声明
/*头部处理方法*/
static ngx_int_t ngx_http_myfilter_header_filter(ngx_http_request_t *r);
/*包体处理方法*/
static ngx_int_t ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
2.5.2 函数实现
(1) 头处理方法:最终处理效果头部信息Content-Length的值加上prefix的长度。
/*头部处理方法*/
static ngx_int_t ngx_http_myfilter_header_filter(ngx_http_request_t *r) {
ngx_http_myfilter_ctx_t *ctx;
ngx_http_myfilter_conf_t *conf;
//如果不是返回成功,这时是不需要理会是否加前缀的,直接交由下一个过滤模块,处理响应码非200的情形
if (r->headers_out.status != NGX_HTTP_OK) {
return ngx_http_next_header_filter(r);
}
/*获取HTTP上下文*/
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
if (ctx) {
//该请求的上下文已经存在,这说明ngx_http_myfilter_header_filter已经被调用过1次,直接交由下一个过滤模块处理
return ngx_http_next_header_filter(r);
}
//获取存储配置项参数的结构体
conf = ngx_http_get_module_loc_conf(r, ngx_http_myfilter_module);
//如果enable成员为0,也就是配置文件中没有配置add_prefix配置项,或者add_prefix配置下情的参数值是off,这时直接交由下一个过滤模块处理
if(conf->enable == 0) {
return ngx_http_next_header_filter(r);
}
//conf->enable==1
//构造http上下文结构体ngx_http_myfilter_ctx_t
ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_myfilter_ctx_t));
if (NULL == ctx) {
return NGX_ERROR;
}
ctx->add_prefix = 0;
ngx_http_set_ctx(r, ctx, ngx_http_myfilter_module);
//只处理Content-Type是"text/plain"类型的http响应
if (r->headers_out.content_type.len >= sizeof("text/plain") - 1
&& ngx_strncasecmp(r->headers_out.content_type.data, (u_char *) "text/plain", sizeof("text/plain") - 1) == 0) {
ctx->add_prefix = 1;
if (r->headers_out.content_length_n > 0) {
r->headers_out.content_length_n += filter_prefix.len;
}
}
//交由下一个过滤模块继续处理
return ngx_http_next_header_filter(r);
}
(2) 响应包体处理方法:最终处理效果在包体前面添加前缀。
/*包体处理方法*/
static ngx_int_t ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in) {
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
//如果获取不到上下文,或者上下文结构体中的add_prefix为0或者2时,都不会添加前缀,这时直接交给下一个http过滤模块处理
if (ctx == NULL || ctx->add_prefix != 1) {
return ngx_http_next_body_filter(r, in);
}
//将add_prefix设置为2,这样即使ngx_http_myfilter_body_filter再次回调时,也不会重复添加前缀
ctx->add_prefix = 2;
//从请求的内存池中分配内存,用于存储字符串前缀
ngx_buf_t *b = ngx_create_temp_buf(r->pool, filter_prefix.len);
//将ngx_buf_t 中的指针正确地指向filter_prefix字符串
b->start = b->pos = filter_prefix.data;
b->last = b->pos + filter_prefix.len;
//从请求的内存池中生成Ngx_chain_t链表,将刚分配的ngx_buf_t设置到其buf成员中,并将它添加到原先待发送的http包体前面
ngx_chain_t *cl = ngx_alloc_chain_link(r->pool);
/*note: in表示原来待发送的包体*/
cl->buf = b;
cl->next = in;
//调用下一个模块的htpp包体处理方法,注意这时传入的是新生成的cl链表
return ngx_http_next_body_filter(r, cl);
}
(三) 完整代码与测试
至此,已经完成大部分的工作,我们还需要为这个过滤模块编写模块上下文,编写创建和合并配置项参数结构体的函数等。
3.1 完整代码
/*ngx_http_myfilter_module.c*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
/*用static修饰只在本文件生效,因此允许所有的过滤模块都有自己的这两个指针*/
static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
/*初始化方法,将过滤模块插入到链表头部*/
static ngx_int_t
ngx_http_myfilter_init(ngx_conf_t *cf);
/*头部处理方法*/
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r);
/*包体处理方法*/
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
typedef struct
{
ngx_flag_t enable;
}ngx_http_myfilter_conf_t;
/*请求上下文*/
typedef struct
{
ngx_int_t add_prefix;
}ngx_http_myfilter_ctx_t;
/*在包体中添加的前缀*/
static ngx_str_t filter_prefix=ngx_string("[my filter prefix]");
/*处理感兴趣的配置项*/
static ngx_command_t ngx_http_myfilter_commands[]=
{
{
ngx_string("add_prefix"), //配置项名称
NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG,//配置项只能携带一个参数并且是on或者off
ngx_conf_set_flag_slot,//使用nginx自带方法,参数on/off
NGX_HTTP_LOC_CONF_OFFSET,//使用create_loc_conf方法产生的结构体来存储
//解析出来的配置项参数
offsetof(ngx_http_myfilter_conf_t, enable),//on/off
NULL
},
ngx_null_command //
};
static void* ngx_http_myfilter_create_conf(ngx_conf_t *cf);
static char*
ngx_http_myfilter_merge_conf(ngx_conf_t *cf,void*parent,void*child);
/*模块上下文*/
static ngx_http_module_t ngx_http_myfilter_module_ctx=
{
NULL, /* preconfiguration方法 */
ngx_http_myfilter_init, /* postconfiguration方法 */
NULL, /*create_main_conf 方法 */
NULL, /* init_main_conf方法 */
NULL, /* create_srv_conf方法 */
NULL, /* merge_srv_conf方法 */
ngx_http_myfilter_create_conf, /* create_loc_conf方法 */
ngx_http_myfilter_merge_conf /*merge_loc_conf方法*/
};
/*定义过滤模块,ngx_module_t结构体实例化*/
ngx_module_t ngx_http_myfilter_module =
{
NGX_MODULE_V1, /*Macro*/
&ngx_http_myfilter_module_ctx, /*module context*/
ngx_http_myfilter_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 /*Macro*/
};
static void* ngx_http_myfilter_create_conf(ngx_conf_t *cf)
{
ngx_http_myfilter_conf_t *mycf;
//创建存储配置项的结构体
mycf = (ngx_http_myfilter_conf_t *)ngx_pcalloc(cf->pool, sizeof(ngx_http_myfilter_conf_t));
if (mycf == NULL)
{
return NULL;
}
//ngx_flat_t类型的变量,如果使用预设函数ngx_conf_set_flag_slot
//解析配置项参数,必须初始化为NGX_CONF_UNSET
mycf->enable = NGX_CONF_UNSET;
return mycf;
}
static char*
ngx_http_myfilter_merge_conf(ngx_conf_t *cf,void*parent,void*child)
{
ngx_http_myfilter_conf_t *prev = (ngx_http_myfilter_conf_t *)parent;
ngx_http_myfilter_conf_t *conf = (ngx_http_myfilter_conf_t *)child;
//合并ngx_flat_t类型的配置项enable
ngx_conf_merge_value(conf->enable, prev->enable, 0);
return NGX_CONF_OK;
}
/*初始化方法*/
static ngx_int_t
ngx_http_myfilter_init(ngx_conf_t*cf)
{
//插入到头部处理方法链表的首部
ngx_http_next_header_filter=ngx_http_top_header_filter;
ngx_http_top_header_filter=ngx_http_myfilter_header_filter;
//插入到包体处理方法链表的首部
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_myfilter_body_filter;
return NGX_OK;
}
/*头部处理方法*/
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r)
{
ngx_http_myfilter_ctx_t *ctx;
ngx_http_myfilter_conf_t *conf;
//如果不是返回成功,这时是不需要理会是否加前缀的,
//直接交由下一个过滤模块
//处理响应码非200的情形
if (r->headers_out.status != NGX_HTTP_OK)
{
return ngx_http_next_header_filter(r);
}
/*获取http上下文*/
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
if(ctx)
{
//该请求的上下文已经存在,这说明
// ngx_http_myfilter_header_filter已经被调用过1次,
//直接交由下一个过滤模块处理
return ngx_http_next_header_filter(r);
}
//获取存储配置项参数的结构体
conf = ngx_http_get_module_loc_conf(r, ngx_http_myfilter_module);
//如果enable成员为0,也就是配置文件中没有配置add_prefix配置项,
//或者add_prefix配置项的参数值是off,这时直接交由下一个过滤模块处理
if (conf->enable == 0)
{
return ngx_http_next_header_filter(r);
}
//conf->enable==1
//构造http上下文结构体ngx_http_myfilter_ctx_t
ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_myfilter_ctx_t));
if(NULL==ctx)
{
return NGX_ERROR;
}
ctx->add_prefix=0;
ngx_http_set_ctx(r,ctx,ngx_http_myfilter_module);
//只处理Content-Type是"text/plain"类型的http响应
if (r->headers_out.content_type.len >= sizeof("text/plain") - 1
&& ngx_strncasecmp(r->headers_out.content_type.data, (u_char *) "text/plain", sizeof("text/plain") - 1) == 0)
{
ctx->add_prefix=1;
if(r->headers_out.content_length_n > 0)
{
r->headers_out.content_length_n+=filter_prefix.len;
}
}
//交由下一个过滤模块继续处理
return ngx_http_next_header_filter(r);
}
/*包体处理方法*/
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
//如果获取不到上下文,或者上下文结构体中的add_prefix为0或者2时,
//都不会添加前缀,这时直接交给下一个http过滤模块处理
if (ctx == NULL || ctx->add_prefix != 1)
{
return ngx_http_next_body_filter(r, in);
}
//将add_prefix设置为2,这样即使ngx_http_myfilter_body_filter
//再次回调时,也不会重复添加前缀
ctx->add_prefix = 2;
//从请求的内存池中分配内存,用于存储字符串前缀
ngx_buf_t* b = ngx_create_temp_buf(r->pool, filter_prefix.len);
//将ngx_buf_t中的指针正确地指向filter_prefix字符串
b->start = b->pos = filter_prefix.data;
b->last = b->pos + filter_prefix.len;
//从请求的内存池中生成ngx_chain_t链表,将刚分配的ngx_buf_t设置到
//其buf成员中,并将它添加到原先待发送的http包体前面
ngx_chain_t *cl = ngx_alloc_chain_link(r->pool);
/*note: in表示原来待发送的包体*/
cl->buf = b;
cl->next = in;
//调用下一个模块的http包体处理方法,注意这时传入的是新生成的cl链表
return ngx_http_next_body_filter(r, cl);
}
3.2 测试
我们的过滤模块只对Content-Type为text/plain的响应有效,查看Nginx的默认配置中的mime.types文件,发现
types{
#...
text/plain txt;
#...
}
也即当请求资源位txt时才会调用我们过滤模块,如果想要强制将响应的Content-Type设置为text/plain呢?
只需要修改nginx.conf文件如下即可:
#user root;
worker_processes 1;
error_log logs/error.log debug;
events {
worker_connections 1024;
}
http {
# 注释掉http块下的配置
# include mime.types;
# default_type application/octet-stream;
keepalive_timeout 65;
server {
listen 1024;
location / {
#在location块下将默认类型设置为text/plain
default_type text/plain;
root html;
add_prefix on;
index index.htm index.html;
}
}
}
将自定义的顾虑模块编译进Nginx:
./configure --add-module=/home/zhangxiao/nginx/nginx-1.0.15/src/myHttpFilterModule/
make;sudo make install
重启nginx,用curl工具进行测试:
curl -v localhost:1024
可以看到返回的包体添加了前缀:
参考链接: