【Linux网络编程】Nginx -- 模块开发(基本模块解析)
【1】处理流程图示
Nginx 一次常规的请求和响应的处理流程
典型的 HTTP 模块在 Nginx 中调用的简化流程
【2】模块开发示例
【2.1】将模块编译进入 Nginx
将模块源代码文件放到一个目录下,并在该目录中编写一个文件用于告知Nginx编译本模块的方式,该文件名必须为config;此时只要在configure脚本执行时加入参数--add-module=PATH(PATH为给定的源代码、config文件的保存目录),便可以在执行正常编译安装流程时完成Nginx编译工作;
【2.1.1】Config 文件解析
#仅在configure执行时使用,一般设置为模块名称
ngx_addon_name=模块完整名称
#保存所有的HTTP模块名称,每个HTTP模块间由空格符相连
HTTP_MODULES="$HTTP_MODULES 模块完整名称"
#用于指定新增模块的源代码,多个待编译的源代码间以空格符相连
#在设置NGX_ADDON_SRCS时使用的参数$ngx_addon_dir的值为--add-module=PATH的PATH参数
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/源代码文件名"
【2.1.2】Nginx 模块编译安装
./configure --prefix=安装目录 --add-module=模块源代码文件目录
make
make install
【2.2】Nginx 模块示例代码解析
【2.2.1】发送内存数据
// 该 HTTP 模块接入 Nginx 的方式
// 1. 不希望该模块对整个 HTTP 请求有效
// 2. 在 nginx.conf 文件中的 http{}、server{}、location{} 块内定义 mytest 配置项
//
// 在 HTTP 框架定义的 NGX_HTTP_CONTENT_PHASE 阶段开始处理请求
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r);
// commands数组用于定义模块的配置文件参数,每一个数组元素都是ngx_command_t类型
// 数组以ngx_null_command结尾
// #define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL }
// 空指令,用于在指令数组的最后当做哨兵,结束数组,避免指定长度,类似NULL的作用
static ngx_command_t ngx_http_mytest_commands[] =
{
// 定义mytest配置项的处理,此处指定mytest配置项由ngx_http_mytest函数处理
{
ngx_string("mytest"),
NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS,
// ngx_http_mytest 是 ngx_command_t 结构体中的 set 成员
// 当在某个配置块中出现 mytest 配置项时,nginx 将会调用 ngx_http_mytest 方法
ngx_http_mytest,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
// 定义 HTTP 框架各阶段的回调方法
// 若HTTP框架初始化时无需完成特定的工作则可将回调置为NULL
static ngx_http_module_t ngx_http_mytest_module_ctx =
{
NULL, /* preconfiguration */
NULL, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
NULL, /* create location configuration */
NULL /* merge location configuration */
};
// 定义 mytest 模块
// 回调方法,init_module、init_process、exit_process、exit_master 由 Nginx 框架代码调用,与 HTTP 模块无关
ngx_module_t ngx_http_mytest_module =
{
NGX_MODULE_V1,
&ngx_http_mytest_module_ctx, /* module context */
ngx_http_mytest_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
};
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
// 首先找到 mytest 配置项所属的配置块,clcf 可以是 main、srv、loc 级别配置项
// 在每一个 http{}、server{} 内部都有一个 ngx_http_core_loc_conf_t 结构体
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
// http 框架在处理用户请求进行到 NGX_HTTP_CONTENT_PHASE 阶段时,若请求的主机域名、URI 与 mytest 配置项所在配置块相匹配
// 将调用 ngx_http_mytest_handler 方法处理该请求
//
// 请求处理函数的原型,见 src/http/ngx_http_request.h
// typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
clcf->handler = ngx_http_mytest_handler;
return NGX_CONF_OK;
}
// ngx_http_request_t中保存了请求的信息
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
// 判断 HTTP 头信息中的方法是否支持
if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
{
return NGX_HTTP_NOT_ALLOWED;
}
// 丢弃 HTTP 请求包体
ngx_int_t rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK)
{
return rc;
}
// 构造并发送响应
// 设置返回的 Content-Type
ngx_str_t type = ngx_string("text/plain");
// 返回包体的内容
ngx_str_t response = ngx_string("Hello World!");
// 设置返回的状态码
r->headers_out.status = NGX_HTTP_OK;
// 响应包具有包体内容,需要设置 Content-Length 长度
r->headers_out.content_length_n = response.len;
// 设置 Content-Type
r->headers_out.content_type = type;
// 发送 HTTP 头部
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK || r->header_only)
{
return rc;
}
// 从 Nginx 的内存池中分配一块内存
// 构造 ngx_buf_t 结构体准备发送包体
ngx_buf_t *b;
b = ngx_create_temp_buf(r->pool, response.len);
if (b == NULL)
{
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
// 将 Hellow World 复制到 ngx_buf_t 指向的内存中
ngx_memcpy(b->pos, response.data, response.len);
// 设置 b->last 指针
b->last = b->pos + response.len;
// 声明这是最后一块缓冲区
b->last_buf = 1;
// 构造发送时的 ngx_chain_t 结构体
ngx_chain_t out;
// 赋值 ngx_buf_t
out.buf = b;
// 设置 next 为 NULL
out.next = NULL;
// ngx_http_output_filter 方法向客户端发送 HTTP 响应包体
return ngx_http_output_filter(r, &out);
}
【2.2.2】发送文件
// 该 HTTP 模块接入 Nginx 的方式
// 1. 不希望该模块对整个 HTTP 请求有效
// 2. 在 nginx.conf 文件中的 http{}、server{}、location{} 块内定义 mytest 配置项
//
// 在 HTTP 框架定义的 NGX_HTTP_CONTENT_PHASE 阶段开始处理请求
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r);
// commands数组用于定义模块的配置文件参数,每一个数组元素都是ngx_command_t类型
// 数组以ngx_null_command结尾
// #define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL }
// 空指令,用于在指令数组的最后当做哨兵,结束数组,避免指定长度,类似NULL的作用
static ngx_command_t ngx_http_mytest_commands[] =
{
// 定义mytest配置项的处理,此处指定mytest配置项由ngx_http_mytest函数处理
{
ngx_string("mytest"),
NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_NOARGS,
// ngx_http_mytest 是 ngx_command_t 结构体中的 set 成员
// 当在某个配置块中出现 mytest 配置项时,nginx 将会调用 ngx_http_mytest 方法
ngx_http_mytest,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
// 定义 HTTP 框架各阶段的回调方法
// 若HTTP框架初始化时无需完成特定的工作则可将回调置为NULL
static ngx_http_module_t ngx_http_mytest_module_ctx =
{
NULL, /* preconfiguration */
NULL, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
NULL, /* create location configuration */
NULL /* merge location configuration */
};
// 定义 mytest 模块
// 回调方法,init_module、init_process、exit_process、exit_master 由 Nginx 框架代码调用,与 HTTP 模块无关
ngx_module_t ngx_http_mytest_module =
{
NGX_MODULE_V1,
&ngx_http_mytest_module_ctx, /* module context */
ngx_http_mytest_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
};
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
// 首先找到 mytest 配置项所属的配置块,clcf 可以是 main、srv、loc 级别配置项
// 在每一个 http{}、server{} 内部都有一个 ngx_http_core_loc_conf_t 结构体
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
// http 框架在处理用户请求进行到 NGX_HTTP_CONTENT_PHASE 阶段时,若请求的主机域名、URI 与 mytest 配置项所在配置块相匹配
// 将调用 ngx_http_mytest_handler 方法处理该请求
//
// 请求处理函数的原型,见 src/http/ngx_http_request.h
// typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
clcf->handler = ngx_http_mytest_handler;
return NGX_CONF_OK;
}
// ngx_http_request_t中保存了请求的信息
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
// 判断 HTTP 头信息中的方法是否支持
if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
{
return NGX_HTTP_NOT_ALLOWED;
}
// 丢弃 HTTP 请求体
ngx_int_t rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK)
{
return rc;
}
// 内存池中分配ngx_buf_t数据体
ngx_buf_t *b;
b = ngx_palloc(r->pool, sizeof(ngx_buf_t));
// 设置文件名
u_char* filename = (u_char*)"/tmp/intro.html";
// 将in_file标志位置1,表示ngx_buf_t发送的是文件而不是内存缓冲数据
// ngx_http_output_filter调用后,若检测到in_file标志为1,则从ngx_buf_t缓冲区中的file成员处获取实际的文件
b->in_file = 1;
// 分配内存
b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
// 打开文件
b->file->fd = ngx_open_file(filename, NGX_FILE_RDONLY | NGX_FILE_NONBLOCK, NGX_FILE_OPEN, 0);
// 配置日志指针
b->file->log = r->connection->log;
// 记录文件名称
b->file->name.data = filename;
// 文件名称长度
b->file->name.len = sizeof(filename) - 1;
if (b->file->fd <= 0)
{
return NGX_HTTP_NOT_FOUND;
}
// 允许 range 协议
r->allow_ranges = 1;
// 获取文件信息
if (ngx_file_info(filename, &b->file->info) == NGX_FILE_ERROR)
{
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
// 设置文件数据起始偏移量
b->file_pos = 0;
// 设置文件数据结束偏移量
b->file_last = b->file->info.st_size;
// 内存池清理添加
ngx_pool_cleanup_t* cln = ngx_pool_cleanup_add(r->pool, sizeof(ngx_pool_cleanup_file_t));
if (cln == NULL)
{
return NGX_ERROR;
}
// 配置内存池清理信息
cln->handler = ngx_pool_cleanup_file;
ngx_pool_cleanup_file_t *clnf = cln->data;
clnf->fd = b->file->fd;
clnf->name = b->file->name.data;
clnf->log = r->pool->log;
// 构造响应
// 设置返回的 Content-Type
ngx_str_t type = ngx_string("text/plain");
// 设置返回的状态码
r->headers_out.status = NGX_HTTP_OK;
// 响应包具有包体内容,需要设置 Content-Length 长度
r->headers_out.content_length_n = b->file->info.st_size;
// 设置 Content-Type
r->headers_out.content_type = type;
// 发送 HTTP 头部
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK || r->header_only)
{
return rc;
}
// 构造发送时的 ngx_chain_t 结构体
ngx_chain_t out;
// 赋值 ngx_buf_t
out.buf = b;
// 设置 next 为 NULL
out.next = NULL;
// ngx_http_output_filter 方法向客户端发送 HTTP 响应包体
return ngx_http_output_filter(r, &out);
}
【2.3】Nginx 模块代码配置与调式
【2.3.1】添加模块相关配置信息
...
http {
...
# 模块开发对应的配置
location /test {
mytest;
}
...
}
...
【2.3.2】运行 Nginx 并附着进程调式
启动 Nginx 并查看 Worker 进程 ID
VSCode 配置
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "nginx_worker_debug",
"type": "cppdbg",
"request": "attach",
"program": "/home/shallysun/code_dev/SourceCode/nginx_1_19/nginx/bin/sbin/nginx",
"processId": "45472",
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
调试效果
【3】Nginx 模块开发典型数据结构解析
【3.1】ngx_command_s / ngx_command_t
// Nginx 在解析配置文件中的一个配置项时,遍历所有模块,对于每一个模块遍历 commands 数组,
// 在数组中检查到 ngx_null_command 时会停止使用当前模块解析该配置项;
//
// 指令结构体,用于定义nginx指令
// ngx_command_t (ngx_core.h)
struct ngx_command_s {
// 指令的名字
ngx_str_t name;
// 指令的类型,是NGX_CONF_XXX的组合,决定指令出现的位置、参数数量、类型等
// NGX_HTTP_MAIN_CONF/NGX_HTTP_SRV_CONF/NGX_HTTP_LOC_CONF
ngx_uint_t type;
// 出现了 name 中指定的配置项后,会调用 set 方法处理配置项参数
// 指令解析函数,是函数指针
// 预设有ngx_conf_set_flag_slot等,见本文件
// cf:解析的环境结构体,重要的是cf->args,是指令字符串数组
// cmd:该指令的结构体
// conf当前的配置结构体,需转型后才能使用
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
// 在配置文件中的偏移量
// 专门给http/stream模块使用,决定存储在main/srv/loc的哪个层次
// NGX_HTTP_MAIN_CONF_OFFSET/NGX_HTTP_SRV_CONF_OFFSET/NGX_HTTP_LOC_CONF_OFFSET
// NGX_STREAM_MAIN_CONF_OFFSET
// 其他类型的模块不使用,直接为0
ngx_uint_t conf;
// 变量在conf结构体里的偏移量,可用offsetof得到
// 主要用于nginx内置的命令解析函数,自己写命令解析函数可以置为0
//
// 当前配置项在整个存储配置项的结构体中的偏移位置
ngx_uint_t offset;
// 解析后处理的数据
// 配置项读取后的处理方法,必须是 ngx_conf_post_t 结构的指针
void *post;
};
【3.2】ngx_http_module_t
// http 框架在读取、重载配置文件时定义了由 ngx_http_module_t 接口描述的 8 个阶段
// http 框架在启动过程中会在每个阶段调用 ngx_http_module_t 中的相应方法
// http模块的函数表,在配置解析阶段被框架调用
typedef struct {
// 解析配置文件之前调用
// ngx_http_block里,创建配置结构体后,开始解析之前调用
// 常用于添加变量定义
ngx_int_t (*preconfiguration)(ngx_conf_t *cf);
// 完成配置文件解析后调用
// ngx_http_block里,解析、合并完配置后调用
// 常用于初始化模块的phases handler
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);
// 当需要创建数据结构用于存储 main 级别的全局配置项时,可以通过 create_main_conf 回调创建存储全局配置项的结构体
// 创建模块的main配置,只有一个,在http main域
void *(*create_main_conf)(ngx_conf_t *cf);
// 初始化模块的main配置,只有一个,在http main域
char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
// 当需要创建数据结构用于存储 srv 级别的配置项时,可以通过 create_srv_conf 回调创建存储 srv 级别配置项的结构体
// 创建、合并模块的srv配置
void *(*create_srv_conf)(ngx_conf_t *cf);
// 用于合并 main 级别和 srv 级别下的同名配置项
char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
// 当需要创建数据结构用于存储 loc 级别的配置项时,可以通过 create_loc_conf 回调创建存储 loc 级别配置项的结构体
// 创建、合并模块的location配置
void *(*create_loc_conf)(ngx_conf_t *cf);
// 用于合并 srv 级别和 loc 级别下的同名配置项
char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;
【3.3】ngx_module_s / ngx_module_t
// 重新定义了填充宏,加入了签名字符串
// 早期(1.9.11之前)的定义是
// #define NGX_MODULE_V1 0, 0, 0, 0, 0, 0, 1
// 注意前两个字段改成了unset(-1)而不是0,表示序号未初始化
#define NGX_MODULE_V1 \
NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX, \
NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE
// 填充宏,填充ngx_module_t的最后8个字段,设置为空指针
// 1.10没有变化
#define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0
// 重要的数据结构,定义nginx模块
// 重要的模块ngx_core_module/ngx_event_module/ngx_http_module
// 1.9.11后有变化,改到了ngx_module.h,可以定义动态模块,使用了spare0等字段
struct ngx_module_s {
// 成员变量 ctx_index index spare0 spare1 version 通常使用宏NGX_MODULE_V1填充
// 每类(http/event)模块各自的index,表示当前模块在一类模块中的序号
// 由管理此类模块的 Nginx 核心模块设置
// 可用于表示优先级以及各个模块的位置
// 初始化为-1
ngx_uint_t ctx_index;
// 在ngx_modules数组里的唯一索引,main()里赋值
// 使用计数器变量ngx_max_module设置
// 表示当前模块在ngx_modules数组中的序号,即当前模块在所有模块中的序号
ngx_uint_t index;
// 1.10,模块的名字,标识字符串,默认是空指针
// 由脚本生成ngx_module_names数组,然后在ngx_preinit_modules里填充
// 动态模块在ngx_load_module里设置名字
char *name;
// 两个保留字段,1.9之前有4个
ngx_uint_t spare0;
ngx_uint_t spare1;
// nginx.h:#define nginx_version 1010000
// 模块的版本,默认为1
ngx_uint_t version;
// 模块的二进制兼容性签名,即NGX_MODULE_SIGNATURE
const char *signature;
// 模块不同含义不同,通常是函数指针表,是在配置解析的某个阶段调用的函数
// core模块的ctx
//typedef struct {
// ngx_str_t name;
// void *(*create_conf)(ngx_cycle_t *cycle);
// char *(*init_conf)(ngx_cycle_t *cycle, void *conf);
//} ngx_core_module_t;
//
//用于指向一类模块的上下文结构体,指向特定类型模块的公共接口
void *ctx;
// 模块支持的指令,数组形式,最后用空对象表示结束,处理 nginx.conf 中的配置项
ngx_command_t *commands;
// 模块的类型标识,相当于RTTI,如CORE/HTTP/STRM/MAIL等
// Nginx 官方取值为 NGX_HTTP_MODULE NGX_CORE_MODULE NGX_CONF_MODULE NGX_EVENT_MODULE NGX_MAIL_MODULE
ngx_uint_t type;
// 以下7个函数会在进程(Nginx)的启动或结束阶段被调用
// 若不需要Nginx在某个阶段调用相应的回调函数则置为NULL
// init_master目前nginx不会调用,当 master 进程启动时回调
ngx_int_t (*init_master)(ngx_log_t *log);
// 在ngx_init_cycle里被调用
// 在master进程里,fork出worker子进程之前
// 做一些基本的初始化工作,数据会被子进程复制
//
// 初始化所有模块时回调
ngx_int_t (*init_module)(ngx_cycle_t *cycle);
// 在ngx_single_process_cycle/ngx_worker_process_init里调用
// 在worker进程进入工作循环之前被调用
// 初始化每个子进程自己专用的数据
//
// 在正常服务前被调用
ngx_int_t (*init_process)(ngx_cycle_t *cycle);
// init_thread目前nginx不会调用
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
// exit_thread目前nginx不会调用
void (*exit_thread)(ngx_cycle_t *cycle);
// 在ngx_worker_process_exit调用,在服务停止前调用
void (*exit_process)(ngx_cycle_t *cycle);
// 在ngx_master_process_exit(os/unix/ngx_process_cycle.c)里调用
// 在master进程退出前调用
void (*exit_master)(ngx_cycle_t *cycle);
// 下面8个成员通常用用NGX_MODULE_V1_PADDING填充
// 暂时无任何用处,保留字段
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;
};
参考致谢
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。
【1】深入理解 Nginx 模块开发与架构解析
【2】Nginx模块开发入门