最近在学习nginx,所以参考别人的代码开发一个过滤器模块,这篇博客主要是记载自己对代码的理解。以此也进行学习nginx的模块化开发。
一 开发目的
我们想实现一个模块,在拿到服务端返回的网页时,我们在其中加上自己想要添加的部分。
介绍一下过滤器模块,过滤器模块的流程处理的就是服务器发回客户端过程中这两条绿色的线。
二 实现效果
在nginx默认返回的页面中,我们在返回的响应中加上自己名字缩写,功能很简单,但是以往是在服务器端做,这次我们在nginx层自定义一个模块去做这个工作。
这样无论后端服务器是什么,我们只要是经过nginx代理都能够达到一样的效果。
三 开发步骤
我将ngnix模块化开发定义为下面几个步骤
- 定义模块
- 定义ctx上下文
- 定义命令组
- 实现上下文中相关回调
- 实现自己的功能
3.1 定义模块
这里我们定义一个模块结构体,给模块取一个名字
//定义模块名 这里的模块名要和config里的一致 切记
ngx_module_t nginx_filter_module_my={
NGX_MODULE_V1,
&ngx_http_mxl_filter_module_ctx,
ngx_http_mxl_filter_module_cmd,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
然后我们看看结构体里的参数配置,因为返回的是ngx_module_t,这是nginx定义的一个模块结构体,我在这里贴出注释,我们重点关注,ctx上下文,cmd命令数组。
struct ngx_module_s {
ngx_uint_t ctx_index; //上下文索引
ngx_uint_t index; //模块的索引,用于标识模块自身。
char *name; //模块的名称
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t version; //模块的版本号。
const char *signature;//模块的签名,用于标识模块的唯一性。
void *ctx; //模块的上下文信息,通常是一个指向特定模块上下文结构的指针。
ngx_command_t *commands;//指向该模块命令数组的指针,这些命令定义了配置文件中的指令。
ngx_uint_t type; //模块的类型(核心模块、事件模块、HTTP 模块等)。
ngx_int_t (*init_master)(ngx_log_t *log); //指向初始化 master 进程的函数指针。
ngx_int_t (*init_module)(ngx_cycle_t *cycle); //指向初始化模块的函数指针。
ngx_int_t (*init_process)(ngx_cycle_t *cycle); //指向初始化工作进程的函数指针。
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);//指向初始化线程的函数指针(目前在 NGINX 中未使用)。
void (*exit_thread)(ngx_cycle_t *cycle); //指向退出线程的函数指针(目前在 NGINX 中未使用)。
void (*exit_process)(ngx_cycle_t *cycle); //指向退出工作进程的函数指针。
void (*exit_master)(ngx_cycle_t *cycle); //指向退出 master 进程的函数指针。
uintptr_t spare_hook0;
uintptr_t spare_hook1;
uintptr_t spare_hook2;
uintptr_t spare_hook3;
uintptr_t spare_hook4;
uintptr_t spare_hook5;
uintptr_t spare_hook6;
uintptr_t spare_hook7;
};
我么传入的参数NGX_MODULE_V1就是nginx帮我们定义好的一部分默认参数,包括版本,签名等信息,这里都用默认值。
#define NGX_MODULE_V1 \
NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX, \
NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE
NGX_HTTP_MODULE表示我们的模块是一个http模块
于此同时nginx还有一些其他的模块,有兴趣的读者可以自行了解。
这里列出core模块的几个模块
Core 模块: | |
---|---|
Main 模块 core | 负责整体的配置和初始化。 |
Events 模块 events | 负责处理连接、事件驱动等。 |
HTTP 模块 | 负责处理 HTTP 请求和响应。 |
Mail 模块 | 用于处理邮件代理服务器的相关功能。 |
那么接下来我们定义ctx上下文和cmd命令
3.2 定义ctx上下文
static ngx_http_module_t ngx_http_mxl_filter_module_ctx={
NULL,
ngx_http_mxl_filter_init,
NULL,
NULL,
NULL,
NULL,
ngx_http_mxl_filter_create_loc_conf,
ngx_http_mxl_filter_merge_loc_conf,
};
这里我传入了三个参数,我们先看一下结构体原型
typedef struct {
ngx_int_t (*preconfiguration)(ngx_conf_t *cf); //在解析配置文件前调用的回调函数。
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);//在解析配置文件后调用的回调函数。当 HTTP 模块被初始化时调用
void *(*create_main_conf)(ngx_conf_t *cf);//创建主级别配置结构体的回调函数。返回指向新配置结构体的指针。
char *(*init_main_conf)(ngx_conf_t *cf, void *conf);//初始化主级别配置结构体的回调函数
void *(*create_srv_conf)(ngx_conf_t *cf);//创建服务级别配置结构体的回调函数。
char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);//合并服务级别配置结构体的回调函数
void *(*create_loc_conf)(ngx_conf_t *cf);//创建位置级别配置结构体的回调函数
char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); //合并位置级别配置结构体的回调函数
} ngx_http_module_t;
可以看到ctx结构体中都是各种回调。我们拿一个nginx配置文件来举例
worker_processes 4;
events {
worker_connections 1024;
}
http {
server {
listen 8888;
}
location /{
}
}
主级别(main level)
- 这个级别包括整个配置文件的最外层,比如 worker_processes 和 http 块之外的配置。
- 主要包括:worker_processes,error_log,pid,user 等全局配置指令。
对应的回调函数: - void *(*create_main_conf)(ngx_conf_t *cf);
- 在解析主级别配置块(http 块)前调用,用于创建主级别配置结构体。
- char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
- 在解析完主级别配置块后调用,用于初始化主级别配置结构体。
在配置文件中,worker_processes 4; 这一行就是在主级别配置的。
服务级别(server level)
server 块中的配置属于服务级别
对应的回调函数:
- void *(*create_srv_conf)(ngx_conf_t *cf);
- 在解析 server 块前调用,用于创建服务级别配置结构体。
- char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
- 在解析完 server 块后调用,用于合并服务级别配置结构体。
位置级别(location level)
- location 块中的配置属于位置级别。
对应的回调函数: - void *(*create_loc_conf)(ngx_conf_t *cf);
- 在解析 location 块前调用,用于创建位置级别配置结构体。
- char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
- 在解析完 location 块后调用,用于合并位置级别配置结构体。
回调函数调用顺序
解析配置时调用
- preconfiguration和postconfiguration
解析 http 块时:
- 调用 create_main_conf 创建 HTTP 主级别配置结构体。
- 解析 server 块时,调用 create_srv_conf 创建服务级别配置结构体。
- 解析完 所有server 块后,调用 merge_srv_conf 合并服务级别配置结构体。
- 解析完 http 块后,调用 init_main_conf 初始化 HTTP 主级别配置结构体。
3.3 定义命令组
和前面类似 我们需要定义一个ngx_command_t的结构体数组,每个元素对应一个命令。
typedef struct {
//int
ngx_flag_t enable;
}ngx_http_filter_conf_t;
ngx_command_t ngx_http_mxl_filter_module_cmd[]={
{
ngx_string("prefix"),
//指令 prefix 可以合法地出现在 http、server 和 location 块中。
//指令接受的参数是布尔值(on 或 off)。
NGX_HTTP_MAIN_CONF |NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG,
ngx_conf_set_flag_slot, //nginx提供的解析函数
// 用于指示 prefix 指令的值应存储在 loc_conf 级别的配置结构体中。
NGX_HTTP_LOC_CONF_OFFSET,
//指示指令的值应存储在 ngx_http_filter_conf_t 结构体的 enable 成员中。
offsetof(ngx_http_filter_conf_t, enable),
NULL,
},
ngx_null_command
};
各个参数可以参考注释。
ngx_http_filter_conf_t是我们自定义的结构体用来存解析的结果。里面有一个flag其实是int型。
ngx_conf_set_flag_slot这个解析函数其实是在解析我们的自定义变量prefix 后面的value,可以是on或者off,解析完后将0或者1存入我们指定的ngx_http_filter_conf_t结构体中。
offsetof函数用于获取一个结构体中某个变量的偏移量。
为什么还有一个ngx_null_command,这是nginx用来遍历cmd数组时指定的数组尾部,用来指示遍历完成。
3.4实现上下文中相关回调
还记得在上下文ctx中我们传入了几个函数。
- ngx_http_mxl_filter_init
- ngx_http_mxl_filter_create_loc_conf
- ngx_http_mxl_filter_merge_loc_conf
ngx_http_mxl_filter_init
//这两个变量主要是保存原本的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;
ngx_int_t ngx_http_mxl_filter_init(ngx_conf_t *cf){
ngx_http_next_header_filter = ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_mxl_header_filter;
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_mxl_body_filter;
return NGX_OK;
}
nginx的过滤器链
在Nginx中,HTTP响应头过滤器链由多个过滤器函数组成,每个过滤器函数依次处理HTTP响应头信息。ngx_http_next_header_filter 保存当前过滤器函数链中的下一个过滤器函数的指针。这种设计允许过滤器函数按顺序调用下一个过滤器,以实现逐层处理和修改HTTP响应头。
过滤器链就是一个链表,每个节点都会对响应信息进行处理。而且头部的链和body的链是分开的。
这里需要理解重要的两个指针,一个是top指针,一个是next指针,top指针指向当前要执行的过滤器函数,next指针则指向下一个。相当于top指向了链表头,next指向了top的下一个节点。
那么我们的代码中的ngx_http_mxl_header_filter和 ngx_http_mxl_body_filter都是我们自己实现的过滤函数,分别实现对头部和body的处理。
在init函数中,我们需要将自己实现的函数加入整个nginx的过滤器链条中。
于是我们将next指向当前top,将top指向了我们自定义的过滤函数。
ngx_http_mxl_filter_create_loc_conf 和 ngx_http_mxl_filter_merge_loc_conf
//在解析location时调用 我们需要在这里为location中我们自定义的变量分配空间
void *ngx_http_mxl_filter_create_loc_conf(ngx_conf_t *cf){
//cf中有内存池 从此分配空间
ngx_http_filter_conf_t *conf = ngx_palloc(cf->pool, sizeof(ngx_http_filter_conf_t));
if(conf==NULL) return NULL;
conf->enable=NGX_CONF_UNSET;
return conf;
}
//确保模块配置一致性
char*ngx_http_mxl_filter_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child){
// 将parent和child指针转换为具体的配置结构体类型ngx_http_filter_conf_t。
ngx_http_filter_conf_t *prev = (ngx_http_filter_conf_t*)parent;
ngx_http_filter_conf_t *next = (ngx_http_filter_conf_t*)child;
//这个宏用于合并配置项enable的值
// 如果next->enable(子级配置)的值为默认值(通常是未设置或无效的值),则使用prev->enable(父级配置)的值。
// 如果prev->enable(父级配置)的值也未设置,则使用默认值0。
//printf("ngx_http_zry_filter_merge_loc_conf: %d\n", next->enable);
ngx_conf_merge_value(next->enable, prev->enable, 0);
return NGX_CON
create 函数其实就是为我们的ngx_http_filter_conf_t结构体分配空间,这个结构体前面说过是为了存配置的信息的。所以在开始解析时为它分配空间。
merge函数中是为了模块配置一致性,怎么理解,就是这个配置prefix on 我可以配置在很多地方,可以配置在http或者server里,但是最后我要以哪个为准呢,这里的ngx_conf_merge_value是一个宏定义
这个定义指明如果next->enable(子级配置)的值为默认值(通常是未设置或无效的值),则使用prev->enable(父级配置)的值。
如果prev->enable(父级配置)的值也未设置,则使用默认值0。
3.5 实现功能
这里其实就是实现3.4节提到的头部过滤函数和body过滤函数
//定义需要插入的html
static ngx_str_t prefix=ngx_string("<h2>mxl<h2>");
//实现自己的过滤函数
//中间逻辑自己写 但是结尾记得调用系统的过滤函数 被我们保存在next函数指针中
ngx_int_t ngx_http_mxl_header_filter(ngx_http_request_t *r) {
if (r->headers_out.status != NGX_HTTP_OK) {
return ngx_http_next_header_filter(r);
}
//r->headers_out.content_type.len == sizeof("text/html")
r->headers_out.content_length_n += prefix.len;
return ngx_http_next_header_filter(r);
}
// 自定义的 body 过滤函数。创建一个新的缓冲区,包含 prefix,并将其链入到原有的响应 body 链表中。
// 最后调用原始的 body 过滤函数。
ngx_int_t ngx_http_mxl_body_filter(ngx_http_request_t *r, ngx_chain_t *chain) {
//创建一个buf
ngx_buf_t *b = ngx_create_temp_buf(r->pool, prefix.len);
//设置buf起始位置指针 避免memcpy
b->start = b->pos = prefix.data;
//设置结束位置指针
b->last = b->pos + prefix.len;
//从内存池中分配一个链的节点
ngx_chain_t *c1 = ngx_alloc_chain_link(r->pool);
//头插法
c1->buf = b;
c1->next = chain;
return ngx_http_next_body_filter(r, c1);
}
头部过滤函数其实就是在返回的长度加上对应的长度值。
body过滤函数我们需要将自己的html片段插入报文中。body这里使用了一个链表结构的写入,chain是一个链表结构,到时候进行send的时候就把链表里的内容依次发送。
所以我们先分配一个缓冲区,然后创建一个链表节点将缓冲区的信息写入,将这个链表节点插入到写入链的头部就好了。
四 完整代码及安装模块
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
typedef struct {
//int
ngx_flag_t enable;
}ngx_http_filter_conf_t;
//定义需要插入的html
static ngx_str_t prefix=ngx_string("<h2>mxl<h2>");
//这两个变量主要是保存原本的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;
//实现自己的过滤函数
//中间逻辑自己写 但是结尾记得调用系统的过滤函数 被我们保存在next函数指针中
ngx_int_t ngx_http_mxl_header_filter(ngx_http_request_t *r) {
if (r->headers_out.status != NGX_HTTP_OK) {
return ngx_http_next_header_filter(r);
}
//r->headers_out.content_type.len == sizeof("text/html")
r->headers_out.content_length_n += prefix.len;
return ngx_http_next_header_filter(r);
}
// 自定义的 body 过滤函数。创建一个新的缓冲区,包含 prefix,并将其链入到原有的响应 body 链表中。
// 最后调用原始的 body 过滤函数。
ngx_int_t ngx_http_mxl_body_filter(ngx_http_request_t *r, ngx_chain_t *chain) {
//创建一个buf
ngx_buf_t *b = ngx_create_temp_buf(r->pool, prefix.len);
//设置buf起始位置指针 避免memcpy
b->start = b->pos = prefix.data;
//设置结束位置指针
b->last = b->pos + prefix.len;
//从内存池中分配一个链的节点
ngx_chain_t *c1 = ngx_alloc_chain_link(r->pool);
//头插法
c1->buf = b;
c1->next = chain;
return ngx_http_next_body_filter(r, c1);
}
ngx_int_t ngx_http_mxl_filter_init(ngx_conf_t *cf){
ngx_http_next_header_filter = ngx_http_top_header_filter;
ngx_http_top_header_filter = ngx_http_mxl_header_filter;
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_mxl_body_filter;
return NGX_OK;
}
//在解析location时调用 我们需要在这里为location中我们自定义的变量分配空间
void *ngx_http_mxl_filter_create_loc_conf(ngx_conf_t *cf){
//cf中有内存池 从此分配空间
ngx_http_filter_conf_t *conf = ngx_palloc(cf->pool, sizeof(ngx_http_filter_conf_t));
if(conf==NULL) return NULL;
conf->enable=NGX_CONF_UNSET;
return conf;
}
//确保模块配置一致性
char*ngx_http_mxl_filter_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child){
// 将parent和child指针转换为具体的配置结构体类型ngx_http_filter_conf_t。
ngx_http_filter_conf_t *prev = (ngx_http_filter_conf_t*)parent;
ngx_http_filter_conf_t *next = (ngx_http_filter_conf_t*)child;
//这个宏用于合并配置项enable的值
// 如果next->enable(子级配置)的值为默认值(通常是未设置或无效的值),则使用prev->enable(父级配置)的值。
// 如果prev->enable(父级配置)的值也未设置,则使用默认值0。
//printf("ngx_http_zry_filter_merge_loc_conf: %d\n", next->enable);
ngx_conf_merge_value(next->enable, prev->enable, 0);
return NGX_CONF_OK;
}
//定义命令组
ngx_command_t ngx_http_mxl_filter_module_cmd[]={
{
ngx_string("prefix"),
//指令 prefix 可以合法地出现在 http、server 和 location 块中。
//指令接受的参数是布尔值(on 或 off)。
NGX_HTTP_MAIN_CONF |NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_CONF_FLAG,
ngx_conf_set_flag_slot, //nginx提供的解析函数
// 用于指示 prefix 指令的值应存储在 loc_conf 级别的配置结构体中。
NGX_HTTP_LOC_CONF_OFFSET,
//指示指令的值应存储在 ngx_http_filter_conf_t 结构体的 enable 成员中。
offsetof(ngx_http_filter_conf_t, enable),
NULL,
},
ngx_null_command
};
//定义一个上下文结构体
static ngx_http_module_t ngx_http_mxl_filter_module_ctx={
NULL,
ngx_http_mxl_filter_init,
NULL,
NULL,
NULL,
NULL,
ngx_http_mxl_filter_create_loc_conf,
ngx_http_mxl_filter_merge_loc_conf,
};
//定义模块名 这里的模块名要和config里的一致 切记
ngx_module_t nginx_filter_module_my={
NGX_MODULE_V1,
&ngx_http_mxl_filter_module_ctx,
ngx_http_mxl_filter_module_cmd,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
编译过程
首先写一个config文件在你的模块目录下
ngx_addon_name=nginx_filter_module_my
HTTP_FILTER_MODULES="$HTTP_FILTER_MODULES nginx_filter_module_my"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_filter_module_my.c"
nginx_filter_module_my 是你的模块名字 和下面第二行的要一致
ngx_http_filter_module_my.c是你的源文件名
$ngx_addon_dir 是下一步configure时指定的文件夹路径,就是你的模块文件夹路径
接下来,配置和编译 Nginx 时,你需要指定模块的路径:
./configure --add-module=/path/to/nginx_filter_module_my
make
sudo make install
我的命令是
./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --with-http_v2_module --with-openssl=../openssl-1.1.1s --add-module=/home/mxl/software/nginx-1.22.1/nginx_filter_module_my
确保你的目录结构类似以下内容:
/path/to/nginx_filter_module_my/
├── config
└── ngx_http_filter_module_my.c
然后make && make install
然后启动nginx ,记得清浏览器缓存
此时访问网页,加上了我们想要的html