概述
阅读本文前,可以先阅读前面的文章,系列连载Nginx架构与Handler模块最详分析(1) Nginx架构与Handler模块最详分析(2) 详解Nginx系列
1.handler 模块
相信大家在看了前面文章的模块概述以后,都对 nginx 的模块有了一个基本的认识。基本上作为第三方开发者最可能开发的就是三种类型的模块,即 handler,filter 和 load-balancer。Handler 模块就是接受来自客户端的请求并直接由nginx产生输出的模块。有些地方说 upstream 模块实际上也是一种 handler 模块,只不过它产生的内容来自于从后端服务器获取的,而非在本机产生的。
在上一章提到,配置文件中使用 location 指令可以配置 content handler 模块,当 Nginx系统启动的时候,每个 handler 模块都有一次机会把自己关联到对应的 location 上。如果有多个 handler 模块都关联了同一个 location,那么实际上只有一个 handler 模块真正会起作用。当然大多数情况下,模块开发人员都会避免出现这种情况。也就是一个handler对应一个location。
handler 模块处理的结果通常有三种情况: 处理成功,处理失败(处理的时候发生了错误)或者是拒绝去处理。在拒绝处理的情况下,这个 location 的处理就会由默认的 handler 模块来进行处理。例如,当请求一个静态文件的时候,如果关联到这个 location 上的一个 handler模块拒绝处理,就会由默认的 ngx_http_static_module 模块进行处理,该模块是一个典型的handler 模块。
模块的基本结构
在这一节我们将会对通常的模块开发过程中,每个模块所包含的一些常用的部分进行说明。这些部分有些是必须的,有些不是必须的。同时这里所列出的这些东西对于其他类型的模块,例如 filter 模块等也都是相同的。
模块配置结构
基本上每个模块都会提供一些配置指令,以便于用户可以通过配置来控制该模块的行为。那么这些配置信息怎么存储呢?
需要定义该模块的配置结构来进行存储。大家都知道Nginx 的配置信息分成了几个作用域(scope,有时也称作上下文),这就是 main, server, 以及location。同样的每个模块提供的配置指令也可以出现在这几个作用域里。那对于这三个作用域的配置信息,每个模块就需要定义三个不同的数据结构去进行存储。当然,不是每个模块都会在这三个作用域都提供配置指令的。那么也就不一定每个模块都需要定义三个数据结构去存储这些配置信息了。视模块的实现而言,需要几个就定义几个。有一点需要特别注意的就是,在模块的开发过程中,我们最好使用 nginx 原有的命名习惯。这样跟原代码的契合度更高,看起来也更舒服。对于模块配置信息的定义,命名习惯是 ngx_http__(main|srv|loc)_conf_t。这里有个例子,就是从我们后面将要展示给大家的 hello module 中截取的。
typedef struct
{
ngx_str_t hello_string;
ngx_int_t hello_counter;
}ngx_http_hello_loc_conf_t;
模块配置指令
一个模块的配置指令是定义在一个静态数组中的。同样地,我们来看一下从 hello module 中截取的模块配置指令的定义。
其实看这个定义,就基本能看出来一些信息。例如,我们是定义了两个配置指令,一个是叫hello_string,可以接受一个参数,或者是没有参数。另外一个命令是 hello_counter,接受一个 NGX_CONF_FLAG 类型的参数。除此之外,似乎看起来有点迷惑。没有关系,我们来详细看一下 ngx_command_t,一旦我们了解这个结构的详细信息,那么我相信上述这个定义所表达的所有信息就不言自明了。ngx_command_t 的定义,位于 src/core/ngx_conf_file.h 中。
name: 配置指令的名称。
type: 该配置的类型,其实更准确一点说,是该配置指令属性的集合。nginx 提供了很多预定义的属性值(一些宏定义),通过逻辑或运算符可组合在一起,形成对这个配置指令的详细的说明。下面列出可在这里使用的预定义属性值及说明。
属性参数值说明如下:
(1)NGX_CONF_NOARGS:配置指令不接受任何参数。
(2)NGX_CONF_TAKE1:配置指令接受 1 个参数。
(3)NGX_CONF_TAKE2:配置指令接受 2 个参数。
(4)NGX_CONF_TAKE3:配置指令接受 3 个参数。
(5)NGX_CONF_TAKE4:配置指令接受 4 个参数。
(6)NGX_CONF_TAKE5:配置指令接受 5 个参数。
(7)NGX_CONF_TAKE6:配置指令接受 6 个参数。
(8)NGX_CONF_TAKE7:配置指令接受 7 个参数。
可以组合多个属性,比如一个指令即可以不填参数,也可以接受 1 个或者 2 个参数。那么就是 NGX_CONF_NOARGS|NGX_CONF_TAKE1|NGX_CONF_TAKE2。如果写上面三个属性在一起,你觉得麻烦,那么没有关系,nginx 提供了一些定义,使用起来更简洁。
(9)NGX_CONF_TAKE12:配置指令接受 1 个或者 2 个参数。
(10)NGX_CONF_TAKE13:配置指令接受 1 个或者 3 个参数。
(11)NGX_CONF_TAKE23:配置指令接受 2 个或者 3 个参数。
(12)NGX_CONF_TAKE123:配置指令接受 1 个或者 2 个或者 3 参数。
(13)NGX_CONF_TAKE1234:配置指令接受 1 个或者 2 个或者 3 个或者 4 个参数。
(14)NGX_CONF_1MORE:配置指令接受至少一个参数。
(15)NGX_CONF_2MORE:配置指令接受至少两个参数。
(16)NGX_CONF_MULTI: 配置指令可以接受多个参数,即个数不定。
(17)NGX_CONF_BLOCK:配置指令可以接受的值是一个配置信息块。也就是一对大括号括起来的内容。里面可以再包括很多的配置指令。比如常见的 server 指令就是这个属性的。
(18)NGX_CONF_FLAG:配置指令可以接受的值是”on”或者”off”,最终会被转成 bool 值。
(19)NGX_CONF_ANY:配置指令可以接受的任意的参数值。一个或者多个,或者”on”或者”off”,或者是配置块。
注意:最后要说明的是,无论如何, nginx 的 配 置 指 令 的 参 数 个 数 不 可 以 超 过NGX_CONF_MAX_ARGS 个。目前这个值被定义为 8,也就是不能超过 8 个参数值。
(20)NGX_DIRECT_CONF:可以出现在配置文件中最外层。例如已经提供的配置指令 daemon,master_process 等。
(21)NGX_MAIN_CONF: http、mail、events、error_log 等。
(22)NGX_ANY_CONF: 该配置指令可以出现在任意配置级别上。
对于我们编写的大多数模块而言,都是在处理 http 相关的事情,也就是所谓的都是NGX_HTTP_MODULE,对于这样类型的模块,其配置可能出现的位置也是分为直接出现在 http 里面,以及其他位置。
(23)NGX_HTTP_MAIN_CONF: 可以直接出现在 http 配置指令里。
(24)NGX_HTTP_SRV_CONF: 可以出现在 http 里面的 server 配置指令里。
(25)NGX_HTTP_LOC_CONF: 可以出现在 http server 块里面的 location 配置指令里。
(26)NGX_HTTP_UPS_CONF: 可以出现在 http 里面的 upstream 配置指令里。
(27)NGX_HTTP_SIF_CONF: 可以出现在 http 里面的 server 配置指令里的 if 语句所在的 block中。
(28)NGX_HTTP_LMT_CONF: 可以出现在 http 里面的 limit_except 指令的 block 中。
(29)NGX_HTTP_LIF_CONF: 可以出现在 http server 块里面的 location 配置指令里的 if 语句所在的 block 中。
先看该函数的返回值,处理成功时,返回 NGX_OK,否则返回 NGX_CONF_ERROR 或者是一个自定义的错误信息的字符串。再看一下这个函数被调用的时候,传入的三个参数。
cf: 该参数里面保存从配置文件读取到的原始字符串以及相关的一些信息。特别注意的是这个参数的 args 字段是一个 ngx_str_t 类型的数组,该数组的首个元素是这个配置指令本身,第二个元素是指令的第一个参数,第三个元素是第二个参数,依次类推。
cmd: 这个配置指令对应的 ngx_command_t 结构。
conf: 就是定义的存储这个配置值的结构体,比如在上面展示的那个ngx_http_hello_loc_conf_t。当解析这个 hello_string 变量的时候,传入的 conf 就指向一个 ngx_http_hello_loc_conf_t 类型的变量。用户在处理的时候可以使用类型转换,转换成自己知道的类型,再进行字段的赋值。
为了更加方便的实现对配置指令参数的读取,nginx 已经默认提供了对一些标准类型的参数进行读取的函数,可以直接赋值给 set 字段使用。下面来看一下这些已经实现的 set 类型函数。
(1)ngx_conf_set_flag_slot: 读取 NGX_CONF_FLAG 类型的参数。
(2)ngx_conf_set_str_slot:读取字符串类型的参数。
(3)ngx_conf_set_str_array_slot: 读取字符串数组类型的参数。
(4)ngx_conf_set_keyval_slot: 读取键值对类型的参数。
(5)ngx_conf_set_num_slot: 读取整数类型(有符号整数 ngx_int_t)的参数。
(6)ngx_conf_set_size_slot:读取 size_t 类型的参数,也就是无符号数。
(7)ngx_conf_set_off_slot: 读取 off_t 类型的参数。
(10)ngx_conf_set_msec_slot: 读取毫秒值类型的参数。
(11)gx_conf_set_sec_slot: 读取秒值类型的参数。
(12)ngx_conf_set_bufs_slot: 读取的参数值是 2 个,一个是 buf 的个数,一个是 buf 的大小。例如: output_buffers 1 128k;
(13)ngx_conf_set_enum_slot: 读取枚举类型的参数,将其转换成整数 ngx_uint_t 类型。
(14)ngx_conf_set_bitmask_slot: 读取参数的值,并将这些参数的值以 bit 位的形式存储。例如:HttpDavModule 模块的 dav_methods 指令。
看到这里,应该就比较清楚了。ngx_http_hello_commands 这个数组每 5 个元素为一组,用来描述一个配置项的所有情况。那么如果有多个配置项,只要按照需要再增加 5 个对应的元素对新的配置项进行说明。
需要注意的是,就是在 ngx_http_hello_commands 这个数组定义的最后,都要加一个ngx_null_command 作为结尾。
模块上下文结构
这是一个 ngx_http_module_t 类型的静态变量。这个变量实际上是提供一组回调函数指针,这些函数有在创建存储配置信息的对象的函数,也有在创建前和创建后会调用的函数。这些函数都将被 nginx 在合适的时间进行调用。
Nginx 里面的配置信息都是上下一层层的嵌套的,对于具体某个 location 的话,对于同一个配置,如果当前层次没有定义,那么就使用上层的配置,否则使用当前层次的配置。这些配置信息一般默认都应该设为一个未初始化的值,针对这个需求,Nginx 定义了一系列的宏定义来代表各种配置所对应数据类型的未初始化值,如下:
又因为对于配置项的合并,逻辑都类似,也就是前面已经说过的,如果在本层次已经配置了,也就是配置项的值已经被读取进来了(那么这些配置项的值就不会等于上面已经定义的那些UNSET 的值),就使用本层次的值作为定义合并的结果,否则,使用上层的值,如果上层的值也是这些 UNSET 类的值,那就赋值为默认值,否则就使用上层的值作为合并的结果。对于这样类似的操作,Nginx 定义了一些宏操作来做这些事情,我们来看其中一个的定义。
显而易见,这个逻辑确实比较简单,所以其它的宏定义也类似,我们就列具其中的一部分吧。
等等。
下面来看一下 hello 模块的模块上下文的定义,加深一下印象。
注意:这里并没有提供 merge_loc_conf 函数,因为我们这个模块的配置指令已经确定只出现在 NGX_HTTP_LOC_CONF 中这一个层次上,不会发生需要合并的情况。
模块的定义
对于开发一个模块来说,我们都需要定义一个 ngx_module_t 类型的变量来说明这个模块本身的信息,从某种意义上来说,这是这个模块最重要的一个信息,它告诉了 nginx 这个模块的一些信息,上面定义的配置信息,还有模块上下文信息,都是通过这个结构来告诉 nginx系统的,也就是加载模块的上层代码,都需要通过定义的这个结构,来获取这些信息。我们先来看下 ngx_module_t 的定义。
再看一下 hello 模块的模块定义。
模块可以提供一些回调函数给 nginx,当 nginx 在创建进程线程或者结束进程线程时进行调用。但大多数模块在这些时刻并不需要做什么,所以都简单赋值为 NULL。
handler 模块的基本结构
除了上一节介绍的模块的基本结构以外,handler 模块必须提供一个真正的处理函数,这个函数负责对来自客户端请求的真正处理。这个函数的处理,既可以选择自己直接生成内容,也可以选择拒绝处理,由后续的 handler 去进行处理,或者是选择丢给后续的 filter 进行处理。来看一下这个函数的原型申明。
typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
r 是 http 请求。里面包含请求所有的信息,这里不详细说明了,可以参考别的章节的介绍。该函数处理成功返回NGX_OK,处理发生错误返回NGX_ERROR,拒绝处理(留给后续的handler进行处理)返回 NGX_DECLINE。返回 NGX_OK 也就代表给客户端的响应已经生成好了,否则返回 NGX_ERROR 就发生错误了。
handler 模块的挂载
handler 模块真正的处理函数通过两种方式挂载到处理过程中,一种方式就是按处理阶段挂载;另外一种挂载方式就是按需挂载。
按处理阶段挂载
为了更精细地控制对于客户端请求的处理过程,nginx 把这个处理过程划分成了 11 个阶段。他们从前到后,依次列举如下:
一般情况下,我们自定义的模块,大多数是挂载在 NGX_HTTP_CONTENT_PHASE 阶段的。挂载的动作一般是在模块上下文调用的 postconfiguration 函数中。
注意:有几个阶段是特例,它不调用挂载地任何的 handler,也就是你就不用挂载到这几个阶段了:
NGX_HTTP_FIND_CONFIG_PHASE
NGX_HTTP_POST_ACCESS_PHASE
NGX_HTTP_POST_REWRITE_PHASE
NGX_HTTP_TRY_FILES_PHASE
所以其实真正是有 7 个 phase 你可以去挂载 handler。
挂载的代码如下:
使用这种方式挂载的 handler 也被称为 content phase handlers。
按需挂载
以这种方式挂载的 handler 也被称为 content handler。当一个请求进来以后,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。
下面来看一下使用这种挂载方式的具体例子。
handler 的编写步骤
好,到了这里,让我们稍微整理一下思路,回顾一下实现一个 handler 的步骤:
(1)编写模块基本结构。包括模块的定义,模块上下文结构,模块的配置结构等。
(2)实现 handler 的挂载函数。根据模块的需求选择正确的挂载方式。
(3)编写 handler 处理函数。模块的功能主要通过这个函数来完成。
现在我们来完整的分析前面提到的 hello handler module 示例的功能和代码。
hello handler 模块
在前面已经看到了这个 hello handler module 的部分重要的结构。该模块提供了 2 个配置指令,仅可以出现在 location 指令的作用域中。这两个指令是 hello_string, 该指令接受一个参数来设置显示的字符串。如果没有跟参数,那么就使用默认的字符串作为响应字符串。
另一个指令是 hello_counter,如果设置为 on,则会在响应的字符串后面追加 Visited Times:的字样,以统计请求的次数。
这里有两点注意一下:
(1)对于 flag 类型的配置指令,当值为 off 的时候,使用 ngx_conf_set_flag_slot 函数,会转化为 0,为 on,则转化为非 0。
(2)另外一个是,我提供了 merge_loc_conf 函数,但是却没有设置到模块的上下文定义中。这样有一个缺点,就是如果一个指令没有出现在配置文件中的时候,配置信息中的值,将永远会保持在 create_loc_conf 中的初始化的值。那如果,在类似 create_loc_conf 这样的函数中,对创建出来的配置信息的值,没有设置为合理的值的话,后面用户又没有配置,就会出现问题。所以要么就去设置,要么就去配置文件里面去配置信息。
下面来完整的给出 ngx_http_hello_module 模块的完整代码。
handler 模块的编译和使用
config 文件的编写
对于开发一个模块,我们是需要把这个模块的 C 代码组织到一个目录里,同时需要编写一个config 文件。这个 config 文件的内容就是告诉 nginx 的编译脚本,该如何进行编译。我们来看一下 hello handler module 的 config 文件的内容,然后再做解释。
其实文件很简单,几乎不需要做什么解释。大家一看都懂了。唯一需要说明的是,如果这个模块的实现有多个源文件,那么都在 NGX_ADDON_SRCS 这个变量里,依次写进去就可以。
编译
对于模块的编译,nginx 并不像 apache 一样,提供了单独的编译工具,可以在没有 apache 源代码的情况下来单独编译一个模块的代码。nginx 必须去到 nginx 的源代码目录里,通过configure 指令的参数,来进行编译。下面看一下 hello module 的 configure 指令:
$ ./configure –prefix=/usr/local/nginx-1.4.1 –add-module=
/home/jizhao/open_source/book_module
使用
使用一个模块需要根据这个模块定义的配置指令来做。比如我们这个简单的 hello handler module 的使用就很简单。在我的测试服务器的配置文件里,就是在 http 里面的默认的 server里面加入如下的配置:
更多 handler 模块示例分析
http access module
该模块的代码位于 src/http/modules/ngx_http_access_module.c 中。该模块的作用是提供对于特定 host 的客户端的访问控制。可以限定特定 host 的客户端对于服务端全部,或者某个server,或者是某个 location 的访问。 该模块的实现非常简单,总共也就只有几个函数。
对于与配置相关的几个函数都不需要做解释了,需要提一下的是函数 ngx_http_access_init,该函数在实现上把本模块挂载到了 NGX_HTTP_ACCESS_PHASE 阶段的 handler 上,从而使自己的被调用时机发生在了 NGX_HTTP_CONTENT_PHASE 等阶段前。因为进行客户端地址的限制检查,根本不需要等到这么后面。
另外看一下这个模块的主处理函数 ngx_http_access_handler。这个函数的逻辑也非常简单,主要是根据客户端地址的类型,来分别选择 ipv4 类型的处理函数 ngx_http_access_inet 还是ipv6 类型的处理函数 ngx_http_access_inet6。而这个两个处理函数内部也非常简单,就是循环检查每个规则,检查是否有匹配的规则,如果有就返回匹配的结果,如果都没有匹配,就默认拒绝。
http static module
从某种程度上来说,此模块可以算的上是“最正宗的”,“最古老”的 content handler。因为本模块的作用就是读取磁盘上的静态文件,并把文件内容作为产生的输出。在 Web 技术发展的早期,只有静态页面,没有服务端脚本来动态生成 HTML 的时候。恐怕开发个 Web 服务器的时候,第一个要开发就是这样一个 content handler。http static module 的代码位于src/http/modules/ngx_http_static_module.c 中,总共只有两百多行近三百行。可以说是非常短小。我们首先来看一下该模块的模块上下文的定义。
是非常的简洁吧,连任何与配置相关的函数都没有。对了,因为该模块没有提供任何配置指令。大家想想也就知道了,这个模块做的事情实在是太简单了,也确实没什么好配置的。唯一需要调用的函数是一个 ngx_http_static_init 函数。好了,来看一下这个函数都干了写什么。
仅仅是挂载这个 handler 到 NGX_HTTP_CONTENT_PHASE 处理阶段。简单吧?下面我们就看一下这个模块最核心的处理逻辑所在的 ngx_http_static_handler 函数。该函数大概占了这个模块代码量的百分之八九十。
首先是检查客户端的 http 请求类型(r->method),如果请求类型为 NGX_HTTP_GET |NGX_HTTP_HEAD | NGX_HTTP_POST , 则 继 续 进 行 处 理 , 否 则 一 律 返 回NGX_HTTP_NOT_ALLOWED 从而拒绝客户端的发起的请求。
其次是检查请求的 url 的结尾字符是不是斜杠‘/’,如果是说明请求的不是一个文件,给后续的 handler 去处理,比如后续的 ngx_http_autoindex_handler(如果是请求的是一个目录下面,可以列出这个目录的文件),或者是 ngx_http_index_handler(如果请求的路径下面有个默认的 index 文件,直接返回 index 文件的内容)。然后接下来调用了一个 ngx_http_map_uri_to_path 函数,该函数的作用是把请求的 http 协议的路径转化成一个文件系统的路径。
然后根据转化出来的具体路径,去打开文件,打开文件的时候做了 2 种检查,一种是,如果请求的文件是个 symbol link,根据配置,是否允许符号链接,不允许返回错误。还有一个检查是,如果请求的是一个名称,是一个目录的名字,也返回错误。如果都没有错误,就读取文件,返回内容。其实说返回内容可能不是特别准确,比较准确的说法是,把产生的内容传递给后续的 filter 去处理。
http log module
该模块提供了对于每一个 http 请求进行记录的功能,也就是我们见到的 access.log。当然这个模块对于 log 提供了一些配置指令,使得可以比较方便的定制 access.log。
这个模块的代码位于 src/http/modules/ngx_http_log_module.c,虽然这个模块的代码有接近1400 行,但是主要的逻辑在于对日志本身格式啊,等细节的处理。我们在这里进行分析主要是关注,如何编写一个 log handler 的问题
由于 log handler 的时候,拿到的参数也是 request 这个东西,那么也就意味着我们如果需要,可以好好研究下这个结构,把我们需要的所有信息都记录下来。对于 log handler,有一点特别需要注意的就是,log handler 是无论如何都会被调用的,就是只要服务端接受到了一个客户端的请求,也就是产生了一个 request 对象,那么这些个 log handler 的处理函数都会被调用的,就是在释放 request 的 时 候 被 调 用 的(ngx_http_free_request 函数)。
那 么 当 然 绝 对 不 能 忘 记 的 就 是 log handler 最 好 , 也 是 建 议 被 挂 载 在NGX_HTTP_LOG_PHASE 阶段。因为挂载在其他阶段,有可能在某些情况下被跳过,而没有执行到,导致你的 log 模块记录的信息不全。还有一点要说明的是,由于 nginx 是允许在某个阶段有多个 handler 模块存在的,根据其处理结果,确定是否要调用下一个 handler。但是对于挂载在 NGX_HTTP_LOG_PHASE 阶段的 handler,则根本不关注这里 handler 的具体处理函数的返回值,所有的都被调用。如下,位于 src/http/ngx_http_request.c 中的 ngx_http_log_request 函数。
本篇文章就分享到这里,欢迎关注,点赞,转发,分享。
最后
以上就是合适小虾米为你收集整理的不同的模块中定义同样的宏为不同的值合法吗_Nginx架构与Handler模块最详分析(3)...的全部内容,希望文章能够帮你解决不同的模块中定义同样的宏为不同的值合法吗_Nginx架构与Handler模块最详分析(3)...所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复