您的位置:首页 > 运维架构 > Nginx

Nginx基础知识. Nginx模块开发

2015-08-25 11:04 656 查看
Nginx模块开发初识

下面将开发一个简单的HTTP模块作为线索进行讲解.


Part1. 模块需要使用的信息存储结构

关于ngx_module_t

既然要进行模块开发, 那么就必须会使用到存储模块的数据结构, 下面先看看这个结构体

//因为是初识, 所以结构体的一些变量暂时不管
typedef struct ngx_module_s ngx_module_t;
struct ngx_module_s {
...

void                 *ctx;          //用于指向一类模块的上下文结构体.  Nginx模块有许多种类, 不同种类的模块之间的功能差别很大. 比如事件类型的模块主要处理I/O事件相关功能. HTTP类型模块主要处理HTTP应用层的功能. 每个模块都有自己的特性, 而ctx将会指向特定类型模块的公共接口. 比如HTTP中的ngx_http_module_t
ngx_command_t        *commands;     //commands用于处理配置文件中的配置项
ngx_uint_t            type;         //表示该模块的类型, 与ctx指针紧密相连. 其取值不列出. 我们将会用到的是HTTP模块: NGX_HTTP_MODULE
...
...
};


关于ngx_http_module_t

上面提到了, 我们开发HTTP模块, 就会需要该模块的上下文. HTTP上下文的信息存储结构是ngx_http_module_t

HTTP框架在读取, 重载配置文件时定义了由ngx_http_module_t接口描述的8个阶段. HTTP框架在启动过程中会在每个阶段调用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);

//当需要创建数据结构用于存储server级别(也就是直属于server{...}的配置项)的全局配置项时, 可以通过此回调方法创建存储全局配置项的结构体
void       *(*create_srv_conf)(ngx_conf_t *cf);
//此回调方法主要用于合并main级别和srv级别下的同名配置项
char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

//当需要创建数据结构用于存储loca级别(也就是直属于location{...}的配置项)的全局配置项时, 可以通过此回调方法创建存储全局配置项的结构体
void       *(*create_loc_conf)(ngx_conf_t *cf);
//此回调方法主要用于合并srv级别和loc级别下的同名配置项
char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;
上面只是把所有函数指针列出来, 并不表明他们的执行顺序就是如此. 顺序可能是如下这样的:

1.) create_main_conf 2.) create_srv_conf 3.) create_loc_conf

4.) preconfiguration 5.) init_main_conf 6.) merge_srv_conf

7.) merge_loc_conf 8.) postconfiguration

关于ngx_command_t

介绍完了HTTP模块的ngx_http_module_t结构体, 下面继续看用于定义模块的配置文件参数的数组, 每个数组元素都是ngx_command_t类型, 数组的结尾必须用ngx_null_command表示

Nginx在解析配置文件中的一个配置项时首先会遍历所有的模块, 对于每个模块而言, 即通过变量commands数组进行. 在数组中检查到ngx_null_command时, 会停止使用当前模块解析该配置项. 每个ngx_command_t结构体中都定义了一个该结构体感兴趣的配置项

struct ngx_command_s {
ngx_str_t             name;                  //配置项的名称, 比如daemon, gzip等
ngx_uint_t            type;                  //1. 配置项可能出现的位置, 在main中? srv中? lco中?    2. 配置项可携带的参数
//出现了name中指定的配置项后, 将会调用set方法处理配置项的参数, 比如配置项daemon的参数on 出现后, 我们怎么处理?off出现又怎么处理
char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
ngx_uint_t            conf;                  //在配置文件中的偏移量. 在main中? srv中? loc?
ngx_uint_t            offset;                //这里会使用到offsetof函数, 表示这一配置指令在这个模块信息存储结构中的位置.可以man一下offsetof
void                 *post;                  //配置项读取后的处理方法, 很少用
}


好了, 上面从整体到局部介绍了我们定义模块需要包含的内容(需要使用到的数据结构), 下面就从局部开始, 一步一步定义我们自己的模块

对于这个简单的HTTP模块, 我们希望它这样介入nginx:

不希望模块对所有的HTTP请求起作用

在nginx配置文件中的location块内定义ben_test配置项, 如果一个用户通过主机名,URI等匹配上了相应的配置块, 且这个配置块下具有ben_test配置项, 那么希望ben_test模块处理这个请求

Part2. 定义模块需要的信息存储结构

首先 ngx_command_t

定义ben_test配置项的处理. 上文介绍了, 下面我们将使用ngx_http_ben_test这个函数来处理这个配置项.

static ngx_command_t ngx_http_ben_test_commands[] = {
{
ngx_string("ben_test"),                     //名字这里是ben_test, 在官方配置中, 这里可能是gzip? 是work_connections?...
NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1,        //指明是放在location block中的, 只带一个参数
ngx_http_ben_test,                //用来处理此指令的handler是此函数
NGX_HTTP_LOC_CONF_OFFSET,            //是放在location block的
offsetof(ngx_http_ben_test_loc_conf_t, words),    //此指令在信息存储结构中的位置(offsetof是一个标准库的函数)
NULL
},

/*
{
...   //当然, 如果此模块还有其他的配置项, 那么也要定义在这个commands数组中. 目前只有ben_test这一个配置项
}
*/
ngx_null_command                    //必须有这个作为结尾
};


接下来ngx_http_module_t

定义好了commands数组, 下面继续定义http模块的数据结构ngx_http_module_t

static ngx_http_module_t ngx_http_ben_test_module_ctx = {
NULL,
NULL,

NULL,
NULL,

NULL,
NULL,

ngx_http_ben_test_create_loc_conf,
NULL
};


这部分是模块上下文,nginx解析配置文件的时候会用到这里面定义的函数来分配内存,拷贝内容。

这里我们暂时不需要太多的功能, 只需要一个初始化我们的存储信息结构的函数就足够

最后 ngx_module_t

把上面的内容整合在一起

ngx_module_t ngx_http_ben_test_module = {
NGX_MODULE_V1,                   //默认的初始化动作
&ngx_http_ben_test_module_ctx,
ngx_http_ben_test_commands,
NGX_HTTP_MODULE,
//下面几个NULL是函数指针设置为空, 这些函数是会在整个nginx执行过程中被调用. 也就是说, 即使我们关闭了http模块, 这些函数还是会执行
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING           //默认的初始化动作
};


这样, ben_test模块在编译时将会被加入到ngx_modules全局数组中.

Part3. 定义存储结构中使用到的函数

上面提到多次存储用的结构体, 那么到底是什么样的呢?

所以, 在介绍函数之前, 我们需要先声明ben_test模块中会使用到的信息存储结构体.

//是的, 它就是用来接收test_ben配置项解析结果的结构体. 此结构体只包含一个字符串类型变量, 说明ben_test的参数应该是一个字符串, 我们将在解析参数后存储到这个变量中
typedef struct{
ngx_str_t words;
}ngx_http_ben_test_loc_conf_t;


鉴于在介绍ngx_http_module_t时, 提到了函数调用的顺序.

下面我们先介绍ngx_http_module_t模块中声明的唯一的一个函数ngx_http_ben_test_create_loc_conf, 它的作用是创建存储全局配置项的结构体

static void *ngx_http_ben_test_create_loc_conf(ngx_conf_t *cf)
{
ngx_http_ben_test_loc_conf_t *conf;          //这是我们自己的模块信息存储结构体
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_ben_test_loc_conf_t));  //使用内存池为它分配一段内存
if(NULL == conf)
return NGX_CONF_ERROR;

conf->words.data = NULL;                    //初始化此字符串为空
conf->words.len = 0;

return conf;
}


很简单吧, 在解析我们模块内容前, 我们会先创建存储全局配置项的结构体.

创建完了结构体, 下面就是在遇到我们的配置项时候, 会调用的函数了. 此函数被声明在commands中对应的项中.

函数名为ngx_http_ben_test. 此函数的原型是:

typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *t);
实际处理请求的方法ngx_http_ben_test将接收一个ngx_http_request_t类型的参数, 返回ngx_int_t类型的结果.

对于此函数的返回值, 它可以是HTTP中响应包的返回码. 比如若是用户在请求行中使用了PUT方法, 但我们是不支持的, 于是返回NGX_HTTP_NOT_ALLOWED, 这样nginx就会构造下面这样的包给用户.

http/1.1 405 NOT ALLOWED
Server: nginx/1.x.xx
....


下面就贴出此函数的代码.

static char *ngx_http_ben_test(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){
ngx_http_core_loc_conf_t *clcf;

//首先找到ben_test配置项所属的模块
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
//将其处理函数设置为ngx_http_ben_test_handler
clcf->handler = ngx_http_ben_test_handler;

//处理参数, 由nginx内部提供, 用于将配置项后的参数存储到此模块的存储结构中去
ngx_conf_set_str_slot(cf, cmd, conf);

return NGX_CONF_OK;
}


这样设置后, HTTP框架会在NGX_HTTP_CONTENT_PHASE阶段调用我们的这个函数ngx_http_ben_test_handler处理这个配置项

NGX_HTTP_CONTENT_PHASE阶段是处理HTTP请求内容, 响应内容产生的阶段, 大部分HTTP模块都会选择介入这个阶段

除了设置好将来某时刻调用的函数外, ngx_http_ben_test函数调用ngx_conf_set_str_slot函数处理配置项的参数. 因为之前已经使用ngx_http_ben_test_create_loc_conf函数创建了此结构体的对象了, 所以此时只需要为其中的变量进行赋值

//此函数只是作为补充便于理解全局出现在这里
char *
ngx_conf_set_str_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *p = conf;  //获取当前配置结构体信息ngx_http___conf_t

ngx_str_t *field, *value;
ngx_conf_post_t *post;

field = (ngx_str_t *) (p + cmd->offset); //ngx_command_t的offset作为p的偏移量
/*ngx_command_t的 ngx_uint_t conf指明offset的位置是在哪个配置结构体信息中(main\server\loc)在ngx_http_config.h中定义了conf的三个取值,如下
#define NGX_HTTP_MAIN_CONF_OFFSET  offsetof(ngx_http_conf_ctx_t, main_conf)
#define NGX_HTTP_SRV_CONF_OFFSET   offsetof(ngx_http_conf_ctx_t, srv_conf)
#define NGX_HTTP_LOC_CONF_OFFSET   offsetof(ngx_http_conf_ctx_t, loc_conf)
typedef struct {
void        **main_conf;
void        **srv_conf;
void        **loc_conf;
} ngx_http_conf_ctx_t; 获取到保存指令参数的配置结构体conf中,offset指的是在结构体重偏移位置
ngx_conf_t *cf是获取当前环境中的读取配置文件的配置信息包含指令名称指令参数
*/

if (field->data) {
return "is duplicate";
}

value = cf->args->elts; //获取指令数组

*field = value[1]; //获取第一个参数

if (cmd->post) {
post = cmd->post;
return post->post_handler(cf, post, field);
}

return NGX_CONF_OK;
}


最后, 就是ngx_http_ben_test_handler函数了, 此函数的作用就是处理请求并产生响应

static ngx_int_t ngx_http_ben_test_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_buf_t *b;
//通过ngx_chain_t来维护我们的输出。ngx_chain_t是一条链,其中的元素都是ngx_buf_t(缓冲区),所以内容生成很灵活,如果需要输出什么,就构造一个缓冲区,然后把缓冲区加入缓冲区链
ngx_chain_t out[2];

//之前已经处理过了此模块需要的信息存储结构, 下面获取此结构
ngx_http_ben_test_loc_conf_t *btcf;
btcf = ngx_http_get_module_loc_conf(r, ngx_http_ben_test_module);

//响应头的类型
r->headers_out.content_type.len = sizeof("text/plain") - 1;
r->headers_out.content_type.data = (u_char *)"text/plain";

b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));

//out[0]用于输出响应头
out[0].buf = b;
out[0].next = &out[1];

b->pos = (u_char *)"My darling ";
b->last = b->pos + sizeof("My darling ") - 1;
b->memory = 1;

b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));

//out[1]用于输出响应体, 它的内容是配置项的参数
out[1].buf = b;
out[1].next = NULL;

b->pos = btcf->words.data;
b->last = b->pos + btcf->words.len;
//该段内容当前是存储在内存中的, 是响应内容的最后一段缓存
b->memory = 1;
b->last_buf = 1;

r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = btcf->words.len + sizeof("My darling ") - 1;

//发送响应头
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[0]);
}


这样, 当有请求过来, 只要匹配我们的配置项, 我们就会对其进行处理并产生响应包.

Part4. 将自定义模块添加到Nginx中

到此为止, 模块的定义已经完成了, 只要将上面的多个数据结构体以及几个函数(除了nginx自带的ngx_conf_set_str_slot)放在一个c文件下就可以了.

只是, 单单写一个c文件有什么用呢? 编译的时候谁知道它的存在呢?

Nginx提供了一种简单的方式将第三方的模块编译到Nginx中. 首先把源代码文件全部放到一个目录中, 同时在该目录中编写一个文件用于通知Nginx如何编译模块, 这个文件必须命名为config. 这样, 只要在configure脚本执行时加入参数 --add-module=/path/to/file就可以了

现在, 源代码我们已经完成了, 下面就是如何编写一个config文件.

config文件其实就是一个可执行的shell脚本. 因为当前我们只是开发HTTP模块, 所以当前我们只需要3个变量

1. ngx_addon_name: 仅在configure执行时使用. 一般设置为自定义的模块名称

2. HTTP_MODULES: 保存所有的HTTP模块的名称, 每个HTTP模块间由空格相连. 因为我们要添加模块, 所以在设置HTTP_MODULES的时候不是直接覆盖, 是添加.

使用方法: HTTP_MODULES="$HTTP_MODULES ngx_http_ben_test_module"

3. NGX_ADDON_SRCS: 用于指定新增的模块的源代码. 指定时可以使用在configure后指定的--add-module=/path/to/file, 其中的目录由变量$ngx_addon_dir指向

所以, 编写config可以像下面这样:

ngx_addon_name=ngx_http_ben_test_module
HTTP_MODULES="$HTTP_MODULES ngx_http_ben_test_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_ben_test_module.c"
到了这里, 我们总结一下大致过程:

创建自定义模块的目录 ---> 编写config ---> 编写源代码 ---> ./configure --add-module=/path/to/file ---> make ---> make install

Part5: 定义自己的配置文件

经过上面的处理步骤, 我们自定义的模块已经成功被添加到nginx中去了.

想要实验此模块, 最后一步要做的就是在配置文件中加入我们的配置项, 我们自定义的配置项目的是: 接收到用户的请求后, 向对方发送一端字符串.

于是, 配置文件像下面这样就足够了

#user  nobody;
worker_processes  1;

events {
worker_connections  1024;
}

http {
keepalive_timeout  65;
server {
listen       80;
server_name  localhost;

location /ben_test {
ben_test hello;
}
}
}


配置完成后, 重新编译安装nginx.

./configure --add-module=/path/to/module

make

make install
.../nginx/sbin/nginx -c /path/to/config

启动后, 访问网址: http://127.0.0.1:80/ben_test, 就应该能接收到服务器返回的数据

Part6. 附加, 注意

这里还有一点要注意, handler模块真正的处理函数(我们这里的ngx_int_t ngx_http_ben_test_handler)通过两种方式挂载到处理过程中,一种方式就是按处理阶段(在ngx_http_module_t中的postconfiguration函数中进行)挂载;另外一种挂载方式就是按需挂载(即本文中的挂在方式)。

下面有个文章整理的这两种方式: http://www.cnblogs.com/paulweihan/p/4664784.html?utm_source=tuicool
本文代码已经过验证, 可实现.

参考: <深入理解nginx>

Nginx从入门到精通
http://blog.chinaunix.net/uid-22400280-id-3218765.html http://blog.sina.com.cn/s/blog_7303a1dc0100x70n.html
这里有一篇编写filter模块的文章:

/article/1328331.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: