Emiller的nginx模块开发指引- 关注web后端技术- 博客园

      本文译自:

      下载链接:    

 

    要非常感谢nginx,它帮助我更加了解蝙蝠侠这个喜剧角色。

    蝙蝠侠很快,nginx也很快。蝙蝠侠在与罪恶斗争,nginx在与浪费CPU、内存泄漏等现象做斗争。蝙蝠侠在压力下能保持良好状态,nginx在强大的服务请求压力下表现出色。

但是,蝙蝠侠如果没有那根蝙蝠侠{wn}腰带(batman utility belt),那他就什么都不是。

在任何时候,蝙蝠侠的{wn}腰带都应该包括 一个锁扣、几个batarang(蝙蝠侠的特殊武器)、几个bat-cuff(护腕)、 夜视眼镜、bat-tracer(xxx?)、几个bat-darts(蝙蝠镖)...或者还包括一个apple iphone。当蝙蝠侠需要使他的敌人失明、失聪、或者晕倒,或者当他需要跟踪他的敌人,或者给他的敌人发个短信,你{zh0}相信他正在他的{wn}腰带上找一个合适的工具。这根腰带对蝙蝠侠的行动如此至关重要,所以,当蝙蝠侠在选择穿裤子还是穿腰带的时候,他肯定会选择穿腰带。事实上他确实选择了腰带,这就是为什么蝙蝠侠穿着紧绷的橡胶衣,而没有穿裤子。

虽然nginx没有这样一条{wn}腰带,但是nginx有一个模块链(module chain),当nginx需要对应答进行gzip或chunked编码时,它会拿出一个模块来做这个工作。当nginx基于IP或http认证来阻拦对某些资源的访问时,也是由一个模块来做这些工作的。同样,当nginx需要和memcahed或者fastCGI交互时,也分别由对应的模块来做相应的工作。

尽管蝙蝠侠的{wn}腰带上有很多小玩意,但是有时候他还是需要一个新的小工具。也许他有新的敌人,而他现在的武器(如batarang或bat-cuff)都不足以对付这个敌人。也许他需要有新的技能,比如在水下呼吸。所以他会让Lucius Fox来帮他设计这些小工具。

这个文档会告诉你nginx模块链表(module chain)的一些细节,这样你就可以做到像Lucius Fox一样。当你完成这个课程,你将可以设计并写出高质量的模块,来完成nginx以前做不了的事。Nginx的模块系统有很多需要注意的细节,所以你可能需要经常返回这篇文档阅读。我已尽力将这些内容梳理清楚,但我比较愚钝,写nginx模块仍然是很费力的一件事情。

你需要熟悉C。不仅仅是C语言语法;你需要熟悉C结构体相关的知识;不能被指针和函数引用所吓倒;对预编译、宏有一定的认识。如果你需要提高一下C语言方面的知识,没有什么比这更好了:K&R。

对HTTP协议的了解很有用,毕竟你是在一个webserver之上工作。

你也应该对nginx的配置文件非常熟悉。如果你不熟悉它,这里有一个简短的介绍:配置文件中有4种context(分别叫main,server,upstream,location)。每个context中可以包含数个拥有一个或多个参数的指令。在main context中的指令对所有事情生效;在server context中的指令对一个特定的host/port生效;在upstream context中的指令对后端的一组server生效;在location context中的指令仅对能匹配到的web地址(如"/","/images"等)生效。一个location context会继承包含它的server context的属性,同样,一个server context会继承main context的属性。Upstream context不会继承任何属性,也不会把自己的属性传递给其它;它有自己的特殊的指令,这些指令不会在其它任何地方生效。我比较多地提到了这4个context,所以...别忘记它们。

让我们开始吧。

Nginx模块中有3个角色我们将要关注:

1. Handler:处理请求并且产生输出。

2. Filter:对handler产生的输出进行操作(比如gzip压缩,chunked编码等)

3. Load-balancer:当有多个符合条件的后端server可供选择时,从中选择一个以便将请求转发过去。

模块承担着所有你能想到的和webserver相关的实际工作:当nginx寻找一个静态文件,或者将请求代理到另外一台server的时候,这时候有一个handler模块来做这些工作;当nginx对输出进行编码,或者执行有一个服务端的包含(server-side include)操作,它将使用filter模块。Nginx的核心模块很简单,它关心网络通信、应用协议。另外,当处理一个请求时,将所有需要调用的合格模块排好序(以便在处理时依次调用)。这种没有中心控制节点的架构,让你可以写一个非常好的自包含模块来做你想做的事情。

    注意:不像apache中的模块,nginx中的模块不是以动态链接库(so)的方式存在。(换句话说,它被编译进nginx这个可执行的bin文件)。

    一个模块是如何被调用的?一般情况下,当nginx启动时,每个handler都有一个机会将自己和配置文件中的某个location关联起来;如果有2个以上的模块关联到同一个location,那么仅有其中一个能成功(当然,一个好的配置书写人员不会上这种冲突产生)。Handler能以3种方式返回:所有处理成功;有错误产生;拒绝处理请求并将它传给默认handler处理(默认handler通常返回一个静态文件)。

如果一个handler是一个到一些后端server组的反向代理,那么就需要用到load-balancer。一个load-balancer决定一个被接收到的请求将要被发送到后端server组中的哪一台。Nginx自带2个load-balancer模块:round-robin(轮询),它像我们在xx牌游戏中发牌一样,(将请求轮流发送给后端server);iphash,它能保证一个客户端的多次请求可以被同一台后端server处理。

如果handler在处理过程没有产生错误,那么filter将被调用。每个location都可以被关联(hook)多个filter,所以,(举个例子),一个应答可以先被压缩,然后被chunked编码。它们的调用顺序在编译时就决定好了。Filter是一种典型的CHAIN OF RESPONSIBILITY(职责链)设计模式。一个filter被调用,做完它的工作,然后再调用下一个,直到{zh1}一个filter被调用,nginx才完成它对response的处理。

Filter链表中最酷的设计在于,每个filter并不需要等待前一个filter完成后才能开始工作,它可以像unix系统管道一样工作,在前一个filter产生输出的同时处理。Filter在buffer之上操作。Buffer通常的大小为4K(系统页面大小),当然,你可以在nginx.conf中改变这个值。这就意味着,不必等到后端server上接收到整个应答,也就是说,只要接收到应答的一小部分,这个模块就可以开始压缩应答,并把这小部分先返回给客户端。

总结一下主要的概念,典型的处理流程如下:

客户端发送HTTP请求 -> nginx根据配置文件中的location配置选择正确的模块 -> (如果需要)load-balancer选择一个后端server -> handler处理完它的事情并把每个输出buffer传递给{dy}个filter -> {dy}个filter传递处理后的buffer给第二个filter -> 第二个filter传递给第三个 -> ... -> 最终应答发送给客户端

我用到了“典型”这个词,因为nginx的模块调用非常可定制化。模块何时被调用,以及如何被调用,都给模块开发人员带来很大的负担。调用通过一系列回调函数来实现,这些回调函数有很多。在以下情况下,你都可以指定一个函数来执行:

1. 在server读取配置文件之前

2. 当每一条配置指令出现在每一个loaction和server context的时候

3. 当nginx初始化main配置

4. 当nginx初始化server配置

5. 当nginx将main配置与server配置合并

6. 当nginx初始化location配置

7. 当nginx将某个location父节点server的配置与这个location节点的配置合并

8. 当nginx master进程启动

9. 当nginx worker进程启动

10. 当nginx master进程退出

11. 处理一个请求

12. 操作应答消息头

13. 操作应答消息体

14. 选择一个后端server

15. 初始化对后端server的一个请求

16. 重新初始化到后端server的请求

17. 处理从后端server接收到的应答

18. 完成与后端server的一次交互

通过这些回调函数你能做很多事情,现在是时候深入研究一下nginx的几个模块了。

我说过,你有非常大的机动性来做一个nginx模块。这一节将描述一些常见的部分。它是一个帮助你读懂一个模块的指引,也是一本写模块时需要翻阅的手册。

一个模块可以分别为main、server、location这3种context分别定义3种配置结构。事实上,大部分模块仅仅需要一个location配置结构。这些配置结构的命名习惯一般是这样的: ngx_http_<module name>_(main|srv|loc)_conf_t。这里有一个例子(从dav模块中拿出):

typedef struct { 

    ngx_uint_t  methods; 

    ngx_flag_t  create_full_put_path; 

    ngx_uint_t  access; 

} ngx_http_dav_loc_conf_t; 

注意,Nginx有一些特殊的数据类型(比如ngx_uin_t、ngx_flag_t)。这些仅仅只是一些元数据类型的别名。

这些结构中的元素由模块指令填充。

    一个模块的指令出现在一个ngx_command_t数组中,这里有一个例子,来说明他们是如何定义的。这个例子来自于我写的一个小模块

static ngx_command_t  ngx_http_circle_gif_commands[] = { 

    { ngx_string("circle_gif"), 

      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, 

      ngx_http_circle_gif, 

      NGX_HTTP_LOC_CONF_OFFSET, 

      0, 

      NULL }, 

 

    { ngx_string("circle_gif_min_radius"), 

      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 

      ngx_conf_set_num_slot, 

      NGX_HTTP_LOC_CONF_OFFSET, 

      offsetof(ngx_http_circle_gif_loc_conf_t, min_radius), 

      NULL }, 

      ... 

      ngx_null_command 

}; 

这里有ngx_command_t(我们定义的结构体)的声明,你能在core/ngx_conf_file.h中找到它。

struct ngx_command_t { 

    ngx_str_t             name; 

    ngx_uint_t            type; 

    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 

    ngx_uint_t            conf; 

    ngx_uint_t            offset; 

    void                 *post; 

}; 

    看起来有点多,但是每个成员都有它的用途和目的。

Name是指令字符串,不含空格。数据类型是ngx_str_t。举个例子,它经常被这样初始化:ngx_str("proxy_pass")。注意:ngx_str_t是一个包含2个成员变量的结构,data和len,data为一个字符串,len为该字符串的长度。在大多数需要使用字符串的时候,nginx都使用该结构体。

Type是一组标志位。通过这个标志位,来说明这个指令带几个参数,配置在何处生效。这些标志位可以被按位或,它们包括:

NGX_HTTP_MAIN_CONF:指令在main配置中生效

NGX_HTTP_SRV_CONF:指令在server配置中生效

NGX_HTTP_LOC_CONF:指令在location配置中生效

NGX_CONF_NOARGS:指令不带参数

NGX_CONF_TAKE1:指令带一个参数

NGX_CONF_TAKE2:指令带二个参数

...

NGX_CONF_TAKE7:指令带7个参数

NGX_CONF_FLAG:指令带有一个bool变量参数("on"或"off")

NGX_CONF_1MORE:指令带有一个以上参数(至少一个)

NGX_CONF_2MORE:指令带有二个以上参数(至少二个)

还有一些其它选项,请参见core/ngx_conf_file.h

Set是函数指针,用来设置模块配置结构(module configuration structrue)中的某个部分(某个变量)。一般情况下,这个函数会把指令的参数传递进来,并在配置结构中设置一个合适的值。这个设置函数带有3个参数:

1. 一个指向ngx_conf_t结构体的指针。该结构体中包含了传递给这个指令的参数。

2. 一个指向当前的ngx_command_t结构体的指针

3. 一个执行模块自定义结构体(module configure structrues)的指针

当系统在解析配置文件时,如果遇到这个指令,则会调用这个设置函数(set所指函数)。Nginx提供了一系列的函数,来设置自定义结构体中不同的类型。这些函数包括:

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",等)并把它保存到size_t变量中

还有一些其它有用的函数(参看core/ngx_conf_file.h)。如果这些系统已有的函数不够用,每个模块也可以把自己的函数放在这。

这些内置函数如何知道把数据存放在哪?这就要提到ngx_command_t中的另外两个成员了,conf和offset。Conf告诉nginx数据将会被保存在main配置,还是server配置,还是location配置中(通过使用NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET、NGX_HTTP_LOC_CONF_OFFSET)。接下来,offset说明了配置中的哪部分将被填充。

Post是模块在读取配置时可能会用到的一个指针,通常情况下,这个字段为NULL。

这个command数组以ngx_null_command结束。

这是一个ngx_http_module_t类型的静态变量,它有一系列来创建这3种配置并合并它们。它的名字是ngx_http_<module_name>_module_ctx。下面,按顺序列出这些函数:

1. Preconfiguration:配置初始化之前调用

2. Postconfiguration:配置初始化之后调用

3. Creating the main conf:创建main配置(比如malloc,memset)。

4. Initializing the main conf:初始化main配置

5. Creating the server conf:创建server配置

6. Merging it with the main conf:将main配置与server配置合并

7. Creating the location conf:创建location配置

8. Merging it with the server conf:将server配置与location配置合并

这些函数因为完成的功能不同,所以都带有不同的参数。这里有ngx_http_module_t的定义(来自http/ngx_http_config.h):

typedef struct { 

    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf); 

    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf); 

    void       *(*create_main_conf)(ngx_conf_t *cf); 

    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf); 

    void       *(*create_srv_conf)(ngx_conf_t *cf); 

    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf); 

    void       *(*create_loc_conf)(ngx_conf_t *cf); 

    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); 

} ngx_http_module_t; 

你可以把你不需要用到的函数设置为NULL,nginx将会检测到它。

大部分handler都只用到{zh1}2个函数。 Create_loc_conf用来为location配置分配空间,merge_loc_conf用来为location配置初始化,并将它与继承而来的配置合并。如果配置是非法的,可以在merge_loc_conf中返回一个错误,server将因为这个错误而中止启动。

这里有一个模块上下文结构的例子:

static ngx_http_module_t  ngx_http_circle_gif_module_ctx = { 

    NULL,                          /* preconfiguration */ 

    NULL,                          /* postconfiguration */ 

    NULL,                          /* create main configuration */ 

    NULL,                          /* init main configuration */ 

    NULL,                          /* create server configuration */ 

    NULL,                          /* merge server configuration */ 

    ngx_http_circle_gif_create_loc_conf,  /* create location configuration */ 

    ngx_http_circle_gif_merge_loc_conf /* merge location configuration */ 

}; 

    是时候来更深入的了解一下了。这些配置回调函数在所有的模块中都非常相似,并且它们使用几乎相同的ngix api。了解它们非常有必要。

2.3.1 create_loc_conf

下面有一个真实的create_loc_conf函数,它来自我写的一个circle_gif模块。它的参数是一个指令结构体(ngx_conf_t),返回一个新创建的模块配置结构体(在这个例子中,叫ngx_http_circle_gif_loc_conf_t)。

static void * 

ngx_http_circle_gif_create_loc_conf(ngx_conf_t *cf) 

    ngx_http_circle_gif_loc_conf_t  *conf; 

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_circle_gif_loc_conf_t)); 

    if (conf == NULL) { 

        return NGX_CONF_ERROR; 

    } 

    conf->min_radius = NGX_CONF_UNSET_UINT; 

    conf->max_radius = NGX_CONF_UNSET_UINT; 

    return conf; 

    首先需要注意的是nginx的内存分配。我们使用ngx_palloc(对malloc的包装)和ngx_pcalloc(对calloc的包装)来获得新的内存。(nginx使用内存池ngx_pool_t来管理内存,内存分配后由nginx系统在适当的时候回收)

表示未设置(UNSET)的常量包括:NGX_CONF_UNSET_UINT、NGX_CONF_UNSET_PTR、NGX_CONF_UNSET_SIZE、NGX_CONF_UNSET_MSEC,和一个针对所有类型的NGX_CONF_UNSET。未设置常量用来告诉合并函数这个值可以被覆盖。

2.3.2 merge_loc_conf

    下面是我们在circle_gif函数中用到的merge_loc_conf函数:

static char * 

ngx_http_circle_gif_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) 

    ngx_http_circle_gif_loc_conf_t *prev = parent; 

    ngx_http_circle_gif_loc_conf_t *conf = child; 

 

    ngx_conf_merge_uint_value(conf->min_radius, prev->min_radius, 10); 

    ngx_conf_merge_uint_value(conf->max_radius, prev->max_radius, 20); 

 

    if (conf->min_radius < 1) { 

        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,  

            "min_radius must be equal or more than 1"); 

        return NGX_CONF_ERROR; 

    } 

    if (conf->max_radius < conf->min_radius) { 

        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,  

            "max_radius must be equal or more than min_radius"); 

        return NGX_CONF_ERROR; 

    } 

 

    return NGX_CONF_OK; 

Nginx为不同的数据类型都提供了很好的merge函数(通过宏定义实现,取名通常为ngx_conf_merge_<data_type>_value),这种函数包含3个参数:

1. 需要被设置的变量值

2. 如果#1未被设置(==NGX_CONF_UNSET),可继承的值

3. 默认值。如果#1和#2都未被设置,则取该值

   最终结果被保存到{dy}个参数。可用的merge函数包括ngx_conf_merge_size_value、ngx_conf_merge_msec_value等等。你可以到core/ngx_conf_file.h中去看一个完整的列表。

问:这些函数是如何将结果写入{dy}个参数的呢?{dy}个参数的传递方式是采用传值(pass by value),而不是传递指针。

答:事实上,这些“函数”仅仅是一些宏定义。(所以它们在编译之前,会被展开成一些if语句)

    同样需要注意的是,该函数是如何产生错误的:它可以在日志文件中记录一些信息,然后返回NGX_CONF_ERROR。这个返回值将会中止nginx的启动。(因为日志记录级别是NGX_LOG_EMERG,这些被记录的信息同样会被打印到标准输出(屏幕)。core/ngx_log.h中定义了一系列日志级别可供参考)

下面,我们加上一个间接关联的一层,ngx_module_t结构体。这个变量通常被命名为ngx_http_<module name>_module。(通常,每个变量是一个全局变量)。这个结构体中包含了模块上下文(module context)和模块指令(directives)的引用(reference,就是指针),还包括一些其它的回调函数(比如exit thread, exit process等)。模块定义(module definition)有时候就是为了用来查找跟模块相关的数据。模块定义通常看起来像这样:

ngx_module_t  ngx_http_<module name>_module = { 

    NGX_MODULE_V1, 

    &ngx_http_<module name>_module_ctx, /* module context */ 

    ngx_http_<module name>_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 

}; 

...将<module name>替换成模块名字。每个模块可以添加一些回调函数,以便在进程/线程创建/退出时调用。但是,大多数模块都保持简单,没有添加这些函数。(core/ngx_conf_file.h有这些函数的参数定义)

一个模块的安装方法取决于该模块是一个handler,还是一个filter,或者是一个load-banlancer。细节将在对应的章节中说明。

    现在,我们将拿一些简单的模块,仔细来看它们是如何工作的。

Handler大概需要做4件事情:获取location配置;产生一个合适的应答;发送HTTP头部;发送HTTP消息体。模块中的handler函数有一个参数——request结构体。Request结构体有很多关于客户端请求的非常有用的信息,比如说HTTP方法、URI、HTTP 头部信息。我们将一个个来看这些4个步骤。

3.1.1 获取location配置

这部分比较简单。你需要做的是调用ngx_http_get_module_loc_conf,传入request结构体和模块定义(一个被全局定义的变量)。这里是在我的circle_gif模块的handler中的相关部分:

static ngx_int_t 

ngx_http_circle_gif_handler(ngx_http_request_t *r) 

    ngx_http_circle_gif_loc_conf_t  *circle_gif_config; 

    circle_gif_config = ngx_http_get_module_loc_conf(r, ngx_http_circle_gif_module); 

    ... 

现在,我们可以访问我们在配置merge函数中所设置的变量了。

3.1.2 生成应答

这是模块真正做事的部分,也是非常有趣的部分。

Request结构体在这里将非常有用,特别是下面这些元素:

typedef struct { 

... 

/* the memory pool, used in the ngx_palloc functions */ 

    ngx_pool_t                       *pool;  

    ngx_str_t                         uri; 

    ngx_str_t                         args; 

    ngx_http_headers_in_t             headers_in; 

... 

} ngx_http_request_t; 

Uri是请求访问的路径。如"/query.cgi"。

Args请求中'?'后的部分。如"name=john"。

Headers_in中有很多有用的东西(请求的HTTP头信息)。比如cookie,浏览器信息。但是大多数模块都不需要其中的任何东西,如果你感兴趣,可以查阅http/ngx_http_request.h。

这些信息已经足够产生一个有用的输出。完整的ngx_http_request_t的定义能在http/ngx_http_request.h中找到。

3.1.3 发送HTTP头部

应答头部保存在一个叫headers_out的结构体中。Request结构体中有一个指针指向headers_out结构体。在handler函数中,设置headers_out中的相关变量,然后调用ngx_http_send_header(r)。Headers_out中一些有用的部分包括:

typedef stuct { 

... 

    ngx_uint_t                        status; 

    size_t                            content_type_len; 

    ngx_str_t                         content_type; 

    ngx_table_elt_t                  *content_encoding; 

    off_t                             content_length_n; 

    time_t                            date_time; 

    time_t                            last_modified_time; 

.. 

} ngx_http_headers_out_t; 

(其余的变量定义可以在http/http_request.h中找到)

    

举个例子,如果一个模块想把content_type设置成"image/gif",content_length设置成100,返回200OK,那么,下面的代码能做到这些事情:

    r->headers_out.status = NGX_HTTP_OK; 

    r->headers_out.content_length_n = 100; 

    r->headers_out.content_type.len = sizeof("image/gif") - 1; 

    r->headers_out.content_type.data = (u_char *) "image/gif"; 

    ngx_http_send_header(r); 

大部分合法的HTTP头在这个结构体中都有,你可以很方便的设置它们。但是,有一些HTTP头的设置难度比你上面所看到的要稍大一点。举个例子,content-encoding这HTTP头,它的数据类型是(ngx_table_elt_t*),所以模块必须为它分配内存。我们通过ngx_list_push来分配内存,这个函数带有个ngx_list_t类型的参数,返回一个在链表中创建的新的元素。下面的代码将content-encoding设置为"deflate"并把HTTP头发送出去。

    r->headers_out.content_encoding = ngx_list_push(&r->headers_out.headers); 

    if (r->headers_out.content_encoding == NULL) { 

        return NGX_ERROR; 

    } 

    r->headers_out.content_encoding->hash = 1; 

    r->headers_out.content_encoding->key.len = sizeof("Content-Encoding") - 1; 

    r->headers_out.content_encoding->key.data = (u_char *) "Content-Encoding"; 

    r->headers_out.content_encoding->value.len = sizeof("deflate") - 1; 

    r->headers_out.content_encoding->value.data = (u_char *) "deflate"; 

    ngx_http_send_header(r); 

    当一个HTTP头需要同时有多个值的时候,我们通常使用这个机制。理论上,它可以方便地添加或删除某些值,而保持其余的不变,因为不需要返回到字符串操作(使用的是链表)

3.1.4 发送HTTP消息体

    现在,模块已经在内存中产生了一个应答,它需要把这个应答放到一个buffer中,然后把这个buffer放到一个链表中,{zh1}调用"send body"函数输出该链。

这个链表用来做什么?Nginx让handler每次产生一个buffer(这个buffer被放入一个链接节点中);每个链接节点都有一个指向下一节点的指针,当它是{zh1}一个节点时,指针指向NULL。我们假设只有一个buffer,这样就很简单。

首先,模块需要声明buffer和链接节点:

    ngx_buf_t    *b; 

    ngx_chain_t   out; 

下一步就是为buffer(ngx_buf_t)分配内存,并让它指向我们的应答数据:

    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; 

    } 

    b->pos = some_bytes; /* first position in memory of the data */ 

    b->last = some_bytes + some_bytes_length; /* last position */ 

    b->memory = 1; /* content is in read-only memory */ 

    /* (i.e., filters should copy it rather than rewrite in place) */ 

    b->last_buf = 1; /* there will be no more buffers in the request */ 

现在,将buffer放到我们的链接节点中:

    out.buf = b; 

    out.next = NULL; 

{zh1},我们发送消息体,并返回真个输出过滤链的状态码:

    return ngx_http_output_filter(r, &out); 

Buffer链是nginx的I/O模型中最关键的部分,所以你得非常熟悉它们是如何工作的。

问:为什么buffer中会有一个last_buf变量?我们可以通过buffer的next指针指向空来判断这个buffer为整个链表中的{zh1}一个。

答:一个链有可能是不xx的,比如说,有多个buffer,但不是所有的buffer都在这个请求或应答中。有些buffer在链表的末尾,但并不是在整个请求的末尾。这就是我们为什么需要last_buf变量的原因。

我为你的handler能产生一个应答而小小的鼓掌。有些时候你可以通过一段C代码来生成那个应答,但是大部分时候你需要和另外一个server通信(比如说,你要写一个模块实现另外一种网络协议)。你可以自己实现网络通信方面的细节,但是,当你收到一个特定的应答时,发生了些什么?当你等待应答的时候,你不想用自己的事件循环(event loop)来阻塞整个程序的主事件循环。你会使nginx的性能大大降低。幸运的是,nginx让你可以在它已有的机制中设置相应的hook函数(回调函数)来处理和后端server("upstreams")的通信。这样一来,你就可以在不妨碍nginx对其它请求的处理的基础上,实现和后端server通信。本节描述一个模块如何和后端server(upstream)进行通信,比如说,memcached、fastcgi,后者其它HTTP server。

3.2.1 upstream 回调函数(callbacks)概要

    不像其它模块的handler函数,upstream模块的handler函数仅做了一点点“实际工作”。它不调用ngx_http_output_filter(输出应答),而是仅仅设置了一些回调函数。这些回调函数在后端server可读或可写时会被调用。实际上,有6个这样的回调函数(hook):

Create_request 创建一个将要被发送到upstream(后端server)的请求(一个buffer或者一个buffer链)

Reinit_request 当到upstream(后端server)的连接被重置(reset)时,这个函数将会被调用(正好在第2次调用create_request之前)。

Process_header 处理upstream应答的头一小块,并且设置一个到upstream有效负载的指针。

Abort_request  当客户端中断请求时,这个函数将被调用。

Finalize_request 当nginx完成从upstream读取应答时,会调用finalize_request。

Inpurt_filter 是一个可以在读取到的应答消息体上进行操作的filter。

    这些函数是如何被设置的?下面是一个例子,一个简化版本的proxy模块的handler函数:

static ngx_int_t 

ngx_http_proxy_handler(ngx_http_request_t *r) 

    ngx_int_t                   rc; 

    ngx_http_upstream_t        *u; 

    ngx_http_proxy_loc_conf_t  *plcf; 

    plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module); 

/* set up our upstream struct */ 

    u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t)); 

    if (u == NULL) { 

        return NGX_HTTP_INTERNAL_SERVER_ERROR; 

    } 

    u->peer.log = r->connection->log; 

    u->peer.log_error = NGX_ERROR_ERR; 

    u->output.tag = (ngx_buf_tag_t) &ngx_http_proxy_module; 

    u->conf = &plcf->upstream; 

/* attach the callback functions */ 

    u->create_request = ngx_http_proxy_create_request; 

    u->reinit_request = ngx_http_proxy_reinit_request; 

    u->process_header = ngx_http_proxy_process_status_line; 

    u->abort_request = ngx_http_proxy_abort_request; 

    u->finalize_request = ngx_http_proxy_finalize_request; 

    r->upstream = u; 

    rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init); 

    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { 

        return rc; 

    } 

    return NGX_DONE; 

它做了一些通常handler该做的事情,但是重要的部分在于这些回调函数。同时,注意一下ngx_http_read_client_request_body这个函数。这里设置了另外一个回调函数。当nginx完成从客户端读取时,将调用该函数。

这些回调函数将做些什么呢?通常,reinit_request,abort_request和finalize_request会设置或者重置一些内部状态,一般只有几行。而真正有用的函数是create_request和process_header。

3.2.2 create_request回调函数

为了让问题变得简单,让我们假设我们有一个后端server,它读取一个字节,并且打印出2个字节。那么我们的函数将是什么样的呢?

Create_request函数需要为这个单字节的请求分配一个buffer,为这个buffer分配一个链表节点,然后把upstream结构体(中的request_buf指针)指向该链表节点。它看起来将像这样:

static ngx_int_t 

ngx_http_character_server_create_request(ngx_http_request_t *r) 

/* make a buffer and chain */ 

    ngx_buf_t *b; 

    ngx_chain_t *cl; 

    b = ngx_create_temp_buf(r->pool, sizeof("a") - 1); 

    if (b == NULL) 

        return NGX_ERROR; 

    cl = ngx_alloc_chain_link(r->pool); 

    if (cl == NULL) 

        return NGX_ERROR; 

/* hook the buffer to the chain */ 

    cl->buf = b; 

/* chain to the upstream */ 

    r->upstream->request_bufs = cl; 

/* now write to the buffer */ 

    b->pos = "a"; 

    b->last = b->pos + sizeof("a") - 1; 

    return NGX_OK; 

这并不像想象的那么糟糕,是吧?当然,在实际中,在一些应用场景下,你可能需要使用请求URI。它以ngx_str_t类型存在于r->uri,GET参数在在r->args中。不要忘了你能访问请求头和cookie。

3.2.3 process_header回调函数

现在,是时候来讲process_header这个函数了。就像create_request在request结构体中添加了一个指针一样,process_header将应答指针(response pointer)移到客户端将要接收的部分。它从后端server(upstream)读取header,并且设置相应的客户端应答header。

这里有一个非常小的例子,读取那个2个字节的应答。让我们假设{dy}个字节是状态码("status" character)。如果是一个疑问号,我们就不理会第二字节,返回客户端404文件未找到。如果是一个空格符,我们就以200 OK返回另外一个字符到客户端。确实,这不是一个很有用的协议,但它是一个很好的例证。我们如何来写这个process_header函数?

static ngx_int_t 

ngx_http_character_server_process_header(ngx_http_request_t *r) 

    ngx_http_upstream_t       *u; 

    u = r->upstream; 

    /* read the first character */ 

    switch(u->buffer.pos[0]) { 

        case '?': 

            r->header_only; /* suppress this buffer from the client */ 

            u->headers_in.status_n = 404; 

            break; 

        case ' ': 

            u->buffer.pos++; /* move the buffer to point to the next character */ 

            u->headers_in.status_n = 200; 

            break; 

    } 

    return NGX_OK; 

就这么简单。处理头部,改变指针,就完成了。注意headers_in实际上是一个应答头部结构体,像我们之前所看到的(cf.http/ngx_http_request.h),但是它能被从后端server(upstream)接收到的header所填充。一个真正的代理模块(proxying module)将做更多的头部处理,这里没有提及到错误处理,但是你有了一个大概的了解。

但是,当我们从后端server所接收到的包中没有包含整个头部,该怎么办?

3.2.4 保持状态(keeping state)

还记得我说过,abort_request、reinit_request、reinit_request,能够被用来重置内部状态。这是因为很多upstream模块有内部状态。模块需要定义一个自定义context结构体来保存和跟踪此刻从后端server收到了多少数据。这里和上面说的"Module Context"不一样。那是一个系统事先定义好的结构。而这个自定义context结构体则可以包含任何你所需要的元素和数据。这个context结构体可以在create_request函数中初始化,可能会像这样:

    ngx_http_character_server_ctx_t   *p;   /* my custom context struct */ 

    p = ngx_pcalloc(r->pool, sizeof(ngx_http_character_server_ctx_t)); 

    if (p == NULL) { 

        return NGX_HTTP_INTERNAL_SERVER_ERROR; 

    } 

    ngx_http_set_ctx(r, p, ngx_http_character_server_module); 

{zh1}一行实际上是将自定义context结构体以一个模块名注册到一个特殊的请求之上,以便后续可以很方便地获得。任何时候,当你需要这个context结构体时,(很有可能在其它的回调函数中),只需要这样做:

    ngx_http_proxy_ctx_t  *p; 

    p = ngx_http_get_module_ctx(r, ngx_http_proxy_module); 

指针p将指向当前状态。设置、重设、在里面放任何数据,想怎么做就这么做。当一个后端server以块(chunk)的形式返回数据,我们使用一个持久的状态机来读取数据,而且不会阻塞程序的主循环,这是一个非常好的方法。

Handler通过在指令结构体中所指定的回调函数中添加代码来完成安装(该指令能使这个模块生效)。举个例子,我的circle gif指令结构体是这样:

     { ngx_string("circle_gif"), 

      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, 

      ngx_http_circle_gif, 

      0, 

      0, 

      NULL } 

在这个例子中,回调函数是第3个参数ngx_http_circle_gif。回忆一下,这个回调函数的参数包括:指令结构体(ngx_conf_t,包含用户参数),相关的ngx_command_t结构体,和一个到模块配置结构的指针。对于我的circle gif模块来说,这个函数会像这样:

static char * 

ngx_http_circle_gif(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_circle_gif_handler; 

    return NGX_CONF_OK; 

这里有2个步骤:首先,为这个location获取core模块的locale配置,然后为handler赋值。非常简单,是吧?

现在,我已经说了所有我知道的关于handler模块的一切。是时候转移到filter模块上了,以及在输出过滤链上的一些组件。

    Filters对handlers产生的应答进行操作。Header filter对HTTP头进行操作,body filter对HTTP体进行操作。

    一个header filter包括3个基本步骤:

1. 决定是否需要对某个应答进行操作

2. 对某个应答进行操作

3. 调用下一个filter

举个例子,这里有一个"not modified"header filter的简化版本,如果客户端的If-Modified-Since头与服务端应答中的Last-Modified头相匹配的话,就把状态码设置为"304 Not Modified"。注意,header filter仅有一个参数,即ngx_http_request_t结构体。这样,我们就可以访问客户端头部和马上就可以发送的应答头部。

static 

ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r) 

    time_t  if_modified_since; 

 

    if_modified_since = ngx_http_parse_time(r->headers_in.if_modified_since->value.data, 

                              r->headers_in.if_modified_since->value.len); 

 

/* step 1: decide whether to operate */ 

    if (if_modified_since != NGX_ERROR &&  

        if_modified_since == r->headers_out.last_modified_time) { 

/* step 2: operate on the header */ 

        r->headers_out.status = NGX_HTTP_NOT_MODIFIED; 

        r->headers_out.content_type.len = 0; 

        ngx_http_clear_content_length(r); 

        ngx_http_clear_accept_ranges(r); 

    } 

/* step 3: call the next filter */ 

    return ngx_http_next_header_filter(r); 

Headers_out结构体和我们在handler那一节所看到的是一样的。我们可以操作它。

    Buffer链使得写body filter有一点难度,因为body filter在一次仅能操作其中一个buffer(链节点)。模块必须决定,是覆盖这个buffer,用一个新allocate的buffer替换它,还是在它前面或后面插入新的buffer。在一些复杂的情况下,模块仅收到几个buffer,所以它有一个不完整的buffer链,它必须在其上操作。不幸的是,nginx并没有提供一个高层次的API来操作这些buffer链,所以,body filter可能很难被理解(也很难写)。但是,这里有一些xxx将会掌握。

    一个body filter的原型看起来将像这样(这些代码来自nginx源码中的chunked filter):

static ngx_int_t ngx_http_chunked_body_filter(ngx_http_request_t *r, ngx_chain_t *in); 

    {dy}个参数是我们的老朋友了—request结构体。第二个参数是一个到当前部分链头部的指针(这个链可能包含0个、1个或者多个buffer)。

让我们举个简单的例子。假设我们想把"<I!--served by nginx>"这段文字插到每个请求的末尾。首先,我们需要明确是否这个请求的{zh1}一个buffer包含在当前的buffer链当中。像我所说过的那样,这里没有这样一个奇特的API,所以我们需要自己使用一个for循环来做这件事情:

    ngx_chain_t *chain_link; 

    int chain_contains_last_buffer = 0; 

    for ( chain_link = in; chain_link != NULL; chain_link = chain_link->next ) { 

        if (chain_link->buf->last_buf) 

            chain_contains_last_buffer = 1; 

    } 

现在,如果还没有{zh1}一个buffer,让我们退出:

 if (!chain_contains_last_buffer) 

        return ngx_http_next_body_filter(r, in); 

    非常好,现在,{zh1}一个buffer已经被保存到了链表当中。现在,我们分配一个新的buffer:

    ngx_buf_t    *b; 

    b = ngx_calloc_buf(r->pool); 

    if (b == NULL) { 

        return NGX_ERROR; 

    } 

在里面放一些数据:

    b->pos = (u_char *) "<!-- Served by Nginx -->"; 

    b->last = b->pos + sizeof("<!-- Served by Nginx -->") - 1; 

将这个buffer关联到一个新的链表节点:

    ngx_chain_t   added_link; 

    added_link.buf = b; 

    added_link.next = NULL; 

{zh1},将这个链表节点添加到{zh1}一个链表节点的后面:

    chain_link->next = added_link; 

重新设置last_buf变量以反映实际:

    chain_link->buf->last_buf = 0; 

    added_link->buf->last_buf = 1; 

将修改过的链传递给下一个filter:

    return ngx_http_next_body_filter(r, in); 

    最终的函数比我们想像的要做的多很多,但是,buffer链是一个功能非常强大的结构,它让程序员可以增量的处理数据,这样客户端就可以尽快的收到数据。但是,我认为buffer链迫切的需要一个很清晰的调用接口,以防止程序员把buffer链弄成一个不协调相矛盾的状态。现在,你只能小心的操作它。

Filter一般在post-configuration这一步安装。我们在同一个地方安装header filter和body filter。

让我们拿chunked filter模块来做一个简单的例子。它的模块context看起来会像这样:

static ngx_http_module_t  ngx_http_chunked_filter_module_ctx = { 

    NULL,                                  /* preconfiguration */ 

    ngx_http_chunked_filter_init,          /* postconfiguration */ 

  ... 

}; 

下面是ngx_http_chunked_filter_init函数:

static ngx_int_t 

ngx_http_chunked_filter_init(ngx_conf_t *cf) 

    ngx_http_next_header_filter = ngx_http_top_header_filter; 

ngx_http_top_header_filter = ngx_http_chunked_header_filter; 

    ngx_http_next_body_filter = ngx_http_top_body_filter; 

    ngx_http_top_body_filter = ngx_http_chunked_body_filter; 

    return NGX_OK; 

这里发生了什么?如果你还记得,filter使用了一种CHAIN OF RESPONSIBILITY(职责链)的设计模式。当一个handler产生了一个应答,它将调用2个函数:ngx_http_output_filter,它将调用全局函数ngx_http_top_body_filter;ngx_http_send_header,它将调用全局函数ngx_http_top_header_filter。

Ngx_http_top_body_filter和ngx_http_top_header_filter分别是body和header filter链的头部。每一个链上的节点都有一个到下一个节点的函数引用(这个引用被叫做ngx_http_next_body_filter和ngx_http_next_header_filter)。当一个filter结束执行时,它将调用下一个filter,直到一个被特别定义的write filter被调用,它将包装HTTP应答。在这个filter_init函数中,你看到的是,这个模块将自己的filter函数添加到filter链中,它将老的top filter保存到自己的next变量中,同时声明自己的函数为新的top filter。(这样,{zh1}一个被安装的filter将{dy}个被执行)。

Note:这些是如何工作的呢?

每个filter返回一个错误码,或者使用下面的语句返回:

return ngx_http_next_body_filter()

这样,如果执行到了filter链的末尾,一个“OK”应答将被返回,但是如果在这个过程中产生了错误,这个链将被截断,nginx将返回一个适当的错误信息。它是一个xx由函数引用实现的单向链表,有着快速的错误处理能力。

一个load-balancer仅仅是决定哪个后端server将接收到某个特殊的请求。已经实现的方式包括采用轮询(round-robin)的方式将请求分发给后端server;hash请求中的某些信息将请求分发给后端server。这节将拿upstream_hash模块做一个例子,来讲load-balancer的安装和调用。Upstream_hash使用一个在nginx.conf中指定的变量来hash,选定一个后端server。

一个load-balancer模块有6个部分:

1. 使模块生效的配置指令(比如,hash),将调用一个注册函数。

2. 注册函数将定义一些合理的server参数,并且注册一个upstream initialization函数。

3. Upstream initialization函数在配置被确认之后就会被调用。它的工作包括:

    a. 解析server名字到特定的IP

    b. 为socket分配空间

    c. 设置peer initialization回调函数

4. Peer initialization函数,每个请求都会被调用一次。设置load-balance函数将要访问或操作的数据。

5. Load-balance函数决定请求将被路由到哪里。对于每一个客户端请求,它至少被调用一次(如果{dy}次调用后端server失败了,它将被调用多次)。

6. {zh1},在与一个特殊的后端server进行通信之后,peer release函数可以用来更新统计信息。

现在,我们逐个来讲。

回忆一下,指令声明时会指定在什么时候这个指令是合法的,以及当碰到这个指令时,该执行的函数。一个load-balancer的指令应该设置了NGX_HTTP_UPS_CONF这个标志位,所以nginx知道这个指令只在upstream块中生效。它应该会提供一个到注册函数的指针。这里是upstream_hash模块中的指令定义:

    { ngx_string("hash"), 

      NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS, 

      ngx_http_upstream_hash, 

      0, 

      0, 

      NULL }, 

    

上面的回调函数ngx_http_upstream_hash就是注册函数。之所以如此命名,是因为它在周围的upstream配置上注册了一个upstream initialization函数。更多的,这个注册函数定义了:在特定的upstream块中,哪些到server指令的选项是合法的。(比如,weight=,fail_timeout=)。这是upstream_hash模块的注册函数:

ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 

 { 

    ngx_http_upstream_srv_conf_t  *uscf; 

    ngx_http_script_compile_t      sc; 

    ngx_str_t                     *value; 

    ngx_array_t                   *vars_lengths, *vars_values; 

 

    value = cf->args->elts; 

 

    /* the following is necessary to evaluate the argument to "hash" as a $variable */ 

    ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); 

 

    vars_lengths = NULL; 

    vars_values = NULL; 

 

    sc.cf = cf; 

    sc.source = &value[1]; 

    sc.lengths = &vars_lengths; 

    sc.values = &vars_values; 

    sc.complete_lengths = 1; 

    sc.complete_values = 1; 

 

    if (ngx_http_script_compile(&sc) != NGX_OK) { 

        return NGX_CONF_ERROR; 

    } 

    /* end of $variable stuff */ 

 

    uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); 

 

    /* the upstream initialization function */ 

    uscf->peer.init_upstream = ngx_http_upstream_init_hash; 

 

    uscf->flags = NGX_HTTP_UPSTREAM_CREATE; 

 

    /* OK, more $variable stuff */ 

    uscf->values = vars_values->elts; 

    uscf->lengths = vars_lengths->elts; 

 

    /* set a default value for "hash_method" */ 

    if (uscf->hash_function == NULL) { 

        uscf->hash_function = ngx_hash_key; 

    } 

 

    return NGX_CONF_OK; 

 } 

    

为了避免在不同的主题之间跳跃,我们稍后解释$varialbe,它是非常简单的。设置一个回调函数,设置一些标志位。有哪些标志位呢?

NGX_HTTP_UPSTREAM_CREATE:让server指令可以出现在upstream块中。我几乎不能想像什么情况下你可以不用这个。

NGX_HTTP_UPSTREAM_WEIGHT:让server指令有一个weight=选项。

NGX_HTTP_UPSTREAM_MAX_FAILS:允许max_fails=选项。

NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:允许fail_timeout=选项。

NGX_HTTP_UPSTREAM_DOWN:允许down选项。

NGX_HTTP_UPSTREAM_BACKUP:允许backup选项。

每个模块都可以访问这些配置值,由模块来决定将用它们来做什么。那就是说,max_fails不会自动地限制(连接后端server的次数),所有关于失败处理的逻辑xx又模块作者所决定。后面将详细解释这些。现在,我们仍未完成对回调函数的跟踪。下一步,我们来看upstream initialization函数(即上一个函数中init_upstream函数)。

Upstream initializtion函数的目的是:解析域名;为socket分配空间;设置另一个回调函数。下面是upstream_hash函数做了些什么:

ngx_int_t 

ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) 

    ngx_uint_t                       i, j, n; 

    ngx_http_upstream_server_t      *server; 

    ngx_http_upstream_hash_peers_t  *peers; 

 

    /* set the callback */ 

    us->peer.init = ngx_http_upstream_init_upstream_hash_peer; 

 

    if (!us->servers) { 

        return NGX_ERROR; 

    } 

 

    server = us->servers->elts; 

 

    /* figure out how many IP addresses are in this upstream block. */ 

    /* remember a domain name can resolve to multiple IP addresses. */ 

    for (n = 0, i = 0; i < us->servers->nelts; i++) { 

        n += server[i].naddrs; 

    } 

 

    /* allocate space for sockets, etc */ 

    peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t) 

            + sizeof(ngx_peer_addr_t) * (n - 1)); 

 

    if (peers == NULL) { 

        return NGX_ERROR; 

    } 

 

    peers->number = n; 

 

    /* one port/IP address per peer */ 

    for (n = 0, i = 0; i < us->servers->nelts; i++) { 

        for (j = 0; j < server[i].naddrs; j++, n++) { 

            peers->peer[n].sockaddr = server[i].addrs[j].sockaddr; 

            peers->peer[n].socklen = server[i].addrs[j].socklen; 

            peers->peer[n].name = server[i].addrs[j].name; 

        } 

    } 

 

    /* save a pointer to our peers for later */ 

    us->peer.data = peers; 

 

    return NGX_OK; 

这个函数比我们想像的更棘手。大部分工作看起来应该是抽象的,但是它不是,这就是我们生活的世界。一个简化事情的策略是:调用另外一个模块的upstream initialization函数,让它去做所有繁琐的工作(为连接分配额内存,等),而重写u->peer.init回调函数。举个例子,参看http/modules/ngx_http_upstream_ip_hash_module.c

从我们的视角来看,重要的一点是,设置一个到peer initialization函数的指针。在这个例子中,它是ngx_http_upstream_init_upstream_hash_peer。

Peer initialization函数在每个请求会被调用一次。它为模块设置一个数据结构,模块将会用到这个结构来选择一个合适的后端server来处理请求。这个结构在重试后端server的时候,是持久保持的。所以,它可以非常方便的用来保存失败的连接数,或者一个计算好的hash值。按约定,这个结构通常被叫做ngx_http_upstream_<module name>_peer_data_t。

还有,peer initialization函数设置2个回调函数:

get:load-balance函数

free:peer release函数(一般用来更新统计信息)

好像这还不够,它还会初始化一个变量,叫tries。如果tries一直是正值,(如果后端失败,)nginx就会保持重试。当tries为0时,nginx放弃重试。Get和free函数需要正确的设置tries变量。

这里有从upstream_hash模块中拿出来的peer initialization函数:

static ngx_int_t 

ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, 

    ngx_http_upstream_srv_conf_t *us) 

    ngx_http_upstream_hash_peer_data_t     *uhpd; 

    ngx_str_t val; 

    /* evaluate the argument to "hash" */ 

    if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) { 

        return NGX_ERROR; 

    } 

    /* data persistent through the request */ 

    uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t) 

     + sizeof(uintptr_t)  

       * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number  

                  / (8 * sizeof(uintptr_t))); 

    if (uhpd == NULL) { 

        return NGX_ERROR; 

    } 

 

    /* save our struct for later */ 

    r->upstream->peer.data = uhpd; 

 

    uhpd->peers = us->peer.data; 

 

    /* set the callbacks and initialize "tries" to "hash_again" + 1*/ 

    r->upstream->peer.free = ngx_http_upstream_free_hash_peer; 

    r->upstream->peer.get = ngx_http_upstream_get_hash_peer; 

    r->upstream->peer.tries = us->retries + 1; 

 

    /* do the hash and save the result */ 

    uhpd->hash = us->hash_function(val.data, val.len); 

    return NGX_OK; 

    好像还不错,现在,我们已经准备好了来选择一个后端server。

现在,该讲到主要的内容了。模块在这里选择一个后端server。Load-balance函数的原型将像这样:

static ngx_int_t  

ngx_http_upstream_get_<module_name>_peer(ngx_peer_connection_t *pc, void *data); 

Data就是一个关于客户端连接信息的结构体。Pc将包含我们将要连接的server的信息。Load-balance函数的工作就是填写pc->sockaddr,pc->socklen和pc->name的值。如果你知道一些网络编程的知识,这些变量你应该很熟悉。但是实际上他们对于当前的工作并不重要,我们并不关心它们代表什么,我们仅仅想知道如何找合适的值去填写它们。

这个函数必须找到一个存在的server的列表,从中选择一个,把它的值赋给pc。让我们看一下upstream_hash做了什么。

Upstream_hash模块在这之前,在ngx_http_upstream_init_hash函数中,会将server列表放到ngx_http_upstream_peer_data_t这个结构中。现在,这个结构以data变量存在:

    ngx_http_upstream_hash_peer_data_t *uhpd = data; 

    peer列表现在保存在uhpd->peers->peer中。现在,让我们用计算好的hash值,除以数组的大小,来从这个数组中选择一个peer。

    ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number]; 

现在,到了很重要的{zh1}一步:

    pc->sockaddr = peers->sockaddr; 

    pc->socklen  = peers->socklen; 

    pc->name     = &peers->name; 

 

    return NGX_OK; 

    

就这些了。如果load-balancer返回NGX_OK,它的意思是“继续下去,并且尝试这个server”。如果返回NGX_BUSY,它表示所有所有的后端server都暂时不可用,nginx应该再次尝试。

但是,我们如何来保持跟踪哪些是不可用的?如果我们不想让它重试,由该如何?

在每发生一次与后端链接之后,peer release函数会被调用。它的目的是用来跟踪错误。这里是函数的原型:

void  

ngx_http_upstream_free_<module name>_peer(ngx_peer_connection_t *pc, void *data,  ngx_uint_t state); 

前2个参数和我们在load-balancer函数中看到的是一样的。第3个参数是一个state变量,它表示链接是否成功。它可能包含2个被按位或到一起的值:NGX_PEER_FAILED(链接失败)和NGX_NEXT_PEER(链接失败,或者链接成功到是程序返回失败)。0表示链接成功。

对这些失败事件如何处理xx取决于模块作者。如果这些事件需要要被处理,结果应该保存在data,一个对每个请求的定制结构体。

Peer release函数的一个至关重要的目的是:如果你不想让nginx一直保持重试调用load-balance,需要设置pc->tries为0。最简单的peer release函数看起来会像这样:

   pc->tries = 0; 

这将保证如果后端server有错误产生,则会返回客户端一个502 Bad proxy错误。

这里有一个更复杂一点的例子,来自upstream_hash模块。如果一个后端链接失败了,它将在bit数组(叫tried,一个uintptr_t型的数组)中把它标志为失败,然后继续选择一个新的后端server,直到它找到一个没有失败的。

#define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t)) 

#define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t)) 

 

static void 

ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data, 

    ngx_uint_t state) 

    ngx_http_upstream_hash_peer_data_t  *uhpd = data; 

    ngx_uint_t                           current; 

 

    if (state & NGX_PEER_FAILED 

            && --pc->tries) 

    { 

        /* the backend that failed */ 

        current = uhpd->hash % uhpd->peers->number; 

 

       /* mark it in the bit-vector */ 

        uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current); 

 

        do { /* rehash until we're out of retries or we find one that hasn't been tried */ 

            uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t)); 

            current = uhpd->hash % uhpd->peers->number; 

        } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries); 

    } 

由于load-balance函数仅看uhpd->hash的{zx1}值,所以这个函数可以达到预期效果。

很多应用并不需要重试或其它高可靠性的逻辑,但是,从这里你能看到,仅仅使用几行代码,我们就能提供这样一个功能。

现在,你应该准备好了来看一个nginx模块,并且尝试理解它是如何工作的。在/src/http/moudles中,你能看到已有的模块。找一个和你将要做的相似的模块来阅读。看起来都很熟悉吧?应该是这样。通过这篇指南和模块源代码去理解它在做什么。

但是emiller没有写一个如何来读nginx模块的指南。我们不是在读模块,我们在写,在创作,与世界分享。

首先,你需要一个地方来创作你的模块。为你的模块新建一个目录,它可以放在硬盘上的任何地方,但是和nginx的源代码分离。你的新模块在开始时应该包含2个文件:

"config"

"ngx_http_<your module>_module.c"

这个"config"文件将被./configure包含,它的内容由模块的类型决定。

Filter模块的"config"文件:

ngx_addon_name=ngx_http_<your module>_module 

HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_<your module>_module" 

NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_<your module>_module.c" 

其它模块的"config"文件:

ngx_addon_name=ngx_http_<your module>_module 

HTTP_MODULES="$HTTP_MODULES ngx_http_<your module>_module" 

NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_<your module>_module.c" 

现在,你可以写你的C文件了。我建议copy一个已经存在的、和你的需要相似的模块,但是,把它重命名为"ngx_http_<your module>_module.c"。让这个成为你的模型,你修改它的一些行为来适合你的需求。当你理解和重写不同部分时,经常查看这篇指南。

如果你准备好了来编译模块,到nginx的目录下,输入:

./configure --add-module=path/to/your/new/module/directory 

然后,像往常一样,make,make install。如果所有的步骤都没有出问题,你的模块就会被正确的编译进来。非常棒,是吧?不需要动nginx的代码,添加一个模块到nginx的新版本非常快,仅仅需要使用这个./configure命令就可以了。顺便说一下,如果你的模块需要动态链接库,你可以将这个添加到你的"config"文件:

CORE_LIBS="$CORE_LIBS -lfoo" 

这里,foo是你所需要的库。

如果你做了一个非常酷,非常有用的模块,请一定将这个消息发给nginx邮件列表,和大家一起分享。

郑重声明:资讯 【Emiller的nginx模块开发指引- 关注web后端技术- 博客园】由 发布,版权归原作者及其所在单位,其原创性以及文中陈述文字和内容未经(企业库qiyeku.com)证实,请读者仅作参考,并请自行核实相关内容。若本文有侵犯到您的版权, 请你提供相关证明及申请并与我们联系(qiyeku # qq.com)或【在线投诉】,我们审核后将会尽快处理。
—— 相关资讯 ——