Nginx 模块开发

Nginx 模块概述

Nginx 模块有三种角色:

  • 处理请求并产生输出的 Handler 模块;

  • 处理由 Handler 产生的输出的 Filter(滤波器)模块;

  • 当出现多个后台服务器时,Load-balancer (负载均衡器)模块负责选择其中一个后台服务器发送请求;

通常,服务器启动时,任何 Handler 模块都有可能去处理配置文件中的 location 定义。若出现多个Handler 模块被配置成需要处理某一特定的 location 时,最终只有其中一个Handler 模块是成功的。Handler 模块有三种返回方式:

  1. 接收请求,并成功返回;

  2. 接收请求,但是出错返回;

  3. 拒绝请求,使默认的 Handler 模块处理该请求;

若 Handler 模块的作用是把一个请求反向代理到后台服务器,则会出现另一种类型的空间模块—— Load-balancer。 Load-balancer 负责决定将请求发送给哪个后端服务器。Nginx 目前支持两种 Load-balancer 模块:round-robin (轮询,处理请求就像打扑克时发牌那样)和"IP hash" method(众多请求时,保证来自同一 IP 的请求被分发的同一个后端服务器)。

若 Handler 模块没有产生错误返回时,则会调用 Filter 模块。每个location 配置里都可以添加多个Filter 模块 ,因此响应可以被压缩和分块。Filter 模块之间的处理顺序是在编译时就已经确定的。Filter 模块采用“CHAIN OF RESPONSIBILITY”链式的设计模式。当有请求到达时,请求依次经过这条链上的全部 Filter 模块,一个Filter 被调用并处理,接下来调用下一个Filter,直到最后一个Filter 被调用完成,Nginx 才真正完成响应流程。

总结如下,典型的处理形式如下:

Client sends HTTP request → Nginx chooses the appropriate handler based on the location config →  
 (if applicable) load-balancer picks a backend server →   
Handler does its thing and passes each output buffer to the first filter →   
First filter passes the output to the second filter → second to third → third to fourth → etc.   
→ Final response sent to client  

Nginx 模块的结构

模块的配置结构

模块最多可以定义三个配置结构:main、server、location。绝大多数模块仅需要一个location 配置。名称约定如下以ngx_http__(main|srv|loc)_conf_t为例的dav module:

Nginx 模块的数据结构如下定义:

在该数据结构中,其中最重要的是两个成员 ctx和commands,这里两个成员会在分别在下面的模块配置指令和模块上下文中讲解;若是HTTP 模块时,type 字段必须定义为NGX_HTTP_MODULE;

模块配置指令

模块指令存储在一个 ngx_command_t 类型的静态数组结构中,例如:

ngx_command_t 类型定义在 core/ngx_conf_file.h

name :配置指令的名称; type :该配置的类型,指定配置项的出现位置以及可携带参数的个数,下面规定只是其中一部分,更多信息可查看文件core/ngx_conf_file.h

set :这是一个函数指针,当Nginx 在解析配置时,若遇到该配置指令,将会把读取到的值传递给这个函数进行分解处理。因为具体每个配置指令的值如何处理,只有定义这个配置指令的人是最清楚的。来看一下这个函数指针要求的函数原型。

该函数处理成功时,返回 NGX_OK,否则返回 NGX_CONF_ERROR 或者是一个自定义的错误信息的字符串。该函数传入三个类型的参数:

  1. cf :指向ngx_conf_t 结构的指针,该结构包括从配置指令传递的参数;

  2. cmd:指向当前ngx_command_t 结构;

  3. conf:指向模块配置结构;

为了方便实现对配置指令参数的读取,Nginx 已经默认提供了对一些标准类型的参数进行读取的函数,可以直接赋值给set 字段使用。下面是一部分已经实现的set 类型函数,更多可参考文件core/ngx_conf_file.h

  • ngx_conf_set_flag_slot : 把 "on" 或 "off" 解析为 1 或 0;

  • ngx_conf_set_str_slot : 解析字符串并保存 ngx_str_t类型;

  • ngx_conf_set_num_slot: 解析一个数字并将其保存为int 类型;

  • ngx_conf_set_size_slot: 解析数据大小 ("8k", "1m", etc.) 并将其保存为size_t;

conf :用于指示配置项所处内存的相对偏移量,仅在type 中没有设置NGX_DIRECT_CONF 和NGX_MAIN_CONF 时才生效。对于HTTP 模块,conf 必须设置,它的取值如下:

  • NGX_HTTP_MAIN_CONF_OFFSET:使用create_main_conf 方法产生的结构体来存储解析出的配置项参数;

  • NGX_HTTP_SRV_CONF_OFFSET:使用 create_srv_conf 方法产生的结构体来存储解析出的配置项参数;

  • NGX_HTTP_LOC_CONF_OFFSET:使用 create_loc_conf 方法产生的结构体来存储解析出的配置项参数;

offset :表示当前配置项在整个存储配置项的结构体中的偏移位置。

模块上下文

这是一个静态的 ngx_http_module_t 结构,它的名称是ngx_http__module_ctx。以下是该结构的定义,具体可查阅文件 http/ngx_http_config.h

  • preconfiguration

  • postconfiguration

  • creating the main conf (i.e., do a malloc and set defaults)

  • initializing the main conf (i.e., override the defaults with what's in nginx.conf)

  • creating the server conf

  • merging it with the main conf

  • creating the location conf

  • merging it with the server conf

在以上的结构内容中,大多数模块只使用最后两项:ngx_http_create_loc_conf和ngx_http_merge_loc_conf;例如:

下面针对最后两项进行说明,以下是以 circle_gif 模块为例子,该模块源码

create_loc_conf 函数

该函数是传入一个 ngx_conf_t 结构的参数,返回新创建模块的配置结构,在这里是返回:ngx_http_circle_gif_loc_conf_t

merge_loc_conf 函数

Nginx 为不同的数据类型提供了merge 函数,可查阅 core/ngx_conf_file.h;merge_loc_conf 函数定义如下:

模块的定义

对任何开发模块,都需要定义一个 ngx_module_t 类型的变量来说明这个模块本身的信息,它告诉了 Nginx 这个模块的一些信息。这个变量是 ngx_http__module;例如:更多例子可查找文件 core/ngx_conf_file.h

Handler 模块

Handler 模块必须提供一个真正的处理函数,这个函数负责处理来自客户端的请求。该函数既可以选择自己直接生成内容,也可以选择拒绝处理,并由后续的 Handler 去进行处理,或者是选择丢给后续的 Filter 模块进行处理。以下是该函数的原型:

其中r 是 request 结构http 请求,包含客户端请求所有的信息,例如:request method, URI, and headers。 该函数处理成功返回NGX_OK,处理发生错误返回NGX_ERROR,拒绝处理(留给后续的Handler 进行处理)返回NGX_DECLINE。 返回NGX_OK 也就代表给客户端的响应已经生成,否则返回NGX_ERROR 就发生错误了。

Handler 模块处理过程中做了四件事情:获取 location 配置生成合适的响应发送响应的 header 头部发送响应的 body 包体

获取 location 配置

获取 location 配置 指向调用 ngx_http_get_module_loc_conf 函数即可,该函数传入的参数是 request 结构和 自定义的 module 模块。例如:circle gif模块;

生成合适的响应

这里主要是 request 结构,其定义如下:更多可参考文件 http/ngx_http_request.h

其中参数的意义如下:

  • uri 是 request 请求的路径,e.g. "/query.cgi".

  • args 是请求串参数中问号后面的参数(e.g. "name=john").

  • headers_in 包含有用的stuff,例如:cookies 和browser 信息。

发送响应的 header 头部

发送响应头部有函数ngx_http_send_header(r) 实现。响应的header 头部在 headers_out 结构中,定义如下:更多可参考文件 http/ngx_http_request.h

例如,一个模块设置为 Content-Type to "image/gif", Content-Length to 100, and return a 200 OK response,则其实现为:

假如content_encoding 是 (ngx_table_elt_t*)类型时,则模块需要为这些类型分配内存,可以调用ngx_list_push 函数,实现如下:

发送响应的 body 包体

到此,该模块已经产生响应,并把它存储在内存中。发送包体的步骤是:首先分配响应特殊的缓冲区,然后分配缓冲区链接到chain link,然后在 chain link 调用发送函数。

1、chain links 是 Nginx 使 Handler 模块在缓冲区中产生响应。在 chain 中每个 chain link 有一个指向下一个 link 的指针。首先,模块声明缓冲区 buffer 和 chain link:

2、然后分配缓冲区 buffer,使响应数据指向它:

3、接着,把模块挂载到 chain link 上:

4、最后,发送包体:

Handler 模块挂载

Handler 模块真正的处理函数通过两种方式挂载到处理过程中:按处理阶段挂载;按需挂载。

按处理阶段挂载

为了更精细地控制对于客户端请求的处理过程,Nginx 把这个处理过程划分成了11个阶段。依次列举如下:

一般情况下,我们自定义的模块,大多数是挂载在NGX_HTTP_CONTENT_PHASE阶段的。挂载的动作一般是在模块上下文调用的postconfiguration 函数中。注意:有几个阶段是特例,它不调用挂载任何的Handler,也就是你就不用挂载到这几个阶段了:

按需挂载

以这种方式挂载的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。但是使用这个方法挂载上去的handler 有一个特点是必须在NGX_HTTP_CONTENT_PHASE 阶段才能被执行。如果你想自己的handler 更早的阶段被执行,那就不要使用这种挂载方式。 以下是例子:

挂载函数:

Handler 模块编写

Handler 模块编写步骤如下:

  1. 编写模块基本结构:包括模块的定义,模块上下文结构,模块的配置结构等;

  2. 实现 handler 的挂载函数;根据模块的需求选择正确的挂载方式;

  3. 编写 handler 处理函数;模块的功能主要通过这个函数来完成;

Filter 模块

Filter 处理由Handler 模块产生的响应,即仅处理由服务器发往客户端的HTTP 响应,并不处理由客户端发往服务器的 HTTP 请求。Filter 模块包括过滤头部(Header Filter)和过滤包体(Body Filter ),Filter 模块过滤头部处理HTTP 的头部(HTTP headers),Filter 包体处理响应内容(response content)(即HTTP 包体),这两个阶段可以对HTTP 响应头部和内容进行修改。 Filter 模块 HTTP 响应的方法如下:定义在文件 src/http/ngx_http_core_module.h

其中,参数 r 是当前的请求,chain 是待发送的 HTTP 响应包体;

所有 HTTP 过滤模块都需要实现上面的两个方法,在 HTTP 过滤模块组成的链表中,链表元素就是处理方法。HTTP 框架定义了链表入口:

过滤模块链表中通过 next 遍历,其定义如下:

当执行发送 HTTP 头部或 HTTP 响应包体时,HTTP 框架是从 ngx_http_top_header_filter 和 ngx_http_top_body_filter 开始遍历 HTTP 头部过滤模块和 HTTP 包体过来模块。其源码实现在文件:src/http/ngx_http_core_module.c

Filter 模块相关结构

Filter 模块是采用链表形式的,其基本结构是ngx_chain_t 和 ngx_buf_t;这两种结构定义如下:

Filter 过滤头部

header filter 包含三个基本步骤:

  1. 决定是否处理响应;

  2. 对响应进行处理;

  3. 调用下一个 filter;

例如下面的"not modified" header filter:其中 headers_out 结构可参考文件 http/ngx_http_request.h

Filter 过滤包体

Filter 包体只能在chain link缓冲区buffer 中操作。模块必须决定是否修改输入缓冲区,或分配新的缓冲区替换当前缓冲区,或是在当前缓冲区之后还是之前插入新的缓冲区。很多模块接收多个缓冲区,导致这些模块在不完整的chain 缓冲区中操作。Filter 包体操作如下:

以下是一个例子:

Filter 模块挂载

Filters 模块和Handler 模块一样,也是挂载到post-configuration ,如下面代码所示:

其中 ngx_http_chunked_filter_init 处理如下定义:

由于 Filter 模块是 “CHAIN OF RESPONSIBILITY” 链表模式的。Handler 模块生成响应后,Filter 模块调用两个函数:ngx_http_output_filter 和 ngx_http_send_header,其中ngx_http_output_filter 函数是调用全局函数 ngx_http_top_body_filter;ngx_http_send_header 函数是调用全局函数 ngx_http_top_header_filter。

Filter 模块的执行方式如下图所示:

Filter 模块编写

Filter 模块编写步骤如下

  • 编写基本结构:模块定义,上下文结构,基本结构;

  • 初始化过滤模块:把本模块中处理的 HTTP 头部的 ngx_http_output_header_filter_pt 方法与处理HTTP 包体的ngx_http_output_body_filter_pt 方法插入到过滤模块链表首部;

  • 实现处理 HTTP 响应的方法:处理 HTTP 头部,即 ngx_http_output_header_filter_pt 方法的实现,处理HTTP 包体的方法,即ngx_http_output_body_filter_pt 方法的实现;

  • 编译安装;

开发 Nginx 新模块

把自己开发的模块编译到 Nginx 中需要编写两个文件:

  1. "config",该文件会被 ./configure 包含;

  2. "ngx_http__module.c",该文件是定义模块的功能;

config 文件的编写如下:

关于 "ngx_http__module.c" 文件的编写,可参考上面的Handler 模块,同时可参考Nginx 现有的模块:src/http/modules/;例如下面的“Hello World ”代码:

写好上面的两个文件后,在编译 Nginx 时,步骤如下:

参考资料:

Emiller's Guide To Nginx Module Development

nginx模块开发篇

https://github.com/simpl/ngx_devel_kit

Last updated

Was this helpful?