nginx的模块非常之多,可以认为所有代码都是以模块的形式组织,这包括核心模块和功能模块,针对不同的应用场合,并非所有的功能模块都要被用到,附录A给出的是默认configure(即简单的http服务器应用)下被连接的模块,这里虽说是模块连接,但nginx不会像apache或lighttpd那样在编译时生成so动态库而在程序执行时再进行动态加载,nginx模块源文件会在生成nginx时就直接被编译到其二进制执行文件中,所以如果要选用不同的功能模块,必须对nginx做重新配置和编译。对于功能模块的选择,如果要修改默认值,需要在进行configure时进行指定,比如新增http_flv功能模块(默认是没有这个功能的,各个选项的默认值可以在文件auto/options内看到)
[root@localhost nginx-1.2.0]# ./configure --with-http_flv_module
执行后,生成的objs/ngx_modules.c文件内就包含有对ngx_http_flv_module模块的引用了,要再去掉http_flv功能模块,则需要重新configure,即不带--with-http_flv_module配置后再编译生成新的nginx执行程序。通过执行./configure –help,我们可以看到更多的配置选项。
虽然nginx的模块的很多,并且某个模块的功能各不相同,但是可以根据功能特性,我们大致可以分为四类:
1) handlers : 处理客户端请求并产生响应内容,比如ngx_http_static_moudle模块,负责客户端的静态页面请求,并将对应的静态磁盘文件作为响应内容输出.
2)filters : 对handlers产生的响应内容做各种过滤处理(即增,删,改),比如 ngx_http_not_modify_filter_moudle,如果通过时间判断前后2次请求的响应内容没有发生任何改变,那么可以直接响应"304 Not Modified"状态标识,让客户端使用缓存即可,而原本发送的响应内容将被清除掉.
3)upstream : 如果存在后端真实的服务器,nginx 可以利用upstream模块充当反向代理的角色,对客户端的请求只负责转发到后端的真实服务器,如ngx_http_proxy_moudle模块.
4)load-balance : 在nginx充当中间代理时,由于后端真实服务器往往多于一个,对于某一次客户端的请求,如何选择对应的后端真实服务器来进行处理,这就有类似于ngx_http_upstream_ip_hash_module这样的模块来实现不同的负载均衡算法(Load Balance)。
在此,我们先来了解一些数据结构:
struct ngx_module_s {
ngx_uint_t ctx_index; //在同类模块中的序号
ngx_uint_t index; //在所有模块中序号
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t spare2;
ngx_uint_t spare3;
ngx_uint_t version; //当前模块的版本号
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;
};
结构体ngx_module_s中值得注意的几个字段 ctx , commands, type,其中commands字段表示当前模块的可以解析的配置项目,表示模块类型的type值只有5种,而同一类型的模块的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;
............................
typedef struct {
ngx_str_t *name;
void *(*create_conf)(ngx_cycle_t *cycle);
char *(*init_conf)(ngx_cycle_t *cycle, void *conf);
ngx_event_actions_t actions;
} ngx_event_module_t;
........................
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;
上面表中的第3列的数据类型比较重要,它的字段基本上都是一些回调函数,这些回调函数会在其模块对应的配置文件解析过程 前/中/后 会适时被调用,做一些内存准备,初始化,配置值检查,配置值填充,合并,回调函数挂载等初始工作.
需要C/C++ Linux服务器架构师学习资料加qun获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
下面我们以ngx_http_core_moudle模块为例,type 为 NGX_HTTP_MOUDLE, ctx 指向ngx_http_moudle_t结构体变量ngx_http_core_module_ctx.
static ngx_http_module_t ngx_http_core_module_ctx = {
ngx_http_core_preconfiguration, /* preconfiguration */
NULL, /* postconfiguration */
ngx_http_core_create_main_conf, /* create main configuration */
ngx_http_core_init_main_conf, /* init main configuration */
ngx_http_core_create_srv_conf, /* create server configuration */
ngx_http_core_merge_srv_conf, /* merge server configuration */
ngx_http_core_create_loc_conf, /* create location configuration */
ngx_http_core_merge_loc_conf /* merge location configuration */
};
根据上面的代码,我们可以很明显看到各个回调函数的回调时机,例如:ngx_http_core_preconfiguration将在进行http块配置解析前被调用,所以内在ngx_http_block()函数里看到这样的代码:
static char *
ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
for (m = 0; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = ngx_modules[m]->ctx;
if (module->preconfiguration) {
if (module->preconfiguration(cf) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
}
}
至于这些回调函数内的具体逻辑,如前所述一般是一些初始或默认值填充工作,但也有回调函数挂载的设置,比如ngx_http_static_module模块的postconfiguration字段回调函数ngx_http_static_init()就是将自己的处理函数ngx_http_static_handler()挂载在http处理状态机上,但总体来看这毕竟都只是一些简单的初始准备工作.
handlers模块
对于客户端http请求过程,为了获得更强的控制力,nginx将其细分成多个阶段处理,每个阶段都有零个或多个回调函数专门处理,当我们编写handler模块的时候,必须把模块功能挂载在正确的阶段点。如前面所描述的ngx_http_static_moudle 将自己的功能模块处理函数ngx_http_static_handler()挂载在NGX_HTTP_CONTENT_PHASE阶段.
Http请求处理过程一共分为11个阶段,每一个阶段对应的处理功能都比较单一,这样能尽量让nginx模块代码更为内聚:
并非某个阶段都能挂载自定义的回调函数,比如NGX_HTTP_TRY_FILE_PHASE阶段就是针对配置项try_files的特定处理阶段段。NGX_HTTP_FIND_CONFIG_PHASE、NGX_HTTP_POST_ACCESS_PHASE与NGX_HTTP_POST_REWRITE_PHASE这三个阶段也是为了完成nginx特定的功能,就算给这几个阶段加上回调函数,也永远不会被调用。我们的自定义模块回调函数挂载在NGX_HTTP_CONTENT_PHASE阶段的情况比较多,毕竟大部分情况下的业务需求是修改HTTP响应数据,nginx自身的产生响应内容的模块,像ngx_http_static_module、ngx_http_random_index_module、ngx_http_index_module、ngx_http_gzip_static_module、ngx_http_dav_module等都是挂载在这个阶段。
大多数情况下,功能模块会在其对应配置解析完后的回调函数,也就是ngx_http_moudle_t结构体的postconfiguration字段指向的函数内将当前模块的回调功能函数挂载到这11个阶段其中一个上.
以ngx_http_static_module为例:
ngx_http_module_t ngx_http_static_module_ctx = {
NULL, /* preconfiguration */
ngx_http_static_init, /* 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 */
};
static ngx_int_t
ngx_http_static_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_static_handler;
return NGX_OK;
在模块ngx_http_static_module的postconfiguration回调函数ngx_http_static_init()内,将ngx_http_static_module模块的核心功能函数ngx_http_static_handler()挂载在Http请求处理流程中的NGX_HTTP_CONTENT_PHASE阶段。这样,当一个客户端的http静态页面请求发送到nginx服务器,nginx就能够调用到我们这里注册的ngx_http_static_handler()函数,
各个功能模块将其自身的功能函数挂载在cmcf->phases后,内部的情况如下图所示:
回调函数会根据模块的不同而不同.这些回调函数的调用都时有条件的,调用后也要做一些根据返回值的结果处理.比如某次处理是否进入到阶段NGX_HTTP_CONTENT_PARSE的回调函数的处理,这需要一个事前判断.所以在函数ngx_http_init_phase_handlers()里对所有这个回调函数进行一次重组.
struct ngx_http_phase_handler_s {
ngx_http_phase_handler_pt checker; //阶段检查函数
ngx_http_handler_pt handler;
ngx_uint_t next;
};
但从上图中可以看到,该函数只把有回调函数的处理阶段给提取了出来,同时利用ngx_http_phase_handler_t结构体数组对这些回调函数进行重组,不仅加上了进入回调函数的条件判断checker函数,而且通过next字段的使用,把原本的二维数组实现转化为可直接在一维函数数组内部跳动;一般来讲,二维数组的遍历需要两层循环,而遍历一维函数数组就只需一层循环。
再来看对http请求进行分段处理的核心函数ngx_http_core_run_phase:
void
ngx_http_core_run_phases(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_http_phase_handler_t *ph;
ngx_http_core_main_conf_t *cmcf;
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
ph = cmcf->phase_engine.handlers;
while (ph[r->phase_handler].checker) {
rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);
if (rc == NGX_OK) {
return;
}
}
}
ngx_http_core_run_phases函数中r->phase_handler标志当前处理的序号,对于一个客户端的最开始的请求的时刻, 该值当然就是0了,while循环判断如果存在checker函数(末尾数组元素的checker函数为null),那就调用该checker函数并有可能调用相应的回调函数,以NGX_HTTP_ACCESS_PHASE阶段的ngx_http_core_access_phase()函数为例:
ngx_int_t
ngx_http_core_access_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
{
if (r != r->main) {
r->phase_handler = ph->next;
return NGX_AGAIN;
}
rc = ph->handler(r);
if (rc == NGX_DECLINED) {
r->phase_handler++;
return NGX_AGAIN;
}
if (rc == NGX_AGAIN || rc == NGX_DONE) {
return NGX_OK;
}
ngx_http_finalize_request(r, rc);
return NGX_OK;
}
可以看到,一个功能模块的handler函数可以返回多种类型的值,并且这些值有其固有的含义:
Filter模块:
对于http请求处理handlers产生的响应内容,在输出客户端之前需要做过滤处理,这些过滤处理对于完整功能的增强实现和性能的提升是非常有必要的,比如过滤模块ngx_http_chunked_filter_moudle,那么就无法完整支持http中chunk的功能。如果没有ngx_http_not_modified_filter_module过滤模块,那么就无法让客户端使用本地缓存来提高性能;诸如这些都需要过滤模块的支持。由于响应数据包括响应头和响应体,所以以此对应,任一filter模块必须提供处理响应头的header过滤函数(比如ngx_http_not_modified_filter_module模块提供的ngx_http_not_modified_header_filter()函数)或处理响应体的body过滤功能函数(比如ngx_http_copy_filter_module模块提供的ngx_http_copy_filter()函数)或两者皆有(比如ngx_http_chunked_filter_module模块提供的ngx_http_chunked_header_filter()函数和ngx_http_chunked_body_filter()函数)。
所有的header过滤功能函数和body过滤功能函数会分别组成各自的两条过滤链,如下图所示(使用附录A所列模块):
这2条过滤链怎么形成的呢?在源文件ngx_http.c中,可以看到有2个函数指针变量:
ngx_int_t (*ngx_http_top_header_filter) (ngx_http_request_t *r);
ngx_int_t (*ngx_http_top_body_filter) (ngx_http_request_t *r, ngx_chain_t *ch);
这是整个nginx范围内可见的全局变量;然后在每一个filter模块内,我们还会看到类似于这样的定义(如果当前模块只有header过滤功能函数或只有body过滤功能函数,那么如下定义也就只有相应的那个变量):
static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
注意到static修饰符,也就是说这两个变量是属于模块范围内可见的局部变量。有了这些函数指针变量,再在各个filter模块的postconfiguration回调函数(该函数会在其对应配置解析完后被调用做一些设置工作,前面已经描述过)内,全局变量与局部变量的巧妙赋值使得最终行成了两条过滤链。以header过滤链为例,通过附录A的模块列表ngx_modules变量,可以看到ngx_http_header_filter_module是具有header过滤功能函数的序号最小的过滤模块,其postconfiguration回调函数如下:
618: ngx_http_header_filter_init(ngx_conf_t *cf)
619: {
620: ngx_http_top_header_filter = ngx_http_header_filter;
621:
622: return NGX_OK;
623: }
232: static ngx_int_t
233: ngx_http_chunked_filter_init(ngx_conf_t *cf)
234: {
235: ngx_http_next_header_filter = ngx_http_top_header_filter;
236: ngx_http_top_header_filter = ngx_http_chunked_header_filter;
}
其它过滤模块的类此加入,逐步形成最终的完整header过滤链;当然,body过滤链的形成过程也与此类似。两条过滤链形成后,其对应的调用入口分别在函数ngx_http_send_header()和函数ngx_http_output_filter()内:
1889: ngx_int_t
1890: ngx_http_send_header(ngx_http_request_t *r)
1891: {
1892: …
1897: return ngx_http_top_header_filter(r);
1898: }
1899:
1901: ngx_int_t
1902: ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)
1903: {
1904: …
1912: rc = ngx_http_top_body_filter(r, in);
1913: …
1919: return rc;
1920: }
这两个函数非常简单,主要是通过过滤链的链头函数指针全局变量进入到两条过滤链内,进而依次执行链上的各个函数。比如这里ngx_http_top_header_filter指向的是ngx_http_not_modified_header_filter()函数,因此进入到该函数内执行,而在该函数的执行过程中又会根据情况,继续通过当前模块内的函数指针局部变量ngx_http_next_header_filter间接的调用到header过滤链的下一个过滤函数,这对保证过滤链的前后承接是非常必要的,除非我们遇到无法继续处理的错误,此时只有返回NGX_ERROR这样的值:
52: static ngx_int_t
53: ngx_http_not_modified_header_filter(ngx_http_request_t *r)
54: {
55: …
70: return ngx_http_next_header_filter(r);
71: }
根据HTTP协议具备的响应头影响或决定响应体内容的特点,所以一般是先对响应头进行过滤,根据头过滤处理返回值再对响应体进行过滤处理,如果在响应头过滤处理中出错或某些特定情况下,响应体过滤处理可以不用再进行。