写一个Nginx的模块没有那么难
Nginx作为世界第二大Web服务器(第一是Apache),越来越受到大家的青睐。受到欢迎的一个重要原因,是他的高扩展性。它是由多个不同功能、不同层次、不同类型且耦合度极低的模块组成。当我们开发自己的模块时,不仅可以使用core模块、events模块、log模块,而且我们开发的模块是嵌入到二进制文件中执行的,因此我们自己开发的模块,也能具有优秀的性能,享受Nginx的高并发特性。
本文以编写一个http_hello_module为例,介绍编写一个Nginx的http模块的步骤,以及其中涉及的数据结构。以期打破大家对编写nginx模块的恐惧。
该示例模块的功能是:每次访问http时,http进行访问计数,并把计数的值回显给客户端。
为了说明自定义的http模块,是如何读取配置文件的值,即在Nginx.conf文件中配置值,我们引入了两个配置项hello_string、hello_counter。这两个配置指令,仅可以出现在location指令的作用域中。hello_string用于展示字符串类型,它接收一个参数来设置回显的字符串,或是零个参数,则使用默认的字符串作为回显的字符串。而hello_counter,用于控制是否开启访问统计,如果设置为 on,则会在相应的字符串后面追加 Visited Times:的字样,以统计请求的次数。我们的例子中,其中一个location的配置如下。
location /test {
hello_string balabala;
hello_counter on;
}
我们自定义模块的访问效果如图:

编写http_hello_module
一个简单的http模块,只需要定义一个结构体ngx_module_t的变量,他告诉Nginx框架,我们定义的模块,
- 属于什么模块(如http模块、filter模块)
- 需要哪些配置项,在哪个领域定义(如server域、http域、location域)。我们的例子中,需要两个配置项:hello_string,hello_counter。在location作用域配置。
- 配置项的类型,存储结构。我们的例子中,一个是string类型,一个是int类型,定义结构如下:
typedef struct
{
ngx_str_t hello_string;
ngx_int_t hello_counter;
}ngx_http_hello_loc_conf_t;
- 如何接收客户端请求,对请求,又应该做出怎样的应答。
下面,我们逐步了解ngx_module_t结构体是如何完成上述内容的。ngx_module_t结构体的每个成员定义如下:
typedef struct ngx_module_s ngx_module_t;
struct ngx_module_s {
ngx_uint_t ctx_index; // 表示当前模块在这类模块中的序号。既用于表达优先级,又用于 Nginx 框架快速获得模块的数据
ngx_uint_t index; // 当前模块在所有模块的序号
char *name;
ngx_uint_t spare0; // spare系列的保留变量,暂未使用
ngx_uint_t spare1;
ngx_uint_t version; // 模块的版本号,便于将来的扩展。
const char *signature;
// 用于指向一类模块的上下文结构体。ctx将会指向特定类型模块的公共接口。例如在http模块中,ctx指向ngx_http_module_t结构体
void *ctx;
ngx_command_t *commands; // 将处理nginx.conf中的配置项
ngx_uint_t type; // 模块类型。官方的取值包括:NGX_HTTP_MODULE, NGX_CORE_MODULE, NGX_CONF_MODULE, NGX_EVENT_MODULE, NGX_MAIL_MODULE
/*
* 以下七个函数指针,表示在七个阶段,会调用这七个方法
*/
ngx_int_t (*init_master)(ngx_log_t *log); // 在master进程启动时。框架暂时没用调用。所以,写了也没啥卵用
ngx_int_t (*init_module)(ngx_cycle_t *cycle); // 在初始化所有模块时被调用。在master/worker模式下,在启动worker进程前被调用
ngx_int_t (*init_process)(ngx_cycle_t *cycle); // 在每个worker进程的初始化过程会被调用
ngx_int_t (*init_thread)(ngx_cycle_t *cycle); // 不支持多线程,所以没啥卵用
void (*exit_thread)(ngx_cycle_t *cycle); // 不支持
void (*exit_process)(ngx_cycle_t *cycle); // worker 进程会在退出前调用
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_t结构体的前7个成员,我们用Nginx提供的宏NGX_MODULE_V1进行初始化。
#define NGX_MODULE_V1 \
NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX, \
NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE
对于ngx_module_t结构体的最后8个成员,我们用Nginx提供的宏NGX_MODULE_V1_PADDING进行初始化。
#define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0
对于在七个阶段会调用的函数指针,我们的http_hello_module并不需要,所以统一设置为NULL。
因为我们的例子是一个http模块,因此type值,等于NGX_HTTP_MODULE。
通过以上分析可知,为了定义结构体ngx_module_s的对象,我们只需要定义以下两个结构体的对象:
- 结构体ngx_http_module_t的对象,赋值给ctx,用于告知Nginx框架,我们的自定义模块如何处理客户端的请求
- 结构体ngx_command_t的数组对象,赋值给commands,用于告知Nginx框架,我们的自定义模块需要的配置项
ngx_module_s的成员commands定义
对于有配置项的模块,需要定义一个结构体,用于存储配置项的值。本例中,我们需要两个配置项, hello_string和hello_counter,分别是string和int类型,因此,存储配置项的结构体定义如下。
typedef struct
{
ngx_str_t hello_string;
ngx_int_t hello_counter;
} ngx_http_hello_loc_conf_t;
有一点需要注意的是,在模块的开发过程中,我们最好使用 Nginx 原有的命名习惯。这样跟源代码的契合度更高,看起来也更舒服。对于模块配置信息的定义,命名习惯是ngx_http_<module name>_(main|srv|loc)_conf_t。
commands数组用于定义模块的配置文件参数,每一个数组都是ngx_command_t类型,数组的结尾用ngx_null_command表示。Nginx框架在解析配置文件时,对于一个配置项,会遍历所有的模块。对每一个模块,都会遍历该模块的commmands数组,直到遇到ngx_null_command。
ngx_command_t结构体定义了自己感兴趣的一个配置项:
typedef struct ngx_command_s ngx_command_t;
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;
};
结构体各成员的说明如下:
- name: 配置指令的名称。
- type: 配置项类型,该配置项可以出现的位置,如:server{}或location{},以及它可以携带的参数个数。例如:NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1 表示该配置项出现在location位置,接收0个或1个参数。
- set:是一个函数指针。出现了name中指定的配置项后,将会调用set方法处理配置项的参数
- conf: 该字段指定当前配置项存储的内存位置。因为 http 模块对所有 http 模块所要保存的配置信息,划分了 main, server 和 location 三个地方进行存储,每个地方都有一个内存池用来分配存储这些信息的内存。这里可能的值为 NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET 或 NGX_HTTP_LOC_CONF_OFFSET。
- offset: 指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。
- post: 配置项读取后的处理方法
因为我们的例子有两个配置项,因此依次定义了两个ngx_command_t,数组用ngx_null_command结尾。
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, // 配置项的值,用LOCATION的内存池存储
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
};
对于hello_string配置项,它的处理函数实现如下,ngx_conf_set_str_slot是Nginx提供的函数,读取string类型的配置项。
static char *
ngx_http_hello_string(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_hello_loc_conf_t* local_conf;
local_conf = conf;
char* rv = ngx_conf_set_str_slot(cf, cmd, conf);
return rv;
}
对于hello_counter配置项,他的处理函数实现如下,ngx_conf_set_flag_slot是Nginx提供的函数,读取on/off值的配置项。
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);
return rv;
}
ngx_module_s的成员ctx定义
ctx是结构体ngx_http_module_t的对象指针,主要用于定义在读取配置文件的各个阶段的处理函数。
下面展示的代码,便是ngx_http_module_t这个结构体:
typedef struct {
// 解析配置文件前调用
ngx_int_t (*preconfiguration)(ngx_conf_t *cf);
// 完成配置文件的解析后调用
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);
// 创建数据结构,用于存储main级别(直属于http{...}块的配置项)的全局配置项
void *(*create_main_conf)(ngx_conf_t *cf);
// 初始化用于main级别的配置项
char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
//创建数据结构,用于存储srv级别(直属于虚拟主机server{...}块的配置项)的配置项
void *(*create_srv_conf)(ngx_conf_t *cf);
//合并main和srv级别下的同名配置项
char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
// 创建数据结构,用于存储loc级别(直属于location{...}块的配置项)的配置项
void *(*create_loc_conf)(ngx_conf_t *cf);
// 合并srv和location级别下的同名配置项
char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;
在我们的例子中,ngx_http_module_t的各个字段的实现如下, 我们只关心location配置项的读取以及在配置文件解析完成后的处理。location配置项读取hello_string和hello_counter的值,配置文件解析完成后,我们挂载请求的处理函数。
static ngx_http_module_t ngx_http_hello_module_ctx = {
NULL, /* preconfiguration */
ngx_http_hello_init, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_http_hello_create_loc_conf, /* create location configuration */
NULL /* merge location configuration */
};
我们先来看一下ngx_http_hello_create_loc_conf是如何存储loc级别的配置项。 ngx_http_hello_create_loc_conf是告诉Ngixn模块,我们使用的loc级别的配置项的信息,并对配置项进行初始化。
static void *ngx_http_hello_create_loc_conf(ngx_conf_t *cf)
{
ngx_http_hello_loc_conf_t* local_conf = NULL;
local_conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_hello_loc_conf_t));
if (local_conf == NULL)
{
return NULL;
}
ngx_str_null(&local_conf->hello_string);
local_conf->hello_counter = NGX_CONF_UNSET;
return local_conf;
}
在配置文件处理完后,我们挂载模块的处理函数。即告诉Nginx框架,当符合条件的请求过来时,我们自定义的模块如何处理请求,如何响应请求。挂载是在配置文件解析完毕,由函数ngx_http_hello_init完成的。
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,即由它处理客户端请求、做出具体响应。
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);
/* we response to 'GET' and 'HEAD' requests only */
if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
return NGX_HTTP_NOT_ALLOWED;
}
/* discard request body, since we don't need it here */
rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK) {
return rc;
}
ngx_str_set(&r->headers_out.content_type, "text/html");
/* send the header only, if the request type is http 'HEAD' */
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);
}
/* allocate a buffer for your response body */
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if (b == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
/* attach this buffer to the buffer chain */
out.buf = b;
out.next = NULL;
/* adjust the pointers of the buffer */
b->pos = ngx_hello_string;
b->last = ngx_hello_string + content_length;
b->memory = 1; /* this buffer is in memory */
b->last_buf = 1; /* this is the last buffer in the buffer chain */
/* set the status line */
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = content_length;
/* send the headers of your response */
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
return rc;
}
/* send the buffer chain of your response */
return ngx_http_output_filter(r, &out);
}
如何把http_hello_module编译进Nginx
上面介绍了,编写一个http模块的基本要素。接下来,介绍如何把这个简单的模块编译进Nginx框架,从而让它发挥作用。
方案一:在执行configure命令时,添加--add-module参数
Nginx提供了一种简单的方式将第三方的模块编译进Nginx中。首先是把第三方的源代码全部放在一个目录中, 并在该目录下新建一个名为config的文件,这个config文件的目的,就是告诉Nginx如何编译我们自己的模块。在执行Nginx的configure命令时, 加入参数 --add-module=PATH,PATH就是我们存放config的目录,就可以把我们开发的http_hello_module编译进Nginx。
我们的http_hello_module是一个http模块,因此config文件中只需要定义以下三个变量:
- ngx_addon_name: 仅在configure执行时使用,一般设置为模块名称。
- HTTP_MODULES: 添加自定义模块的名称。
- NGX_ADDON_SRCS:指定新增模块的源代码。如果有多个文件,则以空格隔开。
我们的http_hello_module例子,它的config文件完整内容如下。$ngx_addon_dir变量的值,等于我们在执行configure命令时,参数--add-module的值。
ngx_addon_name=ngx_http_hello_module
HTTP_MODULES="$HTTP_MODULES ngx_http_hello_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_hello_module.c"
因为我们本例是编写一个http module,因此赋值了HTTP_MODULES。如果是编写其他模块,如http 过滤模块、Nginx核心模块、事件模块、http头部过滤模块,则应该分别对变量HTTP_FILTER_MODULES、CORE_MODULES、EVENT_MODULES、HTTP_HEADER_FILTER_MODULES。
方案二:在执行configure命令后,修改Makefile文件
Nginx提供的另一种方法是直接修改Makefile文件。在执行完configure脚本后,会在objs/Makefile和objs/ngx_modules.c文件。我们可以直接修改这两个文件,从而完成http_hello_module编译进Nginx的目标。