Nginx基础教程(46)Nginx HTTP请求处理之开发filter模块:Nginx过滤模块开发:给响应内容“美颜”的魔法

今天,咱们来聊聊怎么给Nginx装上个“美颜滤镜”,让它能够随心所欲地打扮那些发给客户端的响应内容。

1. 初识Nginx过滤模块:不仅仅是流量指挥

Nginx之所以强大,很大程度上得益于其高度模块化的设计。从结构上分,Nginx分为核心模块、基础模块和第三方模块;而从功能上分,则可分为Handlers(处理器模块)、Filters(过滤器模块)和Proxies(代理类模块)。

简单来说,Handlers负责处理请求并生成响应内容,Filters则对响应内容进行加工处理,而Proxies则负责与后端服务交互,实现代理和负载均衡

这其中,过滤模块就像是Nginx生产线上的质量检测员,对即将出厂的产品(响应内容)进行最后的包装和调整。

1.1 过滤模块的工作原理

过滤模块在Nginx处理流程中的位置非常特殊——它在服务器准备好响应内容之后,在将内容发送给客户端之前工作。想象一下,它就像是快递公司在包裹发出前的最后一道检查工序,确保包裹外观整洁、内容完整。

过滤模块的处理过程分为两个阶段:

HTTP响应头过滤:对响应头部进行处理HTTP响应体过滤:对响应主体内容进行处理

这两个阶段分别由两个不同的函数完成:



ngx_http_top_header_filter(r);      // 头部过滤函数
ngx_http_top_body_filter(r, in);    // 主体过滤函数

1.2 过滤模块的执行顺序

在Nginx中,过滤模块的执行顺序是在编译时就决定的。当你编译完Nginx后,可以在objs目录下看到一个ngx_modules.c的文件,里面有一个ngx_modules数组,所有模块的执行顺序就由此决定。

有趣的是,过滤模块的执行顺序是反向的——最早执行的过滤模块在数组的最后,而最后执行的却在数组的前面。这就像是排队买票,最后来的人反而最先买到票(如果队伍是反向的话)。

以下是Nginx自带的一些过滤模块及其执行顺序(部分):

过滤模块

功能描述

ngx_http_not_modified_filter_module

检查缓存,返回304

ngx_http_range_body_filter_module

处理range请求

ngx_http_copy_filter_module

将文件内容读到内存

ngx_http_headers_filter_module

设置expire和Cache-control头

ngx_http_userid_filter_module

添加统计用户cookie

ngx_http_charset_filter_module

字符集转换

ngx_http_ssi_filter_module

处理SSI请求

ngx_http_gzip_filter_module

压缩内容

2. 如何开发一个简单的过滤模块

理论说了这么多,现在让我们动手开发一个简单的过滤模块,亲身体验一下“美颜师”的工作。

假设我们要开发一个简单的过滤模块,它的功能是在所有文本格式的响应内容前加上一段自定义的前缀文字,比如
"[my filter prefix]"

2.1 编写config文件

首先,我们需要创建一个config文件,告诉Nginx编译系统如何编译我们的模块:



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模块使用的
HTTP_MODULES
变量。

2.2 定义模块结构

接下来,我们定义模块的基本结构:



#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
 
/* 定义配置结构体 */
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_myfilter_commands[] = {
    {
        ngx_string("add_prefix"),
        NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_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
};
 
/* 模块上下文 */
static ngx_http_module_t ngx_http_myfilter_module_ctx = {
    NULL,                          /* preconfiguration */
    ngx_http_myfilter_init,        /* postconfiguration */
    NULL,                          /* create main configuration */
    NULL,                          /* init main configuration */
    NULL,                          /* create server configuration */
    NULL,                          /* merge server configuration */
    ngx_http_myfilter_create_conf, /* create location configuration */
    NULL                           /* merge location configuration */
};
 
/* 模块定义 */
ngx_module_t ngx_http_myfilter_module = {
    NGX_MODULE_V1,
    &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
};

2.3 实现配置相关函数

我们需要实现创建配置和合并配置的函数:



/* 创建location配置 */
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;
    }
    
    mycf->enable = NGX_CONF_UNSET;
    
    return mycf;
}
 
/* 合并location配置 */
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_conf_merge_value(conf->enable, prev->enable, 0);
    
    return NGX_CONF_OK;
}

2.4 初始化过滤模块

这是过滤模块开发中的关键一步——将我们的过滤函数挂载到Nginx的过滤链上:



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模块设计中非常巧妙的一部分。每个过滤模块通过保存下一个过滤模块的函数指针,然后在适当的时候调用它,形成了一个过滤器链

2.5 实现头部过滤函数

头部过滤函数主要用于判断是否需要处理响应内容,并做好相应的准备工作:



// 定义要添加的前缀
static ngx_str_t filter_prefix = ngx_string("[my filter 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);
    }
    
    // 获取上下文
    ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
    if (ctx) {
        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;
    }
    
    ctx->add_prefix = 0;
    ngx_http_set_ctx(r, ctx, ngx_http_myfilter_module);
    
    // 检查Content-Type是否为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;
        
        // 更新Content-Length
        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.6 实现包体过滤函数

包体过滤函数是实际添加前缀的地方:



/* 包体过滤函数 */
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;
    
    // 创建包含前缀的buffer
    ngx_buf_t *b = ngx_create_temp_buf(r->pool, filter_prefix.len);
    if (b == NULL) {
        return NGX_ERROR;
    }
    
    b->start = b->pos = filter_prefix.data;
    b->last = b->pos + filter_prefix.len;
    b->last_buf = 0;  // 不是最后一块buffer
    
    // 创建新的chain节点
    ngx_chain_t *c1 = ngx_alloc_chain_link(r->pool);
    if (c1 == NULL) {
        return NGX_ERROR;
    }
    
    c1->buf = b;
    c1->next = in;
    
    return ngx_http_next_body_filter(r, c1);
}

这个包体过滤函数的作用是在响应内容前添加我们自定义的前缀。它首先检查上下文,判断是否需要添加前缀,然后创建一个包含前缀的新缓冲区,并将其插入到响应chain的前面。

3. 编译与测试

3.1 编译模块

将我们的模块源码编译进Nginx:



./configure --add-module=/path/to/our/filter/module
make
make install

3.2 配置Nginx

在Nginx配置文件中启用我们的过滤模块:



server {
    listen 8080;
    
    location / {
        root /;
        add_prefix on;  # 启用我们的过滤模块
    }
}

3.3 测试效果

现在,我们可以测试我们的过滤模块了。假设在/目录下有一个test.txt文件,内容为”test”,当我们访问
http://localhost:8080/test.txt
时,返回的内容应该是
"[my filter prefix]test"

4. 过滤模块的高级用法

除了上面演示的简单文本处理,Nginx过滤模块还可以实现更多复杂的功能。让我们看看一些实际应用场景。

4.1 响应内容替换

Nginx自带了一个
ngx_http_sub_module
模块,它可以用来替换响应中的字符串。



location / {
    sub_filter '<a href="http://127.0.0.1:8080/'  '<a href="https://$host/';
    sub_filter '<img src="https://127.0.0.1:8080/' '<img src="https://$host/';
    sub_filter_once on;
}

这个配置可以将响应中的特定链接替换为其他链接。

4.2 响应内容切片


ngx_http_slice_module
模块可以将大响应拆分为多个小片段,这对于大文件的缓存和传输非常有用。



location / {
    slice 1m;  # 设置为1MB的片段
    proxy_cache cache;
    proxy_cache_key $uri$is_args$args$slice_range;
    proxy_set_header Range $slice_range;
    proxy_cache_valid 200 206 1h;
    proxy_pass https://127.0.0.1:8000;
}

4.3 安全过滤

过滤模块还可以用于安全检测,比如检测请求中是否包含攻击特征。



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;
}

5. 过滤模块开发中的注意事项

在开发Nginx过滤模块时,有一些重要的事项需要牢记:

5.1 内存管理

Nginx使用自己的内存管理机制,在分配内存时一定要使用Nginx提供的函数(如
ngx_pcalloc
),而不是标准的C库函数。

5.2 缓冲区处理

Nginx使用
ngx_buf_t
结构和
ngx_chain_t
链表来管理响应内容。理解这些结构的工作原理对于开发过滤模块至关重要。


ngx_buf_t
表示一个缓冲区,包含指向数据起始和结束的指针
ngx_chain_t
是一个单向链表,将多个缓冲区连接在一起

5.3 过滤模块的顺序

过滤模块的执行顺序是在编译时决定的,而且顺序是反向的。这在设计过滤模块时要特别注意,确保你的模块在正确的阶段执行。

5.4 性能考虑

过滤模块会对每个经过Nginx的响应进行处理,因此要特别注意性能问题。避免在过滤模块中进行复杂的计算或大量的内存分配。

结语:从“美颜师”到“魔法师”

通过本文的学习,我们不仅了解了Nginx过滤模块的基本概念和工作原理,还亲手开发了一个简单的过滤模块。从这个过程中,我们可以看到Nginx模块化设计的强大之处——它允许我们在Nginx的数据流经路径上插入自定义的处理逻辑,实现对响应内容的灵活控制。

从简单的字符串添加到复杂的内容转换,从安全检测到响应优化,过滤模块为Nginx赋予了无限的可能性。掌握了过滤模块的开发,你就如同从单纯的“交通指挥”升级为了拥有“魔法”的流量巫师,能够随心所欲地操控经过Nginx的数据流。

当然,本文只是抛砖引玉,Nginx过滤模块的世界还有很多值得探索的地方。希望这篇文章能够为你打开一扇窗,让你看到Nginx更多深不可测的潜力。记住,唯一限制你能力的,就是你的想象力!

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
暮初筠溪的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容