欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

模块化的ngx

程序员文章站 2022-05-22 18:45:14
...

提到ngx,一个永远绕不开的概念就是“模块”,模块在ngx中扮演者举足轻重的角色,你甚至可以认为在ngx中一切都是模块,它是一种或多种功能的一个组合体,是ngx实现灵活扩展的一个基本保障。

 

其实这个模块的概念跟积木及其相似,在生活中,我们可以用一堆各式各样的积木拼成各种东西,比如房子、动物、飞船等,如果现有的积木无法完成你想要拼成的东西,你并不需要否定现有的积木,而是可以引入新的积木并与之进行配合。比如在拼装汽车时缺少一个轮胎形状的积木,那我们可以单独造一个这样的积木,而不是修改现有的积木(有可能修改完后又无法拼成之前的了)。

 

在ngx配置文件中无法明确看出模块的影子,但其实一切皆模块,配置文件中任何一条指令都属于某个模块,之前所有文章中的任何例子都有很多模块参与,但因侧重点不同并没有一一列出,这次我们找几个例在来一一说明。

 

来看一个最简单的:

location / {

   return 200 “hello”;

}

这个例子有两条指令,它背后正好有两个模块参与其中。第一条指令location属于ngx_http_core_module模块,从名字就可以看出来,它是ngx处理http协议的核心模块;另一条指令return属于ngx_http_rewrite_module模块。

 

假设现在有这样一个需求,我们想在某个请求返回的数据前和后加上一些特殊的数据,而这些数据则有其它location提供,但目前指令return是无法完成的,不过我们可以对其进行改造,比如改造成这样:

return 200 “#{/before} hello #{/after}”

我们可以把这个指令进行改造,让他可以从“#{}”中解析数据,并把解析出的数据当成location,然后再内部把解析的location数据拿过来,看起来这种方案是行得通的,但它看起来并不优雅。比如无法完全确保修改完后是否对原功能产生何种影响;比如种语法看起来也很怪;再比如原数据本身如何就包含这种“#{}”字符,那我们就需要对原数据进行字符转义。

 

总之,糟糕透了。

 

对于这种问题,在ngx中一个正规思路是:另外加模块来完成这个功能。而ngx正好有一个ngx_http_addition_module模块可以完成这个需求,所以我们也不必单独搞一个模块了,它用起来像这样:

location / {

   add_before_body  /before;

   add_after_body. /after;

   // *表示拦截所有的mime-type,默认text/html

   addition_types  *;   

   return 200 “hello”;

}

这样一来这个配置就涉及到三个模块了。

 

为了避免过多的干扰,所以基本上以往的例子都是ngx的一个片段配置,它们是无法独立工作的,这次我们拿一个可以完整工作的配置,来看看它都涉及到了哪些模块:

worker_processes 1;

 

events {

    worker_connections  1024;

}

 

http{

   listen   80;

   

   location / {

       return 200 “hello”;

    }

}

这个配置有7个指令,涉及两种模块类型,一个是核心类型的,一个是http类型的。其中worker_processes、events、worker_connections、http属于核心类型,而listen、location、return则属于http类型的。更具体一点的话listen属于ngx_http_core_module模块,location和return属于ngx_http_rewrite_module模块。

 

而另外四个核心指令,ngx在官网上统一把他们归到了ngx_core_module(http://nginx.org/en/docs/ngx_core_module.html)中,但其实在ngx内部并没有一个ngx_core_module模块或.c文件与之对应,比如worker_processes指令实际对应到src/core/nginx.c,events和worker_connections对应到src /event/ngx_event.c,http则是对应到src/http/ngx_http.c,如果不是特殊情况,后续为了方便,我们也统一把这些指令的归属模块叫成ngx_core_module。

 

通过这个稍微完整点的例子,我们基本可以感觉到,ngx的运作模式就是有核心模块,以及其它非核心模块配合完成的,比如正确匹配http请求需要http_core模块,设置自定变量需要rewrite模块,做反向代理又需要proxy模块,所以除了模块还是模块。

 

1如何表示一个模块

 

ngx用下面的结构体表示一个模块:

(为了避免累赘并没有贴出所有字段)

/src/core/ngx_conf_file.h

struct ngx_module_s {

   // …

   ngx_uint_t            ctx_index;

   ngx_uint_t            index;

   // …

   void                  *ctx;

   ngx_command_t        *commands;

   // …

   ngx_uint_t            type;

   // …

}

 

/src/core/ngx_core.h

typedef struct ngx_module_s  ngx_module_t;

ngx中的所有模块都用这个结构体来定义,它有点类似于面向Java语言中的Object,但毕竟C语言并非面向对象语言,所以对于子模块的扩展也没有类似严格的继承语法,它的扩展方式类似于口头约定。比如在ngx中,要扩展一个模块,一般需要约定三种数据,一种是该结构体的ctx字段,另一种是type字段,还有一个是该模块提供的指令集commands,但除了type之外其它两个都不是必须的。

 

其中type用来指定是哪种类型的模块,比如ngx中的核心模块类型使用一个宏定义/src/core/ngx_conf_file.h#NGX_CORE_MODULE,这个宏表示这是一个核心模块。

 

而ctx一般使用一个结构体来表示,具体信息由各个模块自行描述,比如核心模块就使用下面的结构体:

typedef struct {

    ngx_str_t   name;

    void          *(*create_conf)(ngx_cycle_t *cycle);

    char         *(*init_conf)(ngx_cycle_t *cycle, void *conf);

} ngx_core_module_t;

它定义了三个字段,name用来表示某个核心模块的名字,比如http、email等。另外两个是该类型模块的回调函数,ngx会在某个时间点对其进行回调。

 

指令集commands是一个数组,具体有哪些指令也是有各个模块自行描述,但指令的格式是固定的,每个指令都需要用ngx_command_t结构体来表述,比如核心http模块(ngx_http.c)有如下指令集:

static ngx_command_t  ngx_http_commands[] = {

   {  ngx_string("http"),

        XXX|XXX,

        ngx_http_block,

        0,

        0,

        NULL },

 

        ngx_null_command

};

该数组表示该模块只有一个指令,名字叫“http”,该指令对应的方法是ngx_http_block(当解析到指令http时要执行的方法),其它字段含义等后续实际用到再做解释。

 

目前在ngx中有如下几个核心模块:

src/core/ngx_thread_pool.c

src /core/ngx_log.c

src /core/ngx_regex.c

src /core/nginx.c

src /event/ngx_event_openssl.c

src /event/ngx_event.c

src /http/ngx_http.c

src /mail/ngx_mail.c

src /misc/ngx_google_perftools_module.c

src /stream/ngx_stream.c

 

他们的共同特点是都实现了ngx_core_module_t结构体,比如我们在ngx配置文件中最熟悉的指令http{}所属的模块ngx_http.c就是一个核心模块,它的定义是这样的:

static ngx_core_module_t  ngx_http_module_ctx = {

    ngx_string("http"),

    NULL,

    NULL

};

 

ngx_module_t  ngx_http_module = {

    NGX_MODULE_V1,

    &ngx_http_module_ctx,  /* module context */

    ngx_http_commands,     /* module directives */

    NGX_CORE_MODULE,   /* module type */

    // …

};

 

可以看到,它直接声明了一个ngx_core_module_t类型的结构体(ngx内部管这种结构体叫模块上下文—module context),并初始化该模块的名字为“http”,另外由于他不需要ngx执行回调函数,所以将其设置为NULL,这样ngx主框架在启动的时候就不会执行对应位置的回调函数了。然后再用ngx_module_t来声明一个该类型的结构体,把对应的http模块上下文(ngx_http_module_ctx)设置到该结构体ctx位置,type位置则是设置为NGX_CORE_MODULE,这样就基本“实现”了一个核心模块,并且该模块的名字为“http”。

 

基本上ngx模块的表示都是这种套路,如果你要定义新模块类型,或“实现”现有模块类型,按照上面的例子搞一套就可以了。

 

2ngx中的模块类型 

 

目前在ngx*有六种模块类型,具体如下:

src/core/ngx_conf_file.h#NGX_CORE_MODULE

src/core/ngx_conf_file.h#NGX_CONF_MODULE

src/http/ngx_http_config.h#NGX_HTTP_MODULE

src/mail/ngx_mail.h#NGX_MAIL_MODULE

src/event/ngx_event.h#NGX_EVENT_MODULE

src/stream/ngx_stream.h#NGX_STREAM_MODULE

基本上每种类型都有各自ctx描述和各种实现,比如核心模块的ctx为ngx_core_module_t,它的实现有ngx_http.c、ngx_event.c等。

 

不过目前有一个特例,NGX_CONF_MODULE类型的模块并没有定义自己的ctx,而且对它的实现也只有一个“模块”,具体描述如下:

/src/core/ngx_conf_file.c

ngx_module_t  ngx_conf_module = {

    NULL,                       /* module context */

    ngx_conf_commands,  /* module directives */

    NGX_CONF_MODULE, /* module type */   

};

 

也就是说他除了不需要回调函数外,连名字也懒得定义了,不过还好它有一个指令“include”,该指令承包了该模块对外的一切功能。

 

3执行回调函数(加载模块)

 

除了ngx中自带的六种模块类型,用户还可定义自己的模块类型,而且每种模块类型又可以有很多“实现”,所以这么多模块,ngx是如何执行这些模块的回调函数的呢?

 

实际上ngx主流程在启动的时候只关心一种模块类型:NGX_CORE_MODULE。在开始解析配置文件之前,它会先回调各个核心模块的create_conf方法(在ngx_core_module_t结构体中指定),具体调用方式如下:

src/core/ngx_cycle.c

for (i = 0; ngx_modules[i]; i++) {

  if (ngx_modules[i]->type != NGX_CORE_MODULE) {

    // 排除非NGX_CORE_MODULE类型的模块

    continue;

  }

  module = ngx_modules[i]->ctx;

 

  if (module->create_conf) {

    // 回调模块的create_conf

    rv = module->create_conf(cycle);

    // …

 

    cycle->conf_ctx[ngx_modules[i]->index] = rv;

  }

}

等配置文件解析完毕后再调用核心模块的init_conf方法(在ngx_core_module_t结构体中指定),具体调用方式也是一个循环: 

for (i = 0; ngx_modules[i]; i++) {

   if (ngx_modules[i]->type != NGX_CORE_MODULE) {

      // 排除非核心模块

      continue;

   }

   module = ngx_modules[i]->ctx;

 

   if (module->init_conf) {

     // 回调init_conf方法

     if(module->init_conf(cycle,xxx)== yyy) {

       // …

       return NULL;

     }

   }

}

其它模块类型的回调函数则有对应的核心模块的实现来完成,比如NGX_HTTP_MODULE类型的模块都有ngx_http.c这个核心模块的实现完成,它的回调入口是http指令对应的/src/http/ngx_http.c/ngx_http_block()方法。

 

NGX_HTTP_MODULE类型的模块对应的ctx定义如下:

src/http/ngx_http_config.h

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;

所以在ngx_http_block()方法会直接或间接回调该结构体中的方法,具体会回调方式仍然以循环所有模块开始:

for (m = 0; ngx_modules[m]; m++) {

  // 过滤掉非NGX_HTTP_MODULE类型的模块

  if (ngx_modules[m]->type != NGX_HTTP_MODULE) {

    continue;

  }

  module = ngx_modules[m]->ctx;

    

  if (module->create_main_conf) {

    // 回调create_main_conf

    ctx->main_conf[mi] = module->create_main_conf(cf);

    // …

  }

}

 

以此类推,其它回调方法也是如此。

 

目前除了NGX_CONF_MODULE类型模块外,其它非核心模块类型的加载方式(执行回调函数)都跟NGX_HTTP_MODULE类似。比如NGX_EVENT_MODULE以src/event/ngx_event.c#ngx_event_process_init()方法为入口,NGX_MAIL_MODULE以src/email/ngx_mail.c#ngx_mail_block()方法为入口,NGX_STREAM_MODULE则以src/stream/ngx_stream.c#ngx_stream_block()方法为入口。

 

4ngx现有的模块分布

 

为了便于读者查阅代码,这里把各个模块类型在源代码目中的分布做个简单介绍:

 

NGX_CONF_MODULE:

   src/core/ngx_conf_file.c

 

NGX_CORE_MODULE:

具体模块在第二节已经列举,这里不再赘述。

 

NGX_HTTP_MODULE:

   分布在src/http目录及其子目录下

 

NGX_MAIL_MODULE:

   分布在src/email目录下

 

NGX_EVENT_MODULE:

    分布在src/event目录及其子目录下

    

NGX_STREAM_MODULE:

分布在src/stream目录下