一块原始数据“香肠”被扔进Nginx的过滤引擎,经过多道工序的切割、调味、包装,最终变成客户端餐桌上的美味响应。
你有没有想过,当Nginx向客户端返回数据时,这块“数据肉”是如何被切割、调味并装盘的?这就是今天要聊的Nginx过滤引擎——一个专精于响应内容后期加工的神奇流水线。
简单来说,过滤引擎就是Nginx响应处理的最后一道关卡,它位于内容处理模块之后,发送响应到客户端之前。无论是修改HTTP头部、压缩内容,还是添加前缀、删除敏感信息,都在这里完成。
1 过滤引擎基础:Nginx的食品加工流水线
在Nginx的架构中,过滤引擎就像一条精心设计的食品加工流水线。每个过滤模块都是流水线上的一个“加工站”,按照严格的顺序对数据进行处理。
这与我们熟知的内容生成模块(如Proxy Pass)有本质区别:内容生成模块负责“生产原材料”,而过滤模块负责“深加工和包装”。
1.1 过滤模块的类型与执行时机
过滤模块分为两大类:头部过滤器和包体过滤器,它们分别在发送HTTP头部和发送内容主体时被调用。
头部过滤器处理HTTP响应头,包体过滤器则处理实际的内容主体。这种分离设计让Nginx可以更高效地处理数据——头部一旦准备好就可以立即发送,而不必等待整个内容处理完成。
1.2 过滤链:精密仪器中的齿轮组
过滤引擎最精妙的设计在于它的链式执行模型。多个过滤模块像齿轮组一样紧密连接,每个模块处理完后,都会将数据传递给下一个模块。
在Nginx内部,有一个ngx_http_top_header_filter和ngx_http_top_body_filter的全局变量,它们指向当前过滤链的第一个处理函数。每个过滤模块通过保存并调用“下一个处理函数”的指针,实现链式调用。
这种设计的美感在于它的灵活性和可扩展性——可以轻松添加或移除过滤模块,而不影响整体架构。
2 过滤模块执行顺序:不可违背的链条
在Nginx的世界里,过滤模块的执行顺序不是随意的,而是在编译时就已经确定的。当你查看编译后生成的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
};
有趣的是,过滤模块的实际执行顺序与它们在数组中的顺序相反。也就是说,最早执行的是not_modified_filter,最后执行的才是write_filter。
2.1 内部排序机制
Nginx通过auto/modules脚本控制过滤模块的编译顺序,所有第三方过滤模块只能加入到copy_filter和headers_filter模块之间执行。
这种看似严格的排序实际上有其合理性——它确保了关键操作(如gzip压缩)在合适的阶段执行。例如,gzip压缩应该在所有可能修改内容的其他过滤模块之后执行,但必须在最终写入之前执行。
3 手把手实战:编写一个简单的过滤模块
理论说了这么多,现在让我们动手编写一个实际的过滤模块。假设我们要实现一个功能:在所有的文本格式响应前加上一段自定义前缀字符串。
3.1 模块配置与结构定义
首先,我们需要定义模块的配置结构和命令:
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_command_t ngx_http_mytest_commands[] = {
{
ngx_string("add_prefix"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_myfilter_conf_t,enable),
NULL },
ngx_null_command
};
这个配置结构允许我们在Nginx配置中通过add_prefix on/off指令来控制过滤器的启用和禁用。
3.2 模块初始化与挂载
接下来,我们需要初始化模块并将其挂载到过滤链中:
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)
{
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;
}
这段代码展示了Nginx过滤模块的典型挂载方式:保存当前顶部过滤函数的指针,并将自己的函数设置为新的顶部过滤函数。
3.3 头部过滤函数实现
头部过滤函数负责检查响应类型,并决定是否需要对内容进行处理:
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);
}
conf = ngx_http_get_module_loc_conf(r, ngx_http_myfilter_module);
if(conf->enable == 0)
{
return ngx_http_next_header_filter(r);
}
ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_myfilter_ctx_t));
if(ctx == NULL)
{
return NGX_ERROR;
}
// 只对text/plain类型内容添加前缀
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;
}
}
ngx_http_set_ctx(r, ctx, ngx_http_myfilter_module);
return ngx_http_next_header_filter(r);
}
这个函数完成了几个重要任务:检查状态码、确认模块启用状态、判断内容类型,并设置处理上下文。
3.4 包体过滤函数实现
包体过滤函数是实际添加前缀的地方:
// 定义要添加的前缀
static ngx_str_t filter_prefix = ngx_string("[my filter prefix]");
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);
// 检查是否需要添加前缀
if(ctx == NULL || ctx->add_prefix != 1) {
return ngx_http_next_body_filter(r, in);
}
// 设置标记,避免重复添加
ctx->add_prefix = 2;
// 创建前缀缓冲区
ngx_buf_t *b = ngx_create_temp_buf(r->pool, filter_prefix.len);
b->start = b->pos = filter_prefix.data;
b->last = b->pos + filter_prefix.len;
b->last_buf = 0; // 不是最后一块缓冲区
// 创建新的缓冲区链
ngx_chain_t *cl = ngx_alloc_chain_link(r->pool);
cl->buf = b;
cl->next = in;
return ngx_http_next_body_filter(r, cl);
}
这个函数展示了Nginx过滤模块处理内容的核心技术:创建新的缓冲区,并将其插入到现有缓冲区链的前面。
3.5 编译与配置
要编译这个模块,我们需要创建一个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"
注意,对于过滤模块,我们使用HTTP_FILTER_MODULES变量而不是普通的HTTP_MODULES变量。
在Nginx配置中使用这个模块:
server {
listen 8080;
location / {
root /;
add_prefix on;
}
}
4 Nginx内置过滤模块概览
Nginx自带了一系列强大的过滤模块,它们各司其职,共同构成了响应处理流水线:
- ngx_http_not_modified_filter_module:处理304 Not Modified响应,当内容未改变时避免重复传输
- ngx_http_gzip_filter_module:提供内容压缩功能,显著减小传输数据量
- ngx_http_headers_filter_module:添加自定义HTTP头部,如Cache-Control等
- ngx_http_charset_filter_module:处理字符集转换和设置
- ngx_http_ssi_filter_module:处理服务器端包含(SSI)指令
- ngx_http_sub_filter_module:实现字符串替换功能,可用于修改响应内容
- ngx_http_write_filter_module:最终的写入过滤器,负责将数据发送到客户端
5 现代Nginx脚本扩展:告别C语言的高门槛
如果你觉得C语言模块开发太复杂,别担心!现代Nginx提供了更友好的脚本扩展方式。
5.1 Nginx JavaScript模块
Nginx JavaScript(njs)模块允许你使用JavaScript语言编写Nginx逻辑,无需编译C代码。
一个简单的njs示例:
function hello(r) {
r.return(200, "Hello world!");
}
export default {hello};
在Nginx配置中使用:
load_module modules/ngx_http_js_module.so;
events {}
http {
js_import http.js;
server {
listen 8000;
location / {
js_content http.hello;
}
}
}
njs模块可以用于复杂的访问控制、安全检查、响应头操作和灵活的内容处理。
5.2 Perl模块
对于Perl爱好者,Nginx还提供了ngx_http_perl_module,允许在Perl中实现location和变量处理器。
一个简单的Perl模块示例:
package hello;
use nginx;
sub handler {
my $r = shift;
$r->send_http_header("text/html");
return OK if $r->header_only;
$r->print("hello!\n<br/>");
if (-f $r->filename or -d _) {
$r->print($r->uri, " exists!\n");
}
return OK;
}
1;
__END__
6 实际应用场景:过滤引擎的用武之地
过滤引擎在现实世界中有广泛的应用,以下是几个典型场景:
6.1 安全过滤
像文章开头提到的攻击检测模块,可以在请求处理早期识别并阻断恶意请求。
static ngx_int_t ngx_http_fm_handler(ngx_http_request_t *r) {
if(ngx_search(r->uri.data)) {
ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "attack data: %s", r->uri.data);
return NGX_HTTP_NOT_ALLOWED;
}
return NGX_OK;
}
6.2 内容转换
TOTP(基于时间的一次性密码)认证模块,提供了增强的安全认证机制。
6.3 性能优化
gzip过滤模块通过压缩内容减少传输数据量,显著提升性能。
6.4 A/B测试
通过过滤引擎,可以根据条件动态修改内容,实现A/B测试功能。
7 过滤模块开发最佳实践
在开发Nginx过滤模块时,遵循以下最佳实践可以避免常见陷阱:
- 始终考虑性能:过滤模块可能被每个请求调用,应避免昂贵的操作
- 正确管理内存:使用Nginx内存池,避免内存泄漏
- 正确处理缓冲区链:Nginx使用链式缓冲区处理数据,要确保正确连接和传递
- 尊重执行顺序:了解你的模块在过滤链中的位置,确保在正确的阶段执行操作
- 提供配置灵活性:允许用户通过配置指令控制模块行为
结语:从理解到掌握
Nginx的过滤引擎是一个强大而灵活的系统,它通过模块化的设计允许开发者对响应进行各种处理。无论是通过传统的C模块开发,还是使用现代的njs脚本,都可以充分利用这一架构。
理解过滤引擎的工作原理,不仅能帮助你编写自定义过滤模块,还能让你更好地理解Nginx内部工作机制,更有效地使用和调试Nginx。
正如一位Nginx模块开发者所说:"过滤引擎是Nginx响应处理的最后一道关卡,掌握了它,你就掌握了控制输出内容的终极武器。"
503

被折叠的 条评论
为什么被折叠?



