Nginx模块开发入门

Nginx配置文件基本结构

配置文件可以看做是Nginx的灵魂,Nginx服务在启动时会读入配置文件,而后续几乎一切动作行为都是按照配置文件中的指令进行的,因此如果将Nginx本身看做一个计算机,那么Nginx的配置文件可以看成是全部的程序指令。

下面是一个Nginx配置文件的实例:

Nginx配置文件是纯文本文件,你可以用任何文本编辑器如vim或emacs打开它,通常它会在nginx安装目录的conf下,如我的nginx安装在/usr/local/nginx,主配置文件默认放在/usr/local/nginx/conf/nginx.conf。

其中”#”表示此行是注释,由于笔者为了学习扩展开发安装了一个纯净的Nginx,因此配置文件没有经过太多改动。

Nginx的配置文件是以block的形式组织的,一个block通常使用大括号”{}”表示。block分为几个层级,整个配置文件为main层级,这是最大的层级;在main层级下可以有event、http等层级,而http中又会有server block,server block中可以包含location block。

每个层级可以有自己的指令(Directive),例如worker_processes是一个main层级指令,它指定Nginx服务的Worker进程数量。有的指令只能在一个层级中配置,如worker_processes只能存在于main中,而有的指令可以存在于多个层级,在这种情况下,子block会继承父block的配置,同时如果子block配置了与父block不同的指令,则会覆盖掉父block的配置。指令的格式是”指令名 参数1 参数2 … 参数N;”,注意参数间可用任意数量空格分隔,最后要加分号。

最后要提到的是配置文件是可以包含的,如上面配置文件中”include mime.types”就包含了mine.types这个配置文件,此文件指定了各种HTTP Content-type。

一般来说,一个server block表示一个Host,而里面的一个location则代表一个路由映射规则,这两个block可以说是HTTP配置的核心。

下图是Nginx配置文件通常结构图示。

关于Nginx配置的更多内容请参看Nginx官方文档。

Nginx模块工作原理概述

(Nginx本身支持多种模块,如HTTP模块、EVENT模块和MAIL模块,本文只讨论HTTP模块)

Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块(当然,多个location可以复用同一个模块)。handler模块负责处理请求,完成响应内容的生成,而filter模块对响应内容进行处理。因此Nginx模块开发分为handler开发和filter开发(本文不考虑load-balancer模块)。下图展示了一次常规请求和响应的过程。

下面本文展示一个简单的Nginx模块开发全过程,我们开发一个叫echo的handler模块,这个模块功能非常简单,它接收”echo”指令,指令可指定一个字符串参数,模块会输出这个字符串作为HTTP响应。例如,做如下配置:

直观来看,要实现这个功能需要三步:1、读入配置文件中echo指令及其参数;2、进行HTTP包装(添加HTTP头等工作);3、将结果返回给客户端。下面本文将分部介绍整个模块的开发过程。

定义模块配置结构

首先我们需要一个结构用于存储从配置文件中读进来的相关指令参数,即模块配置信息结构。根据Nginx模块开发规则,这个结构的命名规则为ngx_http_[module-name]_[main|srv|loc]_conf_t。其中main、srv和loc分别用于表示同一模块在三层block中的配置信息。这里我们的echo模块只需要运行在loc层级下,需要存储一个字符串参数,因此我们可以定义如下的模块配置:

其中字段ed用于存储echo指令指定的需要输出的字符串。注意这里ed的类型,在Nginx模块开发中使用ngx_str_t类型表示字符串,这个类型定义在core/ngx_string中:

其中两个字段分别表示字符串的长度和数据起始地址。注意在Nginx源代码中对数据类型进行了别称定义,如ngx_int_t为intptr_t的别称,为了保持一致,在开发Nginx模块时也应该使用这些Nginx源码定义的类型而不要使用C原生类型。除了ngx_str_t外,其它三个常用的nginx type分别为:

定义指令

一个Nginx模块往往接收一至多个指令,echo模块接收一个指令”echo”。Nginx模块使用一个ngx_command_t数组表示模块所能接收的所有模块,其中每一个元素表示一个条指令。ngx_command_t是ngx_command_s的一个别称(Nginx习惯于使用”_s”后缀命名结构体,然后typedef一个同名”_t”后缀名称作为此结构体的类型名),ngx_command_s定义在core/ngx_config_file.h中:

其中name是词条指令的名称,type使用掩码标志位方式配置指令参数,相关可用type定义在core/ngx_config_file.h中:

其中NGX_CONF_NOARGS表示此指令不接受参数,NGX_CON F_TAKE1-7表示精确接收1-7个,NGX_CONF_TAKE12表示接受1或2个参数,NGX_CONF_1MORE表示至少一个参数,NGX_CONF_FLAG表示接受”on|off”……

set是一个函数指针,用于指定一个参数转化函数,这个函数一般是将配置文件中相关指令的参数转化成需要的格式并存入配置结构体。Nginx预定义了一些转换函数,可以方便我们调用,这些函数定义在core/ngx_conf_file.h中,一般以”_slot”结尾,例如ngx_conf_set_flag_slot将”on或off”转换为”1或0″,再如ngx_conf_set_str_slot将裸字符串转化为ngx_str_t。

conf用于指定Nginx相应配置文件内存其实地址,一般可以通过内置常量指定,如NGX_HTTP_LOC_CONF_OFFSET,offset指定此条指令的参数的偏移量。

下面是echo模块的指令定义:

指令数组的命名规则为ngx_http_[module-name]_commands,注意数组最后一个元素要是ngx_null_command结束。

参数转化函数的代码为:

这个函数除了调用ngx_conf_set_str_slot转化echo指令的参数外,还将修改了核心模块配置(也就是这个location的配置),将其handler替换为我们编写的handler:ngx_http_echo_handler。这样就屏蔽了此location的默认handler,使用ngx_http_echo_handler产生HTTP响应。

创建合并配置信息

下一步是定义模块Context。

这里首先需要定义一个ngx_http_module_t类型的结构体变量,命名规则为ngx_http_[module-name]_module_ctx,这个结构主要用于定义各个Hook函数。下面是echo模块的context结构:

可以看到一共有8个Hook注入点,分别会在不同时刻被Nginx调用,由于我们的模块仅仅用于location域,这里将不需要的注入点设为NULL即可。其中create_loc_conf用于初始化一个配置结构体,如为配置结构体分配内存等工作;merge_loc_conf用于将其父block的配置信息合并到此结构体中,也就是实现配置的继承。这两个函数会被Nginx自动调用。注意这里的命名规则:ngx_http_[module-name][create|merge][main|srv|loc]_conf。

下面是echo模块这个两个函数的代码:

其中ngx_pcalloc用于在Nginx内存池中分配一块空间,是pcalloc的一个包装。使用ngx_pcalloc分配的内存空间不必手工free,Nginx会自行管理,在适当是否释放。

create_loc_conf新建一个ngx_http_echo_loc_conf_t,分配内存,并初始化其中的数据,然后返回这个结构的指针;merge_loc_conf将父block域的配置信息合并到create_loc_conf新建的配置结构体中。

其中ngx_conf_merge_str_value不是一个函数,而是一个宏,其定义在core/ngx_conf_file.h中:

同时可以看到,core/ngx_conf_file.h还定义了很多merge value的宏用于merge各种数据。它们的行为比较相似:使用prev填充conf,如果prev的数据为空则使用default填充。

编写Handler

下面的工作是编写handler。handler可以说是模块中真正干活的代码,它主要有以下四项职责:

读入模块配置。

处理功能业务。

产生HTTP header。

产生HTTP body。

下面先贴出echo模块的代码,然后通过分析代码的方式介绍如何实现这四步。这一块的代码比较复杂:

handler会接收一个ngx_http_request_t指针类型的参数,这个参数指向一个ngx_http_request_t结构体,此结构体存储了这次HTTP请求的一些信息,这个结构定义在http/ngx_http_request.h中:

由于ngx_http_request_s定义比较长,这里我只截取了一部分。可以看到里面有诸如uri,args和request_body等HTTP常用信息。这里需要特别注意的几个字段是headers_in、headers_out和chain,它们分别表示request header、response header和输出数据缓冲区链表(缓冲区链表是Nginx I/O中的重要内容,后面会单独介绍)。

第一步是获取模块配置信息,这一块只要简单使用ngx_http_get_module_loc_conf就可以了。

第二步是功能逻辑,因为echo模块非常简单,只是简单输出一个字符串,所以这里没有功能逻辑代码。

第三步是设置response header。Header内容可以通过填充headers_out实现,我们这里只设置了Content-type和Content-length等基本内容,ngx_http_headers_out_t定义了所有可以设置的HTTP Response Header信息:

设置好头信息后使用ngx_http_send_header就可以将头信息输出,ngx_http_send_header接受一个ngx_http_request_t类型的参数。

第四步也是最重要的一步是输出Response body。这里首先要了解Nginx的I/O机制,Nginx允许handler一次产生一组输出,可以产生多次,Nginx将输出组织成一个单链表结构,链表中的每个节点是一个chain_t,定义在core/ngx_buf.h:

其中ngx_chain_t是ngx_chain_s的别名,buf为某个数据缓冲区的指针,next指向下一个链表节点,可以看到这是一个非常简单的链表。ngx_buf_t的定义比较长而且很复杂,这里就不贴出来了,请自行参考core/ngx_buf.h。ngx_but_t中比较重要的是pos和last,分别表示要缓冲区数据在内存中的起始地址和结尾地址,这里我们将配置中字符串传进去,last_buf是一个位域,设为1表示此缓冲区是链表中最后一个元素,为0表示后面还有元素。因为我们只有一组数据,所以缓冲区链表中只有一个节点,如果需要输入多组数据可将各组数据放入不同缓冲区后插入到链表。下图展示了Nginx缓冲链表的结构:

缓冲数据准备好后,用ngx_http_output_filter就可以输出了(会送到filter进行各种过滤处理)。ngx_http_output_filter的第一个参数为ngx_http_request_t结构,第二个为输出链表的起始地址&out。ngx_http_out_put_filter会遍历链表,输出所有数据。

以上就是handler的所有工作,请对照描述理解上面贴出的handler代码。

组合Nginx Module

上面完成了Nginx模块各种组件的开发下面就是将这些组合起来了。一个Nginx模块被定义为一个ngx_module_t结构,这个结构的字段很多,不过开头和结尾若干字段一般可以通过Nginx内置的宏去填充,下面是我们echo模块的模块主体定义:

开头和结尾分别用NGX_MODULE_V1和NGX_MODULE_V1_PADDING 填充了若干字段,就不去深究了。这里主要需要填入的信息从上到下以依次为context、指令数组、模块类型以及若干特定事件的回调处理函数(不需要可以置为NULL),其中内容还是比较好理解的,注意我们的echo是一个HTTP模块,所以这里类型是NGX_HTTP_MODULE,其它可用类型还有NGX_EVENT_MODULE(事件处理模块)和NGX_MAIL_MODULE(邮件模块)。

这样,整个echo模块就写好了,下面给出echo模块的完整代码:

Nginx不支持动态链接模块,所以安装模块需要将模块代码与Nginx源代码进行重新编译。安装模块的步骤如下:

1、编写模块config文件,这个文件需要放在和模块源代码文件放在同一目录下。文件内容如下:

2、进入Nginx源代码,使用下面命令编译安装

这样就完成安装了,例如,我的源代码文件放在/home/yefeng/ngxdev/ngx_http_echo下,我的config文件为:

编译安装命令为:

这样echo模块就被安装在我的Nginx上了,下面测试一下,修改配置文件,增加以下一项配置:

然后用curl测试一下:

结果如下:

可以看到模块已经正常工作了,也可以在浏览器中打开网址,就可以看到结果:

本文只是简要介绍了Nginx模块的开发过程,由于篇幅的原因,不能面面俱到。因为目前Nginx的学习资料很少,如果读者希望更深入学习Nginx的原理及模块开发,那么阅读源代码是最好的办法。在Nginx源代码的core/下放有Nginx的核心代码,对理解Nginx的内部工作机制非常有帮助,http/目录下有Nginx HTTP相关的实现,http/module下放有大量内置http模块,可供读者学习模块的开发,另外在http://wiki.nginx.org/3rdPartyModules上有大量优秀的第三方模块,也是非常好的学习资料。

[3] Clément Nedelcu, Nginx Http Server. Packt Publishing, 2010

Original: https://www.cnblogs.com/leoo2sk/archive/2011/04/19/nginx-module-develop-guide.html
Author: T2噬菌体
Title: Nginx模块开发入门

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/543689/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • java BigDecimal解决浮点数的精度丢失和大数计算问题

    抛出浮点数问题: 先考个题,输入什么? System.out.println(0.1 + 0.2); 答案: 0.30000000000000004 在我们日常数学计算中,0.1+…

    Java 2023年6月16日
    0109
  • 第十到第十六周—— BLOG_3

    引言:java的课程进入尾声,但是编程的路才刚开始。 前言:这三周的大作业主要考察各个类之间的关系,数据的封装,类的继承,多态,接口,抽象类,集合框架等多个知识的综合运用。 自学正…

    Java 2023年6月8日
    081
  • Java设计模式之(十)——组合模式

    1、什么是组合模式? Compose objects into tree structures to represent part-whole hierarchies.Compos…

    Java 2023年5月29日
    071
  • 【SSM框架】MyBatis笔记 — 表之间的关联关系;MyBatis事务;MyBatis缓存机制;ORM概述

    一、表之间的关联关系: 关联关系是有方向的。 1、四种关联关系: 1)一对多关联:一个老师可以教多个学生,多个学生只有一个老师来教,站在老师方,就是一对多关联。 2)多对一关联:一…

    Java 2023年6月8日
    0106
  • Docker 完整版教程

    Docker 安装 一、安装前必读 在安装 Docker 之前,先说一下配置,我这里是Centos7 Linux 内核:官方建议 3.10 以上,3.8以上貌似也可。 注意:本文的…

    Java 2023年6月16日
    072
  • SpringSecurity 默认表单登录页展示流程源码

    SpringSecurity 默认表单登录页展示流程源码 本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程,涉及1.FilterSecurity…

    Java 2023年6月9日
    0111
  • 二分法查找案例

    public class BinarySearch { public static int binarySearch(int[] arr, int num) {int start …

    Java 2023年6月5日
    087
  • 2.搭建SSH

    1.创建项目在Eclipse中创建项目,右键解决报错即可导入MyEclipse中,防止Myeclipse中总是报错问题(注意jdk版本问题)2.导包: org.springfram…

    Java 2023年6月13日
    059
  • MongoDB笔记(一)

    MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统。 MongoDB 简介 MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系…

    Java 2023年6月8日
    087
  • 差分数组入门

    差分数组 什么是差分数组? 差分数组:差分数组就是原始数组相邻元素之间的差。 其实差分数组是一个 辅助数组,从侧面来表示给定某一数组的变化,一般用来对数组进行区间修改的操作。 比如…

    Java 2023年6月15日
    077
  • 3、并发问题

    线程不安全 java;gutter:true; public class TestThread3 implements Runnable{</p> <pre&gt…

    Java 2023年6月8日
    072
  • MySQL查询结果集字符串操作之多行合并与单行分割

    前言 我们在做项目写sql语句的时候,是否会遇到这样的场景,就是需要把查询出来的多列,按照字符串分割合并成一列显示,或者把存在数据库里面用逗号分隔的一列,查询分成多列呢,常见场景有…

    Java 2023年6月13日
    076
  • 领域驱动设计-CQRS

    CQRS 代表 命令查询职责分离。这是我第一次听到Greg Young描述的模式。其核心概念是,您可以使用与用于读取信息的模型不同的模型来更新信息。在某些情况下,这种分离可能很有价…

    Java 2023年6月15日
    083
  • java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.

    百度翻译就是:driverClassName需要jdbcUrl 在配置文件中的 改成: 就可以了。 spring.datasource.url 数据库的 JDBC URL。 spr…

    Java 2023年5月29日
    082
  • 常用命令

    移动文件:mv [源文件] [目标文件]删除文件或目录:rm –d删除目录 –f强制删除 –r递归删除 [文件或目录名]修改文件或目录群组:chgrp –r递归 [群组] [文件或…

    Java 2023年6月9日
    070
  • Spring Boot @Autowired 没法自动注入的问题

    Application 启动类: @SpringBootApplication @EnableConfigurationProperties @ComponentScan(base…

    Java 2023年5月30日
    094
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球