vlambda博客
学习文章列表

Nginx 第三方模块使用与开发

Nginx 允许引入第三方模块来扩展 Nginx 的功能。官方网站 NGINX 3rd Party Modules 列出了 Nginx 很多的第三方模块。除此之外,很多很有用的模块也能在 github 等网站上找到。

添加模块

接下来通过添加 njs 模块为例来介绍如何添加第三方模块。njs 是 Nginx + JavaScript 的缩写,简单来说,就是 Nginx 里面可以运行 JavaScript,用 JavaScript 来构建动态的 Web 应用。Nginx NJS 包含两个 Nginx 扩展模块:ngx_http_js_module 和 ngx_stream_js_module。

首先克隆 njs 模块的代码:

git clone https://github.com/nginx/njs.git

查看当前已安装的 Nginx 的编译信息:

[root@nginx-plus1 nginx-1.14.2]# /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.14.2
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC) 
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --with-debug --with-stream --prefix=/usr/local/nginx --with-http_ssl_module

进入 Nginx 源代码目录,执行 ./configure,在最后通过 --add-module 指令加上 njs 模块。执行完成后会在当前目录生成编译所需的相关文件(例如 Makefile 和 objs 目录等)。

cd /root/nginx-1.14.2

# 复制前面nginx -V看到的参数,在最后--add-module加上njs模块的路径
./configure --with-debug --with-stream --prefix=/usr/local/nginx --with-http_ssl_module --add-module=/root/njs/nginx

执行完上面命令后,objs 目录下的 ngx_modules.c 文件中的 ngx_modules 数组中可以看到新增的 njs 模块:

cat /root/nginx-1.14.2/objs/ngx_modules.c

接下来执行 make 命令开始编译:

make

编译完成后注意不需要执行 make install 命令,只需要将生成的 Nginx 二进制文件覆盖原先的二进制文件即可。

cp /root/nginx-1.14.2/objs/nginx /usr/local/nginx/sbin/nginx -f

现在展示如何使用 njs 模块的功能。在 nginx 的 conf 目录下新建 js 目录,用于存放 javascript 代码。在 js 目录下创建 http.js 文件,内容如下:

function summary(r{
    var a, s, h;

    s = "JS summary\n\n";
    //打印请求方法,HTTP版本,Host,客户端地址,URI
    s += "Method: " + r.method + "\n";
    s += "HTTP version: " + r.httpVersion + "\n";
    s += "Host: " + r.headersIn.host + "\n";
    s += "Remote Address: " + r.remoteAddress + "\n";
    s += "URI: " + r.uri + "\n";
    
    //打印请求头
    s += "Headers:\n";
    for (h in r.headersIn) {
        s += "  header '" + h + "' is '" + r.headersIn[h] + "'\n";
    }
    //打印参数内容
    s += "Args:\n";
    for (a in r.args) {
        s += "  arg '" + a + "' is '" + r.args[a] + "'\n";
    }

    return s;
}

function baz(r{
    //设置响应状态码
    r.status = 200;
    //设置响应头
    r.headersOut.foo = 1234;
    r.headersOut['Content-Type'] = "text/plain; charset=utf-8";
    r.headersOut['Content-Length'] = 15;
    r.sendHeader();
    //设置响应内容
    r.send("nginx");
    r.send("java");
    r.send("script");

    r.finish();
}

//export default用于导出常量、函数、文件、模块等
export default {foo, summary, baz, hello};

编写 Nginx 配置文件 /usr/local/nginx/conf/nginx.conf:

events {}
http {
#导入js文件
js_import js/http.js;

#设置变量,变量值为调用js文件的相应函数的返回值
js_set $foo http.foo;
js_set $summary http.summary;

server {
listen 8000;

location / {
add_header X-Foo $foo; #将变量foo的结果添加到响应头中
js_content http.baz; #执行其中 JS 内容并输出
}

location = /summary {
return 200 $summary; #返回变量summary的结果
}
}
}

启动 Nginx:

/usr/local/nginx/sbin/nginx

客户端请求 Nginx:

[root@nginx-plus1 ~]# curl localhost:8000 -i
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Tue, 15 Jun 2021 14:04:39 GMT
Content-Type: text/plain; charset=utf-8
Connection: keep-alive
foo: 1234
Content-Length: 15
X-Foo: foo

nginxjavascript  


[root@nginx-plus1 ~]# curl localhost:8000/summary?name=chengzw
JS summary

Method: GET
HTTP version: 1.1
Host: localhost:8000
Remote Address: 127.0.0.1
URI: /summary
Headers:
  header 'User-Agent' is 'curl/7.29.0'
  header 'Host' is 'localhost:8000'
  header 'Accept' is '*/*'
Args:
  arg 'name' is 'chengzw'
  

动态模块

Nginx 在 Nginx 1.9.11(release at 2016-02-09) 版本中新增了动态模块(Dynamic Module) 的支持。动态模块在第一次通过 ./configure --add-dynamic-module 编译后,之后如果要对动态模块进行升级,只需要重新编译动态模块,然后替换在 modules 目录内该动态模块的 .so 文件即可,无需替换 Nginx 二进制文件。

下面还是以 njs 模块的例子来演示如何添加动态模块。使用 --add-dynamic-module 指令以动态模块的方式添加 njs 模块:

./configure --with-debug --with-stream --prefix=/usr/local/nginx --with-http_ssl_module --add-dynamic-module=/root/njs/nginx

# 编译
make

编译完成后注意不需要执行 make install 命令,只需要将生成的 Nginx 二进制文件覆盖原先的二进制文件即可。

cp /root/nginx-1.14.2/objs/nginx /usr/local/nginx/sbin/nginx -f

如果是使用 make install 命令安装,会自动在 Nginx 运行目录下创建 modules 目录,这里我们需要手动创建 modules 目录:

mkdir /usr/local/nginx/modules

查看 modules 目录,可以看到有 http 和 stream 两个 njs 模块对应的文件:

[root@nginx-plus1 nginx]# ll /usr/local/nginx/modules
total 7392
-rwxr-xr-x 1 root root 3825792 Jun 11 08:03 ngx_http_js_module.so
-rwxr-xr-x 1 root root 3738944 Jun 11 08:03 ngx_stream_js_module.so

使用 load_module 指令加载动态模块,注意 load_module 指令必须在所有 block (包括 events、http、stream、mail)之前使用:

# 加载动态模块
load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;

events {}
http {
#导入js文件
js_import js/http.js;

#设置变量,变量值为调用js文件的相应函数的返回值
js_set $foo http.foo;
js_set $summary http.summary;

server {
listen 8000;

location / {
add_header X-Foo $foo; #将变量foo的结果添加到响应头中
js_content http.baz; #执行其中 JS 内容并输出
}

location = /summary {
return 200 $summary; #返回变量summary的结果
}
}
}

自定义 HTTP 模块

我们定义一个简单的 HTTP 模块,当客户端发送请求时,返回 Hello World!

模块开发有以下步骤:

  • 1.编写模块基本结构。包括模块配置项,模块上下文,模块配置信息。
  • 2.实现 handler 的挂载函数。根据模块的需求选择正确的挂载方式。
  • 3.编写 handler 处理函数。模块的功能主要通过这个函数来完成。
  • 4.编写编译的文件 config。

定义模块配置项

模块配置项 ngx_command_s 结构体定义在 src/core/ngx_conf_file.h 文件中,其结构如下所示:

struct ngx_command_s {
    // 配置项的名称,例如gzip
    ngx_str_t             name;
    
    // 配置项类型,type将指定配置项可以出现的位置。例如出现在location{}或者server{}中,
    // 以及它可以携带的参数个数
    ngx_uint_t            type;
    
    // 出现了name中定义的配置项后,将会调用该方法处理配置项的参数
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    
    //该字段被NGX_HTTP_MODULE类型模块所用 (我们编写的基本上都是NGX_HTTP_MOUDLE,只有一些nginx核心模块是非NGX_HTTP_MODULE),
    // 该字段指定当前配置项存储的内存位置。实际上是使用哪个内存池的问题。因为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。
    ngx_uint_t            conf;
    
    //指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。因为对于配置信息的存储,一般我们都是定义个结构体来存储的。
    // 那么比如我们定义了一个结构体A,该项配置的值需要存储到该结构体的b字段。那么在这里就可以填写为offsetof(A, b)。
    // 对于有些配置项,它的值不需要保存或者是需要保存到更为复杂的结构中时,这里可以设置为0。
    ngx_uint_t            offset;
    
    // 该字段存储一个指针。可以指向任何一个在读取配置过程中需要的数据,以便于进行配置读取的处理。
    // 大多数时候,都不需要,所以简单地设为NULL即可。
    void                 *post;
};

我们定义了一个配置项,该配置项可以出现在 location 配置块中,该配置项没有参数。当在某个配置块中出现 mytest 配置时,Nginx 将会调用 ngx_http_mytest 方法。

/*
定义模块配置项
*/

static ngx_command_t  ngx_http_mytest_commands[] =
{

    {
        ngx_string("mytest"),
        NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
        // 当在某个配置块中出现 mytest 配置时,Nginx 将会调用 ngx_http_mytest 方法。
        ngx_http_mytest,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command //在ngx_http_hello_commands这个数组定义的最后,都要加一个ngx_null_command作为结尾。
};

定义模块上下文

模块上下文 ngx_http_module_t 结构体定义在 src/http/ngx_http_config.h 文件中。这是一个 ngx_http_module_t 类型的静态变量,这个变量实际上是提供一组回调函数指针,这些函数将被 Nginx 在合适的时间进行调用。其结构如下所示:

typedef struct {

    // 解析配置文件前调用
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    
    // 完成配置文件的解析后调用
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    // 当需要创建数据结构用于存储main级别(直属于http{...}块的配置项)的全局配置项时,
    // 可以通过create_main_conf回调方法创建存储全局配置项的结构体
    void       *(*create_main_conf)(ngx_conf_t *cf);
    
    // 常用于初始化main级别配置项
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);
    
    // 当需要创建数据结构用于存储srv级别(直属于server{...}块的配置项)的配置项时,
    // 可以通过create_srv_conf回调方法创建存储srv级别配置项的结构体
    void       *(*create_srv_conf)(ngx_conf_t *cf);
    
    // 主要用于合并main级别和srv级别下同名的配置项
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
     
    // 当需要创建数据结构用于存储loc级别(直属于location{...}块的配置项)的配置项时,
    // 可以通过create_loc_conf回调方法创建存储loc级别配置项的结构体
    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;

HTTP 框架在读取,重载配置文件时定义了由 ngx_http_module_t 接口描述的 8 个阶段,HTTP 框架在启动过程中会在每个阶段中调用 ngx_http_module_t 中相应的方法。当然,如果 ngx_http_module_t 中的某个回调方法设置为 NULL 空指针时,那么 HTTP 框架是不会调用它的。

本例中我们没有实现 HTTP 框架初始化时会调用的 ngx_http_module_t 中的 8 个方法。

/*
定义模块上下文
*/

static ngx_http_module_t  ngx_http_mytest_module_ctx =
{
    NULL,                              /* preconfiguration */
    NULL,                    /* postconfiguration */

    NULL,                              /* create main configuration */
    NULL,                              /* init main configuration */

    NULL,                              /* create server configuration */
    NULL,                              /* merge server configuration */

    NULL,          /* create location configuration */
    NULL            /* merge location configuration */
};

定义模块

对于开发一个模块来说,我们都需要定义一个 ngx_module_t 类型的变量来说明这个模块本身的信息,从某种意义上来说,这是这个模块最重要的一个信息,它告诉了 Nginx 这个模块的一些信息,上面定义的配置信息,还有模块上下文信息,都是通过这个结构来告诉 Nginx 系统的,也就是加载模块的上层代码,都需要通过定义的这个结构,来获取这些信息。

#define NGX_MODULE_V1          0, 0, 0, 0, 0, 0, 1
#define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0

// src/core/ngx_core.h
typedef struct ngx_module_s      ngx_module_t;
// src/core/ngx_conf_file.h
struct ngx_module_s {

    // 下面的 ctx_index,index,spare0,spare1,spare2,spare3,version 变量不需要在定义时赋值,
    // 可以用 Nginx 准备好的宏 NGX_MODULE_V1 来定义,它已经定义好了这7个值
    ngx_uint_t            ctx_index;
    ngx_uint_t            index;

    ngx_uint_t            spare0;
    ngx_uint_t            spare1;
    ngx_uint_t            spare2;
    ngx_uint_t            spare3;

    ngx_uint_t            version;
    
    // 指向模块的上下文结构体
    void                 *ctx;
    
    // 模块配置项
    ngx_command_t        *commands;
    
    // 模块的类型,它与ctx指针是紧密相关的。在官方Nginx中,它的取值范围是以下5种:
    // NGINX_HTTP_MODULE,NGINX_CORE_MODULE,NGINX_CONF_MODULE,NGX_EVENT_MODULE,NGX_MAIL_MODULE
    ngx_uint_t            type;

    // 在Nginx启动、停止的过程中,以下7个函数指针表示有7个执行点会分别调用这7种方法。
    // 如果不需要在Nginx启动或者停止的过程中执行它,就简单设置为NULL即可。
    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);

    // 以下8个字段为保留字段,目前没有使用,可以使用Nginx提供的NGX_MODULE_V1_PADDING 宏来填充。
    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;
};

定义 mytest 模块,这样 mytest 模块在编译时将会被加入到 ngx_modules 全局数组中。Nginx 在启动时,会调用所有模块的初始化回调方法,这个例子中我们没有实现它们。

/*
定义模块
*/

ngx_module_t  ngx_http_mytest_module =
{
    NGX_MODULE_V1,
    // 模块上下文
    &ngx_http_mytest_module_ctx,           /* module context */
    // 模块配置项
    ngx_http_mytest_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。

定义 handler 挂载函数

在 Nginx 的 ngx_http_phases 中定义了 HTTP 框架对请求进行处理的 11 个阶段,结构体定义在 src/http/ngx_http_core_module.h :

typedef enum {
    // 1.在接收到完整的HTTP头部后读取请求内容的阶段
    NGX_HTTP_POST_READ_PHASE = 0,

    // 2.在将请求的URI与location表达式匹配之前,修改请求的URI的阶段(所谓的重定向)
    NGX_HTTP_SERVER_REWRITE_PHASE,

    // 3.根据请求的URI寻找匹配的location表达式,
    // 这个阶段只能由ngx_http_core_module模块实现,不建议其他HTTP模块重新定义这一阶段的行为
    NGX_HTTP_FIND_CONFIG_PHASE,
    
    // 4.寻找到匹配的location之后再次利用rewrite修改请求的URI
    NGX_HTTP_REWRITE_PHASE,
    
    // 5.这一阶段是用于在rewrite重写URI后,防止错误的nginx.conf配置导致死循环(递归地修改URI)
    // 因此,这一阶段仅由ngx_http_core_module模块处理。目前,控制死循环的方式很简单,首先检查rewrite的次数,
    // 如果一个请求超过10次重定向,就认为进入了死循环,这时候就会向用户返回500,表示服务器内部错误
    NGX_HTTP_POST_REWRITE_PHASE,

    // 6.决定请求访问权限之前,HTTP模块可以介入处理的阶段
    NGX_HTTP_PREACCESS_PHASE,

    // 7.这个阶段用于让HTTP模块判断是否允许这个请求访问Nginx服务器
    NGX_HTTP_ACCESS_PHASE,
    
    // 8.在NGX_HTTP_ACCESS_PHASE阶段中,当HTTP模块的handler处理函数返回不允许访问的错误代码时(实际就是NGX_HTTP_FORBIDDEN或者NGX_HTTP_UNAUTHORIZED),
    // 这里将负责向用户发送拒绝服务的错误响应,因此,这个阶段实际上是用于给NGX_HTTP_ACCESS_PHASE阶段收尾的
    NGX_HTTP_POST_ACCESS_PHASE,

    // 9.这个阶段完全是为了给try_files配置项而设立的,当Nginx请求静态文件资源时,
    // try_files配置项可以使这个请求顺序地访问多个静态文件资源,如果某一次访问失败,则继续访问try_files中指定的下一个静态资源。
    NGX_HTTP_TRY_FILES_PHASE,
    
    // 10.处理HTTP请求内容的阶段,这是大部分HTTP模块最愿意介入的阶段
    NGX_HTTP_CONTENT_PHASE,
    
    // 11.处理完请求后记录日志的阶段
    NGX_HTTP_LOG_PHASE
} ngx_http_phases;

一般情况下,我们自定义的模块,大多数是挂载在 NGX_HTTP_CONTENT_PHASE 阶段的,默认也就是属于这个模块。

有几个阶段是特例,它们没有 Hook 挂载点(也就意味着,在这几个阶段不允许挂载任何第三方处理逻辑),它们仅由 HTTP 框架实现:

  • NGX_HTTP_FIND_CONFIG_PHASE
  • NGX_HTTP_POST_ACCESS_PHASE
  • NGX_HTTP_POST_REWRITE_PHASE
  • NGX_HTTP_TRY_FILES_PHASE

函数的挂载分为两种方式:一种方式就是按处理阶段挂载,以这种方式挂载的 handler 被称为 content phase handlers;另外一种挂载方式就是按需挂载,以这种方式挂载的 handler 被称为 content handler。

按处理阶段挂载

按处理阶段挂载的动作一般是在模块上下文调用的 postconfiguration 函数中。本例介绍按需挂载,这里不展开说明。

按需挂载

当一个请求进来以后,Nginx 从 NGX_HTTP_POST_READ_PHASE 阶段开始依次执行每个阶段中所有 handler。执行到 NGX_HTTP_CONTENT_PHASE 阶段的时候,如果这个 location 有一个对应的 content handler 模块,那么就去执行这个 content handler 模块真正的处理函数。否则继续依次执行 NGX_HTTP_CONTENT_PHASE 阶段中所有 content phase handlers,直到某个函数处理返回 NGX_OK 或者 NGX_ERROR。

换句话说,当某个 location 处理到 NGX_HTTP_CONTENT_PHASE 阶段时,如果有 content handler 模块,那么 NGX_HTTP_CONTENT_PHASE 挂载的所有 content phase handlers 都不会被执行了。

但是使用这个方法挂载上去的 handler 有一个特点是必须在 NGX_HTTP_CONTENT_PHASE 阶段才能执行到。如果你想自己的 handler 在更早的阶段执行,那就不要使用这种挂载方式。

那么在什么情况会使用这种方式来挂载呢?一般情况下,某个模块对某个 location 进行了处理以后,发现符合自己处理的逻辑,而且也没有必要再调用 NGX_HTTP_CONTENT_PHASE 阶段的其它 handler 进行处理的时候,就动态挂载上这个 handler。

本例我们使用按需挂载的方式。当 Nginx 接收完 HTTP 请求的头部信息时,就会调用 HTTP 框架处理请求。在 ngx_http_mytest 方法中,我们定义了请求的处理方法为 ngx_http_mytest_handler,

/*
按需挂载,ngx_http_mytest_handler方法是真正处理请求的方法
*/

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

    //首先找到mytest配置项所属的配置块,clcf貌似是location块内的数据
//结构,其实不然,它可以是main、srv或者loc级别配置项,也就是说在每个
//http{}和server{}内也都有一个ngx_http_core_loc_conf_t结构体
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

    //http框架在处理用户请求进行到NGX_HTTP_CONTENT_PHASE阶段时,如果
//请求的主机域名、URI与mytest配置项所在的配置块相匹配,就将调用我们
//实现的ngx_http_mytest_handler方法处理这个请求
    clcf->handler = ngx_http_mytest_handler;

    return NGX_CONF_OK;
}

定义 handler 处理函数

当出现 mytest 配置项时,ngx_http_mytest 方法会被调用,这时将 ngx_http_core_loc_conf_t 结构的 handler 成员指定为 ngx_http_mytest_handler,另外,HTTP 框架在接收完 HTTP 请求的头部后,会调用 handler 挂载函数指向的 handler 处理函数。下面看一下 handler 成员的原型 ngx_http_handler_pr:

// src/http/ngx_http_request.h
typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);

从上面这段代码可以看出,实际处理请求的方法 ngx_http_mytest_handler 在接收一个 ngx_http_request_t 类型的参数 r,返回一个 ngx_int_t 类型的结果。

/*
定义handler处理函数,是真正处理请求的方法
*/

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
    //必须是GET或者HEAD方法,否则返回405 Not Allowed
    if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)))
    {
        return NGX_HTTP_NOT_ALLOWED;
    }

    //丢弃请求中的包体
    ngx_int_t rc = ngx_http_discard_request_body(r);
    if (rc != NGX_OK)
    {
        return rc;
    }

    //设置返回的Content-Type。注意,ngx_str_t有一个很方便的初始化宏
//ngx_string,它可以把ngx_str_t的data和len成员都设置好
    ngx_str_t type = ngx_string("text/plain");
    //返回的包体内容
    ngx_str_t response = ngx_string("Hello World!");
    //设置返回状态码
    r->headers_out.status = NGX_HTTP_OK;
    //响应包是有包体内容的,所以需要设置Content-Length长度
    r->headers_out.content_length_n = response.len;
    //设置Content-Type
    r->headers_out.content_type = type;

    //发送http头部
    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only)
    {
        return rc;
    }

    //构造ngx_buf_t结构准备发送包体
    ngx_buf_t                 *b;
    b = ngx_create_temp_buf(r->pool, response.len);
    if (b == NULL)
    {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    //将Hello World拷贝到ngx_buf_t指向的内存中
    ngx_memcpy(b->pos, response.data, response.len);
    //注意,一定要设置好last指针
    b->last = b->pos + response.len;
    //声明这是最后一块缓冲区
    b->last_buf = 1;

    //构造发送时的ngx_chain_t结构体
    ngx_chain_t  out;
    //赋值ngx_buf_t
    out.buf = b;
    //设置next为NULL
    out.next = NULL;

    //最后一步发送包体,http框架会调用ngx_http_finalize_request方法
//结束请求
    return ngx_http_output_filter(r, &out);
}

定义 config 文件

config 文件其实就是一个可执行的 Shell 脚本。如果只想开发一个 HTTP 模块,那么 config 文件中需要定义以下 3 个变量:

  • ngx_addon_name:仅在configure 执行时使用,一般设置为模块名称。
  • HTTP_MODULES:保存所有的 HTTP 模块名称,每个 HTTP 模块间由空格符相连。在重新设置 HTTP_MODULES 变量时,不要直接覆盖它,因为 configure 调用到自定义的 config 脚本前,已经将各个 HTTP 模块设置到 HTTP_MODULES 变量中了。
  • NGX_ADDON_SRCS:用于指定新增模块的源代码,多个待编译的源代码间以空格符相连。注意:在设置 NGX_ADDON_SRCS 时可以使用  $ngx_addon_dir 变量,它等价于 configure 时执行 --add-module=PATH 的 PATH 参数。
ngx_addon_name=ngx_http_mytest_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c"

编译和使用模块

将模块源代码文件 ngx_http_mytest_module.c 和 config 文件放在以下目录中:

[root@nginx-plus1 ~]# ll /root/mymodules/mytest/
total 12
-rw-r--r-- 1 root root  164 Jun 16 10:20 config
-rw-r--r-- 1 root root 4567 Jun 16 10:45 ngx_http_mytest_module.c

编译时使用 --add-module 指令添加我们自定义的模块,这里使用 make install 重新安装 Nginx:

cd /root/nginx-1.14.2/
./configure --prefix=/usr/local/nginx --add-module=/root/mymodules/mytest
# 删除原先的Nginx文件
rm -rm /usr/local/nginx
# 编译
make
# 安装
make install 

编辑 Nginx 配置文件 /usr/local/nginx/conf/nginx.conf:

worker_processes  1;

events {
worker_connections 1024;
}


http {
server {
listen 80;
location / {
mytest; #使用我们自定义的配置项
}
}
}

启动 Nginx:

/usr/local/nginx/sbin/nginx

客户端请求,可以看到响应了我们自定义的内容:

[root@nginx-plus1 ~]# curl localhost
Hello World!

参考链接

  • http://tengine.taobao.org/book/chapter_03.html
  • https://tangocc.github.io/2018/07/05/nginx-modul-myself/
  • 深入理解 Nginx 模块开发与结构解析