先介绍一下nginx模块的概念。
nginx 将各功能模块组织成一条链,当有请求到达的时候,请求依次经过这条链上的部分或者全部模块进行处理。每个模块实现特定的功能,例如,实现对请求解压缩的模块,实现SSI 的模块,实现与上游服务器进行通讯的模块,实现与 FastCGI 服务进行通讯的模块。有两个模块比较特殊,他们居于 nginx core 和各功能模块的中间。这两个模块就是 http 模块和 mail 模块。这 2 个模块在 nginx core 之上实现了另外一层抽象,处理与 HTTP 协议和 email相关协议(SMTP/POP3/IMAP)有关的事件,并且确保这些事件能被以正确的顺序调用其他的一些功能模块。
目前 HTTP 协议是被实现在 http 模块中的,但是有可能将来被剥离到一个单独的模块中,以扩展 nginx 支持 SPDY 协议。
nginx 的模块根据其功能基本上可以分为以下几种类型:
1.event module:搭建了独立于操作系统的事件处理机制的框架,及提供了各具体事件的处理。包括 ngx_events_module, ngx_event_core_module 和 ngx_epoll_module 等。nginx 具体使用何种事件处理模块,这依赖于具体的操作系统和编译选项。
2.phase handler:此类型的模块也被直接称为 handler 模块。主要负责处理客户端请求并产生待响应内容,比如 ngx_http_static_module 模块,负责客户端的静态页面请求处理并将对应的磁盘文件准备为响应内容输出。
3.output filter:也称为 filter 模块,主要是负责对输出的内容进行处理,可以对输出进行修改。例如,可以实现对输出的所有 html 页面增加预定义的 footbar 一类的工作,或者对输出的图片的 URL 进行替换之类的工作。
4.upstream:upstream 模块实现反向代理的功能,将真正的请求转发到后端服务器上,并从后端服务器上读取响应,发回客户端。upstream 模块是一种特殊的 handler,只不过响应内容不是真正由自己产生的,而是从后端服务器上读取的。
5.load-balancer:负载均衡模块,实现特定的算法,在众多的后端服务器中,选择一个服务器出来作为某个请求的转发服务器。
这里详细介绍一下handler模块。
我们需要了解一下 phase handler 这个概念。phase 字面的意思,就是阶段。所以 phase handlers 也就好理解了,就是包含若干个处理阶段的一些 handler。在每一个阶段,包含有若干个 handler,再处理到某个阶段的时候,依次调用该阶段的 handler 对 HTTP Request进行处理。通常情况下,一个 phase handler 对这个 request 进行处理,并产生一些输出。通常 phase handler 是与定义在配置文件中的某个 location 相关联的。
一个 phase handler 通常执行以下几项任务:
1.获取 location 配置。
2.产生适当的响应。
3.发送 response header。
4.发送 response body。
当 nginx 读取到一个 HTTP Request 的 header 的时候,nginx 首先查找与这个请求关联的虚拟主机的配置。如果找到了这个虚拟主机的配置,那么通常情况下,这个 HTTP Request 将会经过十一个阶段的处理。这就是nginx http模块中的十一个状态,这里暂不介绍。
在内容产生阶段,为了给一个 request 产生正确的响应,nginx 必须把这个 request 交给一个合适的 content handler 去处理。如果这个 request 对应的 location 在配置文件中被明确指定了一个 content handler,那么 nginx 就可以通过对 location 的匹配,直接找到这个对应的 handler,并把这个 request 交给这个 content handler 去处理。这样的配置指令包括像perl,flv,proxy_pass,mp4 等。
如果一个 request 对应的 location 并没有直接有配置的 content handler,那么 nginx 依次尝试:
作为第三方开发者,最可能开发的就是三种类型的模块,即 handler,filter 和 load-balancer。handler 模块就是接受来自客户端的请求并产生输出的模块。有些地方说 upstream 模块实际上也是一种 handler 模块,只不过它产生的内容来自于从后端服务器获取的,而非在本机产生的。
刚才提到,配置文件中使用 location 指令可以配置 content handler 模块,当 nginx系统启动的时候,每个 handler 模块都有一次机会把自己关联到对应的 location 上。如果有多个 handler 模块都关联了同一个 location,那么实际上只有一个 handler 模块真正会起作用。当然大多数情况下,模块开发人员都会避免出现这种情况。
handler 模块处理的结果通常有三种情况: 处理成功,处理失败(处理的时候发生了错误)或者是拒绝去处理。在拒绝处理的情况下,这个 location 的处理就会由默认的 handler 模块来进行处理。例如,当请求一个静态文件的时候,如果关联到这个 location 上的一个 handler模块拒绝处理,就会由默认的 ngx_http_static_module 模块进行处理,该模块是一个典型的handler 模块。
一个模块的配置指令是定义在一个静态数组中的。同样地,我们来看一下模块配置指令的定义。
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:该配置的类型,其实更准确一点说,是该配置指令属性的集合。nginx 提供了很多预定义的属性值(一些宏定义),通过逻辑或运算符可组合在一起。
set:这是一个函数指针,当 nginx 在解析配置的时候,如果遇到这个配置指令,将会把读取到的值传递给这个函数进行分解处理。因为具体每个配置指令的值如何处理,只有定义这个配置指令的人是最清楚的。函数原型
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
解释参数
cf:该参数里面保存从配置文件读取到的原始字符串以及相关的一些信息。特别注意的是这个参数的 args 字段是一个 ngx_str_t 类型的数组,该数组的首个元素是这个配置指令本身,第二个元素是指令的第一个参数,第三个元素是第二个参数,依次类推。
cmd:这个配置指令对应的 ngx_command_t 结构。
conf:定义的存储这个配置值的结构体。
继续介绍ngx_command_s的各字段。
conf:该字段被 NGX_HTTP_MODULE 类 型 模 块 所 用 ,该字段指定当前配置项存储的内存位置。实际上是使用哪个内存池的问题。因为 http模块对所有 要保存的配置信息,划分了 main, server 和 location 三个地方进行存储,每个地方都有一个内存池用来分配存储这些信息的内存。这里可能的值为 NGX_HTTP_MAIN_CONF_OFFSET 、 NGX_HTTP_SRV_CONF_OFFSET 或NGX_HTTP_LOC_CONF_OFFSET 。
offset:指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。因为对于配置信息的存储,一般我们都是定义个结构体来存储的。那么比如我们定义了一个结构体 A,该项配置的值需要存储到该结构体的 b 字段。那么在这里就可以填写为 offsetof(A, b)。对于有些配置项,它的值不需要保存或者是需要保存到更为复杂的结构中时,这里可以设置为 0。
post:该字段存储一个指针。可以指向任何一个在读取配置过程中需要的数据,以便于进行配置读取的处理。大多数时候,都不需要,所以简单地设为 0 即可。
下面举个例子。有一点需要特别注意的就是,在模块的开发过程中,我们最好使用 nginx 原有的命名习惯。这样跟原代码的契合度更高,看起来也更舒服。对于模块配置信息的定义,命名习惯是 ngx_http_
。
// 自己定义的模块配置信息结构体
typedef struct
{
ngx_str_t hello_string;
ngx_int_t hello_counter;
}ngx_http_hello_loc_conf_t;
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
};
需要注意的是,就是在 ngx_http_hello_commands 这个数组定义的最后,都要加一个ngx_null_command
作为结尾。
设置的函数会有两个地方被调用。一是执行nginx -c nginx.conf
命令,这里有两件事,解析.conf文件,将解析到的数据放到模块中,另外一件事是设置每一次请求的入口函数;二是每一次请求的时候,调用相应的入口函数。
对应的.conf文件
location /test {
hello_string jizhao;
hello_counter on;
}
配置好之后,当访问这个地址的时候, http://127.0.0.1/test 的时候,就可以看到返回的结果。
jizhao Visited Times:1
当然你访问多次,这个次数是会增加的。
这是一个 ngx_http_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;
按照常理来讲,这个结构体里应该存储了很多状态信息,但是实际上却提供一组回调函数指针,这些函数有在创建存储配置信息的对象的函数,也有在创建前和创建后会调用的函数。其实http模块配置文件的解析,都是来自这8个回调函数。http模块的状态信息来源于conf文件,回调函数解析配置文件后加载到内存中。一句话总结,http模块的状态信息来源于conf文件的解析。conf文件是所有进程共用的,解析出来的数据所有进程都能使用,不需要解析多次。
这8个回调函数执行的先后顺序是1、3、5、7、8、6、4、2。nginx所有的模块精细化都做得很好,就是时机把握得很好。它们是在解析.conf文件时调用的,postconfiguration()函数是在.conf文件解析完成后做一个初始化,一般就是设置每个函数调用的回调函数。
nginx 里面的配置信息都是上下一层层的嵌套的,对于具体某个 location 的话,对于同一个配置,如果当前层次没有定义,那么就使用上层的配置,否则使用当前层次的配置。
举例
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 */
};
这里并没有提供 merge_loc_conf 函数,因为这个模块的配置指令已经确定只出现在 NGX_HTTP_LOC_CONF 中这一个层次上,不会发生需要合并的情况。
上面的两个回调函数
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;
}
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;
}
对于开发一个模块来说,我们都需要定义一个 ngx_module_t 类型的变量来说明这个模块本身的信息,从某种意义上来说,这是这个模块最重要的一个信息,它告诉了 nginx 这个模块的一些信息,上面定义的配置信息,还有模块上下文信息,都是通过这个结构来告诉 nginx系统的,也就是加载模块的上层代码,都需要通过定义的这个结构,来获取这些信息。
我们先来看下 ngx_module_t 的定义
typedef struct ngx_module_s ngx_module_t;
struct ngx_module_s {
ngx_uint_t ctx_index;
ngx_uint_t index;
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t abi_compatibility;
ngx_uint_t major_version;
ngx_uint_t minor_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_t ngx_http_hello_module = {
NGX_MODULE_V1,
&ngx_http_hello_module_ctx, /* module context */
ngx_http_hello_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
};
这里使用了一些宏定义。模块可以提供一些回调函数给 nginx,当 nginx 在创建进程线程或者结束进程线程时进行调用。但大多数模块在这些时刻并不需要做什么,所以都简单赋值为 NULL。
实现相应的业务功能,并组装成http request。
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;
}
/* set the 'Content-type' header */
/*
*r->headers_out.content_type.len = sizeof("text/html") - 1;
*r->headers_out.content_type.data = (u_char *)"text/html";
*/
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);
}
对于开发一个模块,我们是需要把这个模块的 C 代码组织到一个目录里,同时需要编写一个config 文件。这个 config 文件的内容就是告诉 nginx 的编译脚本该如何编译。
# 文件名config,与模块的.c文件放在同一目录
# 模块名
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"
对于模块的编译,nginx 并不像 apache 一样,提供了单独的编译工具,可以在没有 apache 源代码的情况下来单独编译一个模块的代码。nginx 必须去到 nginx 的源代码目录里,通过configure 指令的参数,来进行编译。下面看一下 hello module 的 configure 指令
./configure –prefix=/usr/local/nginx –add-module=/home/ss/nginx/ngx_http_hello_module
然后再进行make编译与make install安装,再重新启动nginx,模块就加载成功了。