当前位置:首页 > 技术文章 > 正文内容

写一个Nginx的模块没有那么难

arlanguage4个月前 (12-25)技术文章24

Nginx作为世界第二大Web服务器(第一是Apache),越来越受到大家的青睐。受到欢迎的一个重要原因,是他的高扩展性。它是由多个不同功能、不同层次、不同类型且耦合度极低的模块组成。当我们开发自己的模块时,不仅可以使用core模块、events模块、log模块,而且我们开发的模块是嵌入到二进制文件中执行的,因此我们自己开发的模块,也能具有优秀的性能,享受Nginx的高并发特性。

本文以编写一个http_hello_module为例,介绍编写一个Nginx的http模块的步骤,以及其中涉及的数据结构。以期打破大家对编写nginx模块的恐惧。

该示例模块的功能是:每次访问http时,http进行访问计数,并把计数的值回显给客户端。

为了说明自定义的http模块,是如何读取配置文件的值,即在Nginx.conf文件中配置值,我们引入了两个配置项hello_string、hello_counter。这两个配置指令,仅可以出现在location指令的作用域中。hello_string用于展示字符串类型,它接收一个参数来设置回显的字符串,或是零个参数,则使用默认的字符串作为回显的字符串。而hello_counter,用于控制是否开启访问统计,如果设置为 on,则会在相应的字符串后面追加 Visited Times:的字样,以统计请求的次数。我们的例子中,其中一个location的配置如下。

location /test {
    hello_string balabala;
    hello_counter on;
}

我们自定义模块的访问效果如图:



编写http_hello_module

一个简单的http模块,只需要定义一个结构体ngx_module_t的变量,他告诉Nginx框架,我们定义的模块,

  1. 属于什么模块(如http模块、filter模块)
  2. 需要哪些配置项,在哪个领域定义(如server域、http域、location域)。我们的例子中,需要两个配置项:hello_string,hello_counter。在location作用域配置。
  3. 配置项的类型,存储结构。我们的例子中,一个是string类型,一个是int类型,定义结构如下:
typedef struct
{
    ngx_str_t hello_string;
    ngx_int_t hello_counter;
}ngx_http_hello_loc_conf_t;
  1. 如何接收客户端请求,对请求,又应该做出怎样的应答。

下面,我们逐步了解ngx_module_t结构体是如何完成上述内容的。ngx_module_t结构体的每个成员定义如下:

typedef struct ngx_module_s      ngx_module_t;

struct ngx_module_s {
    ngx_uint_t            ctx_index; // 表示当前模块在这类模块中的序号。既用于表达优先级,又用于 Nginx 框架快速获得模块的数据
    ngx_uint_t            index;    // 当前模块在所有模块的序号
    char                 *name;
    ngx_uint_t            spare0; // spare系列的保留变量,暂未使用
    ngx_uint_t            spare1;
    ngx_uint_t            version; // 模块的版本号,便于将来的扩展。
    const char           *signature;
    // 用于指向一类模块的上下文结构体。ctx将会指向特定类型模块的公共接口。例如在http模块中,ctx指向ngx_http_module_t结构体
    void                 *ctx; 
    ngx_command_t        *commands; // 将处理nginx.conf中的配置项
    ngx_uint_t            type; // 模块类型。官方的取值包括:NGX_HTTP_MODULE, NGX_CORE_MODULE, NGX_CONF_MODULE, NGX_EVENT_MODULE, NGX_MAIL_MODULE
    /*
     * 以下七个函数指针,表示在七个阶段,会调用这七个方法
     */
    ngx_int_t           (*init_master)(ngx_log_t *log); // 在master进程启动时。框架暂时没用调用。所以,写了也没啥卵用
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle); // 在初始化所有模块时被调用。在master/worker模式下,在启动worker进程前被调用
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle); // 在每个worker进程的初始化过程会被调用
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle); // 不支持多线程,所以没啥卵用
    void                (*exit_thread)(ngx_cycle_t *cycle); // 不支持
    void                (*exit_process)(ngx_cycle_t *cycle); // worker 进程会在退出前调用
    void                (*exit_master)(ngx_cycle_t *cycle);  // 在master进程退出前被调用
    //没有用
    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_module_t结构体的前7个成员,我们用Nginx提供的宏NGX_MODULE_V1进行初始化。

#define NGX_MODULE_V1                                                         \
    NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX,                           \
    NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE

对于ngx_module_t结构体的最后8个成员,我们用Nginx提供的宏NGX_MODULE_V1_PADDING进行初始化。

#define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0

对于在七个阶段会调用的函数指针,我们的http_hello_module并不需要,所以统一设置为NULL。

因为我们的例子是一个http模块,因此type值,等于NGX_HTTP_MODULE。

通过以上分析可知,为了定义结构体ngx_module_s的对象,我们只需要定义以下两个结构体的对象:

  • 结构体ngx_http_module_t的对象,赋值给ctx,用于告知Nginx框架,我们的自定义模块如何处理客户端的请求
  • 结构体ngx_command_t的数组对象,赋值给commands,用于告知Nginx框架,我们的自定义模块需要的配置项

ngx_module_s的成员commands定义

对于有配置项的模块,需要定义一个结构体,用于存储配置项的值。本例中,我们需要两个配置项, hello_string和hello_counter,分别是string和int类型,因此,存储配置项的结构体定义如下。

typedef struct
{
    ngx_str_t hello_string;
    ngx_int_t hello_counter;
} ngx_http_hello_loc_conf_t;

有一点需要注意的是,在模块的开发过程中,我们最好使用 Nginx 原有的命名习惯。这样跟源代码的契合度更高,看起来也更舒服。对于模块配置信息的定义,命名习惯是ngx_http_<module name>_(main|srv|loc)_conf_t。

commands数组用于定义模块的配置文件参数,每一个数组都是ngx_command_t类型,数组的结尾用ngx_null_command表示。Nginx框架在解析配置文件时,对于一个配置项,会遍历所有的模块。对每一个模块,都会遍历该模块的commmands数组,直到遇到ngx_null_command。

ngx_command_t结构体定义了自己感兴趣的一个配置项:

typedef struct ngx_command_s     ngx_command_t;
struct ngx_command_s {
    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: 配置指令的名称。
  • type: 配置项类型,该配置项可以出现的位置,如:server{}或location{},以及它可以携带的参数个数。例如:NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1 表示该配置项出现在location位置,接收0个或1个参数。
  • set:是一个函数指针。出现了name中指定的配置项后,将会调用set方法处理配置项的参数
  • conf: 该字段指定当前配置项存储的内存位置。因为 http 模块对所有 http 模块所要保存的配置信息,划分了 main, server 和 location 三个地方进行存储,每个地方都有一个内存池用来分配存储这些信息的内存。这里可能的值为 NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET 或 NGX_HTTP_LOC_CONF_OFFSET。
  • offset: 指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。
  • post: 配置项读取后的处理方法

因为我们的例子有两个配置项,因此依次定义了两个ngx_command_t,数组用ngx_null_command结尾。

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, // 配置项的值,用LOCATION的内存池存储
                offsetof(ngx_http_hello_loc_conf_t, hello_string), // 存储的精确位置
                NULL },

        {
                ngx_string("hello_counter"),
                NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
                ngx_http_hello_counter,
                NGX_HTTP_LOC_CONF_OFFSET,
                offsetof(ngx_http_hello_loc_conf_t, hello_counter),
                NULL },

        ngx_null_command
};

对于hello_string配置项,它的处理函数实现如下,ngx_conf_set_str_slot是Nginx提供的函数,读取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);
    return rv;
}

对于hello_counter配置项,他的处理函数实现如下,ngx_conf_set_flag_slot是Nginx提供的函数,读取on/off值的配置项。

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);
    return rv;
}

ngx_module_s的成员ctx定义

ctx是结构体ngx_http_module_t的对象指针,主要用于定义在读取配置文件的各个阶段的处理函数。

下面展示的代码,便是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); 
    
    //创建数据结构,用于存储srv级别(直属于虚拟主机server{...}块的配置项)的配置项
    void       *(*create_srv_conf)(ngx_conf_t *cf); 
    
    //合并main和srv级别下的同名配置项
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf); 
    
    // 创建数据结构,用于存储loc级别(直属于location{...}块的配置项)的配置项
    void       *(*create_loc_conf)(ngx_conf_t *cf);
    
    // 合并srv和location级别下的同名配置项
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); 
} ngx_http_module_t;

在我们的例子中,ngx_http_module_t的各个字段的实现如下, 我们只关心location配置项的读取以及在配置文件解析完成后的处理。location配置项读取hello_string和hello_counter的值,配置文件解析完成后,我们挂载请求的处理函数。

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_http_hello_create_loc_conf是如何存储loc级别的配置项。 ngx_http_hello_create_loc_conf是告诉Ngixn模块,我们使用的loc级别的配置项的信息,并对配置项进行初始化。

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;
}

在配置文件处理完后,我们挂载模块的处理函数。即告诉Nginx框架,当符合条件的请求过来时,我们自定义的模块如何处理请求,如何响应请求。挂载是在配置文件解析完毕,由函数ngx_http_hello_init完成的。

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);

    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;
}

从上述代码可知,我们挂载的处理函数为ngx_http_hello_handler,即由它处理客户端请求、做出具体响应。

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_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);
    content_length = ngx_strlen(ngx_hello_string);

    /* we response to 'GET' and 'HEAD' requests only */
    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
        return NGX_HTTP_NOT_ALLOWED;
    }

    /* discard request body, since we don't need it here */
    rc = ngx_http_discard_request_body(r);

    if (rc != NGX_OK) {
        return rc;
    }

    ngx_str_set(&r->headers_out.content_type, "text/html");

    /* send the header only, if the request type is http 'HEAD' */
    if (r->method == NGX_HTTP_HEAD) {
        r->headers_out.status = NGX_HTTP_OK;
        r->headers_out.content_length_n = content_length;

        return ngx_http_send_header(r);
    }

    /* allocate a buffer for your response body */
    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (b == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    /* attach this buffer to the buffer chain */
    out.buf = b;
    out.next = NULL;

    /* adjust the pointers of the buffer */
    b->pos = ngx_hello_string;
    b->last = ngx_hello_string + content_length;
    b->memory = 1;    /* this buffer is in memory */
    b->last_buf = 1;  /* this is the last buffer in the buffer chain */

    /* set the status line */
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = content_length;

    /* send the headers of your response */
    rc = ngx_http_send_header(r);

    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }

    /* send the buffer chain of your response */
    return ngx_http_output_filter(r, &out);
}

如何把http_hello_module编译进Nginx

上面介绍了,编写一个http模块的基本要素。接下来,介绍如何把这个简单的模块编译进Nginx框架,从而让它发挥作用。

方案一:在执行configure命令时,添加--add-module参数

Nginx提供了一种简单的方式将第三方的模块编译进Nginx中。首先是把第三方的源代码全部放在一个目录中, 并在该目录下新建一个名为config的文件,这个config文件的目的,就是告诉Nginx如何编译我们自己的模块。在执行Nginx的configure命令时, 加入参数 --add-module=PATH,PATH就是我们存放config的目录,就可以把我们开发的http_hello_module编译进Nginx。

我们的http_hello_module是一个http模块,因此config文件中只需要定义以下三个变量:

  • ngx_addon_name: 仅在configure执行时使用,一般设置为模块名称。
  • HTTP_MODULES: 添加自定义模块的名称。
  • NGX_ADDON_SRCS:指定新增模块的源代码。如果有多个文件,则以空格隔开。

我们的http_hello_module例子,它的config文件完整内容如下。$ngx_addon_dir变量的值,等于我们在执行configure命令时,参数--add-module的值。

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"

因为我们本例是编写一个http module,因此赋值了HTTP_MODULES。如果是编写其他模块,如http 过滤模块、Nginx核心模块、事件模块、http头部过滤模块,则应该分别对变量HTTP_FILTER_MODULES、CORE_MODULES、EVENT_MODULES、HTTP_HEADER_FILTER_MODULES。

方案二:在执行configure命令后,修改Makefile文件

Nginx提供的另一种方法是直接修改Makefile文件。在执行完configure脚本后,会在objs/Makefile和objs/ngx_modules.c文件。我们可以直接修改这两个文件,从而完成http_hello_module编译进Nginx的目标。

扫描二维码推送至手机访问。

版权声明:本文由AR编程网发布,如需转载请注明出处。

本文链接:http://www.arlanguage.com/post/440.html

分享给朋友:

“写一个Nginx的模块没有那么难” 的相关文章

三年前端还不会配置Nginx?刷完这篇就够了

一口气看完,比自学强十倍!什么是NginxNginx是一个开源的高性能HTTP和反向代理服务器。它可以用于处理静态资源、负载均衡、反向代理和缓存等任务。Nginx被广泛用于构建高可用性、高性能的Web应用程序和网站。它具有低内存消耗、高并发能力和良好的稳定性,因此在互联网领域非常受欢迎。为什么使用N...

Nginx常用配置及和基本功能讲解

作者:京东物流 殷世杰Nginx已经广泛应用于J-one和Jdos的环境部署上,本文对Nginx的常用的配置和基本功能进行讲解,适合Ngnix入门学习。1 核心配置找到Nginx安装目录下的conf目录下nginx.conf文件,Nginx的基本功能配置是由它提供的。1.1 配置文件结构Nginx的...

听说你的Nginx还不会记录Response Body?

相信大家都遇到过在排查线上问题或Debug的时候,在某一瞬间,特别想开启Nginx的Response Body日志,来帮助自己快速的定位问题;但找半天发现只有$request_body/$upstream_addr/$upstream_response_time这些相近变量可用;这个时候不要慌......

高端Linux 脚本很有用,赶紧学起来!

Linux 脚本?准确的说叫 Linux Shell 脚本,Shell 脚本是一种被设计用来运行命令行解释器,他是 Linux 系统的一大特色之一。命令行是 Linux 系统中一种非常重要的交互方式,而它的实现基于 Linux 内核和 Shell 程序,为用户提供了强大和灵活的操作方式。使用 Lin...

php高并发的瓶颈到底在哪

php高并发的瓶颈到底在哪?是同步阻塞?还是nginx+fpm不断创建-销毁进程资源过度消耗?高并发到底是什么问题,是语言问题嘛,为什么说php不适合高并发?求大佬指点从2009年后一直用lnmp,从5.2.17一直到现在的PHP7.4,做的项目无数个,大到日IP10W+、PV50W+的平台,小到日...

Nginx的location里面的root、alias的使用技巧与区别

1. 介绍1.1 介绍福哥在将TFLinux的PHP+Apache的组合转换成PHP+FPM+Nginx的过程里遇到了一个问题,就是Apache的虚拟主机转为Nginx里面的虚拟主机的时候参数怎么解决呢?今天福哥就将Nginx配置文件里面的location、root、alias的使用技巧给大家做一个...