nginx模块开发实战

必备知识

  想要进行nginx模块开发,首先你需要比较熟悉C语言,其次你需要对HTTP协议有一定的了解。

从配置开始

  Nginx主配置文件中主要包括六块:main,events,http,server,location,upstream 结构如下:
#main
...
events {
    ...
}
http {
    ...
    upstream {
        ...
    }
    server {
        ...
        location {
           ...
        }
    }
}

main块:主要控制Nginx子进程的所属用户/用户组、派生子进程数、错误日志位置/级别、pid位置、子进程优先级、进程对应cpu、进程能够打开的文件描述符数目等。
events块:控制Nginx处理连接的方式。
http块:是Nginx处理http请求的主要配置模块,大多数配置都在这里面进行。
server块:是Nginx中主机的配置块,可以配置多个虚拟主机。
location块:是server中对应的目录级别的控制块,可以有多个。
upstream块:是Nginx做反向代理和负载均衡的配置块,可以有多个。
  其中我们经常关注的模块有main、server、upstream和location。main部分设置的指令将影响其它所有设置;server部分的指令主要用于指定主机和端口;upstream的指令用于设置一系列的后端服务器;location部分用于匹配网页位置(比如,根目录“/”,“/images”,等等)。他们之间的关系式:server继承main,location继承server;upstream既不会继承指令也不会被继承,它有自己的特殊指令,不需要在其他地方应用。 
  

模块概述

  Nginx模块主要有三种角色: 
    handlers :处理http请求并构造输出。
    filters :处理handler产生的输出。
    load-balancers :当有多于一个的后端服务器时,选择一台将http请求发送过去。
  Nginx中许多工作都是由模块来完成的。任何时候,Nginx提供文件或者转发请求到另一个server,都是通过handler来实现的。而当需要对输出在服务端加一些东西的话,filter就派上用场了。如果handler的作用是把请求反向代理到后端服务器,那么就会用到模块的第三种角色load-balancer了。如果handler正常返回,那么filter就会被调用。filter采用了经典的“接力链表”模式:一个filter被调用并处理,接下来调用下一个filter,直到最后一个filter被调用完成,Nginx才真正完成响应流程。
  总结一下,一个典型的请求响应周期: 客户端发送HTTP请求 → Nginx基于location的配置选择一个合适的handler → (如果有) load-balancer选择一个后端服务器 → Handler处理请求并将响应发送给第一个filter → 第一个filter讲输出交给第二个filter → 第二个给第三个→ 以此类推 → 最终响应发送给客户端 。
  之所以说“典型地”是因为Ngingx的模块具有很强的定制性。模块开发者需要花很多精力精确定义模块在何时如何产生作用。模块调用实际上是通过一系列的回调函数做到的。理论上来讲,你的函数可以在以下时候被执行:
  ● server读取配置文件之前
  ● 读取location和server的每一条配置指令
  ● 当Nginx初始化main配置段时
  ● 当Nginx初始化server配置段时(例如:host/port)
  ● 当Nginx合并server配置和main配置时
  ● 当Nginx初始化location配置时
  ● 当Nginx合并location配置和它的父server配置时
  ● 当Nginx的主进程启动时
  ● 当一个新的worker进程启动时
  ● 当一个worker进程退出时
  ● 当主进程退出时
  ● handler一个请求
  ● Filter响应头
  ● Filter响应体
  ● 选择一个后端服务器
  ● 初始化一个将发往后端服务器的请求
  ● 重新初始化一个将发往后端服务器的请求
  ● 处理来自后端服务器的响应
  ● 完成与后端服务器的交互
  

模块的组成

1.模块的存储结构

 模块的存储struct有三种,分别是main,server和location。绝大多数模块仅需要一个location配置。名称约定如下:ngx_http_<module name>_(main|srv|loc)_conf_t. 例如:
typedef struct
{
    ngx_str_t hello_string;
    ngx_int_t hello_counter;
} ngx_http_hello_loc_conf_t;

这是后面将要讲的hello模块的存储结构的定义。

2.模块指令的定义

  配置指令是用来定义你所开发模块的配置信息,例如指令的名称、参数等信息。它是一个静态的ngx_command_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_null_command
};

下面我们来分析下ngx_command_t 结构体。

struct ngx_command_t {
    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 这个字段表示我们所开发模块的指令名称。ngx_str_t 就理解成一个String类型就可以了。
type 表示该指令的作用域,即这个指令配置在哪里,以及指令参数的个数。 ngx_uint_t 理解成一个无符号整型。  
  可选的值有如下一些:
  NGX_HTTP_MAIN_CONF:指令出现在main配置部分是合法的
  NGX_HTTP_SRV_CONF:指令在server配置部分出现是合法的 config
  NGX_HTTP_LOC_CONF:指令在location配置部分出现是合法的
  NGX_HTTP_UPS_CONF:指令在upstream配置部分出现是合法的
  NGX_CONF_NOARGS:指令没有参数
  NGX_CONF_TAKE1:指令读入1个参数
  NGX_CONF_TAKE2:指令读入2个参数
  ……
  NGX_CONF_TAKE7:指令读入7个参数
  NGX_CONF_FLAG:指令读入1个布尔型数据 (“on” or “off”)
  NGX_CONF_1MORE:指令至少读入1个参数
  NGX_CONF_2MORE:指令至少读入2个参数
  ……
  NGX_CONF_TAKE12:配置指令接受1个或者2个参数。
  NGX_CONF_TAKE13:配置指令接受1个或者3个参数。
  ……
  NGX_CONF_MULTI: 配置指令可以接受多个参数,即个数不定。
  NGX_CONF_BLOCK:配置指令可以接受的值是一个配置信息块。也就是一对大括号括起来的内容。里面可以再包括很多的配置指令。比如常见的server指令就是这个属性的。
  NGX_CONF_ANY:配置指令可以接受的任意的参数值。一个或者多个,或者’on’或者’off’,或者是配置块。
set 是一个函数指针,它指向的函数用来进行模块配置;它一般用来将配置文件中的参数传递给程序,并保存在配置结构体中。此函数有三个入参:
指向结构体 ngx_conf_t 的指针, 这个结构体里包含需要传递给指令的参数
指向结构体 ngx_command_t 的指针
指向模块自定义配置结构体的指针
这个函数会在遇到指令时执行,Nginx提供了多个函数用来保存特定类型的参数数据,这些函数包含有:
    ngx_conf_set_flag_slot:将 “on” or “off” 转换成 1 or 0
    ngx_conf_set_str_slot:将字符串保存为 ngx_str_t
    ngx_conf_set_num_slot:解析一个数字并保存为int
    ngx_conf_set_size_slot:解析一个数据大小, 并保存为size_t
conf 该字段指定当前配置项存储的内存位置,因为http模块对所有http模块所要保存的配置信息,划分了main, server和location三个地方进行存储,每个地方都有一个内存池用来分配存储这些信息的内存。可取的值有:NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET及NGX_HTTP_LOC_CONF_OFFSET,当然这里你可以填写0,即默认为NGX_HTTP_MAIN_CONF_OFFSET。
offset 指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。例如上面提到的offsetof(ngx_http_hello_loc_conf_t, hello_string) 表示存储到ngx_http_hello_loc_conf_t结构体的hello_string结构成员中。
post 该字段存储一个指针。可以指向任何一个在读取配置过程中需要的数据,以便于进行配置读取的处理。大多数时候,都不需要,所以简单地设为0即可。

3.模块上下文

  模块上下文定义了一大坨函数引用,用来创建和合并三个部分的配置(main,server,location),它是一个静态的ngx_http_module_t结构体,命名方式一般是:ngx_http__module_ctx 。这些函数引用依次是:
  ◇ preconfiguration: 在读入配置前调用
  ◇ postconfiguration: 在读入配置后调用
  ◇ create_main_conf: 在创建main配置时调用
  ◇ init_main_conf: 在初始化main配置时调用
  ◇ init_main_conf: 在创建server配置时调用
  ◇ merge_srv_conf: 合并server和main配置时调用
  ◇ create_loc_conf: 创建location配置时调用
  ◇ merge_loc_conf: 合并location和server配置时调用
来看下结构体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;

4.模块的定义

  任何模块,我们都需要定义一个ngx_module_t 类型的变量,来说明这个模块本身的信息,这是这个模块最重要的一个信息,它告诉了nginx这个模块的一些信息,上面定义的配置信息,还有模块上下文信息。加载模块的上层代码,都需要通过定义的这个结构,来获取这些信息。
  

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;
};
#define NGX_NUMBER_MAJOR  3
#define NGX_NUMBER_MINOR  1
#define NGX_MODULE_V1 0, 0, 0, 0, NGX_DSO_ABI_COMPATIBILITY, NGX_NUMBER_MAJOR, NGX_NUMBER_MINOR
#define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0

举个hello模块的例子:

ngx_module_t ngx_http_hello_module = {
        NGX_MODULE_V1,                 /* 前7个结构成员*/
        &ngx_http_hello_module_ctx,    /* 模块上下文 */
        ngx_http_hello_commands,       /* 模块指令 */
        NGX_HTTP_MODULE,               /* 模块类型,我们所开发的基本都是NGX_HTTP_MODULE类型 */
        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          /* 后8个结构成员*/
};

Handler模块

  Handler一般做4件事:获取location配置、生成合适的响应、发送响应头、发送响应体。Handler有一个参数,即请求结构体。请求结构体包含很多关于客户请求的有用信息,比如说请求方法,URI,请求头等等。

Handler模块结构

  在开发Handler模块时,除了提供上一节介绍的的基本结构体外,handler还需要提供一个处理函数。这个函数负责对来自客户端请求的真正处理。这个函数的处理,既可以选择自己直接生成内容,也可以选择拒绝处理,由后续的handler去进行处理,或者是选择丢给后续的filter进行处理。来看一下这个函数的原型声明:
  

/*r 是http请求, 里面包含请求所有的信息 */
typedef ngx_int_t (*ngx_http_handler_pt) (ngx_http_request_t * r);

该函数处理成功返回NGX_OK,处理发生错误返回NGX_ERROR,拒绝处理(留给后续的handler进行处理)返回NGX_DECLINE。 返回NGX_OK也就代表给客户端的响应已经生成好了,否则返回NGX_ERROR就发生错误了。

Handler模块的挂载

  当一切该定义的结构都已经定义好之后,你需要指定所编写的模块挂载的具体方式及位置。handler有两种挂载方式:
   1. 按处理阶段挂载
   2. 按需挂载

按处理阶段挂载

  为了更精细地控制对于客户端请求的处理过程,nginx把这个处理过程划分成了11个阶段。他们从前到后,依次列举如下:
   ● NGX_HTTP_POST_READ_PHASE:读取请求内容阶段
   ● NGX_HTTP_SERVER_REWRITE_PHASE:Server请求地址重写阶段
   ● NGX_HTTP_FIND_CONFIG_PHASE:配置查找阶段
   ● NGX_HTTP_REWRITE_PHASE:Location请求地址重写阶段
   ● NGX_HTTP_POST_REWRITE_PHASE:请求地址重写提交阶段
   ● NGX_HTTP_PREACCESS_PHASE:访问权限检查准备阶段
   ● NGX_HTTP_ACCESS_PHASE:访问权限检查阶段
   ● NGX_HTTP_POST_ACCESS_PHASE:访问权限检查提交阶段
   ● NGX_HTTP_TRY_FILES_PHASE:配置项try_files处理阶段
   ● NGX_HTTP_CONTENT_PHASE:内容产生阶段
   ● NGX_HTTP_LOG_PHASE:日志模块处理阶段
一般情况下,我们自定义的模块,大多数是挂载在NGX_HTTP_CONTENT_PHASE阶段的。挂载的动作一般是在模块上下文调用的postconfiguration函数中。使用这种方式挂载的handler也被称为 content phase handler。举个hello模块挂载的实例:

static ngx_int_t ngx_http_hello_init(ngx_conf_t *cf)
{
        ngx_http_handler_pt  *h;
        ngx_http_core_main_conf_t  *cmcf;
        /** http模块主配置 */
        cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
        /** 挂载到NGX_HTTP_CONTENT_PHASE阶段 */
        h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
        if (h == NULL) {
            return NGX_ERROR;
        }
        /** 指定handler处理函数 */
        *h = ngx_http_hello_handler;
        return NGX_OK;
}

注意:有几个阶段是特例,它不调用任何挂载的handler,所以你在开发过程中不要挂载到这几个阶段:
 ● NGX_HTTP_FIND_CONFIG_PHASE
 ● NGX_HTTP_POST_ACCESS_PHASE
 ● NGX_HTTP_POST_REWRITE_PHASE
 ● NGX_HTTP_TRY_FILES_PHASE
所以其实只有7个phase你可以挂载自己的handler。

按需挂载

  这种方式挂载的handler被称为content handler。当一个请求进来以后,nginx从第一个阶段开始依次执行每个阶段中所有handler。执行到内容产生阶段阶段的时候,如果这个location有一个对应的content handler模块,那么就去执行这个content handler模块真正的处理函数。否则继续依次执行内容产生阶段中所有content phase handler,直到某个函数处理返回NGX_OK或者NGX_ERROR。所以当某个location处理到NGX_HTTP_CONTENT_PHASE阶段时,如果有content handler模块,那么NGX_HTTP_CONTENT_PHASE挂载的所有content phase handler都不会被执行了。使用这个方法挂载上去的handler有一个特点是必须在NGX_HTTP_CONTENT_PHASE阶段才能执行到。如果你想自己的handler在更早的阶段执行,那就不要使用这种挂载方式。同样举个例子:
  

static char * ngx_http_hello_string(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
        ngx_http_core_loc_conf_t  *clcf;
        /** 获取location配置 */
        clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
        /** 指定handler */
        clcf->handler = ngx_http_hello_handler;
        return NGX_CONF_OK;
}
总结

 编写handler模块大致分为以下几个步骤:
    1. 模块基本结构的编写。包括模块的配置结构,模块指令的定义,模块上下文结构,模块的定义等。
    2. 实现handler的挂载函数。根据模块的需求选择正确的挂载方式。
    3. 编写handler处理函数。模块的功能主要通过这个函数来完成。

handler模块实战

  这一节我们将要实现一个自己的hello模块,它的主要功能很简单,当请求匹配到此模块处理时,返回一个配置的字符串,并且增加一个访问次数的统计。

代码编写

创建一个文件ngx_http_hello_module.c ,输入如下完整代码:

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
/** 该模块的配置结构*/
typedef struct
{
    ngx_str_t hello_string;
    ngx_int_t hello_counter;
} ngx_http_hello_loc_conf_t;

static ngx_int_t ngx_http_hello_init(ngx_conf_t *cf);
static void *ngx_http_hello_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_hello_string(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static char *ngx_http_hello_counter(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

/**
 *该模块的配置指令,这里我们定义了两个指令,分别是hello_string,和hello_counter
 */
static ngx_command_t ngx_http_hello_commands[] = {
   {
    ngx_string("hello_string"),
    /* 指定该指令的作用域是location,无参或者有一个参数*/
    NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1,
    /* 回调函数,用于解析参数*/
    ngx_http_hello_string,
    /* 该指令配置信息存储的位置在location区*/
    NGX_HTTP_LOC_CONF_OFFSET,
    /* 存储的具体结构及位置*/
    offsetof(ngx_http_hello_loc_conf_t, hello_string),
    NULL
   },
   {
     ngx_string("hello_counter"),
     /* 指定该指令的作用域是location,有一个布尔类型的参数*/
     NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
     /* 回调函数,用于解析参数*/
     ngx_http_hello_counter,
     /* 该指令配置信息存储的位置在location区*/
     NGX_HTTP_LOC_CONF_OFFSET,
     offsetof(ngx_http_hello_loc_conf_t, hello_counter),
     NULL
   },
    /* 该数组读到此处停止*/
    ngx_null_command
};

static int ngx_hello_visited_times = 0;

/**
 * 该指令上下文的定义。
 *    ngx_http_hello_init:这个回调函数负责在读取完配置信息之后挂载此模块
 *    ngx_http_hello_create_loc_conf:此回调函数负责在读取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 */
};

/** 模块的定义 */
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
};
/**
 * 该模块真正的处理函数,这里用于生成响应内容
 */
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_hello_string */
           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);
      /* 获取ngx_hello_string的长度*/
      content_length = ngx_strlen(ngx_hello_string);
      /* 只处理get方式的请求 */
      if (!(r->method & (NGX_HTTP_GET))) {
              return NGX_HTTP_NOT_ALLOWED;
      }
      /* 这里我们不需要处理请求体,所以主动丢弃请求体 */
      rc = ngx_http_discard_request_body(r);
      if (rc != NGX_OK) {
              return rc;
      }
      /* 设置响应的content type为text/html */
      ngx_str_set(&r->headers_out.content_type, "text/html");
      /* 申请内存buffer b存储响应体 */
      b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
      if (b == NULL) {
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
      }
      /* 绑定buffer b到buffer链中 */
      out.buf = b;
      out.next = NULL;
      /* buffer指针指向正确的内容 */
      b->pos = ngx_hello_string;
      b->last = ngx_hello_string + content_length;
      b->memory = 1;
      /* buffer b是 buffer chain中最后一个buffer*/
      b->last_buf = 1;
      /* 设置状态行 */
      r->headers_out.status = NGX_HTTP_OK;
      r->headers_out.content_length_n = content_length;
      /* 发送响应头 */
      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);
}

/** 申请内存空间,用于存储模块配置结构*/
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;
}

/** 解析hello_string指令的参数为String类型*/
static char * ngx_http_hello_string(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_hello_loc_conf_t* local_conf;
    local_conf = conf;
    char* rv = ngx_conf_set_str_slot(cf, cmd, conf);
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "hello_string:%s", local_conf->hello_string.data);
    return rv;
}

/** 解析hello_string指令的参数为boolean类型*/
static char *ngx_http_hello_counter(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_hello_loc_conf_t* local_conf;
    local_conf = conf;
    char* rv = NULL;
    rv = ngx_conf_set_flag_slot(cf, cmd, conf);
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "hello_counter:%d", local_conf->hello_counter);
    return rv;
}

/** 挂载该模块的逻辑,此处是按处理阶段挂载,挂载到NGX_HTTP_CONTENT_PHASE阶段 */
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);
        /**ngx_array_push(ngx_array_t *a)函数表示在数组a上新追加一个元素,并返回指向新元素的指针。*/
        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;
}

config文件

代码编写完之后,我就需要配置和装载该模块,创建一个config文件放到源码同目录,内容如下:

#该模块的名字
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的模块都不是动态链接的,换句话说,Nginx的模块都是静态编译的。此处我们开发的模块放在路径:/home/chengli/ngx-hello-module ,具体信息如下图:
  
 这里写图片描述
 /home/chengli/ngx-install-dir 是我们将要安装的目录。另外两个文件是下载的nginx-1-10.1的源码:
 这里写图片描述
 这时cd到nginx-1.10.1目录,执行如下命令进行配置:

./configure –prefix=/home/chengli/ngx-install-dir –add-module=/home/chengli/ngx-hello-module

配置完毕执行make,make install进行编译安装。成功安装之后目录结构如下图:
这里写图片描述
修改nginx.conf,为了不影响开发环境正常运行,listen端口改成8088.。并添加如下location定义:

......
server {
      #监听端口改成8088
      listen       8088;
      server_name  localhost;
      ......
      #添加如下配置
      location /hello {
          #hello模块的hello_string指令
          hello_string "created by chengli";
          #hello_counter指令
          hello_counter on;
      }
     ......
  }
  ......

启动nginx,并访问http://10.18.20.5:8088/hello
这里写图片描述
至此,我们的hello模块开发完成。

filter模块

概述

  过滤模块是过滤响应头和响应内容的模块,可以对响应的头和内容进行处理。它的处理时间段是在获取回复内容之后,和向用户发送响应之前。它的处理过程分为两个阶段:过滤响应的头部和过滤响应的主体,在这两个阶段可以分别对头部和主体进行修改。过滤响应头部和过滤响应体的入口函数如下:
  

/** 过滤响应头入口 */
ngx_http_top_header_filter(r);

/** 过滤响应体入口 */
ngx_http_top_body_filter(r, in);

执行顺序

    filter模块的执行时有顺序的,这个顺序是在编译时就确定了的。编译完之后可以到objs/ngx_modules.c文件中查看所有模块。以下是编译之后的实例:
char *ngx_module_names[] = {
    "ngx_core_module",
    "ngx_errlog_module",
    "ngx_conf_module",
    "ngx_regex_module",
    "ngx_events_module",
    "ngx_event_core_module",
    "ngx_epoll_module",
    "ngx_http_module",
    "ngx_http_core_module",
    "ngx_http_log_module",
    "ngx_http_upstream_module",
    "ngx_http_static_module",
    "ngx_http_autoindex_module",
    "ngx_http_index_module",
    "ngx_http_auth_basic_module",
    "ngx_http_access_module",
    "ngx_http_limit_conn_module",
    "ngx_http_limit_req_module",
    "ngx_http_geo_module",
    "ngx_http_map_module",
    "ngx_http_split_clients_module",
    "ngx_http_referer_module",
    "ngx_http_rewrite_module",
    "ngx_http_proxy_module",
    "ngx_http_fastcgi_module",
    "ngx_http_uwsgi_module",
    "ngx_http_scgi_module",
    "ngx_http_memcached_module",
    "ngx_http_empty_gif_module",
    "ngx_http_browser_module",
    "ngx_http_upstream_hash_module",
    "ngx_http_upstream_ip_hash_module",
    "ngx_http_upstream_least_conn_module",
    "ngx_http_upstream_keepalive_module",
    "ngx_http_upstream_zone_module",
    /** 这是我们上文中编写的hello模块 */
    "ngx_http_hello_module",
    /** 以下是所有ftiler模块 */
    "ngx_http_write_filter_module",
    "ngx_http_header_filter_module",
    "ngx_http_chunked_filter_module",
    "ngx_http_range_header_filter_module",
    "ngx_http_gzip_filter_module",
    "ngx_http_postpone_filter_module",
    "ngx_http_ssi_filter_module",
    "ngx_http_charset_filter_module",
    "ngx_http_userid_filter_module",
    "ngx_http_headers_filter_module",
    /** 以下是我们将要介绍的filter模块 */
    "ngx_http_content_bold_filter_module",
    "ngx_http_copy_filter_module",
    "ngx_http_range_body_filter_module",
    "ngx_http_not_modified_filter_module",
    NULL
};

从ngx_http_write_filter_module到ngx_http_not_modified_filter_module是本例中所有的filter模块,他们的执行顺序是反过来的。也就是会先执行ngx_http_not_modified_filter_module,最后执行ngx_http_write_filter_module。实际上所有filter模块头部过滤函数和主体过滤函数共同组成了一个链表结构。ngx_http_top_header_filter和ngx_http_top_body_filter是两个全局变量,也是头部过滤和主体过滤函数的入口。为了方便理解,举一个filter模块挂载的实例:

/**
 * ngx_http_top_header_filter和ngx_http_top_body_filter是Nginx中两个全局变量,
 * ngx_http_next_header_filter和ngx_http_next_body_filter是我们模块中定义的局部变量。
 */
static ngx_int_t ngx_http_content_bold_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter = ngx_http_content_bold_header_filter;
    ngx_http_next_body_filter = ngx_http_top_body_filter;
    ngx_http_top_body_filter = ngx_http_content_bold_body_filter;
    return NGX_OK;
}

这样所有filter模块串起来之后就变成下面这样:
header_filter1→ header_filter2→ header_filter3→ …→ header_filterN→ body_filter1→ body_filter2→ body_filter3→ body_filterN

filter模块实战

这里我们要实现一个自己的filter模块,它的主要功能是把上节handler模块输出的内容,加粗显示。

代码编写

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

/** 模块的配置结构 */
typedef struct
{
    ngx_int_t bold_flag;
} ngx_http_bold_filter_loc_conf_t;

static ngx_int_t ngx_http_content_bold_init(ngx_conf_t *cf);
static void *ngx_http_content_bold_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_content_bold_string(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static ngx_http_output_body_filter_pt    ngx_http_next_body_filter;
static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;

/** 模块指令,这里我们创建了一个配置指令content_bold */
static ngx_command_t ngx_http_content_bold_commands[] = {
   {
    ngx_string("content_bold"),
    /* 该指令只能出现在location块中,并且有一个参数 */
    NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
    /* 读取解析参数的回调函数 */ 
    ngx_http_content_bold_string, 
    NGX_HTTP_LOC_CONF_OFFSET, 
    offsetof(ngx_http_bold_filter_loc_conf_t, bold_flag), 
    NULL
   },
    ngx_null_command 
};

/**
 * 该模块上下文的定义。
 *    ngx_http_content_bold_init:这个回调函数负责在读取完配置信息之后注册filter函数
 *    ngx_http_content_bold_create_loc_conf:此回调函数负责在读取location配置之后,创建模块配置结构的存储空间
 */
static ngx_http_module_t ngx_http_content_bold_module_ctx = {
        NULL,                         
        ngx_http_content_bold_init,    
        NULL,                        
        NULL,                         
        NULL,                        
        NULL,                         
        ngx_http_content_bold_create_loc_conf,
        NULL                          
};

/* 该模块的定义,指定了该模块对应的指令、上下文及模块类型 */
ngx_module_t ngx_http_content_bold_filter_module = {
        NGX_MODULE_V1,
        &ngx_http_content_bold_module_ctx,  
        ngx_http_content_bold_commands,    
        NGX_HTTP_MODULE,             
        NULL,                         
        NULL,                        
        NULL,                       
        NULL,                     
        NULL,                        
        NULL,                      
        NULL,                       
        NGX_MODULE_V1_PADDING
};
static u_char header_str[11] = "<html><h1>";
static u_char tail_str[13] = "</h1></html>";

/**
 * 请求体处理函数,过滤请求体功能主要是在这里完成的
 * 这里主要是在输入链表的首位分别加上header_str和tail_str已达到加粗的目的
 */
static ngx_int_t ngx_http_content_bold_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    ngx_chain_t *out;
    ngx_chain_t *last_chain_ele;
    ngx_buf_t *b;
    ngx_buf_t *e;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "content_bold filter");
    ngx_http_bold_filter_loc_conf_t * my_conf;
    /* 这里是获取该指令的配置信息 */
    my_conf = ngx_http_get_module_loc_conf(r, ngx_http_content_bold_filter_module);
    /* 如果没有配置content_bold或者值为off。则什么都不处理直接调用下一个body_filter */
    if (my_conf->bold_flag == NGX_CONF_UNSET || my_conf->bold_flag == 0) {
        return ngx_http_next_body_filter(r, in); 
    }
    /* 如果响应体内容为空,也什么都不做直接调用下一个body_filter */
    if (in == NULL) {
        return ngx_http_next_body_filter(r, in);
    }
    /** 为链表首尾结点申请内存 */
    out = ngx_pcalloc(r->pool, sizeof(ngx_chain_t));
    last_chain_ele = ngx_pcalloc(r->pool, sizeof(ngx_chain_t));
    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    e = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (out==NULL || b == NULL||e == NULL||last_chain_ele==NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    /* 为第一个结点赋值:header_str */
    b->pos = header_str;
    b->last = header_str + ngx_strlen(header_str);
    b->memory = 1; 
    b->last_buf = 0;
    out->buf = b;
    out->next = in;
    ngx_chain_t *last_but_one = ngx_pcalloc(r->pool, sizeof(ngx_chain_t));
    while(in) {
       if(in!=NULL){
             in->buf->last_buf = 0;
             last_but_one = in;
       }
       in = in -> next;
    }
    /* 为最好一个结点赋值:tail_str */
    e->pos = tail_str;
    e->last = tail_str + ngx_strlen(tail_str);
    e->memory = 1; 
    e->last_buf = 1;
    last_chain_ele->buf = e;
    last_but_one->next = last_chain_ele;
    return ngx_http_next_body_filter(r, out);
}

/**
 * 头部过滤函数,这里主要是更改content_length
 */
static ngx_int_t ngx_http_content_bold_header_filter(ngx_http_request_t *r)
{
  r->headers_out.content_length_n = r->headers_out.content_length_n+ ngx_strlen(header_str) + ngx_strlen(tail_str);
  return ngx_http_next_header_filter(r);
}

/**
 * 为该模块的配置结构申请存储空间
 */
static void * ngx_http_content_bold_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_bold_filter_loc_conf_t* local_conf = NULL;
    local_conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_bold_filter_loc_conf_t));
    if (local_conf == NULL)
    {
        return NULL;
    }
    local_conf->bold_flag = NGX_CONF_UNSET;
    return local_conf;
}

/**
 * 解析指令参数
 */
static char *ngx_http_content_bold_string(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_bold_filter_loc_conf_t* local_conf;
    local_conf = conf;
    char* rv = NULL;
    rv = ngx_conf_set_flag_slot(cf, cmd, conf);
    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "bold_flag:%d", local_conf->bold_flag);
    return rv;
}

/**
 * 挂载header_filter处理函数和body_filter处理函数
 */
static ngx_int_t ngx_http_content_bold_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter = ngx_http_content_bold_header_filter;
    ngx_http_next_body_filter = ngx_http_top_body_filter;
    ngx_http_top_body_filter = ngx_http_content_bold_body_filter;

    return NGX_OK;
}

config文件

#模块的名称,要与模块的定义变量名保持一致
ngx_addon_name=ngx_http_content_bold_filter_module
#模块的类型为filter
HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_content_bold_filter_module"
#模块源码路径
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_content_bold_filter_module.c"

编译与使用

此处我们开发的模块放在路径:/home/chengli/ngx-content-bold-filter-module ,具体信息如下图:
![这里写图片描述](https://img-blog.csdn.net/20161108101529594)
这里我们的filter模块与我们上节的hello模块放到的同一个目录:

这里写图片描述
切换到nginx-1.10.1目录,执行如下命令进行配置:

./configure --prefix=/home/chengli/ngx-install-dir --add-module=/home/chengli/ngx-hello-module --add-module=/home/chengli/ngx-content-bold-filter-module

同样configure完之后执行make,make install进行编译安装。
配置nginx.conf,添加content_bold指令进行测试:

......
server {
      #监听端口改成8088
      listen       8088;
      server_name  localhost;
      ......
      #添加如下配置
      location /hello {
          #hello模块的hello_string指令
          hello_string "created by chengli";
          #hello_counter指令
          hello_counter on;
          #content_bold指令
          content_bold on;
      }
     ......
  }
  ......

重新启动nginx,并访问http://10.18.20.5:8088/hello
这里写图片描述
成功实现字体加粗功能。

总结

  想要进行Nginx模块开发,首先需要简单了解下Nginx模块运作流程,其次要知道模块的组成部分及挂载方式。此外还需要了解一些Nginx自定义的结构体(最好的方式就是看源码)。知道上述内容之后就可以进行开发了,个人觉得主要关注的逻辑还是在handler和filter处理函数中。其他的东西即使不了解按照实例照搬即可。

猜你喜欢

转载自blog.csdn.net/lchpersonal521/article/details/53079510
今日推荐