Nginx模块开发(一)
(2011-12-23 13:54:28)
标签:
杂谈 |
分类: Nginx模块开发 |
目标:加深对模块(Handler)的写法的理解,并提供一些扩展阅读。
典型的HTTP请求周期
一个典型的周期是这样的:
还是Hello World!
上篇文章已经给出了Hello World这个模块的代码。大家按照文件所述进行的话,应该已经看到结果了。这篇是对代码的分析。
模块相关的数据结构
所谓大军未动,粮草先行,写nginx模块,首先要做的不是实现模块代码,而是设计模块的使用方法、使用范围,定义模块工作流程,然后将这些信息一并填入nginx模块的数据结构。理解nginx模块,也需要从这些数据结构入手。
模块配置
1.
2.
3. |
模块配置有三种,分别是main配置,server配置和location配置。绝大多数模块仅需要location配置。模块配置名称约定如下:ngx_http_<module name>_(main|srv|loc)_conf_t。通过观察上面这个模块配置,可以得到下面几点信息:
l
l
模块定义
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14. |
下面对这个配置进行解释:
l
l
l
l
l
l
init_master:master进程初始化时调用,直到现在,nginx也没有真正使用过init_master;
init_module:master进程解析配置以后初始化模块时调用一次;
init_process:worker进程初始化时调用一次;
init_thread:多线程时,线程初始化时调用。Unix/Linux环境下未使用多线程;
exit_thread:多线程退出时调用;
exit_process:worker进程退出时调用一次;
exit_master:master进程退出时调用一次。
l
模块指令
0.
1.
2.
3.
4.
5.
6.
7.
8.
9. |
nginx指令定义是利用ngx_command_t数组,数组的最后一项是ngx_null_command,表示定义完成。
我们来看一下指令的定义。首先是指令名称,利用ngx_string宏定义。
第二行是指令属性,可以用“|”将使指令具备多个属性。
常用的属性有:
l
l
l
l
l
l
l
l
l
l
l
l
l
第三行是定义处理这个指令的回调函数,这个是核心啦。nginx自带了很多处理函数,用于将配置的参数解析为nginx数据类型并存储,可以帮助我们简化编程,比如:
l
l
l
l
l
l
l
我们这里使用了自定义的ngx_http_hello_world,是因为除了参数解析以外,我们还需要将我们的模块挂接到nginx处理器中。
第四行配置是指定配置解析后存放在哪里,有三个选择项:NGX_HTTP_MAIN_CONF_OFFSET,NGX_HTTP_SRV_CONF_OFFSET,或者NGX_HTTP_LOC_CONF_OFFSET,表示分别存入main_conf、srv_conf、loc_conf,对应于HTTP全局配置、某个主机的配置和某个URI的配置。多说两句,这个配置和第二行指令的属性其实是有联系的,比如某个指令可以用在main配置部分、server配置部分,但不能用在location配置部分,那应该使用srv_conf来存储,存在loc_conf中有冗余,存在man_conf中有覆盖。
第五行是承接第四行的配置,表示数据具体保存在main_conf、srv_conf、loc_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={ }; |
上面的代码段给出了nginx解析我们hello_world模块的配置时调用的回调函数定义,注释内的是各个回调函数的原型。一共是8个,分为4对,没有用到的填为NULL。
nginx解析配置文件的时候按照下面的顺序调用各个回调函数:
nginx先调用create_(main|srv|loc)_conf创建main_conf、srv_conf、loc_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 { root } |
对应的内存映像示意图
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配置都会调用一次,结果实现了代码以后,运行出现问题:在一个配置了多个location的nginx系统中,模块一瞬间多次建立并断开连接,不幸直接被后端屏蔽掉了。在这里出现的错误是本来应该放在模块工作时才执行的代码被设计到模块配置时执行了,实际正常的做法是将建立连接放在init_module中,如果是每个worker都需要建立一个连接,那么建立连接这件事还得推迟到init_process中。
逻辑代码实现
Handler入口
Hello World这个例子是一个很典型handler模块。所谓handler模块,就是负责生成响应内容的模块,就比如现在hello_world模块就是要生成一段内容“hello world, XXX”。
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
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.
2. |
大家可以看到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是一个缓冲区定义,有四个指针将整个缓冲区分为三个部分,如下图所示。对于只读缓冲区,第一段start到pos的区域是已读取的部分,而第二段pos到last的部分是需要读取而未读取,需要重点关注的,第三段不可能出现;对于只写缓冲区,第一段不可能出现,第二段pos到last是已写入的,第三段last到end是还可以写入的。对于单独作用的缓冲区,很容易处理,但是对于可读可写的缓冲区,就需要小心处理这些指针。Hello World模块中缓冲区的作用是只写,可以看到我们只设置了pos和last标记我们写入的数据。建议对此不熟悉的人同时将四个指针都正确设置,不断加深理解。还需要注意,ngx_buf_t存的是指针,可以指向任何以任意方式分配的合法内存,比如我们这里直接指向静态数据区。
ngx_buf_t有几个重要的标志位:
l
l
l
l
l
因为nginx可以提前flush输出,所以这些buf被输出后就可以重复使用,可以避免重分配,提高系统性能,被称为free_buf,而没有被输出的buf就是busy_buf。nginx没有特别的集成这个特性到自身,但是提供了一个函数ngx_chain_update_chains来帮助开发者维护这两个缓冲区队列。具体的应用可以参见ngx_http_gzip_filter_module。