模块化的ngx
提到ngx,一个永远绕不开的概念就是“模块”,模块在ngx中扮演者举足轻重的角色,你甚至可以认为在ngx中一切都是模块,它是一种或多种功能的一个组合体,是ngx实现灵活扩展的一个基本保障。
其实这个模块的概念跟积木及其相似,在生活中,我们可以用一堆各式各样的积木拼成各种东西,比如房子、动物、飞船等,如果现有的积木无法完成你想要拼成的东西,你并不需要否定现有的积木,而是可以引入新的积木并与之进行配合。比如在拼装汽车时缺少一个轮胎形状的积木,那我们可以单独造一个这样的积木,而不是修改现有的积木(有可能修改完后又无法拼成之前的了)。
在ngx配置文件中无法明确看出模块的影子,但其实一切皆模块,配置文件中任何一条指令都属于某个模块,之前所有文章中的任何例子都有很多模块参与,但因侧重点不同并没有一一列出,这次我们找几个例在来一一说明。
来看一个最简单的:
location / {
return 200 “hello”;
}
这个例子有两条指令,它背后正好有两个模块参与其中。第一条指令location属于ngx_http_core_module模块,从名字就可以看出来,它是ngx处理http协议的核心模块;另一条指令return属于ngx_http_rewrite_module模块。
假设现在有这样一个需求,我们想在某个请求返回的数据前和后加上一些特殊的数据,而这些数据则有其它location提供,但目前指令return是无法完成的,不过我们可以对其进行改造,比如改造成这样:
return 200 “#{/before} hello #{/after}”
我们可以把这个指令进行改造,让他可以从“#{}”中解析数据,并把解析出的数据当成location,然后再内部把解析的location数据拿过来,看起来这种方案是行得通的,但它看起来并不优雅。比如无法完全确保修改完后是否对原功能产生何种影响;比如种语法看起来也很怪;再比如原数据本身如何就包含这种“#{}”字符,那我们就需要对原数据进行字符转义。
总之,糟糕透了。
对于这种问题,在ngx中一个正规思路是:另外加模块来完成这个功能。而ngx正好有一个ngx_http_addition_module模块可以完成这个需求,所以我们也不必单独搞一个模块了,它用起来像这样:
location / {
add_before_body /before;
add_after_body. /after;
// *表示拦截所有的mime-type,默认text/html
addition_types *;
return 200 “hello”;
}
这样一来这个配置就涉及到三个模块了。
为了避免过多的干扰,所以基本上以往的例子都是ngx的一个片段配置,它们是无法独立工作的,这次我们拿一个可以完整工作的配置,来看看它都涉及到了哪些模块:
worker_processes 1;
events {
worker_connections 1024;
}
http{
listen 80;
location / {
return 200 “hello”;
}
}
这个配置有7个指令,涉及两种模块类型,一个是核心类型的,一个是http类型的。其中worker_processes、events、worker_connections、http属于核心类型,而listen、location、return则属于http类型的。更具体一点的话listen属于ngx_http_core_module模块,location和return属于ngx_http_rewrite_module模块。
而另外四个核心指令,ngx在官网上统一把他们归到了ngx_core_module(http://nginx.org/en/docs/ngx_core_module.html)中,但其实在ngx内部并没有一个ngx_core_module模块或.c文件与之对应,比如worker_processes指令实际对应到src/core/nginx.c,events和worker_connections对应到src /event/ngx_event.c,http则是对应到src/http/ngx_http.c,如果不是特殊情况,后续为了方便,我们也统一把这些指令的归属模块叫成ngx_core_module。
通过这个稍微完整点的例子,我们基本可以感觉到,ngx的运作模式就是有核心模块,以及其它非核心模块配合完成的,比如正确匹配http请求需要http_core模块,设置自定变量需要rewrite模块,做反向代理又需要proxy模块,所以除了模块还是模块。
1如何表示一个模块
ngx用下面的结构体表示一个模块:
(为了避免累赘并没有贴出所有字段)
/src/core/ngx_conf_file.h
struct ngx_module_s {
// …
ngx_uint_t ctx_index;
ngx_uint_t index;
// …
void *ctx;
ngx_command_t *commands;
// …
ngx_uint_t type;
// …
}
/src/core/ngx_core.h
typedef struct ngx_module_s ngx_module_t;
ngx中的所有模块都用这个结构体来定义,它有点类似于面向Java语言中的Object,但毕竟C语言并非面向对象语言,所以对于子模块的扩展也没有类似严格的继承语法,它的扩展方式类似于口头约定。比如在ngx中,要扩展一个模块,一般需要约定三种数据,一种是该结构体的ctx字段,另一种是type字段,还有一个是该模块提供的指令集commands,但除了type之外其它两个都不是必须的。
其中type用来指定是哪种类型的模块,比如ngx中的核心模块类型使用一个宏定义/src/core/ngx_conf_file.h#NGX_CORE_MODULE,这个宏表示这是一个核心模块。
而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;
它定义了三个字段,name用来表示某个核心模块的名字,比如http、email等。另外两个是该类型模块的回调函数,ngx会在某个时间点对其进行回调。
指令集commands是一个数组,具体有哪些指令也是有各个模块自行描述,但指令的格式是固定的,每个指令都需要用ngx_command_t结构体来表述,比如核心http模块(ngx_http.c)有如下指令集:
static ngx_command_t ngx_http_commands[] = {
{ ngx_string("http"),
XXX|XXX,
ngx_http_block,
0,
0,
NULL },
ngx_null_command
};
该数组表示该模块只有一个指令,名字叫“http”,该指令对应的方法是ngx_http_block(当解析到指令http时要执行的方法),其它字段含义等后续实际用到再做解释。
目前在ngx中有如下几个核心模块:
src/core/ngx_thread_pool.c
src /core/ngx_log.c
src /core/ngx_regex.c
src /core/nginx.c
src /event/ngx_event_openssl.c
src /event/ngx_event.c
src /http/ngx_http.c
src /mail/ngx_mail.c
src /misc/ngx_google_perftools_module.c
src /stream/ngx_stream.c
他们的共同特点是都实现了ngx_core_module_t结构体,比如我们在ngx配置文件中最熟悉的指令http{}所属的模块ngx_http.c就是一个核心模块,它的定义是这样的:
static ngx_core_module_t ngx_http_module_ctx = {
ngx_string("http"),
NULL,
NULL
};
ngx_module_t ngx_http_module = {
NGX_MODULE_V1,
&ngx_http_module_ctx, /* module context */
ngx_http_commands, /* module directives */
NGX_CORE_MODULE, /* module type */
// …
};
可以看到,它直接声明了一个ngx_core_module_t类型的结构体(ngx内部管这种结构体叫模块上下文—module context),并初始化该模块的名字为“http”,另外由于他不需要ngx执行回调函数,所以将其设置为NULL,这样ngx主框架在启动的时候就不会执行对应位置的回调函数了。然后再用ngx_module_t来声明一个该类型的结构体,把对应的http模块上下文(ngx_http_module_ctx)设置到该结构体ctx位置,type位置则是设置为NGX_CORE_MODULE,这样就基本“实现”了一个核心模块,并且该模块的名字为“http”。
基本上ngx模块的表示都是这种套路,如果你要定义新模块类型,或“实现”现有模块类型,按照上面的例子搞一套就可以了。
2ngx中的模块类型
目前在ngx*有六种模块类型,具体如下:
src/core/ngx_conf_file.h#NGX_CORE_MODULE
src/core/ngx_conf_file.h#NGX_CONF_MODULE
src/http/ngx_http_config.h#NGX_HTTP_MODULE
src/mail/ngx_mail.h#NGX_MAIL_MODULE
src/event/ngx_event.h#NGX_EVENT_MODULE
src/stream/ngx_stream.h#NGX_STREAM_MODULE
基本上每种类型都有各自ctx描述和各种实现,比如核心模块的ctx为ngx_core_module_t,它的实现有ngx_http.c、ngx_event.c等。
不过目前有一个特例,NGX_CONF_MODULE类型的模块并没有定义自己的ctx,而且对它的实现也只有一个“模块”,具体描述如下:
/src/core/ngx_conf_file.c
ngx_module_t ngx_conf_module = {
NULL, /* module context */
ngx_conf_commands, /* module directives */
NGX_CONF_MODULE, /* module type */
};
也就是说他除了不需要回调函数外,连名字也懒得定义了,不过还好它有一个指令“include”,该指令承包了该模块对外的一切功能。
3执行回调函数(加载模块)
除了ngx中自带的六种模块类型,用户还可定义自己的模块类型,而且每种模块类型又可以有很多“实现”,所以这么多模块,ngx是如何执行这些模块的回调函数的呢?
实际上ngx主流程在启动的时候只关心一种模块类型:NGX_CORE_MODULE。在开始解析配置文件之前,它会先回调各个核心模块的create_conf方法(在ngx_core_module_t结构体中指定),具体调用方式如下:
src/core/ngx_cycle.c
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {
// 排除非NGX_CORE_MODULE类型的模块
continue;
}
module = ngx_modules[i]->ctx;
if (module->create_conf) {
// 回调模块的create_conf
rv = module->create_conf(cycle);
// …
cycle->conf_ctx[ngx_modules[i]->index] = rv;
}
}
等配置文件解析完毕后再调用核心模块的init_conf方法(在ngx_core_module_t结构体中指定),具体调用方式也是一个循环:
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {
// 排除非核心模块
continue;
}
module = ngx_modules[i]->ctx;
if (module->init_conf) {
// 回调init_conf方法
if(module->init_conf(cycle,xxx)== yyy) {
// …
return NULL;
}
}
}
其它模块类型的回调函数则有对应的核心模块的实现来完成,比如NGX_HTTP_MODULE类型的模块都有ngx_http.c这个核心模块的实现完成,它的回调入口是http指令对应的/src/http/ngx_http.c/ngx_http_block()方法。
NGX_HTTP_MODULE类型的模块对应的ctx定义如下:
src/http/ngx_http_config.h
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;
所以在ngx_http_block()方法会直接或间接回调该结构体中的方法,具体会回调方式仍然以循环所有模块开始:
for (m = 0; ngx_modules[m]; m++) {
// 过滤掉非NGX_HTTP_MODULE类型的模块
if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = ngx_modules[m]->ctx;
if (module->create_main_conf) {
// 回调create_main_conf
ctx->main_conf[mi] = module->create_main_conf(cf);
// …
}
}
以此类推,其它回调方法也是如此。
目前除了NGX_CONF_MODULE类型模块外,其它非核心模块类型的加载方式(执行回调函数)都跟NGX_HTTP_MODULE类似。比如NGX_EVENT_MODULE以src/event/ngx_event.c#ngx_event_process_init()方法为入口,NGX_MAIL_MODULE以src/email/ngx_mail.c#ngx_mail_block()方法为入口,NGX_STREAM_MODULE则以src/stream/ngx_stream.c#ngx_stream_block()方法为入口。
4ngx现有的模块分布
为了便于读者查阅代码,这里把各个模块类型在源代码目中的分布做个简单介绍:
NGX_CONF_MODULE:
src/core/ngx_conf_file.c
NGX_CORE_MODULE:
具体模块在第二节已经列举,这里不再赘述。
NGX_HTTP_MODULE:
分布在src/http目录及其子目录下
NGX_MAIL_MODULE:
分布在src/email目录下
NGX_EVENT_MODULE:
分布在src/event目录及其子目录下
NGX_STREAM_MODULE:
分布在src/stream目录下
上一篇: 写一个核心模块