1. 目标与预备知识
本文以开发一个简单nginx模块为例,讲述nginx模块开发的入门知识。此模块名叫shell,顾名思义,它的功能和Linux shell类似;但为了简单起见,我们只提供以下几个指令:
- ls: 列出指定目录下的文件以及子目录;支持三个选项:-l -h -a;
- cat: 输出一个文件的内容(暂不支持多文件,虽然这有点违背cat命令的本意--连接多个文件);没有选项;
- head: 输出一个文件的前n行;支持一个选项:-n,n为正整数;
- tail: 输出一个文件的后n行;支持一个选项:-n,n为正整数;
- 1.a) nginx会回调ngx_module_t::ctx (对于http模块是ngx_http_module_t类型) 中的钩子函数,创建模块的conf结构体。一个模块根据自己所处的层(http, server, location等等)以及自己的需要,决定自己需要那些conf结构体。例如我们的shell module,只需要loc conf,所以就在ngx_module_t.ctx->create_loc_conf()函数中创建了一个ngx_http_shell_loc_conf_t实例。
- 1.b) nginx还会回调ngx_module_t::ctx中的merge函数来合并conf结构体;这是因为一个模块的配置指令可能在不同层级,最终要合并到一起。例如我们的shell module,document_root指令可以出现在server层,也可以出现在location层。nignx在处理server层指令时,把documrent_root的参数存储在server层的conf结构体中;到了location层,就把上层(server层)的documrent_root的参数和下层document_root参数合并,合并方式就是我们定义的merge_loc_conf函数(里面再调用nginx提供的工具函数ngx_conf_merge_str_value,实现下层覆盖上层)。从下面的测试可以看到,nginx启动时,为我们的shell module创建了10个conf (调用了10次create_loc_conf函数,即创建了10个ngx_http_shell_loc_conf_t实例)。看我们的nginx.conf:http层创建了1个;端口为8080的server层创建1个,内部的location创建1个;9090server创建1个,其中的6个location创建6个。并且,nginx对这10个conf进行了9次merge(调用了9次merge_loc_conf函数),应该是最终形成7个最底层(location层)conf实例。
- 2. 处理一个配置块(例如location),先调用每个module的ctx中的钩子函数,create/merge conf,如上所述。然后处理指令,即调用ngx_module_t::commands中的ngx_command_t::set函数。顾名思义,set函数不是对应指令(head,tail,ls,cat)的content handler函数,而是它们的配置函数。例如,我们的shell module,在处理指令"head -8"时,就调用ngx_http_shell_head函数(即head指令的set函数)。这个函数做了一个重要的事情:clcf->handler = ngx_http_shell_handler; 这个函数才是指令的content handler函数,即nginx运行时,处理"curl http://127.0.0.1/shell/head/{filename}"时,调用ngx_http_shell_handler来生成response。
- 3. 此外,我们还为shell module注册了phase handlers。nginx运行时,处理一个请求时,经过不同的阶段时,就会调用不同的phase handler。
2. 模块代码结构
modules/└── ngx_http_shell_module├── config├── ngx_http_shell_module.c├── 其他代码文件
一般情况下,模块的目录为ngx_{module-type}_{module-name}_module;模块入口代码文件为ngx_{module-type}_{module-name}_module.c。除了入口代码文件和其他代码文件之外,还有一个至关重要的文件:config。注意,它和nginx.conf是毫无关系的;这个config文件定义了新增模块的名字,源代码的位置等信息,是为nginx配置、编译和安装服务的;它的内容如下:
ngx_addon_name=ngx_http_shell_module
HTTP_MODULES="$HTTP_MODULES ngx_http_shell_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_shell_module.c $ngx_addon_dir/file1.c $ngx_addon_dir/file2.c"
假设模块代码已经完成,我们需要重新configure:
./configure --prefix=/usr/local/nginx-1.10.0 \
--user=nobody \
--group=nobody \
--without-select_module \
--without-poll_module \
--with-http_ssl_module \
--add-module=modules/ngx_http_shell_module <--- 加入我们新增的模块
然后重新编译并安装:
make
make install
为了测试方便,我把这些写成一个脚本test.sh:
#!/bin/bash
case "$1" in
config)
echo "config"
./configure --prefix=/usr/local/nginx-1.10.0 \
--user=nobody \
--group=nobody \
--without-select_module \
--without-poll_module \
--with-http_ssl_module \
--add-module=modules/ngx_http_shell_module
exit $?
;;
deploy)
/usr/local/nginx-1.10.0/sbin/nginx -s stop
rm -fr /usr/local/nginx-1.10.0/
make
make install
/usr/local/nginx-1.10.0/sbin/nginx
exit $?
;;
*)
echo "Usage: $0 config|deploy"
exit 1
;;
esac
若只更新代码时而没有改变代码结构(增加、删除代码文件),只需要重新编译安装:
./test.sh deploy
若改变了代码结构,则需要重新configure,并重新编译安装:
./test.sh config
./test.sh deploy
好了,准备工作差不多了,下面开始实际内容:编写模块的代码。
3. 模块开发
3.1 配置文件
user nobody;
worker_processes 4;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost-proxied;
root /data/upstream1;
location / {
}
}
server {
listen 9090;
server_name localhost-proxy;
document_root /home/yuanguo.hyg/workspace;
location / {
proxy_pass http://localhost:8080/;
}
location ~\.(gif|jpg|png)$ {
root /data/images;
}
location /shell/ls/ {
ls -a;
ls -l;
ls -h;
}
location /shell/head/ {
head -8;
}
location /shell/tail/ {
tail -12;
}
location /shell/cat/ {
cat;
}
}
}
继承前一篇博客点击打开链接,若用户请求.gif或.jpg或.png结尾的文件,则直接从/data/images/目录提供。不同的是,用户可以使用:
http://{server-ip}/shell/{ls|cat|head|tail}/{sub-path}
来查看目录或读取文件内容。{sub-path}的宿主目录由document_root来配置。其他请求仍转发给localhost:8080服务来处理。
举例来说,当用户请求http://192.168.75.138/shell/cat/workspace/test.cpp时,会输出文件/home/workspace/test.cpp的内容;当用户请求http://192.168.75.138/shell/cat/workspace/会列出目录/home/workspace/下的目录和文件。head和tail类似。
当nginx接收到一个http请求时,它通过查找配置文件来把请求映射到一个location,location中配置的指令来处理请求。指令会启动它所在的模块,模块中定义了此指令的handler和filter(本文暂不涉及filter);请求最终由handler来处理。粗略地,可由下图表示(图片来自网络):
3.2 模块配置结构体
模块的配置结构用于存储从配置文件读进来的指令的参数(在本例中,document_root的参数、ls的参数、head和tail的参数)。根据nginx模块开发规则,这个结构命名为:ngx_{module-type}_{module-name}_{main|srv|oc}_conf。其中main、srv和loc分别用于表示同一模块在mian、server和location三层中的配置信息。我们的shell模块只需要loc配置。问题:document_root可以出现在main和server层中,它的参数也可以保存在loc配置中,为什么?
typedef struct
{
ngx_str_t doc_root;
ngx_array_t* ls_opts;
ngx_str_t head_n;
ngx_str_t tail_n;
} ngx_http_shell_loc_conf_t;
字符串类型的参数doc_root是指令document_root的参数;这个指令可以出现在main、server或location块中。ls_opts是指令ls的参数,它是字符串数组类型(因为我们允许三个字符串作为参数“-l”, "-a", "-h")。head_n和tail_n分别是指令head和tail的参数,它们和linux shell命令head和tail的-n参数类似。在nginx中,字符串通过ngx_str_t来表示;字符串操作也被重新定义,例如:ngx_strlen, ngx_strcmp, ngx_memset, ngx_memcpy等;数组由ngx_array_t表示,它也有一些操作函数,例如:ngx_array_create, ngx_array_init, ngx_array_push, ngx_array_destroy等。
3.3 模块定义
模块是通过如下这么一个结构体定义的。模块的入口文件(ngx_http_shell_module.c)的核心就是定义这么一个结构体,当然这个结构体涉及到其他内嵌结构体和一些钩子函数。
struct ngx_module_s {
ngx_uint_t ctx_index;
ngx_uint_t index;
char *name;
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t version;
const char *signature;
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;
};
这个结构体就定义了整个模块;按照nginx的命名规则,结构体变量的名字是ngx_{module-type}_{module-name}_module;所以我们的shell模块定义为:
ngx_module_t ngx_http_shell_module =
{
NGX_MODULE_V1,
&ngx_http_shell_module_ctx,
ngx_http_shell_commands,
NGX_HTTP_MODULE,
init_master,
init_module,
init_process,
init_thread,
exit_thread,
exit_process,
exit_master,
NGX_MODULE_V1_PADDING
};
下面几节逐一看其中内嵌的结构体或者钩子函数。
3.4 填充字段
我们暂不关心下面两部分字段:
ngx_uint_t ctx_index;
ngx_uint_t index;
char *name;
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t version;
const char *signature;
和
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_http_shell_module结构体中,这两部分分别由NGX_MODULE_V1和NGX_MODULE_V1_PADDING填充成默认值。
#define NGX_MODULE_V1 \
NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX, \
NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE
#define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0
3.5 模块类型
由于我们的shell模块是http模块,所以在ngx_http_shell_module结构体中,ngx_uint_t type被初始化成NGX_HTTP_MODULE。
3.6 初始化和退出钩子函数
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);
一般情况下不需要关心这些钩子函数,直接设置成NULL即可。但是,为了搞清楚它们分别在什么时候被调用,我简单的把它们实现为输出一行log。注意这里面没有与init_module对应的exit_module函数,其他三对都是init和exit对应。
static ngx_int_t init_master(ngx_log_t *log)
{
ngx_log_error(NGX_LOG_ERR, log, 0, "%s", __func__);
return NGX_OK;
}
static ngx_int_t init_module(ngx_cycle_t *cycle)
{
ngx_log_error(NGX_LOG_ERR, cycle->log, 0, "%s", __func__);
return NGX_OK;
}
static ngx_int_t init_process(ngx_cycle_t *cycle)
{
ngx_log_error(NGX_LOG_ERR, cycle->log, 0, "%s", __func__);
return NGX_OK;
}
static ngx_int_t init_thread(ngx_cycle_t *cycle)
{
ngx_log_error(NGX_LOG_ERR, cycle->log, 0, "%s", __func__);
return NGX_OK;
}
static void exit_thread(ngx_cycle_t *cycle)
{
ngx_log_error(NGX_LOG_ERR, cycle->log, 0, "%s", __func__);
}
static void exit_process(ngx_cycle_t *cycle)
{
ngx_log_error(NGX_LOG_ERR, cycle->log, 0, "%s", __func__);
}
static void exit_master(ngx_cycle_t *cycle)
{
ngx_log_error(NGX_LOG_ERR, cycle->log, 0, "%s", __func__);
}
这样实现之后,在启动nginx时,会在error.log中看见如下log:
# cat /usr/local/nginx-1.10.0/logs/error.log
2016/05/17 18:40:33 [error] 78301#0: init_module
2016/05/17 18:40:33 [error] 78305#0: init_process
2016/05/17 18:40:33 [error] 78306#0: init_process
2016/05/17 18:40:33 [error] 78303#0: init_process
2016/05/17 18:40:33 [error] 78304#0: init_process
init_master没有打印log,这可能是因为master初始化时log模块还未就绪(待确认);init_thread何时调用(待确认);我们配置了4个worker processes(在nginx.conf配置worker_processes),所以init_process被调用4次;
在nginx退出时,可以看见如下log:
# /usr/local/nginx-1.10.0/sbin/nginx -s quit
[root@localhost nginx-1.10.0]# cat /usr/local/nginx-1.10.0/logs/error.log
......
2016/05/17 18:47:25 [notice] 78323#0: signal process started
2016/05/17 18:47:25 [error] 78303#0: exit_process
2016/05/17 18:47:25 [error] 78304#0: exit_process
2016/05/17 18:47:25 [error] 78305#0: exit_process
2016/05/17 18:47:25 [error] 78306#0: exit_process
2016/05/17 18:47:25 [error] 78302#0: exit_master
可见,4个worker processes退出之后master process退出。
3.7 模块的context
模块定义结构体ngx_module_t ngx_http_shell_module中包含模块的context(字段ctx被初始化为&ngx_http_shell_module_ctx)。现在我们
就来定义结构体变量ngx_http_shell_module_ctx,它的类型是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;
其中包含八个钩子函数,分别在不同时刻被nginx调用。我们只需关注其中最后两个。我还是希望把它们实现为只输出一行log的占位函数以便观察它们在什么时候被调用,但是对于create_main_conf和create_srv_conf却不行(它们需要实际的创建main conf和server conf)。所以,最终我把create_main_conf和create_srv_conf设置为NULL(这样nginx就会调用默认的函数);把preconfiguration、postconfiguration、init_main_conf和merge_srv_conf实现为只打印log的占位函数;着重实现最后两个函数create_loc_conf和merge_loc_conf。结构体变量ngx_http_shell_module_ctx的定义如下:
static ngx_http_module_t ngx_http_shell_module_ctx =
{
preconfiguration,
postconfiguration,
NULL,
ini