加载中…
个人资料
crk_world
crk_world
  • 博客等级:
  • 博客积分:0
  • 博客访问:135,291
  • 关注人气:57
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

Nginx模块开发(一)

(2011-12-23 13:54:28)
标签:

杂谈

分类: Nginx模块开发

目标:加深对模块(Handler)的写法的理解,并提供一些扩展阅读。


 

典型的HTTP请求周期

一个典型的周期是这样的:


 

还是Hello World

上篇文章已经给出了Hello World这个模块的代码。大家按照文件所述进行的话,应该已经看到结果了。这篇是对代码的分析。

模块相关的数据结构

所谓大军未动,粮草先行,写nginx模块,首先要做的不是实现模块代码,而是设计模块的使用方法、使用范围,定义模块工作流程,然后将这些信息一并填入nginx模块的数据结构。理解nginx模块,也需要从这些数据结构入手。

模块配置

1.         typedef struct {

2.             ngx_str_t output_words;

3.         } ngx_http_hello_world_loc_conf_t;

模块配置有三种,分别是main配置,server配置和location配置。绝大多数模块仅需要location配置。模块配置名称约定如下:ngx_http_<module name>_(main|srv|loc)_conf_t。通过观察上面这个模块配置,可以得到下面几点信息:

l  定义的字段和字段类型,可以发散到结构的内存布局;

l  通过命名可以看出配置被使用的情况,是个location配置,可以发散到可能有效配置范围是http {} / server {} / location {},可能的保存位置是LOCATION_CONF


 

模块定义

1.         ngx_module_t ngx_http_hello_world_module = {

2.             NGX_MODULE_V1,

3.             &ngx_http_hello_world_module_ctx,

4.             ngx_http_hello_world_commands,

5.             NGX_HTTP_MODULE,

6.             NULL,

7.             NULL,

8.             NULL,

9.             NULL,

10.         NULL,

11.         NULL,

12.         NULL,

13.         NGX_MODULE_V1_PADDING

14.     };

下面对这个配置进行解释:

l  1ngx_http_hello_world_module是我们模块的名字,这个名字编译时也要用到;

l  2nginx模块规范的版本号,一致使用NGX_MODULE_V1

l  3:指向包含解析模块配置文件时用到的回调函数集合的定义的数据结构的指针。这个结构的一般命名法是<模块名>_ctx

l  4:模块提供的指令集的数据结构,这个结构的一般命名法是<模块名>_commands

l  5:模块类别,我们这个模块是HTTP模块,所以值是NGX_HTTP_MODULE

l  6-12:模块生命周期各回调函数的指针,从上到下依次是:

init_mastermaster进程初始化时调用,直到现在,nginx也没有真正使用过init_master

init_modulemaster进程解析配置以后初始化模块时调用一次;

init_processworker进程初始化时调用一次;

init_thread:多线程时,线程初始化时调用。Unix/Linux环境下未使用多线程;

exit_thread:多线程退出时调用;

exit_processworker进程退出时调用一次;

exit_mastermaster进程退出时调用一次。

l  13:未用字段,使用NGX_MODULE_V1_PADDING补齐。


 

模块指令

0.         static ngx_command_t ngx_http_hello_world_commands[]={

1.             { ngx_string("hello_world"),

2.              NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,

3.              ngx_http_hello_world,

4.              NGX_HTTP_LOC_CONF_OFFSET,

5.              offsetof(ngx_http_hello_world_loc_conf_t, output_words),

6.              NULL

7.             },

8.             ngx_null_command

9.         };

nginx指令定义是利用ngx_command_t数组,数组的最后一项是ngx_null_command,表示定义完成。

我们来看一下指令的定义。首先是指令名称,利用ngx_string宏定义。

第二行是指令属性,可以用“|”将使指令具备多个属性。

常用的属性有:

l  NGX_HTTP_MAIN_CONF:指令出现在main配置部分(http块)是合法的;

l  NGX_HTTP_SRV_CONF:指令出现在server配置部分(server块)是合法的;

l  NGX_HTTP_LOC_CONF: 指令出现在location配置部分(location块)是合法的;

l  NGX_HTTP_UPS_CONF:指令出现在upstream配置部分(upstream块)是合法的;

l  NGX_CONF_NOARGS:指令没有参数;

l  NGX_CONF_TAKE1:指令读入1个参数;

l  NGX_CONF_TAKE2:指令读入2个参数;

l  ...

l  NGX_CONF_TAKE7:指令读入7个参数;

l  NGX_CONF_FLAG:指令读入1个布尔型数据(“on”或“off”);

l  NGX_CONF_1MORE:指令至少读入1个参数;

l  NGX_CONF_2MORE:指令至少读入2个参数;

l  更多属性请参阅nginx源码的ngx_conf_file.h

第三行是定义处理这个指令的回调函数,这个是核心啦。nginx自带了很多处理函数,用于将配置的参数解析为nginx数据类型并存储,可以帮助我们简化编程,比如:

l  ngx_conf_set_flag_slot:将“on”或者“off”转换成10

l  ngx_conf_set_str_slot:将字符串保存为ngx_str_t

l  ngx_conf_set_num_slot:解析一个数字并保存为ngx_int_t

l  ngx_conf_set_size_slot:解析一个数据大小(如:“8k”,“1m”),并保存为size_t

l  ngx_conf_set_enum_slot:根据枚举定义将字符串翻译成ngx_int_t

l  ngx_http_set_complex_value_slot:解析一个包含nginx变量的字符串并保存为ngx_http_complex_value_t

l  更多处理函数可以参见ngx_conf_file.h

我们这里使用了自定义的ngx_http_hello_world,是因为除了参数解析以外,我们还需要将我们的模块挂接到nginx处理器中。

第四行配置是指定配置解析后存放在哪里,有三个选择项:NGX_HTTP_MAIN_CONF_OFFSETNGX_HTTP_SRV_CONF_OFFSET,或者NGX_HTTP_LOC_CONF_OFFSET,表示分别存入main_confsrv_confloc_conf,对应于HTTP全局配置、某个主机的配置和某个URI的配置。多说两句,这个配置和第二行指令的属性其实是有联系的,比如某个指令可以用在main配置部分、server配置部分,但不能用在location配置部分,那应该使用srv_conf来存储,存在loc_conf中有冗余,存在man_conf中有覆盖。

第五行是承接第四行的配置,表示数据具体保存在main_confsrv_confloc_conf指向的结构体的哪个位置(offset偏移)。大家可能会问,这个main_conf等等怎么来的,nginx给我们挖的坑长得是个什么样子,这个我们在介绍ngx_http_hello_world_module_ctx会说到。

最后一行,是一个补充字段,一般不用的,填入NULL。只是对于某些特殊的处理函数,比如ngx_conf_set_enum_slot,会用这个指针来指向enum定义表。

有了这六项数据,我们就能定义出一个可以被nginx识别并且正常解析的指令。


 

模块上下文

接下来再看ngx_http_hello_world_module_ctx,它配置了解析配置文件用到的回调函数:

static ngx_http_module_t ngx_http_hello_world_module_ctx={

    NULL,     

    NULL,     

 

    NULL,     

    NULL,     

 

    NULL,     

    NULL,     

 

    ngx_http_hello_world_create_loc_conf,

               

    ngx_http_hello_world_merge_loc_conf,

              

};

上面的代码段给出了nginx解析我们hello_world模块的配置时调用的回调函数定义,注释内的是各个回调函数的原型。一共是8个,分为4对,没有用到的填为NULL

nginx解析配置文件的时候按照下面的顺序调用各个回调函数:

 

nginx先调用create_(main|srv|loc)_conf创建main_confsrv_confloc_conf,分配内存,设置变量的初值,接着就会调用preconfiguration,初始化http组件和nginx其他组件的交互,比如添加nginx变量到nginx script组件,等等。做完了这些准备工作,nginx开始解析配置文件中的“http”块。因为“http”块中有“server”块、“upstream”块,“server”块中还有“location”块,所以nginx在解析“http”块的过程中,还会多次调用create_(srv|loc)_conf来构造这些子块的环境。如果这样说不太容易理解的话,可以仔细看看下面这个http配置的例子在解析完以后的内存映像示意。

理解了这一点,后面的事情就简单了。nginx对唯一的main_conf调用init_main_conf,对每一个“server”块的配置,调用merge_srv_conf,合并那些在定义在“http”块中的“server”块配置,对每一个location调用merge_loc_conf,合并那些定义在上层块中的“location”配置。最后,nginx调用postconfigation再次进行http组件和nginx其他组件的交互,这次比preconfigation多了很多配置定义的数据可以使用。

http {

    server_names_hash_bucket_size     64;

root             /home/weiyue/htdocs;

 

    server {

        listen        3128 default;

        access_log    off;

 

        location ~ /test.js {

            expires   12h;

        }

    }

}

对应的内存映像示意图

 SHAPE  \* MERGEFORMAT

http

main_conf

server_names_hash_bucket_size 64

srv_conf

空的

 

loc_conf

root /home/weiyue/htdocs

 

servers

server

srv_conf

listen 3128 default

loc_conf

access_log off

locations

location

loc_conf

location /test.js

match-type regx

expires 12h

数据结构总结

nginx模块的核心数据结构就是上面所述的三个:一个综述,一个指令定义,一个配置回调,是比较简单的。但需要小心的是这三个数据结构中都包含回调函数指针,必须弄清楚每个回调函数都有什么用途,这一点是非常重要。有些代码,本身实现的功能非常简单,但是回调函数用错了,看起来相当山寨,而且有各种各样的bug

指令定义中的回调函数自然是处理指令本身,这点非常清楚。综述中的回调函数定义的是模块的生命周期各个阶段的行为,配置回调中的回调函数定义的是单纯的解析模块配置时使用的回调函数,调用是在init_module以前,这点看起来也不是很难理解,但是实践起来就可能出现问题。比如某应用希望在使用配置文件定义的某个参数,与后端建立一条连接,建立连接时会使用到定义的参数。最开始的实践,设计是在merge_loc_conf的时候建立连接,如果连接已经建立,就先断开连接再建立连接,但是merge_loc_conf是对每一个location配置都会调用一次,结果实现了代码以后,运行出现问题:在一个配置了多个locationnginx系统中,模块一瞬间多次建立并断开连接,不幸直接被后端屏蔽掉了。在这里出现的错误是本来应该放在模块工作时才执行的代码被设计到模块配置时执行了,实际正常的做法是将建立连接放在init_module中,如果是每个worker都需要建立一个连接,那么建立连接这件事还得推迟到init_process中。

逻辑代码实现

Handler入口

Hello World这个例子是一个很典型handler模块。所谓handler模块,就是负责生成响应内容的模块,就比如现在hello_world模块就是要生成一段内容“hello world, XXX”。

1.         static char *ngx_http_hello_world(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

2.         {

3.             ngx_http_core_loc_conf_t *clcf;

4.          

5.             clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

6.             clcf->handler = ngx_http_hello_world_handler;

7.          

8.             ngx_conf_set_str_slot(cf,cmd,conf);

9.          

10.         return NGX_CONF_OK;

11.     }

逻辑上可以这样理解这段代码,配置指令“hello_world”使nginx解析配置时调用上面的处理函数,处理函数在nginx内部挂上一个钩子,在这里就是ngx_http_hello_world_handler。这个钩子会存放在“hello_world”指令所在的“location”配置中。需要注意,这个钩子是被存入ngx_http_core_module的“location”配置,而不是ngx_http_hello_world_module自己的“location”配置。nginx将在每一个与这个“location”对应的请求的处理过程中调用这个钩子函数。从这里也我们可以看到如何取得其他模块的模块配置。

模块上下文的回调函数

1.         static void *ngx_http_hello_world_create_loc_conf(ngx_conf_t *cf);

2.         static char *ngx_http_hello_world_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);

大家可以看到create_XXX_conf就是为模块配置分配内存空间,然后初始化各个成员的初始值。ngx_pcalloc除了分配内存,还会用0填充,所以类似ngx_str_t的初始化在分配内存的同时已经完成了。在Hello World模块中,使用ngx_pcalloc分配内存以后,显式地给ngx_str_t赋值是重复的行为,应该避免。对于其他类型,初始化字段使用的值可能是非0值,比如数值型使用-1做初始值。使用NGX_CONF_UNSET_XXX宏来初始化是可读的,具体请参见ngx_conf_file.h

merge_XXX_conf函数也很好理解,就是内容拷贝,其调用时机大家在前面应该已经有所了解,这里不再赘述。请尽量使用nginx自带的ngx_conf_merge_XXX_value来做值拷贝,以避免不必要的风险。

工作代码

static ngx_int_t

ngx_http_hello_world_handler(ngx_http_request_t *r);

代码本身没有什么理解难度。但是使用到得nginx缓冲区管理是比较有意思的东西。

ngx_buf_t是一个缓冲区定义,有四个指针将整个缓冲区分为三个部分,如下图所示。对于只读缓冲区,第一段startpos的区域是已读取的部分,而第二段poslast的部分是需要读取而未读取,需要重点关注的,第三段不可能出现;对于只写缓冲区,第一段不可能出现,第二段poslast是已写入的,第三段lastend是还可以写入的。对于单独作用的缓冲区,很容易处理,但是对于可读可写的缓冲区,就需要小心处理这些指针。Hello World模块中缓冲区的作用是只写,可以看到我们只设置了poslast标记我们写入的数据。建议对此不熟悉的人同时将四个指针都正确设置,不断加深理解。还需要注意,ngx_buf_t存的是指针,可以指向任何以任意方式分配的合法内存,比如我们这里直接指向静态数据区。

ngx_buf_t有几个重要的标志位:

l  last_buf:标示整个响应最后一个缓冲区,nginx会立即发送缓冲的所有数据;

l  last_in_chain:标示当前URI处理的最后一个缓冲区,用于使用subrequest的情况,基于缓冲的操作发现此标记时必须处理;

l  flush:标示需要立即发送缓冲的所有数据;

l  in_file:标示缓冲区是文件缓冲,使用file_posfile_last标示缓冲的文件段;

l  memory:标示缓冲区是内存缓冲,使用上述四个执政标示缓冲使用情况,需要与in_file互斥使用。

 

因为nginx可以提前flush输出,所以这些buf被输出后就可以重复使用,可以避免重分配,提高系统性能,被称为free_buf,而没有被输出的buf就是busy_bufnginx没有特别的集成这个特性到自身,但是提供了一个函数ngx_chain_update_chains来帮助开发者维护这两个缓冲区队列。具体的应用可以参见ngx_http_gzip_filter_module

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有