Nginx基础教程(33)Nginx HTTP框架综述之过滤引擎:Nginx过滤引擎揭秘:如何像加工香肠一样处理网络响应

一块原始数据“香肠”被扔进Nginx的过滤引擎,经过多道工序的切割、调味、包装,最终变成客户端餐桌上的美味响应。

你有没有想过,当Nginx向客户端返回数据时,这块“数据肉”是如何被切割、调味并装盘的?这就是今天要聊的Nginx过滤引擎——一个专精于响应内容后期加工的神奇流水线

简单来说,过滤引擎就是Nginx响应处理的最后一道关卡,它位于内容处理模块之后,发送响应到客户端之前。无论是修改HTTP头部、压缩内容,还是添加前缀、删除敏感信息,都在这里完成。

1 过滤引擎基础:Nginx的食品加工流水线

在Nginx的架构中,过滤引擎就像一条精心设计的食品加工流水线。每个过滤模块都是流水线上的一个“加工站”,按照严格的顺序对数据进行处理。

这与我们熟知的内容生成模块(如Proxy Pass)有本质区别:内容生成模块负责“生产原材料”,而过滤模块负责“深加工和包装”。

1.1 过滤模块的类型与执行时机

过滤模块分为两大类:头部过滤器和包体过滤器,它们分别在发送HTTP头部和发送内容主体时被调用。

头部过滤器处理HTTP响应头,包体过滤器则处理实际的内容主体。这种分离设计让Nginx可以更高效地处理数据——头部一旦准备好就可以立即发送,而不必等待整个内容处理完成。

1.2 过滤链:精密仪器中的齿轮组

过滤引擎最精妙的设计在于它的链式执行模型。多个过滤模块像齿轮组一样紧密连接,每个模块处理完后,都会将数据传递给下一个模块。

在Nginx内部,有一个ngx_http_top_header_filterngx_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_filterheaders_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过滤模块时,遵循以下最佳实践可以避免常见陷阱:

  1. 始终考虑性能:过滤模块可能被每个请求调用,应避免昂贵的操作
  2. 正确管理内存:使用Nginx内存池,避免内存泄漏
  3. 正确处理缓冲区链:Nginx使用链式缓冲区处理数据,要确保正确连接和传递
  4. 尊重执行顺序:了解你的模块在过滤链中的位置,确保在正确的阶段执行操作
  5. 提供配置灵活性:允许用户通过配置指令控制模块行为

结语:从理解到掌握

Nginx的过滤引擎是一个强大而灵活的系统,它通过模块化的设计允许开发者对响应进行各种处理。无论是通过传统的C模块开发,还是使用现代的njs脚本,都可以充分利用这一架构。

理解过滤引擎的工作原理,不仅能帮助你编写自定义过滤模块,还能让你更好地理解Nginx内部工作机制,更有效地使用和调试Nginx。

正如一位Nginx模块开发者所说:"过滤引擎是Nginx响应处理的最后一道关卡,掌握了它,你就掌握了控制输出内容的终极武器。"

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值