nginx模块开发之一

1. 目标与预备知识

本文以开发一个简单nginx模块为例,讲述nginx模块开发的入门知识。此模块名叫shell,顾名思义,它的功能和Linux shell类似;但为了简单起见,我们只提供以下几个指令:

  • ls:    列出指定目录下的文件以及子目录;支持三个选项:-l -h -a;
  • cat:   输出一个文件的内容(暂不支持多文件,虽然这有点违背cat命令的本意--连接多个文件);没有选项;
  • head:  输出一个文件的前n行;支持一个选项:-n,n为正整数;
  • tail:  输出一个文件的后n行;支持一个选项:-n,n为正整数;
确切的说,本文只是讲述nginx http模块开发的入门知识,因为nginx有多种模块:http模块,event模块,mail模块等,我们只局限于http模块。
除了掌握C语言,你还应具有nginx的入门知识,例如nginx的编译安装,配置文件的结构等;若你还没有这些准备,可以参考前一篇博客:
启动阶段:
  • 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. 模块代码结构

nginx的模块是静态链接的,也就是说你不能向开发apache的模块,或者tomact的wepapp那样,开发一个包,动态的部署到apache或tomcat中。nginx模块需要和nginx的源代码一起编译并安装。为此,我们在nginx的源代码中建立一个modules目录,然后在此目录下开发我们的shell模块。
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
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值