Nginx research, nginx module development

catalogphp

1. 初探nginx架构
2. handler模块
3. Nginx编译、安装、配置
4. Hello World模块开发

 

1. 初探nginx架构 html

nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。咱们也能够手动地关掉后台模式,让nginx在前台运行,而且经过配置让nginx取消master进程,从而可使nginx以单进程方式运行(很显然,生产环境下咱们确定不会这么作,因此关闭后台模式,通常是用来调试用的)。因此,咱们能够看到,nginx是以多进程的方式来工做的,固然nginx也是支持多线程的方式的,只是咱们主流的方式仍是多进程的方式,也是nginx的默认方式
nginx在启动后,会有一个master进程和多个worker进程nginx

1. master进程主要用来管理worker进程,包含:
    1) 接收来自外界的信号
    2) 向各worker进程发送信号
    3) 监控worker进程的运行状态
    4) 当worker进程退出后(异常状况下),会自动从新启动新的worker进程
2. woker进程: 基本的网络事件,则是放在worker进程中来处理了
    1) 多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的
    2) 一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求
    3) worker进程的个数是能够设置的,通常咱们会设置与机器cpu核数一致,这里面的缘由与nginx的进程模型以及事件处理模型是分不开的 

从上文中咱们能够看到,master来管理worker进程,因此咱们只须要与master进程通讯就好了。master进程会接收来自外界发来的信号,再根据信号作不一样的事情。因此咱们要控制nginx,只须要经过kill向master进程发送信号就好了。好比kill -HUP pid,则是告诉nginx,从容地重启nginx,咱们通常用这个信号来重启nginx,或从新加载配置,由于是从容地重启,所以服务是不中断的git

1. 首先master进程在接到信号后,会先从新加载配置文件
2. 而后再启动新的worker进程,并向全部老的worker进程发送信号,告诉他们能够光荣退休了
3. 新的worker在启动后,就开始接收新的请求(并获取新的配置文件)
4. 而老的worker在收到来自master的信号后,就再也不接收新的请求,而且在当前进程中的全部未处理完的请求处理完成后,再退出
/*
固然,直接给master进程发送信号,这是比较老的操做方式,nginx在0.8版本以后,引入了一系列命令行参数,来方便咱们管理。好比
    1) ./nginx -s reload: 重启nginx: 执行命令时,咱们是启动一个新的nginx进程,而新的nginx进程在解析到reload参数后,就知道咱们的目的是控制nginx来从新加载配置文件了,它会向master进程发送信号,而后接下来的动做,就和咱们直接向master进程发送信号同样了
    2) ./nginx -s stop: 中止nginx的运行 
*/

worker进程之间是平等的,每一个进程,处理请求的机会也是同样的。当咱们提供80端口的http服务时,一个链接请求过来,每一个进程都有可能处理这个链接github

1. 每一个worker进程都是从master进程fork过来
2. 在master进程里面,先创建好须要listen的socket(listenfd)以后,而后再fork出多个worker进程
3. 全部worker进程的listenfd会在新链接到来时变得可读(子进程和父进程经过fork共享文件句柄,这使得全部worker进程可以拥有同等的机会处理本次请求),为保证只有一个进程处理该链接,全部worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该链接(connfd)
//全部worker进程都会去争夺listenfd的读权限,但只有一个worker能最终得到,并调用accept得到connfd,进行后续的动做,而其余未争夺到本次listenfd的worker则继续等待下一次链接并争夺listenfd
4. 当一个worker进程在accept这个链接以后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开链接,这样一个完整的请求就是这样的了
5. 咱们能够看到,一个请求,彻底由worker进程来处理,并且只在一个worker进程中处理 

0x1: Nginx Master & Worker架构的优点apache

1. 对于每一个worker进程来讲,独立的进程,不须要加锁,因此省掉了锁带来的开销
2. 同时在编程以及问题查找时,也会方便不少
3. 采用独立的进程,可让互相之间不会影响,一个进程退出后,其它进程还在工做,服务不会中断,master进程则很快启动新的worker进程

0x2: Nginx的异步非阻塞请求模型编程

看看一个请求的完整过程。首先,请求过来,要创建链接,而后再接收数据,接收数据后,再发送数据。具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操做,若是不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu就会让出去给别人用了,对单线程的worker来讲,显然不合适,当网络事件越多时,你们都在等待呢,cpu空闲下来没人用,cpu利用率天然上不去了,更别谈高并发了。好吧,你说加进程数,这跟apache的线程模型有什么区别,注意,别增长无谓的上下文切换。因此,在nginx里面,最忌讳阻塞的系统调用了。不要阻塞,那就非阻塞喽。非阻塞就是,事件没有准备好,立刻返回EAGAIN,告诉你,事件还没准备好呢,你慌什么,过会再来吧。好吧,你过一会,再来检查一下事件,直到事件准备好了为止,在这期间,你就能够先去作其它事情,而后再来看看事件好了没。虽然不阻塞了,但你得不时地过来检查一下事件的状态,你能够作更多的事情了,但带来的开销也是不小的。因此,才会有了异步非阻塞的事件处理机制,具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。它们提供了一种机制,让你能够同时监控多个事件,调用他们是阻塞的,但能够设置超时时间,在超时时间以内,若是有事件准备好了,就返回。这种机制正好解决了咱们上面的两个问题,拿epoll为例(在后面的例子中,咱们多以epoll为例子,以表明这一类函数),当事件没准备好时,放到epoll里面,事件准备好了,咱们就去读写,当读写返回EAGAIN时,咱们将它再次加入到epoll里面。这样,只要有事件准备好了,咱们就去处理它,只有当全部事件都没准备好时,才在epoll里面等着。这样,咱们就能够并发处理大量的并发了,固然,这里的并发请求,是指未处理完的请求,线程只有一个,因此同时能处理的请求固然只有一个了,只是在请求间进行不断地切换而已,切换也是由于异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你能够理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这种事件处理方式是有很大的优点的,不须要建立线程,每一个请求占用的内存也不多,没有上下文切换,事件处理很是的轻量级。并发数再多也不会致使无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。 我以前有对链接数进行过测试,在24G内存的机器上,处理的并发请求数达到过200万。如今的网络服务器基本都采用这种方式,这也是nginx性能高效的主要缘由。
咱们以前说过,推荐设置worker的个数为cpu的核数,在这里就很容易理解了,更多的worker数,只会致使进程来竞争cpu资源了,从而带来没必要要的上下文切换。并且,nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,咱们能够将某一个进程绑定在某一个核上,这样就不会由于进程的切换带来cache的失效。像这种小的优化在nginx中很是常见,同时也说明了nginx做者的苦心孤诣。好比,nginx在作4个字节的字符串比较时,会将4个字符转换成一个int型,再做比较,以减小cpu的指令数等等 vim

Relevant Link:数组

http://tengine.taobao.org/book/chapter_02.html#

 

2. handler模块安全

做为第三方开发者最可能开发的就是三种类型的模块

1. handler: Handler模块就是接受来自客户端的请求并产生输出的模块
配置文件中使用location指令能够配置content handler模块,当Nginx系统启动的时候,每一个handler模块都有一次机会把本身关联到对应的location上(若是有多个handler模块都关联了同一个location,那么实际上只有一个handler模块真正会起做用)
handler模块处理的结果一般有三种状况
    1) 处理成功
    2) 处理失败(处理的时候发生了错误)
    3) 拒绝去处理。在拒绝处理的状况下,这个location的处理就会由默认的handler模块来进行处理。例如,当请求一个静态文件的时候,若是关联到这个location上的一个handler模块拒绝处理,就会由默认的ngx_http_static_module模块进行处理,该模块是一个典型的handler模块 

2. filter
3. load-balancer

0x1: 模块的基本数据结构

1. 模块配置结构

基本上每一个模块都会提供一些配置指令,以便于用户能够经过配置来控制该模块的行为。这些配置信息的存储就须要定义该模块的配置结构来进行存储
Nginx的配置信息分红了几个做用域(scope,有时也称做上下文)

1. main
2. server
3. location

每一个模块提供的配置指令也能够出如今这几个做用域里。那对于这三个做用域的配置信息,每一个模块就须要定义三个不一样的数据结构去进行存储,有一点须要特别注意的就是,在模块的开发过程当中,咱们最好使用nginx原有的命名习惯。这样跟原代码的契合度更高,对于模块配置信息的定义,命名习惯是

ngx_http_<module name>_(main|srv|loc)_conf_t。这里有个例子

2. 模块配置指令

一个模块的配置指令是定义在一个静态数组中的,src/core/ngx_conf_file.h

struct ngx_command_s 
{
    //配置指令的名称
    ngx_str_t             name;

    /*
    该配置的类型,其实更准确一点说,是该配置指令属性的集合。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
    1. NGX_CONF_TAKE12:配置指令接受1个或者2个参数
    2. NGX_CONF_TAKE13:配置指令接受1个或者3个参数
    3. NGX_CONF_TAKE23:配置指令接受2个或者3个参数
    4. NGX_CONF_TAKE123:配置指令接受1个或者2个或者3参数
    5. NGX_CONF_TAKE1234:配置指令接受1个或者2个或者3个或者4个参数
    6. NGX_CONF_1MORE:配置指令接受至少一个参数
    7. NGX_CONF_2MORE:配置指令接受至少两个参数
    8. NGX_CONF_MULTI: 配置指令能够接受多个参数,即个数不定
    
    1. NGX_CONF_BLOCK:配置指令能够接受的值是一个配置信息块。也就是一对大括号括起来的内容。里面能够再包括不少的配置指令。好比常见的server指令就是这个属性的
    2. NGX_CONF_FLAG:配置指令能够接受的值是”on”或者”off”,最终会被转成bool值
    3. NGX_CONF_ANY:配置指令能够接受的任意的参数值。一个或者多个,或者”on”或者”off”,或者是配置块
    值得注意的是,不管如何,nginx的配置指令的参数个数不能够超过NGX_CONF_MAX_ARGS个。目前这个值被定义为8,也就是不能超过8个参数值

    下面介绍一组说明配置指令能够出现的位置的属性。
    1. NGX_DIRECT_CONF:能够出如今配置文件中最外层。例如已经提供的配置指令daemon,master_process等
    2. NGX_MAIN_CONF: http、mail、events、error_log等
    3. NGX_ANY_CONF: 该配置指令能够出如今任意配置级别上
    
    对于咱们编写的大多数模块而言,都是在处理http相关的事情,也就是所谓的都是NGX_HTTP_MODULE,对于这样类型的模块,其配置可能出现的位置也是分为直接出如今http里面,以及其余位置
    1. NGX_HTTP_MAIN_CONF: 能够直接出如今http配置指令里
    2. NGX_HTTP_SRV_CONF: 能够出如今http里面的server配置指令里
    3. NGX_HTTP_LOC_CONF: 能够出如今http server块里面的location配置指令里
    4. NGX_HTTP_UPS_CONF: 能够出如今http里面的upstream配置指令里
    5. NGX_HTTP_SIF_CONF: 能够出如今http里面的server配置指令里的if语句所在的block中
    6. NGX_HTTP_LMT_CONF: 能够出如今http里面的limit_except指令的block中
    7. NGX_HTTP_LIF_CONF: 能够出如今http server块里面的location配置指令里的if语句所在的block中。
    */
    ngx_uint_t            type;

    /*
    set是一个函数指针,当nginx在解析配置的时候,若是遇到这个配置指令,将会把读取到的值传递给这个函数进行分解处理。由于具体每一个配置指令的值如何处理,只有定义这个配置指令的人是最清楚的 
    char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 
    1. cf: 该参数里面保存从配置文件读取到的原始字符串以及相关的一些信息。特别注意的是这个参数的args字段是一个ngx_str_t类型的数组
        1) 该数组的首个元素是这个配置指令自己
        2) 第二个元素是指令的第一个参数
        3) 第三个元素是第二个参数,依次类推
    2. cmd: 这个配置指令对应的ngx_command_t结构
    3. conf: 就是定义的存储这个配置值的结构体。用户在处理的时候可使用类型转换,转换成本身知道的类型,再进行字段的赋值

    为了更加方便的实现对配置指令参数的读取,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类型的参数
    8. ngx_conf_set_msec_slot: 读取毫秒值类型的参数
    9. ngx_conf_set_sec_slot: 读取秒值类型的参数
    10. ngx_conf_set_bufs_slot: 读取的参数值是2个,一个是buf的个数,一个是buf的大小。例如: output_buffers 1 128k;
    11. ngx_conf_set_enum_slot: 读取枚举类型的参数,将其转换成整数ngx_uint_t类型
    12. ngx_conf_set_bitmask_slot: 读取参数的值,并将这些参数的值以bit位的形式存储。例如:HttpDavModule模块的dav_methods指令   
    */
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

    /*
    该字段被NGX_HTTP_MODULE类型模块所用(咱们编写的基本上都是NGX_HTTP_MOUDLE,只有一些nginx核心模块是非NGX_HTTP_MODULE),该字段指定当前配置项存储的内存位置。其实是使用哪一个内存池的问题
    由于http模块对全部http模块所要保存的配置信息,划分了main, server和location三个地方进行存储,每一个地方都有一个内存池用来分配存储这些信息的内存。这里可能的值为 
    1. NGX_HTTP_MAIN_CONF_OFFSET
    2. NGX_HTTP_SRV_CONF_OFFSET
    3. NGX_HTTP_LOC_CONF_OFFSET
    4. 0(NGX_HTTP_MAIN_CONF_OFFSET)
    */
    ngx_uint_t            conf;
    
    /*
    指定该配置项值的精确存放位置,通常指定为某一个结构体变量的字段偏移。由于对于配置信息的存储,通常咱们都是定义个结构体来存储的
    那么好比咱们定义了一个结构体A,该项配置的值须要存储到该结构体的b字段。那么在这里就能够填写为offsetof(A, b)
    对于有些配置项,它的值不须要保存或者是须要保存到更为复杂的结构中时,这里能够设置为0 
    */
    ngx_uint_t            offset;

    //该字段存储一个指针。能够指向任何一个在读取配置过程当中须要的数据,以便于进行配置读取的处理。大多数时候,都不须要,因此简单地设为0便可
    void                 *post;
};

//须要注意的是,就是在ngx_http_hello_commands这个数组定义的最后,都要加一个ngx_null_command做为结尾
#define ngx_null_command  { ngx_null_string, 0, NULL, 0, 0, NULL }

3. 模块上下文结构

这是一个ngx_http_module_t类型的静态变量。这个变量其实是提供一组回调函数指针,这些函数有在建立存储配置信息对象时被调用的函数,也有在建立前和建立后会调用的函数。这些函数都将被nginx在合适的时间进行调用

typedef struct 
{
    //在建立和读取该模块的配置信息以前被调用
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);

    //在建立和读取该模块的配置信息以后被调用
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);
    
    //调用该函数建立本模块位于http block的配置信息存储结构。该函数成功的时候,返回建立的配置对象。失败的话,返回NULL
    void       *(*create_main_conf)(ngx_conf_t *cf);
    //调用该函数初始化本模块位于http block的配置信息存储结构。该函数成功的时候,返回NGX_CONF_OK。失败的话,返回NGX_CONF_ERROR或错误字符串
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    //调用该函数建立本模块位于http server block的配置信息存储结构,每一个server block会建立一个。该函数成功的时候,返回建立的配置对象。失败的话,返回NULL
    void       *(*create_srv_conf)(ngx_conf_t *cf);
    //由于有些配置指令既能够出如今http block,也能够出如今http server block中。那么遇到这种状况,每一个server都会有本身存储结构来存储该server的配置,可是在这种状况下http block中的配置与server block中的配置信息发生冲突的时候,就须要调用此函数进行合并,该函数并不是必须提供,当预计到绝对不会发生须要合并的状况的时候,就无需提供。固然为了安全起见仍是建议提供。该函数执行成功的时候,返回NGX_CONF_OK。失败的话,返回NGX_CONF_ERROR或错误字符串
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
    
    //调用该函数建立本模块位于location block的配置信息存储结构。每一个在配置中指明的location建立一个。该函数执行成功,返回建立的配置对象。失败的话,返回NULL
    void       *(*create_loc_conf)(ngx_conf_t *cf);
    //与merge_srv_conf相似,这个也是进行配置值合并的地方。该函数成功的时候,返回NGX_CONF_OK。失败的话,返回NGX_CONF_ERROR或错误字符串
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

Nginx里面的配置信息都是上下一层层的嵌套的,对于具体某个location的话,对于同一个配置,若是当前层次没有定义,那么就使用上层的配置,不然使用当前层次的配置(就近原则)
这些配置信息通常默认都应该设为一个未初始化的值,针对这个需求,Nginx定义了一系列的宏定义来表明各类配置所对应数据类型的未初始化值,以下

#define NGX_CONF_UNSET       -1
#define NGX_CONF_UNSET_UINT  (ngx_uint_t) -1
#define NGX_CONF_UNSET_PTR   (void *) -1
#define NGX_CONF_UNSET_SIZE  (size_t) -1
#define NGX_CONF_UNSET_MSEC  (ngx_msec_t) -1

4. 模块的定义

对于开发一个模块来讲,咱们都须要定义一个ngx_module_t类型的变量来讲明这个模块自己的信息,这是这个模块最重要的一个信息,它告诉了nginx这个模块的一些信息,上面定义的配置信息,还有模块上下文信息,都是经过这个结构来告诉nginx系统的,也就是加载模块的上层代码,都须要经过定义的这个结构,来获取这些信息

typedef struct ngx_module_s      ngx_module_t;
struct ngx_module_s {
    ngx_uint_t            ctx_index;
    ngx_uint_t            index;
    ngx_uint_t            spare0;
    ngx_uint_t            spare1;
    ngx_uint_t            abi_compatibility;
    ngx_uint_t            major_version;
    ngx_uint_t            minor_version;
    void                 *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;
    ngx_int_t           (*init_master)(ngx_log_t *log);
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
    void                (*exit_thread)(ngx_cycle_t *cycle);
    void                (*exit_process)(ngx_cycle_t *cycle);
    void                (*exit_master)(ngx_cycle_t *cycle);
    uintptr_t             spare_hook0;
    uintptr_t             spare_hook1;
    uintptr_t             spare_hook2;
    uintptr_t             spare_hook3;
    uintptr_t             spare_hook4;
    uintptr_t             spare_hook5;
    uintptr_t             spare_hook6;
    uintptr_t             spare_hook7;
};

#define NGX_NUMBER_MAJOR  3
#define NGX_NUMBER_MINOR  1
#define NGX_MODULE_V1          0, 0, 0, 0,                              \
    NGX_DSO_ABI_COMPATIBILITY, NGX_NUMBER_MAJOR, NGX_NUMBER_MINOR
#define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0

Relevant Link:

http://tengine.taobao.org/book/chapter_03.html

 

3. Nginx编译、安装、配置

0x1: 编译安装

1. wget https://codeload.github.com/nginx/nginx/zip/master
2. cd /usr/local/nginx/nginx-master
3. ./auto/configure --prefix=/usr/local/nginx
make
make install

//启动nginx
/usr/local/nginx/sbin/nginx

//Nginx默认以Deamon进程启动
curl -i http://localhost/

//中止Nginx
/usr/local/nginx/sbin/nginx -s stop

0x2: Nginx配置文件

配置文件能够看作是Nginx的灵魂,Nginx服务在启动时会读入配置文件,然后续几乎一切动做行为都是按照配置文件中的指令进行的

#user  nobody;
worker_processes  1;

error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

pid        logs/nginx.pid;

events {
    worker_connections  1024;
} 

http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   /usr/local/nginx/html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

每一个层级能够有本身的指令(Directive),例如worker_processes是一个main层级指令,它指定Nginx服务的Worker进程数量。有的指令只能在一个层级中配置,如worker_processes只能存在于main中,而有的指令能够存在于多个层级,在这种状况下,子block会继承父block的配置,同时若是子block配置了与父block不一样的指令,则会覆盖掉父block的配置,指令的格式是

指令名 参数1 参数2 … 参数N;
//注意参数间可用任意数量空格分隔,最后要加分号 

在开发Nginx HTTP扩展模块过程当中,须要特别注意的是main、server和location三个层级,由于扩展模块一般容许指定新的配置指令在这三个层级中
最后要提到的是配置文件是能够包含的,如上面配置文件中"include mime.types"就包含了mine.types这个配置文件,此文件指定了各类HTTP Content-type
通常来讲,一个server block表示一个Host,而里面的一个location则表明一个路由映射规则,这两个block能够说是HTTP配置的核心

Relevant Link:

http://tengine.taobao.org/book/chapter_03.html

 

4. Hello World模块开发

0x1: Nginx模块工做原理

Nginx自己支持多种模块,如HTTP模块、EVENT模块和MAIL模块(本文只讨论HTTP模块)
Nginx自己作的工做实际不多,当它接到一个HTTP请求时,它仅仅是经过查找配置文件将这次请求映射到一个location block,而此location中所配置的各个指令则会启动不一样的模块去完成工做,所以模块能够看作Nginx真正的劳动工做者。一般一个location中的指令会涉及一个handler模块和多个filter模块(多个location能够复用同一个模块)

1. handler模块负责处理请求,完成响应内容的生成
2. filter模块对响应内容进行处理
//所以Nginx模块开发分为handler开发和filter开发(暂考虑load-balancer模块)

咱们接下来学习一个简单的Nginx模块开发全过程,咱们开发一个叫echo的handler模块,这个模块功能很是简单,它接收"echo"指令,指令可指定一个字符串参数,模块会输出这个字符串做为HTTP响应。例如,作以下配置

location /echo {
    echo "hello nginx";
}
//nginx根据conf配置文件来指导其自身的行为

直观来看,要实现这个功能须要三步

1. 读入配置文件中echo指令及其参数
2. 进行HTTP包装(添加HTTP头等工做)
3. 将结果返回给客户端 

0x2: 定义模块配置结构

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

typedef struct {
    ngx_str_t  ed;
} ngx_http_echo_loc_conf_t;

0x3: 定义指令

一个Nginx模块每每接收一至多个指令,echo模块接收一个指令“echo”。Nginx模块使用一个ngx_command_t数组表示模块所能接收的全部模块,其中每个元素表示一个条指令。ngx_command_t是ngx_command_s的一个别称(Nginx习惯于使用"_s"后缀命名结构体,而后typedef一个同名"_t"后缀名称做为此结构体的类型名)
下面是echo模块的定义

static ngx_command_t  ngx_http_echo_commands[] = {
    { ngx_string("echo"),
        NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
        ngx_http_echo,
        NGX_HTTP_LOC_CONF_OFFSET,
        offsetof(ngx_http_echo_loc_conf_t, ed),
        NULL },
        ngx_null_command
};
//指令数组的命名规则为ngx_http_[module-name]_commands,注意数组最后一个元素要是ngx_null_command结束

参数转化函数(ngx_http_echo)的代码为

static char *
ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf;
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

    //修改了核心模块配置(也就是这个location的配置),将其handler替换为咱们编写的handler:ngx_http_echo_handler。这样就屏蔽了此location的默认handler,使用ngx_http_echo_handler产生HTTP响应
    
    clcf->handler = ngx_http_echo_handler;
    
    //调用ngx_conf_set_str_slot转化echo指令的参数
    ngx_conf_set_str_slot(cf,cmd,conf);
    
    return NGX_CONF_OK;
}

0x4: 建立合并配置信息

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

static ngx_http_module_t  ngx_http_echo_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */
    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */
    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */
    ngx_http_echo_create_loc_conf,         /* create location configration */
    ngx_http_echo_merge_loc_conf           /* merge location configration */
};

一共有8个Hook注入点,分别会在不一样时刻被Nginx调用,因为咱们的模块仅仅用于location域,这里将不须要的注入点设为NULL便可

1. create_loc_conf用于初始化一个配置结构体,如为配置结构体分配内存等工做
2. merge_loc_conf用于将其父block的配置信息合并到此结构体中,也就是实现配置的继承
//这两个函数会被Nginx自动调用。注意这里的命名规则:ngx_http_[module-name]_[create|merge]_[main|srv|loc]_conf 

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

static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_echo_loc_conf_t  *conf;
    //ngx_pcalloc用于在Nginx内存池中分配一块空间,是pcalloc的一个包装。使用ngx_pcalloc分配的内存空间没必要手工free,Nginx会自行管理,在适当是否释放
    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t));
    if (conf == NULL) {
        return NGX_CONF_ERROR;
    }
    conf->ed.len = 0;
    conf->ed.data = NULL;
    //create_loc_conf新建一个ngx_http_echo_loc_conf_t,分配内存,并初始化其中的数据,而后返回这个结构的指针
    return conf;
}

static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_echo_loc_conf_t *prev = parent;
    ngx_http_echo_loc_conf_t *conf = child;
    ngx_conf_merge_str_value(conf->ed, prev->ed, "");
    //merge_loc_conf将父block域的配置信息合并到create_loc_conf新建的配置结构体中
    return NGX_CONF_OK;
}

0x5: 编写Handler

handler能够说是模块中真正实现功能业务逻辑的代码,它主要有如下四项职责

1. 读入模块配置
2. 处理功能业务
3. 产生HTTP header
4. 产生HTTP body 

code

/*
* Copyright (C) Eric Zhang
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
/* Module config */
typedef struct {
    ngx_str_t  ed;
} ngx_http_echo_loc_conf_t;
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);
/* Directives */
static ngx_command_t  ngx_http_echo_commands[] = {
    { ngx_string("echo"),
        NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
        ngx_http_echo,
        NGX_HTTP_LOC_CONF_OFFSET,
        offsetof(ngx_http_echo_loc_conf_t, ed),
        NULL },
        ngx_null_command
};
/* Http context of the module */
static ngx_http_module_t  ngx_http_echo_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */
    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */
    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */
    ngx_http_echo_create_loc_conf,         /* create location configration */
    ngx_http_echo_merge_loc_conf           /* merge location configration */
};
/* Module */
//完成了Nginx模块各类组件的开发下面就是将这些组合起来了。一个Nginx模块被定义为一个ngx_module_t结构
ngx_module_t  ngx_http_echo_module = {
    NGX_MODULE_V1,
    &ngx_http_echo_module_ctx,             /* module context */
    ngx_http_echo_commands,                /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};
/* Handler function */
//handler会接收一个ngx_http_request_t指针类型的参数,这个参数指向一个ngx_http_request_t结构体,此结构体存储了此次HTTP请求的一些信息
static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r)
{
    ngx_int_t rc;
    ngx_buf_t *b;
    ngx_chain_t out;
    ngx_http_echo_loc_conf_t *elcf;
    //获取模块配置信息
    elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module);
    if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST)))
    {
        return NGX_HTTP_NOT_ALLOWED;
    }
    //设置response header
    r->headers_out.content_type.len = sizeof("text/html") - 1;
    r->headers_out.content_type.data = (u_char *) "text/html";
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = elcf->ed.len;
    if(r->method == NGX_HTTP_HEAD)
    {
        rc = ngx_http_send_header(r);
        if(rc != NGX_OK)
        {
            return rc;
        }
    }
    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if(b == NULL)
    {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer.");
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    out.buf = b;
    out.next = NULL;
    b->pos = elcf->ed.data;
    b->last = elcf->ed.data + (elcf->ed.len);
    b->memory = 1;
    b->last_buf = 1;
    //使用ngx_http_send_header就能够将头信息输出
    rc = ngx_http_send_header(r);
    if(rc != NGX_OK)
    {
        return rc;
    }
    /*
    最后一步也是最重要的一步是输出Response body,Nginx容许handler一次产生一组输出,能够产生屡次,Nginx将输出组织成一个单链表结构
    struct ngx_chain_s 
    {
        ngx_buf_t    *buf;
        ngx_chain_t  *next;
    };
    其中ngx_chain_t是ngx_chain_s的别名,buf为某个数据缓冲区的指针,next指向下一个链表节点,能够看到这是一个很是简单的链表
    ngx_buf_t的定义比较长并且很复杂,这里就不贴出来了,请自行参考core/ngx_buf.h。ngx_but_t中比较重要的是pos和last,分别表示要缓冲区数据在内存中的起始地址和结尾地址,这里咱们将配置中字符串传进去,last_buf是一个位域,设为1表示此缓冲区是链表中最后一个元素,为0表示后面还有元素。由于咱们只有一组数据,因此缓冲区链表中只有一个节点,若是须要输入多组数据可将各组数据放入不一样缓冲区后插入到链表
    缓冲数据准备好后,用ngx_http_output_filter就能够输出了(会送到filter进行各类过滤处理)
    */
    return ngx_http_output_filter(r, &out);
}
static char *
ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf;
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_echo_handler;
    ngx_conf_set_str_slot(cf,cmd,conf);
    return NGX_CONF_OK;
}
static void *
ngx_http_echo_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_echo_loc_conf_t  *conf;
    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t));
    if (conf == NULL) {
        return NGX_CONF_ERROR;
    }
    conf->ed.len = 0;
    conf->ed.data = NULL;
    return conf;
}
static char *
ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_echo_loc_conf_t *prev = parent;
    ngx_http_echo_loc_conf_t *conf = child;
    ngx_conf_merge_str_value(conf->ed, prev->ed, "");
    return NGX_CONF_OK;
}

0x6: Nginx模块的安装

Nginx不支持动态连接模块,因此安装模块须要将模块代码与Nginx源代码进行从新编译。安装模块的步骤以下

1. cd  /usr/local/nginx/nginx-master/src/http/modules/
2. mkdir ngx_http_echo_module
3. cd ngx_http_echo_module
4. vim /usr/local/nginx/nginx-master/src/http/modules/ngx_http_echo_module/config
/*
ngx_addon_name=ngx_http_echo_module
HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c"
*/

5. cd /usr/local/nginx/nginx-master
6. ./auto/configure --prefix=/usr/local/nginx/ --add-module=/usr/local/nginx/nginx-master/src/http/modules/ngx_http_echo_module/
7. make
8. make install
9. vim /usr/local/nginx/conf/nginx.conf
//编辑增长 
location /echo {
    echo "hello nginx";
}

10. 重启
/usr/local/nginx/sbin/nginx -s stop
/usr/local/nginx/sbin/nginx  

11. 访问
http://121.40.254.73/echo

Relevant Link:

http://blog.codinglabs.org/articles/intro-of-nginx-module-development.html
http://blog.csdn.net/poechant/article/details/7627828
http://bg.biedalian.com/2013/08/09/nginx-hello-world.html

 

Copyright (c) 2015 LittleHann All rights reserved