# 7 自定义Nginx模块
##7.1 ngx_command_t 数组
commands 数组用于定义模块的配置文件参数,每一个数组元素都是`ngx_command_t`类型,数组的结尾是用`ngx_numm_command`表示。Nginx在解析配置文件中的一个配置项时首先会遍历所有的模块,对于每一个模块而言,即通过遍历commands数组进行,另外,在数组中检查到ngx_numm_command时,会停止使用当前模块解析该配置项。每一个ngx_command_t结构体定义个如下配置:
```cpp
typedef struct ngx_command_s ngx_command_t;
struct ngx_command_s {
//配置项名称,如"gzip"
ngx_str_t name;
//配置项类型,type将制定配置项可以出现的位置,
//例如:出现在server{}活location{}中,等等
ngx_uint_t type;
//出现了name中指定的配置项后,将会调用set方法处理配置项的参数
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
//在配置文件中的偏移量
ngx_uint_t conf;
//通常用于使用预设的解析方法解析配置项,这是配置模块的一个优秀的设计,
//需要与conf配合使用
ngx_uint_t offset;
//配置项读取后的处理方法,必须是ngx_conf_post_t结构的指针
void *post;
};
```
ngx_null_command只是一个空的ngx_command_s:
```cpp
#define ngx_null_command {ngx_null_string, 0, NULL, 0, 0, NULL}
```
也就是说,对于我们在nginx.cong 中编写的配置项mytest来说,nginx首先会遍历所有的模块(modules),而对于每个模块,会遍历他所对应的ngx_command_t数组,试图找到关于我们配置项目mytest的解析方式。
```cpp
static ngx_command_t ngx_http_mytest_commands[] =
{
{
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_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
```
##7.2 command中用于处理配置项参数的set方法
```cpp
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
//首先找到mytest配置项所属的配置块,clcf貌似是location块内的数据
//结构,其实不然,它可以是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方法处理这个请求
clcf->handler = ngx_http_mytest_handler;
return NGX_CONF_OK;
}
```
关于ngx_http_conf_get_module_loc_conf 的定义可以参考:
http://lxr.nginx.org/source/src/http/ngx_http_config.h#0065
>本质: 就是设置`ngx_http_mytest_handler`, 匹配项被选中的时候, 应该如何解析。
##7.3 定义ngx_http_module_t接口
这部分的代码, 是用于http框架的, 相当于http框架的回掉函数, 由于这里并不需要框架做任何操作。
```cpp
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 */
};
```
##7.4 定义处理“mytest”command的handler
我们这里需要定义配置项被匹配之后的处理方法.
```cpp
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
//必须是GET或者HEAD方法,否则返回405 Not Allowed
if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
{
return NGX_HTTP_NOT_ALLOWED;
}
//丢弃请求中的包体
ngx_int_t rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK)
{
return rc;
}
//设置返回的Content-Type。注意,ngx_str_t有一个很方便的初始化宏
//ngx_string,它可以把ngx_str_t的data和len成员都设置好
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;
}
//构造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;
}
//将Hello World拷贝到ngx_buf_t指向的内存中
ngx_memcpy(b->pos, response.data, response.len);
//注意,一定要设置好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;
//最后一步发送包体,http框架会调ngx_http_finalize_request方法
//结束请求
return ngx_http_output_filter(r, &out);
}
```
##7.5 定义ngx_module_t中的mytest模块
定义HTTP模块方式很简单,例如:
```cpp
ngx_module_t ngx_http_mytest_module;
```
其中ngx_module_t是一个Nginx模块的数据结构,下面来分析一下Nginx模块中的所有成员:
```cpp
typedef struct ngx_module_s ngx_module_t;
struct ngx_module_s {
/*
下面的ctx_index、index、spare0,spare1,
spare2,spare3,version变量不需要再定义时候赋值,
可以用Nginx准备好的宏NGX_MODULE_V1来定义,
已经定义好了这7个值
#define NGX_MODULE_V1 0,0,0,0,0,0,1
对于一类模块(由下面的type成员决定类别)而言,
ctx_index表示当前模块在这类模块中的序号。
这个成员常常是由管理这类模块的一个Nginx核心模块设置的,
对于所有的HTTP模块而言,ctx_index是由
核心模块ngx_http_module设置的。
ctx_index非常重要,Nginx的模块化设计非常依赖于各个模块的顺序,
他们既用于表达优先级,也用于表明每个模块的位置,
借以帮助Nginx框架快速获得某个模块的数据.
*/
ngx_uint_t ctx_index;
/*
index表示当前模块在ngx_module数组中的序号。
注意,ctx_index表示的是当前模块在一类模块中的序号,
而index表示当前模块在所有模块中的序号,同样很关键.
Nginx启动时会根据ngx_modules数组设置各模块的index值,
例如:
ngx_max_module = 0;
for(i = 0; ngx_module[i]; i++) {
ngx_modules[i]->index = ngx_max_module++;
}
*/
ngx_uint_t index;
//spare系列的保留变量,暂未使用
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t spare2;
ngx_uint_t spare3;
//模块的版本,便于将来的拓展,目前只有一种,默认为1
ngx_uint_t version;
/*
ctx用于指向一类模块的上下文结构体,
为什么需要ctx呢?因为前面说过,
Nginx模块有许多种类,不同类模块之间的功能差别很大。
例如,
事件类型的模块主要处理I/O事件的相关功能,
HTTP类型的模块主要处理HTTP应用层的功能。
这样每个模块都有了自己的特性,
而ctx会指向特定类型模块的公共接口。
例如,
在HTTP模块中,ctx需要指向ngx_http_module_t结构体
*/
void *ctx;
/*
commands处理nginx.conf中的配置项
*/
ngx_command_t *commands;
/*
type表示该模块的类型,它与ctx指针是紧密相关的。
在官方Nginx中,它的取值范围有以下5种:
NGX_HTTP_MODULE
NGX_CORE_MODULE
NGX_CONF_MODULE
NGX_EVENT_MODULE
NGX_MAIL_MODULE
*/
ngx_uint_t type;
/*
在Nginx的启动、停止过程中,以下7个函数指针表示7个
执行点分别用调用这7种方法。对于任意一个方法而言,
如果不需要Nginx再某个时刻执行它,那么简单地把它设为NULL
空指针即可。
*/
/*
虽然从字面上理解应当在master进程启动的时,回调
init_master,但到目前为止,框架代码从来不会调动它,
所以设置为NULL
*/
ngx_int_t (*init_master)(ngx_log_t *log);
/*
init_module回调方法在初始化所有模块时候被调用。
在master/worker模式下,这个阶段将在启动worker子进程前完成。
*/
ngx_int_t (*init_module)(ngx_cycle_t *cycle);
/*
init_process 回调方法在正常服务前被调用。
在master/worker模式下,多个worker子进程已经产生。
在每个worker进程的初始化过程会调用所有模块的init_process函数
*/
ngx_int_t (*init_process)(ngx_cycle_t *cycle);
/*
由于Nginx暂时不支持多线程模式,所以init_thread在框架中
没有被调用过,设置为NULL
*/
ngx_int_t (*init_thread)(ngx_cycle_t *cycle);
/*
同上、exit_thread也不支持,设为NULL
*/
void (*exit_thread)(ngx_cycle_t *cycle);
/*
exit_process回调方法在服务停止前被调用。
在/master/worker模式下,worker进程会在退出前调用
*/
void (*exit_process)(ngx_cycle_t *cycle);
/*
exit_master回调方法将在master进程退出前被调用
*/
void (*exit_master)(ngx_cycle_t *cycle);
/*
一下8个spare_hook变量也是保留字段,目前没有使用,
但可用Nginx提供的NGX_MODULE_V1_PADDING宏来填充
#define NGX_MODULE_V1_PADDING 0,0,0,0,0,0,0,0
*/
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;
};
```
所以在定义一个HTTP模块的时候,务必把type字段设置为NGX_HTTP_MODULE.
对于下列回调方法:init_module、init_process、exit_process、exit_master
调用他们的是Nginx框架代码。换句话说,这4个回调方法与HTTP框架无关。
即使nginx.conf中没有设置http{...}这种开启HTTP功能的配置项,这些
回调方法仍然会被调用。因此,通常开发HTTP模块时候都把他们设置为NULL。
这样Nginx不作为web服务器使用时,不会执行HTTP模块的任何代码。
定义HTTP时候,最重要的是要设置ctx和commands这两个成员。
对于HTTP类型模块来说,ngx_module_中的ctx指针必须指向ngx_http_module_t接口。
所以最终我们定义个ngx_module_t 模块如下:
```cpp
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
};
```
这样, mytest 模块在编译的时候, 就可以被加入到ngx_modules的全局数组中了。
## 7.6 完整代码 ngx_http_mytest_module.c
所以全部的编码工作完毕,最终的代码应该如下:
```cpp
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <sys/types.h>
#include <unistd.h>
//定义处理用户请求hello world handler
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
//必须是GET或者HEAD方法,否则返回405 Not Allowed
if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
{
return NGX_HTTP_NOT_ALLOWED;
}
//丢弃请求中的包体
ngx_int_t rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK)
{
return rc;
}
//设置返回的Content-Type。注意,ngx_str_t有一个很方便的初始化宏
//ngx_string,它可以把ngx_str_t的data和len成员都设置好
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;
}
//构造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;
}
//将Hello World拷贝到ngx_buf_t指向的内存中
ngx_memcpy(b->pos, response.data, response.len);
//注意,一定要设置好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;
//最后一步发送包体,http框架会调用ngx_http_finalize_request方法
//结束请求
return ngx_http_output_filter(r, &out);
}
//定义command用于处理配置项参数的set方法
static char *
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
//首先找到mytest配置项所属的配置块,clcf貌似是location块内的数据
//结构,其实不然,它可以是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方法处理这个请求
clcf->handler = ngx_http_mytest_handler;
return NGX_CONF_OK;
}
//定义ngx mytest 配置匹配的command
static ngx_command_t ngx_http_mytest_commands[] =
{
{
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_HTTP_LOC_CONF_OFFSET,
0,
NULL
},
ngx_null_command
};
//定义ngx_http_module_t接口
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模块
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
};
```
## 7.7 配置文件 config
但是之后我们还需要给该模块相同目录下提供一个配置文件"config"表示在nginx编译的时候的一些编译选项。
```bash
ngx_addon_name=ngx_http_mytest_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c"
```
所以我们的模块编写完毕了,当前目录应该有两个文件
```bash
ls
config ngx_http_mytest_module.c
```
## 7.8 重新编译Nginx 并添加自定义模块
进入Nginx源码目录
执行
```bash
./configure --prefix=/usr/local/nginx --add-module=/home/ace/openSource_test/nginx_module_http_test
```
--add-module为刚才自定义模块源码的目录
```bash
make
sudo make install
```
## 测试自定义模块
修改nginx.conf文件
```php
server {
listen 8777;
server_name localhost;
location / {
mytest;#我们自定义模块的名称
}
}
```
重新启动nginx
打开浏览器输入地址和端口
![](https://img.kancloud.cn/88/ae/88aec56dd54a781ee152d6c07b2c1a99_692x240.png)