当前位置: > Linux服务器 > nginx >

Nginx模块开发指南中文版

时间:2016-06-06 12:41来源:linux.it.net.cn 作者:IT
1. 预备知识

    你应当比较熟悉C语言。不光是“C-语法",你起码还得知道结构体和预处理指令,同时保证看到指针和函数引用出现时心里不会发毛。否则的话,就算信春哥也是没用的,看看K&R吧。 

    你得对HTTP协议有一定的了解,毕竟你是在和一个web server打交道。 

    如果你熟悉Nginx的配置文件就太好不过了。如果不熟悉,也没关系,这里简单介绍一下,知道概念先:Nginx配置文件主要分成四部分:main(全局设置)、server(主机设置)、upstream(上游服务器设置)和 location(URL匹配特定位置后的设置)。每部分包含若干个指令。main部分设置的指令将影响其它所有设置;server部分的指令主要用于指定主机和端口;upstream的指令用于设置一系列的后端服务器;location部分用于匹配网页位置(比如,根目录“/”,“/images”,等等)。他们之间的关系式:server继承main,location继承server;upstream既不会继承指令也不会被继承。它有自己的特殊指令,不需要在其他地方的应用。在下面很多地方都会涉及这四个部分,切记。 好了,让我们开始吧。 

2. Nginx模块委派概述

    Nginx的模块有三种角色: 

    * handlers 处理http请求并构造输出
    * filters 处理handler产生的输出
    * load-balancers 当有多于一个的后端服务器时,选择一台将http请求发送过去 

    许多可能你认为是web server的工作,实际上都是由模块来完成的:任何时候,Nginx提供文件或者转发请求到另一个server,都是通过handler来实现的;而当需要Nginx用gzip压缩输出或者在服务端加一些东东的话,filter就派上用场了;Nginx的core模块主要管理网络层和应用层协议,并启动针对特定请求的一系列后续模块。这种分散式的体系结构使得由你自己来实现强大的内部单元成为了可能。 

    注意:不像Apache的模块那样,Nginx的模块都_不是动态链接的。(换句话说,Nginx的模块都是静态编译的) 模块是如何被调用的呢?典型地说,当server启动时,每一个handler都有机会去处理配置文件中的location定义,如果有多个 handler被配置成需要处理某一特定的location时,只有其中一个handler能够“获胜”(掌握正确配置规则的你当然不会让这样的冲突发生啦)。 

    一个handler有三种返回方式:正常;错误;放弃处理转由默认的handler来处理(典型地如处理静态文件的时候)。 

    如果handler的作用是把请求反向代理到后端服务器,那么就是刚才说的模块的第三种角色load-balancer了。load-balancer主要是负责决定将请求发送给哪个后端服务器。Nginx目前支持两种load-balancer模块:round-robin(轮询,处理请求就像打扑克时发牌那样)和IP hash(众多请求时,保证来自同一ip的请求被分发的同一个后端服务器)。 

    如果handler返回(译者注:就是http响应,即filter的输入)正确无误,那么fileter就被调用了。每个location配置里都可以添加多个filter,所以说(比如)响应可以被压缩和分块。多个filter的执行顺序是编译时就确定了的。filter采用了经典的“接力链表(CHAIN OF RESPONSIBILITY)”模式:一个filter被调用并处理,接下来调用下一个filter,直到最后一个filter被调用完成,Nginx 才真正完成响应流程。 

    最帅的部分是在 filter链中,每个filter不会等待之前的filter完全完工,它可以处理之前filter正在输出的内容,这有一点像Unix中的管道。 Filter的操作都基于buffers_,buffer通常情况下等于一个页的大小(4k),你也可以在nginx.conf里改变它的大小。这意味着,比如说,模块可以在从后端服务器收到全部的响应之前,就开始压缩这个响应并流化(stream to)给客户端了。好牛逼啊~ 总结一下上面的内容,一个典型的周期应当是这样的: 客户端发送HTTP request → Nginx基于location的配置选择一个合适的handler → (如果有) load-balancer选择一个后端服务器 → Handler处理请求并顺序将每一个响应buffer发送给第一个filter → 第一个filter讲输出交给第二个filter → 第二个给第三个 → 第三个给第四个 → 以此类推 → 最终响应发送给客户端 我之所以说“典型地”是因为Ngingx的模块具有 很强 的定制性。模块开发者需要花很多精力精确定义模块在何时如何产生作用(我认为是件不容易的事)。模块调用实际上是通过一系列的回调函数做到的,很多很多。名义上来说,你的函数可以在以下时候被执行: 

    * server读取配置文件之前
    * 读取location和server的每一条配置指令
    * 当Nginx初始化main配置段时
    * 当Nginx初始化server配置段时(例如:host/port)
    * 当Nginx合并server配置和main配置时
    * 当Nginx初始化location配置时
    * 当Nginx合并location配置和它的父server配置时
    * 当Nginx的主进程启动时
    * 当一个新的worker进程启动时
    * 当一个worker进程退出时
    * 当主进程退出时
    * handle 一个请求
    * Filter响应头
    * Filter响应体
    * 选择一个后端服务器
    * 初始化一个将发往后端服务器的请求
    * 重新-初始化一个将发往后端服务器的请求
    * 处理来自后端服务器的响应
    * 完成与后端服务器的交互 

    难以置信!有这么多的功能任你处置,而你只需仅仅通过多组有用的钩子(由函数指针组成的结构体)和相应的实现函数。让我们开始接触一些模块吧。 

3. Nginx模块的组成

    我说过,Nginx模块的构建是很灵活的。这一节讲描述的东西会经常出现。它可以帮助你理解模块,也可以作为开发模块的手册。 

3.1. 模块配置Struct(s)

    模块的配置struct有三种,分别是main,server和location。绝大多数模块仅需要一个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_uint_t 和 ngx_flag_t); 这些只是基本类型的别名而已。(如果想知道具体是什么的别名,可以参考 core/ngx_config.h ). 这些类型用在配置结构体中的情形很多。 

3.2. 模块指令

    模块的指令是定义在一个叫做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, 通常都是以像(e.g.) ngx_str("proxy_pass")这样的方式来实例化. 注意: ngx_str_t 包含一个存放字符串内容的data字段,和一个存放字符串长度的len字段。Nginx广泛地使用这个类型来存放字符串。 

    结构体成员type是标识的集合,表明这个指令在哪里出现是合法的、指令的参数有几个。应用中,标识一般是下面多个值的BIT或: 

    * NGX_HTTP_MAIN_CONF: 指令出现在main配置部分是合法的
    * NGX_HTTP_SRV_CONF: 指令在server配置部分出现是合法的 config
    * NGX_HTTP_LOC_CONF: 指令在location配置部分出现是合法的
    * NGX_HTTP_UPS_CONF: 指令在upstream配置部分出现是合法的 

    * NGX_CONF_NOARGS: 指令没有参数
    * NGX_CONF_TAKE1: 指令读入1个参数
    * NGX_CONF_TAKE2: 指令读入2个参数
    * ...
    * NGX_CONF_TAKE7: 指令读入7个参数 

    * NGX_CONF_FLAG: 指令读入1个布尔型数据 ("on" or "off")
    * NGX_CONF_1MORE: 指令至少读入1个参数
    * NGX_CONF_2MORE: 指令至少读入2个参数 

    这里还有很多其他的选项:core/ngx_conf_file.h. 结构体成员 set 是一个函数指针,它指向的函数用来进行模块配置;这个设定函数一般用来将配置文件中的参数传递给程序,并保存在配置结构体中。设定函数有三个入参: 

   1. 指向结构体 ngx_conf_t 的指针, 这个结构体里包含需要传递给指令的参数
   2. 指向结构体 ngx_command_t 的指针
   3. 指向模块自定义配置结构体的指针 

    设定函数会在遇到指令时执行,Nginx提供了多个函数用来保存特定类型的数据,这些函数包含有: 

    * ngx_conf_set_flag_slot: 将 "on" or "off" 转换成 1 or 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中很容易查到。如果你觉得现有这些内置的函数还不能满足你,当然也可以传入自己的函数引用。 

    这些内置函数是如何知道把数据存放在哪里的呢?这就是接下来两个结构体成员 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。 

    ngx_command_t数组以ngx_null_command 为终结符(就好像字符串以'\0'为终结符一样). 

3.3. 模块上下文

    静态的ngx_http_module_t结构体,包含一大坨函数引用,用来创建和合并三段配置 (main,server,location),命名方式一般是:ngx_http_<module name>_module_ctx. 这些函数引用依次是: 

    * preconfiguration 在读入配置前调用
    * postconfiguration 在读入配置后调用
    * create_main_conf 在创建main配置时调用(比如,用来分配空间和设置默认值)
    * init_main_conf 在初始化main配置时调用(比如,把原来的默认值用nginx.conf读到的值来覆盖)
    * init_main_conf 在创建server配置时调用
    * merge_srv_conf 合并server和main配置时调用
    * create_loc_conf 创建location配置时调用
    * merge_loc_conf 合并location和server配置时调用 

    函数的入参各不相同,取决于他们具体要做的事情。这里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只使用最后两个: 一个用来为特定location配置来分配内存,(叫做 ngx_http_<module name>_create_loc_conf), 另一个用来设定默认值以及合并继承过来的配置值(叫做 ngx_http_<module name >_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 */
};

    现在开始讲得更深一点。这些配置回调函数看其来很像,所有模块都一样,而且Nginx的API都会用到这个部分,所以值得好好看看。 

3.3.1. create_loc_conf

    下面这段摘自我自己写的模块circle_gif(源代码),create_loc_conf的骨架大概就是这个样子. 它的入参是(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的包装函数),就不用担心内存的释放了。(TODO: to see why?) UNSET可能的常量有NGX_CONF_UNSET_UINT, NGX_CONF_UNSET_PTR, NGX_CONF_UNSET_SIZE, NGX_CONF_UNSET_MSEC, 以及无所不包的NGX_CONF_UNSET,UNSET让合并函数知道哪些变量是需要覆盖的。 

3.3.2. merge_loc_conf

    下面的例子是我的模块circle_gif中的合并函数: 

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提供了一些好用的合并函数用来合并不同类型的数据(ngx_conf_merge_<data type>_value),这类函数的入参是: 

   1. 当前location 的变量值
   2. 如果第一个参数没有被设置而采用的值
   3. 如果第一第二个参数都没有被设置而采用的值 

    结果会被保存在第一个参数中。能用的合并函数包括 ngx_conf_merge_size_value, ngx_conf_merge_msec_value 等等. 可参见 core/ngx_conf_file.h. 

问: 第一个参数是传值的,那如何能做到将结果保存到第一个参数中?

答: 这些函数都是由预处理命令定义的(在真正编译之前,它们会被扩展成一些if语句)

    同时还需要注意的是错误的产生。函数会往log文件写一些东西,同时返回NGX_CONF_ERROR。这个返回值会将server的启动挂起。(因为被标示为NGX_LOG_EMERG级别,所以错误同时还会输出到标准输出。作为参考,core/ngx_log.h列出了所有的日志级别。) 

3.4. 模块定义

    接下来我们间接地介绍更深一层:结构体ngx_module_t。该结构体变量命名方式为ngx_http_<module name>_module。它包含模块的内容和指令执行方式,同时也还包含一些回调函数(退出线程,退出进程,等等)。模块定义在有的时候会被用作查找的关键字,来查找与特定模块相关联的数据。模块定义通常像是这样: 

ngx_module_t  ngx_http__module = {
    NGX_MODULE_V1,
    &ngx_http__module_ctx, /* module context */
    ngx_http__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>就可以了。模块可以添加一些回调函数来处理线程/进程的创建和销毁,但是绝大多数模块都用NULL忽略这些东东。(关于这些回调函数的入参,可以参考 core/ngx_conf_file.h.) 

3.5. 模块装载

    模块的装载方式取决于模块的类型:handler、filter还是load-balancer。所以具体的装载细节将留在其各自的章节中再做介绍。 

4. Handlers

    接下来我们把模块的细节放到显微镜下面来看,它们到底怎么运行的。 

4.1. 剖析Handler(非代理)

    Handler一般做4件事:获取location配置;生成合适的响应;发送响应头;发送响应体。Handler有一个参数,即请求结构体。请求结构体包含很多关于客户请求的有用信息,比如说请求方法,URI,请求头等等。我们一个个地来看。 

3.1.1. 获取location配置

    这部分很简单。只需要调用 ngx_http_get_module_loc_conf,传入当前请求的结构体和模块定义即可。下面是我的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);
    ...

    现在我们就可以访问之前在合并函数中设置的所有变量了。 

4.1.2. 生成响应

    这才是模块真正干活的地方,很有趣哦。 

    这里要用到请求结构体,主要是这些结构体成员: 

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 是请求的路径, e.g. "/query.cgi". args 请求串参数中问号后面的参数 (e.g. "name=john"). headers_in 包含有很多有用的东西,比如说cookie啊,浏览器信息啊什么的,但是许多模块可能用不到这些东东。如果你感兴趣的话,可以参看http/ngx_http_request.h 。 

    对于生成输出,这些信息应该是够了。完整的ngx_http_request_t结构体定义在http/ngx_http_request.h。 

4.1.3. 发送响应头

    响应头存放在结构体headers_out中,它的引用存放在请求结构体中。 Handler设置相应的响应头的值,然后调用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/ngx_http_request.h找到。) 举例来说,如果一个模块要设置Content-Type 为 "image/gif", Content-Length 为 100, 并返回 HTTP 200 OK 的响应, 代码应当是这样的: 

    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);

    上面的设定方式针对大多数参数都是有效的。但一些头部的变量设定要比上面的例子要麻烦;比如,content_encoding 还含有类型(ngx_table_elt_t*), 所以必须先为此分配空间。可以用一个叫做ngx_list_push的函数来做,它传入一个ngx_list_t(与数组类似),返回一个list中的新成员(类型是ngx_table_elt_t)。下面的代码设置了Content-Encoding为"deflate"并发送了响应头: 

    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);

    当头部有多个值时,这个机制常常被用到。它(理论上讲)使得过滤模块添加、删除某个值而保留其他值的时候更加容易,在操纵字符串的时候,不需要把字符串重新排序。 

4.1.4. 发送响应体

    现在模块已经生成了一个响应,并存放在了内存中。接下来它需要将这个响应分配给一个特定的缓冲区,然后把这个缓冲区加入到链表,然后调用链表中“发送响应体”的函数。 

    链表在这里起什么作用呢?Nginx 中,handler模块(其实filter模块也是)生成响应到buffer中是同时完成的;链表中的每个元素都有指向下一个元素的指针,如果是NULL 则说明链表到头了。简单起见,我们假设只有一个buffer。 

    首先,模块需要先声明buffer和链表: 

    ngx_buf_t    *b;
    ngx_chain_t   out;

    接着,需要给buffer分配空间,并将我们的响应数据指向它: 

    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 */

    现在就可以把数据挂在链表上了: 

    out.buf = b;
    out.next = NULL;

    最后,我们发送这个响应体,返回值是链表在一次调用后的状态:(and return the status code of the output filter chain all in one go) 

    return ngx_http_output_filter(r, &out);

    Buffer链是Nginx IO模型中的关键部分,你得比较熟悉它的工作方式。 

问: 为什么buffer还需要有个`last_buf`变量啊,我们不是可以通过判断next是否是NULL来知道哪个是链表的最末端了吗?

答: 链表可能是不完整的,比如说,当有多个buffer的时候,并不是所有的buffer都属于当前的请求和响应。所以有些buffer可能是buffer链表的表尾,但是不是请求的结束。这给我们引入了接下来的内容……

4.2. 剖析Upstream(又称 Proxy) Handler

    我已经帮你了解了如何让你的handler来产生响应。有些时候你可以用一小段C代码就可以得到响应,但是通常情况下你需要同另外一台server打交道(比如你正在写一个用来实现某种网络协议的模块)。你当然可以自己实现一套网络编程的东东,但是如果你只收到部分的响应,需要等待余下的响应数据,你会怎么办?你不会想阻塞整个事件处理循环吧?这样会毁掉Nginx的良好性能!幸运的是,Nginx允许你在它处理后端服务器(叫做"upstreams")的机制上加入你的回调函数,因此你的模块将可以和其他的server通信,同时还不会妨碍其他的请求。这一节将介绍模块如何和一个upstream(如 Memcached, FastCGI,或者另一个 HTTP server)通信。 

4.2.1. Upstream 回调函数概要

    与其他模块的回调处理函数不一样,upstream模块的处理函数几乎不做“实事”。它压根不调用ngx_http_output_filter。它仅仅是告诉回调函数什么时候可以向upstream server写数据了,以及什么时候能从upstream server读数据了。实际上它有6个可用的钩子: create_request 生成发送到upstream server的请求缓冲(或者一条缓冲链) reinit_request 在与后端服务器连接被重置的情况下(在create_request 被第二次调用之前)被调用 process_header 处理upstream 响应的第一个bit,通常是保存一个指向upstream "payload"的指针 abort_request 在客户端放弃请求时被调用 finalize_request 在Nginx完成从upstream读取数据后调用 input_filter 这是一个消息体的filter,用来处理响应消息体(例如把尾部删除) 这些钩子是怎么勾上去的呢?下面是一个例子,简单版本的代理模块处理函数: 

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;
}

    看上去都是些例行事务,不过重要的是那些回调函数。同时还要注意的是ngx_http_read_client_request_body,它又设置了一个回调函数,在Nginx完成从客户端读数据后会被调用。 

    这些个回调函数都要做些什么工作呢?通常情况下,reinit_request, abort_request, 和 finalize_request用来设置或重置一些内部状态,但这些都是几行代码的事情。真正做苦力的是create_request 和 process_header。 

4.2.2. create_request 回调函数

    简单起见,假设我有一个upstream server,它读入一个字符打印出两个字符。那么函数应该如何来写呢? create_request需要申请一个buffer来存放“一个字符”的请求,为buffer申请一个链表,并且把链表挂到upstream结构体上。看起来就像这样: 

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。r->uri作为一个 ngx_str_t类型也是有效的,GET的参数在r->args中,最后别忘了你还能访问请求头和 cookie信息。 



(责任编辑:IT)
------分隔线----------------------------
栏目列表
推荐内容