0%

nginx 自定义模块

Nginx 有核心模块和第三方模块,我们也可以自定义模块来使用。Nginx 就像一个小型的编程语言,要添加自定义模块就要满足一些 Nginx 的编写要求,就像 C 语言必须有一个 main 函数一样。

这里大部分的内容來自于《Nginx 开发从入门到精通》,只不过整理了一下以满足我自己的思考方式,更详细的内容可以访问 《Nginx 开发从入门到精通》ngx_http_hello_module.c 为主要模块,config 是添加自定义模块到 Nginx 时使用的。

模块所有代码在这里

配置指令

Nginx 都是以指令的形式进行配置的,所以我们的自定义模块也需要自定义指令来实现功能。自定义指令需要借助于 ngx_command_t 结构体。ngx_command_t 结构体定义在 src/core/ngx_conf_file.h 文件中。

1
2
3
4
5
6
7
8
struct ngx_command_s {
ngx_str_t name;
ngx_uint_t type;
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
ngx_uint_t conf;
ngx_uint_t offset;
void *post;
};

src/core/ngx_core.h 文件中将使用 ngx_command_t 来表示 struct ngx_command_s

1
typedef struct ngx_command_s         ngx_command_t;

这是关于自定义指令的描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static ngx_command_t ngx_http_hello_commands[] = {
{
ngx_string("hello_string"),
NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1,
ngx_http_hello_string,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_hello_loc_conf_t, hello_string),
NULL
},
{
ngx_string("hello_counter"),
NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_http_hello_counter,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_hello_loc_conf_t, hello_counter),
NULL
},
ngx_null_command
};
  • name 表示指令的名称,定义好后在 Nginx 配置文件中就可以直接使用该名称作为指令,比如在配置文件中可以使用 hello_string 来配置。
  • type 为指令接受参数的条件。条件由两部分构成:位置和参数个数。

位置由以下参数表示

名称 描述
NGX_DIRECT_CONF 可以出现在配置文件中最外层。例如已经提供的配置指令 daemon, master_process 等。
NGX_MAIN_CONF http, mail, events, error_log 等。
NGX_ANY_CONF 该配置指令可以出现在任意配置级别上。
NGX_HTTP_MAIN_CONF 可以直接出现在 http 配置指令里。
NGX_HTTP_SRV_CONF 可以出现在 http 里面的 server 配置指令里。
NGX_HTTP_LOC_CONF 可以出现在 http server 块里面的 location 配置指令里。
NGX_HTTP_UPS_CONF 可以出现在 http 里面的 upstream 配置指令里。
NGX_HTTP_SIF_CONF 可以出现在 http 里面的 server 配置指令里的 if 语句所在的 block 中。
NGX_HTTP_LMT_CONF 可以出现在 http 里面的 limit_except 指令的 block 中。
NGX_HTTP_LIF_CONF 可以出现在 http server 块里面的 location 配置指令里的 if 语句所在 block 中。

参数个数由以下参数表示

名称 描述
NGX_CONF_NOARGS 配置指令不接受任何参数。
NGX_CONF_TAKE1 配置指令接受 1 个参数。
NGX_CONF_TAKE2 配置指令接受 2 个参数。
NGX_CONF_TAKE3 配置指令接受 3 个参数。
NGX_CONF_TAKE4 配置指令接受 4 个参数。
NGX_CONF_TAKE5 配置指令接受 5 个参数。
NGX_CONF_TAKE6 配置指令接受 6 个参数。
NGX_CONF_TAKE7 配置指令接受 7 个参数。
NGX_CONF_MULTI 配置指令接受多个参数,即个数不定。
NGX_CONF_BLOCK 配置指令可以接受的值是一个配置信息。也就是一对大括号扩起来的内容。里面可以再包括很多个配置指令。比如常见的 server 指令就是这个属性。
NGX_CONF_FLAG 配置指令可以接受的值是 on 或者 off,最终会被转成 bool 值。
NGX_CONF_ANY 配置指令可以接受的任意的参数值

上面这些参数可以使用 | 进行组合,比如上面配置中 NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1 就表示定义在 http server 块中的 location 配置指令中,接受 0 个或者 1 个参数。

  • set 是一个函数指针,为该指令的处理函数,因为指令如何处理只有定义这个指令的人最清楚。

函数处理成功时返回 NGX_OK,否则返回 NGX_CONF_ERROR 或者一个自定义的错误信息的字符串。

函数调用时会传入三个参数:

  1. cf: 该参数里面保存从配置文件读取到的原始字符串以及相关的一些信息。特别注意的是这个参数的 args 字段是一个 ngx_str_t 类型的数组,该数组的首个元素是这个配置指令本身,第二个元素是指令的第一个参数,第三个元素是第二个参数,依次类推。
  2. cmd: 这个配置指令对应的 ngx_command_t 结构。
  3. conf: 就是定义的存储这个配置值的结构体。

为了方便对配置指令参数的读取,Nginx 默认提供了一些对标准类型的参数进行读取的函数,可以直接复制给 set 字段使用。

函数名称 作用
ngx_conf_set_flag_slot 读取 NGX_CONF_FLAG 类型的参数。
ngx_conf_set_str_slot 读取字符串类型的参数。
ngx_conf_set_str_array_slot 读取字符串数组类型的参数。
ngx_conf_set_keyval_slot 读取键值对类型的参数。
ngx_conf_set_num_slot 读取整数类型(有符号)的参数
ngx_conf_set_size_slot 读取 size_t 类型的参数,也就是无符号数。
ngx_conf_set_off_slot 读取 off_t 类型的参数。
ngx_conf_set_msec_slot 读取毫秒值类型的参数。
ngx_conf_set_sec_slot 读取秒值类型的参数。
ngx_conf_set_bufs_slot 读取的参数值是 2 个,一个是 buf 的个数,一个是 buf 的大小。
ngx_conf_set_enum_slot 读取枚举类型的参数,将其转换成 ngx_uint_t 类型。
ngx_conf_set_bitmask_slot 读取参数的值,并将这些参数以 bit 位的形式存储。

下面是对 hello_string 进行处理的代码。

1
2
3
4
5
6
7
8
9
static char *ngx_http_hello_counter(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_hello_loc_conf_t* local_conf;
local_conf = conf;
char* rv = NULL;
rv = ngx_conf_set_flag_slot(cf, cmd, conf);
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "hello_counter:%d", local_conf->hello_counter);
return rv;
}

该函数返回的是指令接收的参数,使用 ntx_http_hello_loc_conf_t 只是为了打印一下输出。

ngx_http_hello_loc_conf_t 的定义如下。在传入 set 函数指针时,conf 指向的就是这个结构体。

1
2
3
4
typedef struct {
ngx_str_t hello_string;
ngx_int_t hello_counter;
}ngx_http_hello_loc_conf_t;
  • conf 该字段被NGX_HTTP_MODULE类型模块所用 (我们编写的基本上都是NGX_HTTP_MOUDLE,只有一些nginx核心模块是非NGX_HTTP_MODULE),该字段指定当前配置项存储的内存位置。实际上是使用哪个内存池的问题。因为http模块对所有http模块所要保存的配置信息,划分了main, server和location三个地方进行存储,每个地方都有一个内存池用来分配存储这些信息的内存。这里可能的值为 NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET或NGX_HTTP_LOC_CONF_OFFSET。当然也可以直接置为0,就是NGX_HTTP_MAIN_CONF_OFFSET。
  • offset: 指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。因为对于配置信息的存储,一般我们都是定义个结构体来存储的。那么比如我们定义了一个结构体A,该项配置的值需要存储到该结构体的b字段。那么在这里就可以填写为offsetof(A, b)。
  • 该字段存储一个指针。可以指向任何一个在读取配置过程中需要的数据,以便于进行配置读取的处理。大多数时候,都不需要,所以简单地设为0即可。

需要注意的是,就是在ngx_http_hello_commands这个数组定义的最后,都要加一个ngx_null_command作为结尾。

模块上下文

模块上下文和 ngx_http_module_t 有关,这个结构体的定义在 src/http/ngx_http_config.h 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
ngx_int_t (*preconfiguration)(ngx_conf_t *cf);
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);

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;

这个变量实际上是提供一组回调函数指针,这些函数有在创建存储配置信息的对象的函数,也有在创建前和创建后会调用的函数。这些函数都将被nginx在合适的时间进行调用。

参数名称 作用
preconfiguration 在创建和读取该模块的配置信息之前被调用。
postconfiguration 在创建和读取该模块的配置信息之后被调用。
create_main_conf 调用该函数创建本模块位于 http block 的配置信息存储结构。该函数成功的时候,返回创建的配置对象。失败的话,返回 NULL。
init_main_conf 调用该函数初始化本模块位于 http block 的配置信息存储结构。该函数成功的时候,返回 NGX_CONF_OK。失败的话,返回 NGX_CONF_ERROR 或错误字符串
create_srv_conf 调用该函数创建本模块位于 http server block 的配置信息存储结构,每个 server block 会创建一个。该函数成功的时候,返回创建的配置对象。失败的话,返回 NULL。
merge_srv_conf 因为有些配置指令既可以出现在 http block,也可以出现在 http server block 中。那么遇到这种情况,每个 server都会有自己存储结构来存储该 server 的配置,但是在这种情况下 http block 中的配置与 server block 中的配置信息发生冲突的时候,就需要调用此函数进行合并,该函数并非必须提供,当预计到绝对不会发生需要合并的情况的时候,就无需提供。当然为了安全起见还是建议提供。该函数执行成功的时候,返回 NGX_CONF_OK。失败的话,返回 NGX_CONF_ERROR 或错误字符串。
create_loc_conf 调用该函数创建本模块位于 location block 的配置信息存储结构。每个在配置中指明的 location 创建一个。该函数执行成功,返回创建的配置对象。失败的话,返回 NULL。
merge_loc_conf 与 merge_srv_conf 类似,这个也是进行配置值合并的地方。该函数成功的时候,返回 NGX_CONF_OK。失败的话,返回 NGX_CONF_ERROR 或错误字符串。

Nginx 里面的配置信息都是上下一层层的嵌套的,对于具体某个 location 的话,对于同一个配置,如果当前层次没有定义,那么就使用上层的配置,否则使用当前层次的配置。

下面为该自定义模块的上下文定义

1
2
3
4
5
6
7
8
9
10
11
12
13
static ngx_http_module_t ngx_http_hello_module_ctx = {
NULL,
ngx_http_hello_init,

NULL,
NULL,

NULL,
NULL,

ngx_http_hello_create_loc_conf,
NULL,
};

上述配置说明了在创建和读取该模块的配置信息之后调用初始化函数,并且在 location 块中配配置信息存储结构。

ngx_http_hello_init 函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static ngx_int_t
ngx_http_hello_init(ngx_conf_t *cf)
{
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf;

cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
*h = ngx_http_hello_handler;
return NGX_OK;
}

ngx_http_hello_handler 函数定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static ngx_int_t
ngx_http_hello_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t out;
ngx_http_hello_loc_conf_t* my_conf;
u_char ngx_hello_string[1024] = {0};
ngx_uint_t content_length = 0;
ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "ngx_http_hello_handler is called!");

my_conf = ngx_http_get_module_loc_conf(r, ngx_http_hello_module);
if (my_conf->hello_string.len == 0)
{
ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "hello string is empty!");
return NGX_DECLINED;
}

if (my_conf->hello_counter == NGX_CONF_UNSET || my_conf->hello_counter == 0)
{
ngx_sprintf(ngx_hello_string, "%s", my_conf->hello_string.data);
}
else
{
ngx_sprintf(ngx_hello_string, "%s Visited Times:%d", my_conf->hello_string.data,
++ngx_hello_visited_times);
}
ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "hello_string:%s", ngx_hello_string);
content_length = ngx_strlen(ngx_hello_string);

if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
return NGX_HTTP_NOT_ALLOWED;
}

rc = ngx_http_discard_request_body(r);

if (rc != NGX_OK) {
return rc;
}

ngx_str_set(&r->headers_out.content_type, "text/html");

if (r->method == NGX_HTTP_HEAD) {
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = content_length;
return ngx_http_send_header(r);
}

b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
out.buf = b;
out.next = NULL;

b->pos = ngx_hello_string;
b->last = ngx_hello_string + content_length;
b->memory = 1;
b->last_buf = 1;

r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = content_length;

rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
return rc;
}
return ngx_http_output_filter(r, &out);
}

模块的定义

上面分别说了指令的配置以及模块的上下文定义,关于模块本身的定义说的不多。

对于开发一个模块来说,我们都需要定义一个 ngx_module_t 类型的变量来说明这个模块本身的信息,从某种意义上来说,这是这个模块最重要的一个信息,它告诉了 Nginx 这个模块的一些信息,上面定义的配置信息,还有模块上下文信息,都是通过这个结构来告诉 Nginx 系统的,也就是加载模块的上层代码,都需要通过定义的这个结构,来获取这些信息。

ngx_module_t 的定义在 src/core/ngx_module.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;

ngx_int_t (*init_master)(ngx_log_t *log);

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);
void (*exit_thread)(ngx_cycle_t *cycle);
void (*exit_process)(ngx_cycle_t *cycle);

void (*exit_master)(ngx_cycle_t *cycle);

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;
};
#define NGX_MODULE_V1 \
NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX, \
NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE

#define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0

src/core/ngx_core.h 中有如下定义:

1
typedef struct ngx_module_s          ngx_module_t;

Nginx 为了简化配置,将前面 7 个配置使用 NGX_MODULE_V1 宏来表示,后 8 个配置使用 NGX_MODULE_V1_PADDING 宏来表示。因此只需要配置 10 个参数就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ngx_module_t ngx_http_hello_module = {
NGX_MODULE_V1,
&ngx_http_hello_module_ctx, //该模块的上下文
ngx_http_hello_commands, //该模块的指令集合
NGX_HTTP_MODULE, //该模块的种类
NULL, //在 master 初始化的函数
NULL, //模块初始化函数
NULL, //初始化工作进程
NULL, //初始化线程
NULL, //离开线程
NULL, //离开工作进程
NULL, //离开 master
NGX_MODULE_V1_PADDING
};

模块挂载

定义好一个模块之后还需要把模块挂载到相应的请求处理阶段上,Nginx 有 11 个请求处理阶段,其中有 4 个阶段不能配置,剩下 7 个可以进行挂载。在 ngx_http_hello_module_ctx 中传入的指针函数 ngx_http_hello_init 就是用来挂载到相应模块上的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static ngx_int_t
ngx_http_hello_init(ngx_conf_t *cf)
{
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf;

cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
*h = ngx_http_hello_handler;
return NGX_OK;
}

其中 ngx_http_conf_get_module_main_conf 根据 conf 结构和模块得到主配置,然后使用 ngx_array_push 在 NGX_HTTP_CONTENT_PHASE 阶段进行挂载。

总结

一个模块可以有多个指令,Nginx 使用 ngx_command_t 来描述一个指令,比如指令名称,指令所在位置,指令参数,指令处理函数,指令存储空间以及指令参数在哪个结构体的哪个属性等。

为了将指令的参数在回调函数中可以被明白的解析,需要定义一个结构体来存储传入的参数,这里使用的结构体是 ngx_http_hello_loc_conf_t,其中 hello_string 指令的参数通过 offsetof(ngx_http_hello_loc_conf_t, hello_string) 传入 ngx_http_hello_loc_conf_t 的 hello_string 属性。封装好后 Nginx 会将 ngx_http_hello_loc_conf_t 传入处理回调函数,因为 C 语言的原因,所以传入的指针为 void* 类型的指针,在处理函数中需要进行转换。hello_string 指令的处理函数为 ngx_http_hello_string,hello_counter 指令的处理函数为 ngx_http_hello_counter

指令在不同阶段可能进行各种初始化工作,Nginx 定义了 ngx_http_module_t 结构体来描述一个指令的上下文环境。可以根据情况来进行上下文的初始化。

最后需要使用 ngx_module_t 结构体来描述该模块本身的信息,模块本身的信息需要传入模块上下文,模块的指令,模块的类型,以及各种钩子函数等。值得注意的是,Nginx 提供了两个宏来减少配置操作,分别时 NGX_MODULE_V1NGX_MODULE_V1_PADDING

hello 模块在模块上下文定义中,在配置读取和创建结束之后传入了 ngx_http_hello_init 函数,该函数会将模块挂载到 NGX_HTTP_CONTENT_PHASE,并且将 ngx_hettp_hello_handler 函数作为主要的处理函数。