Nginx之编写HTTP模块

1. 常用数据结构

1.1 ngx_str_t

typedef struct {
    /*
     * 字符串的有效长度
     */
    size_t      len;
    /*
     * 有效字符串的起始地址,该字符串通常并不以'\0'结尾.
     */
    u_char     *data;
} ngx_str_t;

1.2 ngx_list_t

typedef struct ngx_list_part_s  ngx_list_part_t;

struct ngx_list_part_s {
    /*
     * 指向数组的起始地址
     */
    void             *elts;
    /*
     * 表示当前数组中已经使用了多少个元素
     */
    ngx_uint_t        nelts;
    /*
     * 下一个链表元素 ngx_list_part_t 的地址
     */
    ngx_list_part_t  *next;
};

/* 描述整个链表 */
typedef struct {
    /*
     * 指向链表的最后一个数组元素
     */
    ngx_list_part_t  *last;
    /*
     * 链表的首个数组元素
     */
    ngx_list_part_t   part;
    /*
     * 数组中每个元素的大小
     */
    size_t            size;
    /*
     * 表示每个 ngx_list_part_t 数组的容量,即最可以存储多少个数据
     */
    ngx_uint_t        nalloc;
    /*
     * 链表中管理内存分配的内存池对象
     */
    ngx_pool_t       *pool;
} ngx_list_t;

1.3 ngx_table_elt_t

typedef struct {
    /*
     * 表明 ngx_table_elt_t 可以是某个散列表数据结构
     * (ngx_hash_t 类型)中的成员. ngx_uint_t 类型的 hash
     * 成员可以在 ngx_hash_t 中更快地找到相同 key 的
     * ngx_table_elt_t 数据
     */
    ngx_uint_t        hash;
    ngx_str_t         key;
    ngx_str_t         value;
    /*
     * 指向全小写的 key 字符串
     */
    u_char           *lowcase_key;
} ngx_table_elt_t;

1.4 ngx_buf_t

typedef void *            ngx_buf_tag_t;

typedef struct ngx_buf_s  ngx_buf_t;

struct ngx_buf_s {
    /*
     * 该缓存中有效待处理数据的起始地址
     */
    u_char          *pos;
    /*
     * 该缓存中有效待处理数据的末尾,即 pos 到 last 
     * 之间的内存是希望 Nginx 处理的内容.
     */
    u_char          *last;
    /*
     * 处理文件时,file_pos 与 file_last 的含义与处理内存时的 pos
     * 与 last 相同,file_pos 表示将要处理的文件位置,file_last 
     * 表示截止的文件位置.
     */
    off_t            file_pos;
    off_t            file_last;

    u_char          *start;         /* start of buffer */
    u_char          *end;           /* end of buffer */
    /* 
     * 表示当前缓冲区的类型,如由哪个模块使用就指向这个模块的
     * ngx_module_t 变量的地址
     */
    ngx_buf_tag_t    tag;
    /*
     * 引用的文件
     */
    ngx_file_t      *file;
    /*
     * 当前缓冲区的影子缓存区,该成员很少使用,仅在描述的使用缓冲区
     * 转发上游服务器的响应时才使用了 shadow 成员,这是因为 Nginx 太
     * 节约内存了,分配一块内存并使用 ngx_buf_t 表示接收到的上游服务器
     * 响应后,在向下游客户端转发时可能会把这块内存存储到文件中,也可能
     * 直接向下游发送,此时 Nginx 绝不会重新复制一份内存用于新的目的,
     * 而是再次建立一个 ngx_buf_t 结构体指向原内存,这样多个 ngx_buf_t
     * 结构体指向同一块内存,它们之间的关系就通过 shadow 成员来引用.
     */
    ngx_buf_t       *shadow;


    /* the buf's content could be changed */
    unsigned         temporary:1;

    /*
     * the buf's content is in a memory cache or in a read only memory
     * and must not be changed
     */
    unsigned         memory:1;

    /* the buf's content is mmap()ed and must not be changed */
    unsigned         mmap:1;

    /*
     * 标志位,为 1 表示可回收
     */
    unsigned         recycled:1;
    /*
     * 标志位,为 1 表示这段缓冲区处理的是文件而不是内存
     */
    unsigned         in_file:1;
    /*
     * 标志位,为 1 时表示需要执行 flush 操作
     */
    unsigned         flush:1;
    /*
     * 标志位,对于操作这块内存是否使用同步方式,需谨慎,可能会阻塞 Nginx 进程,
     * Nginx 中所有操作都是异步的,这是它支持高并发的关键。
     */
    unsigned         sync:1;
    /*
     * 标志位,表示是否是最后一块缓冲区,因为 ngx_buf_t 可以由 ngx_chain_t
     * 链表串联起来,因此,当 last_buf 为 1 时,表示当前是最后一块待处理的
     * 缓冲区.
     */
    unsigned         last_buf:1;
    /*
     * 标志位,表示是否是 ngx_chain_t 中的最后一个缓冲区.
     */
    unsigned         last_in_chain:1;

    /*
     * 标志位,表示是否是最后一个影子缓冲区,与 shadow 域配合使用
     */
    unsigned         last_shadow:1;
    /*
     * 标志位,表示当前缓冲区是否属于临时文件.
     */
    unsigned         temp_file:1;

    /* STUB */ int   num;
};

1.5

typedef struct ngx_chain_s           ngx_chain_t;

struct ngx_chain_s {
    /*
     * 指向当前的 ngx_buf_t 缓冲区
     */
    ngx_buf_t    *buf;
    /*
     * 指向下一个ngx_chain_t,若这是最后一个 ngx_chain_t,则置为 NULL
     */
    ngx_chain_t  *next;
};

在向用户发送 HTTP 包体时,就要传入 ngx_chain_t 链表对象,注意,如果这是最后一个 ngx_chain_t,则必须将 next 设置为 NULL,否则永远不会发送成功,且这个请求将一直不会结束.

2. 将自定义的 HTTP 模块编译进 Nginx

在 configure 脚本执行时加入参数:--add-module=<PATH>

2.1 config 文件的写法

开发一个 HTTP 模块,则 config 文件中需定义以下 3 个变量:

  • ngx_addon_name: 仅在 configure 执行时使用,一般设置为模块名称
  • HTTP_MODULES: 保存所有的 HTTP 模块名称,每个 HTTP 模块间由空格符相连。在重新设置 HTTP_MODULES 变量时,不要直接覆盖它,因为 configure 调用到自定义的 config 脚本前,已经将各个 HTTP 模块设置到 HTTP_MODULES 变量中,因此,应如下设置: "$HTTP_MODULES ngx_http_mytest_module"
  • NGX_ADDON_SRCS: 用于指定新增模块的源代码,多个待编译的源代码间以空格符相连。在设置 NGX_ADDON_SRCS 时可以使用 $ngx_addon_dir 变量,它等价于 configure 执行时 --add-module=PATH 的 PATH 惨呼。

因此,对于一个第三方 mytest 模块,可以这样编写 config 文件:

ngx_addon_name=ngx_http_mytest_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c"

3. HTTP 模块的数据结构

ngx_module_t

typedef struct ngx_module_s          ngx_module_t;

struct ngx_module_s {
    /*
     * 表示当前模块在这类模块中的序号.
     */
    ngx_uint_t            ctx_index;
    /*
     * 表示当前模块在 ngx_modules 数组中的序号. ctx_index 表示的是当前模块在
     * 一类模块中的序号,而 index 表示当前模块在所有模块中的序号。
     */
    ngx_uint_t            index;

    /*
     * 模块的名称
     */
    char                 *name;

    ngx_uint_t            spare0;
    ngx_uint_t            spare1;

    ngx_uint_t            version;
    const char           *signature;

    /*
     * ctx 用于指向一类模块的上下文结构体。
     */
    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;
};

ngx_http_module_t

typedef struct {
    /*
     * 解析配置文件前调用
     */
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    /*
     * 完成配置文件的解析后调用
     */
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    /*
     * 当需要创建数据结构用于存储 main 级别(直属于 http{...} 块的配置项)
     * 的全局配置项时,可以通过该回调方法创建存储全局配置项的结构体
     */
    void       *(*create_main_conf)(ngx_conf_t *cf);
    /*
     * 初始化 main 级别的配置项
     */
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    /*
     * 当需要创建数据结构用于存储 srv 级别(直属于 server{...} 块的配置项)
     * 的全局配置项时,可以通过该回调方法创建存储 srv 级别配置项的结构体
     */
    void       *(*create_srv_conf)(ngx_conf_t *cf);
    /*
     * 用于合并 main 级别和 srv 级别下的同名配置项
     */
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    /*
     * 当需要创建数据结构用于存储 loc 级别(直属于 location{...} 块的配置项)
     * 的全局配置项时,可以通过该回调方法创建存储 loc 级别配置项的结构体
     */
    void       *(*create_loc_conf)(ngx_conf_t *cf);
    /*
     * 用于合并 srv 级别和 loc 级别下的同名配置项
     */
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

ngx_command_t

typedef struct ngx_command_s         ngx_command_t;

struct ngx_command_s {
    /*
     * 配置项名称,如"gzip"
     */
    ngx_str_t             name;
    /*
     * 配置项类型,type 将制定配置项可以出现的位置。如 server{} 或
     * location{} 中,以及它可以携带的参数个数.
     */
    ngx_uint_t            type;
    /*
     * 出现了 name 中指定的配置项后,将会调用 set 方法处理配置项的参数
     */
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;
    ngx_uint_t            offset;
    /*
     * 配置项读取后的处理方法,必须是 ngx_conf_post_t 结构的指针
     */
    void                 *post;
};

4. 定义自己的 HTTP 模块

自定义的 HTTP 模块介入 Nginx 的方式:

  • 不希望模块对所有的 HTTP 请求起作用
  • 在 nginx.conf 文件中 http{}、server{}、location{} 块内定义的 mytest 配置项,如果一个用户请求通过主机域名、URI 等匹配了响应的配置块,而这个配置块下具有 mytest 配置项,则 mytest 模块开始处理请求。

在这种方式下,mytest 处理请求是固定在 NGX_HTTP_CONTENT_PAHSE 阶段开始处理请求。

4.1 定义配置项的处理

static ngx_command_t  ngx_http_mytest_commands[] = {

    { ngx_string("mytest"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|
      NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS,
      ngx_http_mytest,
      NGX_HTTP_LOC_CONF_OFFSET, 
      0, 
      NULL },

      ngx_null_command
};

当某个配置块出现 mytest 配置项时,Nginx 将会调用 ngx_http_mytest 方法:

static char *ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t *clcf;

    /*
     * 首先找到 mytest 配置项所属的配置块.
     */
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

    /* HTTP 框架在处理用户请求进行到 NGX_HTTP_CONTENT_PAHSE 阶段时,
     * 如果请求的域名、URI 与 mytest 配置项所在配置块相匹配,则会调用
     * ngx_http_mytest_handler 处理该请求.
     */
    clcf->handler = ngx_http_mytest_handler;

    return NGX_CONF_OK;
}

4.2 ngx_http_mytest_module_ctx

static ngx_http_module_t ngx_http_mytest_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    NULL,                                  /* create location configuration */
    NULL                                   /* merge location configuration */
};

若没有什么工作需要在 HTTP 框架初始化时完成,则可如上定义 ngx_http_module_t 接口。

4.3 定义 mytest 模块

ngx_module_t ngx_http_mytest_module = {
    NGX_MODULE_V1,
    &ngx_http_mytest_module_ctx,
    ngx_http_mytest_commands,
    NGX_HTTP_MODULE,
    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
};

5. 处理用户的请求

当出现 mytest 配置项时,ngx_http_mytest 方法会被调用,该方法中将 ngx_http_core_loc_conf_t 结构的 handler 成员指定为 ngx_http_mytest_handler。这样,当 HTTP 框架在接收完 HTTP 请求的头部后,在 NGX_HTTP_CONTENT_PAHSE 阶段会执行 handler 指向的方法。该方法原型如下:

typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);

5.1 处理方法的返回值

HTTP 框架在 NGX_HTTP_CONTENT_PAHSE 阶段调用 ngx_http_mytest_handler 后,会将 ngx_http_mytest_handler 的返回值作为参数传给 ngx_http_finalize_request 方法。

if (r->content_handler) {
    r->write_event_handler = ngx_http_request_empty_handler;
    ngx_http_finalize_request(r, r->content_handler(r));
    return NGX_OK;
}

因此,ngx_http_finalize_request 决定了 ngx_http_mytest_handler 如何起作用。

四个通用的返回码:

  • NGX_OK:表示成功。Nginx 将会继续执行该请求的后续动作(如执行 subrequest 或撤销这个请求)
  • NGX_DECLINED: 继续在 NGX_HTTP_CONTENT_PASHE 阶段寻找下一个该请求感兴趣的 HTTP 模块来再次处理这个请求
  • NGX_DONE: 表示到此为止,同时 HTTP 框架将暂时不再继续执行这个请求的后续部分。事实上,这时会检查连接的类型,如果是 keepalive 类型的用户请求,就会保持住 HTTP 连接,然后把控制权交给 Nginx。
  • NGX_ERROR:表示错误。这时会调用 ngx_http_terminate_request 终止请求. 如果还有 POST 子请求,那么将会在执行完 POST 请求后再终止本次请求.

6. 发送响应: hello world 示例

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t* r)
{
    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
        return NGX_HTTP_NOT_ALLOWED;
    }

    /* 丢弃请求中的包体 */
    ngx_int_t rc = ngx_http_discard_request_body(r);
    if (rc != NGX_OK) {
        return rc;
    }

    /* 设置返回的 Content-Type */
    ngx_str_t type = ngx_string("text/plain");
    ngx_str_t response = ngx_string("hello world");
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = response.len;
    r->headers_out.content_type = type;

    /* 发送 HTTP 头部 */
    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }

    /* 构造 ngx_buf_t 结构体准备发送包体 */
    ngx_buf_t *b;
    b = ngx_create_temp_buf(r->pool, response.len);
    if (b == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    /* 将 hello world 复制到 ngx_buf_t 指向的内存中 */
    ngx_memcpy(b->pos, response.data, response.len);
    /* 注意设置好 last 指针 */
    b->last = b->pos + response.len;
    /* 声明这是最后一块缓存 */
    b->last_buf = 1;

    /* 构造发送时的 ngx_chain_t 结构体 */
    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;

    /* 发送包体,发送结束后 HTTP 框架会调用 ngx_http_finalize_request 
     * 方法结束请求 */
    return ngx_http_output_filter(r, &out);
}

7. 使用 HTTP 配置

处理 HTTP 配置项可以分为下面 4 个步骤:

  1. 创建数据结构用于存储配置项对应的参数。
  2. 设定配置项在 nginx.conf 中出现时的限制条件与回调方法。
  3. 实现第 2 步中的回调方法,或者使用 Nginx 框架预设的 14 个回调方法。
  4. 合并不同级别的配置块中出现的同名配置项。

7.1 分配用于保存配置参数的数据结构

typedef struct {
    ngx_str_t           my_str;
    ngx_int_t           my_num;
    ngx_flag_t          my_flag;
    size_t              my_size;
    ngx_array_t*        my_str_array;
    ngx_array_t*        my_keyval;
    off_t               my_off;
    ngx_msec_t          my_msec;
    time_t              my_sec;
    ngx_bufs_t          my_bufs;
    ngx_uint_t          my_enum_seq;
    ngx_uint_t          my_bitmask;
    ngx_uint_t          my_access;
    ngx_uint_t          my_path;
}ngx_http_mytest_conf_t;

在 Nginx 中,多个 location 块(或者 http 块、server 块)中的相同配置项是允许同时生效的,也就是说,ngx_http_mytest_conf_t 结构必须在 Nginx 的内存中保存许多份。事实上,HTTP 框架在解析 nginx.conf 文件时只要遇到 http{}、server{} 或者 location{} 配置块就会立刻分配一个新的 ngx_http_mytext_conf_t 结构体。因此,HTTP 模块感兴趣的配置项需要统一地使用一个 struct 结构体来保存,如果 nginx.conf 文件中在 http{} 下有多个 server{} 或者 location{},那么这个 struct 结构体在 Nginx 进程中就会存在多份实例。

普通的 HTTP 模块往往只实现 create_loc_conf 回调方法,因为它们只关注匹配某种 URL 的请求。mytest 模块同样如此,只实现 create_loc_conf 方法,因此,此时 ngx_http_module_t 接口的定义如下:

static ngx_http_module_t ngx_http_mytest_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_mytest_create_loc_conf,       /* create location configuration */
    NULL                                   /* merge location configuration */
};

ngx_http_mytest_create_loc_conf 方法的实现如下:

static void *ngx_http_mytest_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_mytest_conf_t *mycf;
    
    mycf = (ngx_http_mytest_conf_t *)ngx_pcalloc(cf->pool, sizeof(ngx_http_mytest_conf_t));
    if (mycf == NULL) {
        return NULL;
    }
    
    mycf->test_flag = NGX_CONF_UNSET;
}

7.2 设置配置项的解析方式

7.2.1 ngx_command_t 结构体中 type 成员的取值及其意义

处理配置项时获取当前配置块的方式:

  • NGX_DIRECT_CONF:一般由 NGX_CORE_MODULE 类型的核心模块使用,仅与下面的 NGX_MAIN_CONF 同时设置,表示模块需要解析不属于任何 {} 内的全局配置项。它实际上会指定 set 方法里的第 3 个参数 conf 的值,使之指向每个模块解析全局配置项的配置结构体。
  • NGX_ANY_CONF:

配置项可以在哪些 {} 配置块中出现

  • NGX_MAIN_CONF:配置项可以出现在全局配置项中,即不属于任何 {} 配置块
  • NGX_EVENT_CONF:配置项可以出现在 events{} 块内
  • NGX_MAIN_MAIN_CONF: 配置项可以出现在 mail{} 块或者 imap{} 块内的 main
  • NGX_MAIL_SRV_CONF: 配置项可以出现在 server{} 块内,然后该 server{} 块必须属于 mail{} 块或者 imap{} 块
  • NGX_HTTP_MAIN_CONF: 配置项可以出现在 http{} 块内
  • NGX_HTTP_SRV_CONF: 配置项可以出现在 server{} 块内,然而该 server 块必须属于 http{} 块
  • NGX_HTTP_LOC_CONF: 配置项可以出现在 location{} 块内,然而该 location 块必须属于 http{} 块
  • NGX_HTTP_UPS_CONF: 配置项可以出现在 upstream{} 块内,然而该 upstream 块必须属于 http{} 块
  • NGX_HTTP_SIF_CONF: 配置项可以出现在 server{} 块内的 if{} 块中。目前仅有 rewrite 模块会使用,该 if 块必须属于 http{} 块
  • NGX_HTTP_LIF_CONF:配置项可以出现在 location{} 块内的 if{} 块中。目前仅有 rewrite 模块会使用,该 if 块必须属于 http{} 块
  • NGX_HTTP_LMT_CONF:配置项可以出现在 limit_except{} 块内,然后该 limit_except 块必须属于 http{} 块

限制配置项的参数个数

  • NGX_CONF_NOARGS:配置项不携带任何参数
  • NGX_CONF_TAKE1:配置项必须携带 1 个参数
  • NGX_CONF_TAKE2:配置项必须携带 2 个参数
  • NGX_CONF_TAKE3:配置项必须携带 3 个参数
  • NGX_CONF_TAKE4:配置项必须携带 4 个参数
  • NGX_CONF_TAKE5:配置项必须携带 5 个参数
  • NGX_CONF_TAKE6:配置项必须携带 6 个参数
  • NGX_CONF_TAKE7:配置项必须携带 7 个参数
  • NGX_CONF_TAKE12:配置项可以携带 1 个参数或 2 个参数
  • NGX_CONF_TAKE12:配置项可以携带 2 个参数或 3 个参数
  • NGX_CONF_TAKE123:配置项可以携带 1~3 个参数
  • NGX_CONF_TAKE123:配置项可以携带 1~4 个参数

限制配置项后的参数出现的形式

  • NGX_CONF_ARGS_NUMBER:
  • NGX_CONF_BLOCK:配置项定义了一种新的 {} 块。例如,http、server、location 等配置,它们的 type 都必须定义为 NGX_CONF_BLOCK
  • NGX_CONF_ANY:不验证配置项携带的参数个数
  • NGX_CONF_FLAG:配置项携带的参数只能是 1 个,并且参数的值只能是 on 或 off
  • NGX_CONF_1MORE:配置项携带的参数个数必须超过 1 个
  • NGX_CONF_2MORE: 配置项携带的参数个数必须超过 2 个
  • NGX_CONF_MULTI:表示当前配置项可以出现在任意块中(包括不属于任何块的全局配置),它仅用于配合其他配置项使用。type 中未加入 NGX_CONF_MULTI 时,如果一个配置项出现在 type 成员未标明的配置块中,那么 Nginx 会认为该配置项非法,最后将导致 Nginx 启动失败。但如果 type 中加入了 NGX_CONF_MULTI,则认为该配置项一定是合法的,然后又会有两种不同的结果:1. 如果配置项出现在 type 指示的块中,则会调用 set 方法解析配置项;2. 如果配置项没有出现在 type 指示的块中,则不对该配置项做任何处理。因此,NGX_CONF_MULTI 会使得配置项出现未知块时不会出错。

7.2.2 预设的 14 个配置项解析方法

  • ngx_conf_set_flag_slot:如果 nginx.conf 文件中某个配置型的参数是 on 或 off(即希望配置项表达打开或者关闭某个功能的意思),而且在 Nginx 模块的代码中使用 ngx_flag_t 变量来保存这个配置项的参数,就可以将 set 回调方法设为 ngx_conf_set_flag_slot。当 nginx.conf 文件中参数是 on 时,代码中的 ngx_flag_t 类型变量的值将设为 1,为 off 时则设为 0
  • ngx_conf_set_str_slot:如果配置项后只有 1 个参数,同时在代码中我们希望用 ngx_str_t 类型的变量来保存这个配置项的参数,则可以使用该方法
  • ngx_conf_set_str_array_slot:如果这个配置项会出现多次,每个配置项后面跟着 1 个参数,而在程序中我们希望仅用一个 ngx_array_t 动态数组来存储所有的参数,其数组中的每个参数都以 ngx_str_t 来存储,那么则可以使用预设的 ngx_conf_set_str_array_slot 方法
  • ngx_conf_set_keyval_slot:与 ngx_conf_set_str_array_slot 类似,也是用一个 ngx_array_t 数组来存储所有同名配置项的参数。只是每个配置项的参数不再只是 1 个,而必须是两个,且以 "配置项名 关键字 值;" 的形式出现在 nginx.conf 文件中,同时,ngx_conf_set_keyval_slot 将把这些配置项转化为数组,其中每个元素都存储着 key/value 键值对。
  • ngx_conf_set_num_slot:配置项后必须携带 1 个参数,其只能是数字。存储这个参数的变量必须是整型
  • ngx_conf_set_size_slot:配置项后必须携带 1 个参数,表示空间大小,可以是一个数字,这时表示字节数(Byte)。如果数字后跟着 k 或者 K,就表示 Kilobyt,1 KB = 1024B;如果数字后跟着 m 或者 M,就表示 Megabyte,1 MB = 1024KB。ngx_conf_set_size_slot 解析后将把配置项后的参数转化成以字节数为单位的数字
  • ngx_conf_set_off_slot:配置项后必须携带 1 个参数,表示空间上的偏移量。它与设置的参数非常类似,其参数是一个数字时表示 Byte,也可以在后面加单位,但与 ngx_conf_set_size_slot 不同的是,数字后面的单位不仅可以是 k 或者 K、m 或者 M,还可以是 g 或者 G,这时表示 Gigabyte,1GB=1024MB。
  • ngx_conf_set_msec_slot:配置项后必须携带 1 个参数,表示时间。这个参数可以在数字后面加单位,如果单位为 s 或者没有任何单位,那么这个数字表示秒;如果单位为 m,则表示分钟;如果单位为 h,表示小时;如果单位为 d,则表示天;如果单位为 w,则表示周;如果单位为 M,则表示月;如果单位为 y,则表示年。ngx_conf_set_msec_slot 解析后将把配置项后的参数转化成以毫秒为单位的数字
  • ngx_conf_set_sec_slot:与 ngx_conf_set_msec_slot 非常类似,唯一的区别是 ngx_conf_set_sec_slot 是将解析后的配置项的参数转化为以秒为单位的数字
  • ngx_conf_set_bufs_slot:配置项后必须携带一两个参数,第 1 个参数是数字,第 2 个参数表示空间大小。例如,"gzip_buffers 4 7k;"(通常用来表示有多少 ngx_buf_t 缓冲区),其中第 1 个参数不可以携带任何单位,第 2 个参数不带任何单位时表示 Byte,如果以 k 或者 K 为单位,则表示 Kilobyte,如果以 m 或者 M 作为单位,则表示 Megabyte。ngx_conf_set_bufs_slot 解析后会把配置项后的两个参数转化成 ngx_bufs_t 结构体下的两个成员。这个配置项对应于 Nginx 最喜欢用的多缓冲区的解决方案(如接收连接对端发来的 TCP 流)
  • ngx_conf_set_enum_slot:配置项后必须携带 1 个参数,其取值范围必须是我们设定好的字符串之一(类似枚举)。首先,用 ngx_conf_enum_t 结构定义配置项的取值范围,并设定每个值对应的序号。然后,ngx_conf_set_enum_slot 将会把配置项参数转化为对应的序列号
  • ngx_conf_set_bitmask_slot:与 ngx_conf_set_enum_slot 类似,配置项后必须携带 1 个参数,其取值范围必须是设定好的字符串之一。首先,用 ngx_conf_bitmask_t 结构定义配置项的取值范围,并设定每个值对应的比特位。注意,每个值所对应的比特位都要不同。然后 ngx_conf_set_bitmask_slot 将会把配置项参数转化成对应的比特位
  • ngx_conf_set_access_slot:这个方法用于设置目录或者文件的读写权限。配置项后可以携带 1~3 个参数,可以是如下形式:user:rw group:rw all:rw。注意,它的意义与 Linux 上文件或者目录的权限意义是一致的,但是 user/group/all 后面的权限只可以设为 rw(读/写)或者 r(只读),不可以有其他任何形式,如 w 或者 rx 等。ngx_conf_set_access_slot 将会把这些参数转化为一个整型
  • ngx_conf_set_path_slot:这个方法用于设置路径,配置项后必须携带一个参数,表示一个有意义的路径。ngx_conf_set_path_slot 将会把参数转化为 ngx_path_t 结构

8. 请求的上下文

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t* r)
{
    /* 首先调用 ngx_http_get_module_ctx 宏来获取上下文结构体 */
    ngx_http_mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    /* 如果之前没有设置过上下文,则返回NULL */
    if (myctx == NULL) {
        /* 必须在当前请求的内存池r->pool中分配上下文结构体,这样请求结束时
         * 结构体占用的内存才会释放 */
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mytest_ctx_t));
        if (myctx == NULL) {
            return NGX_ERROR;
        }
        /* 将刚分配的结构体设置到当前请求的上下文中 */
        ngx_http_set_ctx(r, myctx, ngx_http_mytest_module);
    }

    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
        return NGX_HTTP_NOT_ALLOWED;
    }

    ngx_int_t rc = ngx_http_discard_request_body(r);
    if (rc != NGX_OK) {
        return rc;
    }

    ngx_str_t type = ngx_string("text/plain");
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_type = type;

    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }

    ngx_buf_t *b;
    b = ngx_palloc(r->pool, sizeof(ngx_buf_t));

    u_char* filename = (u_char*)"/home/rong/samba/nginx-1.13.2/tmp/sbin/test.txt";
    b->in_file = 1;
    b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
    b->file->fd = ngx_open_file(filename, 
                                NGX_FILE_RDONLY|NGX_FILE_NONBLOCK, 
                                NGX_FILE_OPEN, 0);
    b->file->log = r->connection->log;
    b->file->name.data = filename;
    b->file->name.len = sizeof(filename) - 1;
    if (b->file->fd <= 0) {
        return NGX_HTTP_NOT_FOUND;
    }

    if (ngx_file_info(filename, &b->file->info) == NGX_FILE_ERROR) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    r->headers_out.content_length_n = b->file->info.st_size;
    /* 从文件的 file_pos 位置开始发送文件,一直到file_last偏移量处 */
    b->file_pos = 0;
    b->file_last = b->file->info.st_size;

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

    return ngx_http_output_filter(r, &out);
}

static char *ngx_http_mytest(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);

    /* HTTP 框架在处理用户请求进行到 NGX_HTTP_CONTENT_PAHSE 阶段时,
     * 如果请求的域名、URI 与 mytest 配置项所在配置块相匹配,则会调用
     * ngx_http_mytest_handler 处理该请求.
     */
    clcf->handler = ngx_http_mytest_handler;

    return NGX_CONF_OK;
}

9. 访问第三方服务

Nginx 提供了两种全异步方式来与第三方服务器通信:upstream 和 subrequest。

upstreasm 可以保证与第三方服务器交互时(包括三次握手建立 TCP 连接、发送请求、接收响应、四次握手关闭 TCP 连接等)不会阻塞 Nginx 进程处理其他请求。

subrequest 是分解复杂请求的一种设计模式,它本质上与访问第三方服务没有任何关系,subrequest 访问第三方服务最终也是基于 upstream 实现的。

若希望把第三方服务的内容几乎原封不动地返回给用户时,一般使用 upstream 方式,它可以非常高效地透传 HTTP。如果访问第三方服务只是为了获取某些信息,再依据这些信息来构造响应并发送给用户,这时应该用 subrequest 方式。

9.1 upstream 的使用方式

HTTP 模块启用 upstream 机制流程:

  1. 创建 upstream 成员,upstream 在初始状态是 NULL 空指针,可以调用 ngx_http_upstream_create 方法来创建 upstream。
  2. 设置上游服务器地址。在 HTTP 反向代理功能中似乎只能使用 nginx.conf 中配置好的上游服务器,但实际上 upstream 机制并没有这种要求,用户可以以任意方式指定上游服务器的 IP 地址。如从请求的 URL 或 HTTP 头部中动态地获取上游服务器地址,ngx_http_upstreasm_t 中的 resolved 成员就可以帮助用户设置上游服务器。
  3. upstream 在各个执行阶段中都会试图回调它的 HTTP 模块实现的 8 个方法。
  4. 调用 ngx_http_upstream_init 方法启动 upstream 机制。注意,ngx_http_mytest_handler 方法此时此时必须返回 NGX_DONE,这是在要求 HTTP 框架不要按阶段继续向下处理请求了,同时告诉它 HTTP 框架请求必须停留在当前阶段,等待某个 HTTP 模块主动地继续向下处理这个请求(如,在上游服务器主动关闭连接时,upstream 模块就会主动地继续处理这个请求,很可能回向客户端发送 502 响应码)

9.1.1 ngx_http_upstream_t

typedef struct ngx_http_upstream_s    ngx_http_upstream_t;

struct ngx_http_upstream_s {
    /*
     * 处理读事件的回调方法,每一个阶段都有不同的 read_event_handler 
     */
    ngx_http_upstream_handler_pt     read_event_handler;
    /*
     * 处理写事件的回调方法,每一个阶段都有不同的 write_event_handler
     */
    ngx_http_upstream_handler_pt     write_event_handler;

    /*
     * 表示主动向上游服务器发起的连接
     */
    ngx_peer_connection_t            peer;

    /*
     * 当向下游客户端转发响应时(ngx_http_request_t 结构体中的 subrequest_in_memory
     * 标志位为 0),如果打开了缓存且认为上游网速更快(conf 配置中的 buffering 标志
     * 位为 1),这时会使用 pipe 成员来转发响应。在使用这种方式转发响应时,必须由
     * HTTP 模块在使用 upstream 机制前构造 pipe 结构体,否则会出现严重的 coredump
     * 错误.
     */
    ngx_event_pipe_t                *pipe;

    /*
     * request_bufs 以链表的方式把 ngx_buf_t 缓存区链接起来,它表示所有需要发送到
     * 上游服务器的请求内容。所以,HTTP 模块实现的 create_request 回调方法就在于
     * 构造 request_bufs 链表
     */
    ngx_chain_t                     *request_bufs;

    /*
     * 定义了向下游发送响应的方式
     */
    ngx_output_chain_ctx_t           output;
    ngx_chain_writer_ctx_t           writer;

    /*
     * upstream 访问时的所有限制性参数
     */
    ngx_http_upstream_conf_t        *conf;
    ngx_http_upstream_srv_conf_t    *upstream;
#if (NGX_HTTP_CACHE)
    ngx_array_t                     *caches;
#endif

    /*
     * HTTP 模块在实现 process_header 方法时,如果希望 upstream 直接转发响应,
     * 就需要把解析出的响应头部适配为 HTTP 的响应头部,同时需要把包头中的信息
     * 设置到 headers_in 结构体,这样,会把 headers_in 中设置的头部添加到要发
     * 送到下游客户端的响应头部 headers_out 中
     */
    ngx_http_upstream_headers_in_t   headers_in;

    /*
     * 通过 resolved 可以直接指定上游服务器地址
     */
    ngx_http_upstream_resolved_t    *resolved;

    ngx_buf_t                        from_client;

    /*
     * 存储接收自上游服务器发来的响应内容,由于它会被复用,所以具有下列多种意义:
     * 1. 在使用 process_header 方法解析上游响应的包头时,buffer 中将会保存完整的
     *    响应包头;
     * 2. 当下面的 buffering 成员为 1,而且此时 upstream 是向下游转发上游的包体时,
     *    buffer 没有意义;
     * 3. 当 buffering 标志位为 0 时,buffer 缓冲区会被用于反复地接收上游服务器的
     *    包体,进而向下游转发;
     * 4. 当 upstream 并不用于转发上游包体时,buffer 会被用于反复接收上游的包体,
     *    HTTP 模块实现的 input_filter 方法需要关注它.
     */
    ngx_buf_t                        buffer;
    /*
     * 表示来自上游服务器的响应包体的长度
     */
    off_t                            length;

    /*
     * out_bufs 在两种场景下有不同的意义:1. 当不需要转发包体,且使用默认
     * 的 input_filter 方法(也就是 ngx_http_upstream_non_buffered_filter 
     * 方法)处理包体时,out_bufs 将会指向响应包体,事实上,out_bufs 链表
     * 中会产生多个 ngx_buf_t 缓冲区,每个缓冲区都指向 buffer 缓存中的一部
     * 分,而这里的一部分就是每次调用 recv 方法接收到的一段 TCP 流。2. 当
     * 需要转发响应包体到下游时(buffering 标志位为 0,即以下游网速优先),
     * 这个链表指向上一次向下游转发响应到现在这段时间内接收自上游的缓存响应
     */
    ngx_chain_t                     *out_bufs;
    /*
     * 当需要转发响应包体到下游时(buffering 标志位为 0,即以下游网速优先),
     * 它表示上一次向下游转发响应时没有发送完的内容
     */
    ngx_chain_t                     *busy_bufs;
    /*
     * 这个链表将用于回收 out_bufs 中已经发送给下游的 ngx_buf_t 结构体,这
     * 同样应用在 buffering 标志位为 0 即以下游网速优先的场景
     */
    ngx_chain_t                     *free_bufs;

    /*
     * 处理包体前的初始化方法,其中 data 参数用于传递用户数据结构,它实际上
     * 就是下面的 input_filter_ctx 指针
     */
    ngx_int_t                      (*input_filter_init)(void *data);
    /*
     * 处理包体的方法,其中 data 参数用于传递用户数据结构,它实际上就是下面的
     * input_filter_ctx 指针,而 bytes 表示本次接收到的包体长度。返回 NGX_ERROR
     * 时表示处理包体错误,请求需要结束,否则都将继续 upstream 流程
     */
    ngx_int_t                      (*input_filter)(void *data, ssize_t bytes);
    /*
     * 用于传递 HTTP 模块自定义的数据结构,在 input_filter_init 和 input_filter 
     * 方法被回调时会作为参数传递过去
     */
    void                            *input_filter_ctx;

#if (NGX_HTTP_CACHE)
    ngx_int_t                      (*create_key)(ngx_http_request_t *r);
#endif
    /*
     * 用于构造发往上游服务器的请求内容
     */
    ngx_int_t                      (*create_request)(ngx_http_request_t *r);
    /*
     * 与上游服务器的通信失败后,如果按照重试规则还需要再次向上游服务器发起
     * 连接,则会调用 reinit_request 方法
     */
    ngx_int_t                      (*reinit_request)(ngx_http_request_t *r);
    /*
     * 解析上游服务器返回响应的包头,返回 NGX_AGAIN 表示包头还没有接收完整,
     * 返回 NGX_HTTP_UPSTREAM_INVALID_HEADER 表示包头不合法,返回 NGX_ERROR
     * 表示出现错误,返回 NGX_OK 表示解析到完整的包头.
     */
    ngx_int_t                      (*process_header)(ngx_http_request_t *r);
    void                           (*abort_request)(ngx_http_request_t *r);
    /*
     * 请求结束时会调用
     */
    void                           (*finalize_request)(ngx_http_request_t *r,
                                         ngx_int_t rc);
    /*
     * 在上游返回的响应出现 Location 或者 Refresh 头部时表示重定向时,会通过
     * ngx_http_upstream_process_headers 方法调用到可由 HTTP 模块实现的
     * rewrite_redirect 方法
     */
    ngx_int_t                      (*rewrite_redirect)(ngx_http_request_t *r,
                                         ngx_table_elt_t *h, size_t prefix);
    ngx_int_t                      (*rewrite_cookie)(ngx_http_request_t *r,
                                         ngx_table_elt_t *h);

    ngx_msec_t                       timeout;

    /*
     * 用于表示上游响应的错误码、包体长度等信息
     */
    ngx_http_upstream_state_t       *state;

    ngx_str_t                        method;
    /*
     * schema 和 uri 成员仅在记录日志时会用到,除此之外没有意义
     */
    ngx_str_t                        schema;
    ngx_str_t                        uri;

#if (NGX_HTTP_SSL || NGX_COMPAT)
    ngx_str_t                        ssl_name;
#endif

    ngx_http_cleanup_pt             *cleanup;

    /*
     * 是否指定文件缓存路径的标志位
     */
    unsigned                         store:1;
    /*
     * 是否启用文件缓存
     */
    unsigned                         cacheable:1;
    unsigned                         accel:1;
    /*
     * 是否基于 SSL 协议访问上游服务器
     */
    unsigned                         ssl:1;
#if (NGX_HTTP_CACHE)
    unsigned                         cache_status:3;
#endif

    /*
     * 在向客户端转发上游服务器的包体时才有用。当 buffering 为 1 时,表示使用多个
     * 缓冲区以及磁盘文件来转发上游的响应包体。当 Nginx 与上游间的网速远大于 Nginx
     * 与下游客户端间的网速时,让 Nginx 开辟更多的内存甚至使用磁盘文件来缓存上游的
     * 响应包体,这可以减轻上游服务器的压力。当 buffering 为 0 时,表示只使用上面
     * 的这一个 buffer 缓冲区来向下游转发响应包体.
     */
    unsigned                         buffering:1;
    unsigned                         keepalive:1;
    unsigned                         upgrade:1;

    /*
     * request_sent 表示是否已经向上游服务器发送了请求,当 request_sent 为 
     * 1 时,表示 upstream 机制已经向上游服务器发送了全部或者部分的请求。
     * 事实上,这个标志位更多的是为了使用 ngx_output_chain 方法发送请求,
     * 因为该方法发送请求时会自动把未发送完的 request_bufs 链表记录下来,
     * 为了防止反复发送重复请求,必须有 request_sent 标志位记录是否调用过
     * ngx_output_chain 方法
     */
    unsigned                         request_sent:1;
    unsigned                         request_body_sent:1;
    /*
     * 将上游服务器的响应划分为包头和包尾,如果把响应直接转发给客户端,
     * header_sent 标志位表示包头是否发送,header_sent 为 1 时表示已经
     * 把包头转发给客户端了。如果不转发响应到客户端,则 header_sent 
     * 没有意义.
     */
    unsigned                         header_sent:1;
};

9.1.2 upstream 处理上游响应包体的方式

  1. 当请求的 ngx_http_request_t 结构体中 subrequest_in_memory 标志位为 1 时,将采用第 1 中方式,即 upstream 不转发响应包体到下游,由 HTTP 模块实现的 input_filter 方法处理包体;
  2. 当 subrequest_in_memory 为 0 时,upstream 会转发响应包体。
    • 当 ngx_http_upstream_conf_t 配置结构体中的 buffering 标志位为 1 时,将会开启更多的内存和磁盘文件用于缓存上游的响应包体,这意味上游网速更快;
    • 当 buffering 为 0 时,将使用固定大小的缓冲区(即 ngx_http_upstream_t 结构体中的 buffer 缓冲区)来转发响应包体.

9.1.3 设置 upstream 的限制性参数

typedef struct {
    ...
    
    /* 连接上游服务器的超时时间,单位毫秒 */
    ngx_msec_t              connect_timeout;
    
    /* 发送 TCP 包到上游服务器的超时时间,单位毫秒 */
    ngx_msec_t              send_timeout;
    
    /* 接收 TCP 包到上游服务的超时时间,单位毫秒 */
    ngx_msec_t              read_timeout;
    ...
}ngx_http_upstream_conf_t;

该结构体中的三个超时时间必须设置,因为它们默认为 0,若不设置将永远无法与上游服务器建立连接。

下面是设置 conn_timeout 连接超时时间的示例:

static ngx_command_t  ngx_http_mytest_commands[] = {

    { ngx_string("mytest"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|
      NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS,
      ngx_http_mytest,
      NGX_HTTP_LOC_CONF_OFFSET, 
      0, 
      NULL },

    { ngx_string("upstream_conn_timeout"), 
      NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_msec_slot, 
      NGX_HTTP_LOC_CONF_OFFSET,
      /* 给出 conn_timeout   成员在ngx_http_mytest_conf_t结构体中的偏移量*/
      offsetof(ngx_http_mytest_conf_t, upstream.conn_timeout),
      NULL },
      
      ngx_null_command
};

解析到 upstream_conn_timeout 后,在 ngx_http_mytest_handler 中可如下设置:

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t* r)
{
    /* 首先调用 ngx_http_get_module_ctx 宏来获取上下文结构体 */
    ngx_http_mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    /* 如果之前没有设置过上下文,则返回NULL */
    if (myctx == NULL) {
        /* 必须在当前请求的内存池r->pool中分配上下文结构体,这样请求结束时
         * 结构体占用的内存才会释放 */
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mytest_ctx_t));
        if (myctx == NULL) {
            return NGX_ERROR;
        }
        /* 将刚分配的结构体设置到当前请求的上下文中 */
        ngx_http_set_ctx(r, myctx, ngx_http_mytest_module);
    }

    /* 将解析自配置文件中的upstream的限制参数结构体赋给conf */
    ngx_http_mytest_conf_t *mycf = (ngx_http_mytest_conf_t *)
            ngx_http_get_module_loc_conf(r, ngx_http_mytest_module);
    r->upstream->conf = &mycf->upstream;
    
    ...
}

9.1.4 设置需要访问的第三方服务器地址

ngx_http_upstream_t 结构体中的 resolved 成员可以直接设置上游服务器的地址。

typedef struct {
    ngx_str_t                        host;
    in_port_t                        port;
    ngx_uint_t                       no_port; /* unsigned no_port:1 */

    /* 地址个数 */
    ngx_uint_t                       naddrs;
    ngx_resolver_addr_t             *addrs;

    /* 上游服务器的地址 */
    struct sockaddr                 *sockaddr;
    socklen_t                        socklen;
    ngx_str_t                        name;

    ngx_resolver_ctx_t              *ctx;
} ngx_http_upstream_resolved_t;

9.1.5 设置回调方法

三个必须实现的回调方法为 create_request、process_header、finalize_request。

9.1.6 使用 upstream 的示例

该示例 nginx.conf 中的相关配置:

location /test {
    mytest;
}

客户端浏览器则输入: http://xxx:xxx/test?lumia

upstream 的完整示例如下:


#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <ngx_http_upstream.h>

typedef struct {
    /* 正常每个HTTP请求都会有一个独立的ngx_http_upstrem_conf_t结构体
     * 这里为了简便,所有的请求都共享同一个ngx_http_upstream_conf_t
     */
    ngx_http_upstream_conf_t upstream;
}ngx_http_mytest_conf_t;

typedef struct {
    ngx_str_t               backendServer;
        
    /* 保存接收的响应行 */
    ngx_http_status_t       status;
}ngx_http_mytest_ctx_t;

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

static void *ngx_http_mytest_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);

static ngx_int_t mytest_upstream_create_request(ngx_http_request_t *r);
static ngx_int_t mytest_process_status_line(ngx_http_request_t *r);
static ngx_int_t mytest_upstream_process_header(ngx_http_request_t *r);
static void mytest_upstream_finalize_request(ngx_http_request_t *r, ngx_int_t rc);


static ngx_command_t  ngx_http_mytest_commands[] = {

    { ngx_string("mytest"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|
      NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS,
      ngx_http_mytest,
      NGX_HTTP_LOC_CONF_OFFSET, 
      0, 
      NULL },

    { ngx_string("upstream_conn_timeout"), 
      NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_msec_slot, 
      NGX_HTTP_LOC_CONF_OFFSET,
      /* 给出 conn_timeout   成员在ngx_http_mytest_conf_t结构体中的偏移量*/
      offsetof(ngx_http_mytest_conf_t, upstream.connect_timeout),
      NULL },
      
      ngx_null_command
};

static ngx_http_module_t ngx_http_mytest_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_mytest_create_loc_conf,       /* create location configuration */
    ngx_http_mytest_merge_loc_conf         /* merge location configuration */
};

ngx_module_t ngx_http_mytest_module = {
    NGX_MODULE_V1,
    &ngx_http_mytest_module_ctx,
    ngx_http_mytest_commands,
    NGX_HTTP_MODULE,
    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
};

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t* r)
{
    /* 首先调用 ngx_http_get_module_ctx 宏来获取上下文结构体 */
    ngx_http_mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    /* 如果之前没有设置过上下文,则返回NULL */
    if (myctx == NULL) {
        /* 必须在当前请求的内存池r->pool中分配上下文结构体,这样请求结束时
         * 结构体占用的内存才会释放 */
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mytest_ctx_t));
        if (myctx == NULL) {
            return NGX_ERROR;
        }
        /* 将刚分配的结构体设置到当前请求的上下文中 */
        ngx_http_set_ctx(r, myctx, ngx_http_mytest_module);
    }

    /* 将新建的上下文与请求关联起来 */
    if (ngx_http_upstream_create(r) != NGX_OK) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "ngx_http_upstream_create() failed");
        return NGX_ERROR;
    }

    /* 得到配置结构体ngx_http_mytest_conf_t */
    ngx_http_mytest_conf_t *mycf = (ngx_http_mytest_conf_t *)
            ngx_http_get_module_loc_conf(r, ngx_http_mytest_module);
    ngx_http_upstream_t *u = r->upstream;
    /* 用配置文件中的结构体来赋给r->upstream-conf成员 */    
    u->conf = &mycf->upstream;
    /* 决定转发包体时使用的缓冲区 */
    u->buffering = mycf->upstream.buffering;

    u->resolved = (ngx_http_upstream_resolved_t *)ngx_pcalloc(
                    r->pool, sizeof(ngx_http_upstream_resolved_t));
    if (u->resolved == NULL) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 
                      "ngx_pcalloc resolved error. %s.", strerror(errno));
        return NGX_ERROR;
    }

    static struct sockaddr_in backendSockAddr;
    struct hostent *pHost = gethostbyname((char *) "www.google.com");
    if (pHost == NULL) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 
                      "gethostbyname fail. %s.", strerror(errno));
        return NGX_ERROR;
    }

    /* 访问上游服务器的80端口 */
    backendSockAddr.sin_family = AF_INET;
    backendSockAddr.sin_port = htons((in_port_t) 80);
    char *pDmsIP = inet_ntoa(*(struct in_addr*) (pHost->h_addr_list[0]));
    backendSockAddr.sin_addr.s_addr = inet_addr(pDmsIP);
    myctx->backendServer.data = (u_char*)pDmsIP;
    myctx->backendServer.len = strlen(pDmsIP);

    /* 将地址设置到resolved成员中 */
    u->resolved->sockaddr = (struct sockaddr *)&backendSockAddr;
    u->resolved->socklen = sizeof(struct sockaddr_in);
    u->resolved->naddrs = 1;

    /* 设置3个必须实现的回调方法 */
    u->create_request = mytest_upstream_create_request;
    u->process_header = mytest_process_status_line;
    u->finalize_request = mytest_upstream_finalize_request;

    /* 这里必须将count成员加1 */
    r->main->count++;
    /* 启动upstream */
    ngx_http_upstream_init(r);

    /* 必须返回NGX_DONE */
    return NGX_DONE;
}

static char *ngx_http_mytest(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);

    /* HTTP 框架在处理用户请求进行到 NGX_HTTP_CONTENT_PAHSE 阶段时,
     * 如果请求的域名、URI 与 mytest 配置项所在配置块相匹配,则会调用
     * ngx_http_mytest_handler 处理该请求.
     */
    clcf->handler = ngx_http_mytest_handler;

    return NGX_CONF_OK;
}


static void *ngx_http_mytest_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_mytest_conf_t *mycf;

    mycf = (ngx_http_mytest_conf_t *)ngx_pcalloc(cf->pool, sizeof(ngx_http_mytest_conf_t));
    if (mycf == NULL) {
        return NULL;
    }

    /* 以下简单的硬编码ngx_http_upstream_conf_t结构中的各成员,
     * 如超时时间,都设为1分钟 */
    mycf->upstream.connect_timeout = 60000;
    mycf->upstream.send_timeout = 60000;
    mycf->upstream.read_timeout = 60000;
    mycf->upstream.store_access = 0600;
    /*
     * 实际上,buffering 已经决定了将以固定大小的内存作为缓冲区
     * 来转发上游的响应包体,这块固定缓冲区的大小就是buffer_size.
     * 如果buffering为1,就会使用更多的内存缓冲来不及发往下游的
     * 响应
     */
    mycf->upstream.buffering = 0;
    mycf->upstream.bufs.num = 8;
    mycf->upstream.bufs.size = ngx_pagesize;
    mycf->upstream.buffer_size = ngx_pagesize;
    mycf->upstream.busy_buffers_size = 2 * ngx_pagesize;
    mycf->upstream.temp_file_write_size = 2 * ngx_pagesize;
    mycf->upstream.max_temp_file_size = 1024 * 1024 * 1024;

    /*
     * upstream 模块要求hide_headers成员必须初始化(upstream在解析完上游
     * 服务器返回的包头时,会调用ngx_http_upstream_proces_header方法按照
     * hide_headers成员将本应转发给下游的一些HTTP头部隐藏),这里将它赋值为
     * NGX_CONF_UNSET_PTR,是为了在merge合并配置项方法中使用upstream模块
     * 提供的ngx_http_upstream_hide_headers_hash方法初始化Hide_headers
     */
    mycf->upstream.hide_headers = NGX_CONF_UNSET_PTR;
    mycf->upstream.pass_headers = NGX_CONF_UNSET_PTR;

    return mycf;
}

static char *ngx_http_mytest_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_mytest_conf_t *prev = (ngx_http_mytest_conf_t *)parent;
    ngx_http_mytest_conf_t *conf = (ngx_http_mytest_conf_t *)child;

    ngx_hash_init_t hash;
    hash.max_size = 100;
    hash.bucket_size = 1024;
    hash.name = "proxy_headers_hash";
    extern ngx_str_t  ngx_http_proxy_hide_headers[];
    if (ngx_http_upstream_hide_headers_hash(cf, &conf->upstream, 
            &prev->upstream, ngx_http_proxy_hide_headers, &hash)
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}

/* 创建用于发送给上游服务器的HTTP请求 */
static ngx_int_t mytest_upstream_create_request(ngx_http_request_t *r)
{
    /* 发往google上游服务器的请求就是模仿正常的搜索请求,以
     * /search?q=...的URL来发起搜索请求 */
    static ngx_str_t backendQueryLine = 
            ngx_string("GET /search?q=%V HTTP/1.1\r\n"
                       "Host: www.google.com\r\n"
                       "Connection: close\r\n\r\n");
    ngx_int_t queryLineLen = backendQueryLine.len + r->args.len - 2;
    /*
     * 必须在内存池中申请内存,好处如下:
     * 1. 在网络不好的情况下,向上游服务器发送请求时,可能需要epoll多次调度
     *    send才能发送完成,这时必须保证这段内存不被释放;
     * 2. 在请求结束时,这段内存会被自动释放,降低内存泄露的可能
     */
    ngx_buf_t *b = ngx_create_temp_buf(r->pool, queryLineLen);
    if (b == NULL) {
        return NGX_ERROR;
    }
    /* b->last 指向请求的末尾 */
    b->last = b->pos + queryLineLen;

    /* 访问的URL是"/test?lumia",则args即为"lumia" */
    ngx_snprintf(b->pos, queryLineLen, 
                 (char *)backendQueryLine.data, &r->args);
    /* r->upstream->request_bufs是一个ngx_chain_t结构,包含着
     * 要发送给上游服务器的请求 */
    r->upstream->request_bufs = ngx_alloc_chain_link(r->pool);
    if (r->upstream->request_bufs == NULL) {
        return NGX_ERROR;
    }

    /* request_bufs在这里只包含一个ngx_buf_t缓冲区 */
    r->upstream->request_bufs->buf = b;
    r->upstream->request_bufs->next = NULL;

    r->upstream->request_sent = 0;
    r->upstream->header_sent = 0;
    /* header_hash不可以为0 */
    r->header_hash = 1;

    return NGX_OK;
}

/* 解析响应行 */
static ngx_int_t mytest_process_status_line(ngx_http_request_t *r)
{
    size_t                len;
    ngx_int_t             rc;
    ngx_http_upstream_t  *u;

    /* 上下文中才会保存多次解析HTTP响应行的状态,因此先取出请求的上下文 */
    ngx_http_mytest_ctx_t *ctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if (ctx == NULL) {
        return NGX_ERROR;
    }

    u = r->upstream;

    rc = ngx_http_parse_status_line(r, &u->buffer, &ctx->status);
    /* 返回NGX_AGAIN时,表示还没有解析出完整的HTTP响应行,需要接收更多的
     * 字节流在进行解析 */
    if (rc == NGX_AGAIN) {
        return rc;
    }
    /* 返回NGX_ERROR时,表示没有接收到合法的HTTP响应行 */
    if (rc == NGX_ERROR) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 
                      "upstream sent no valid HTTP/1.0 header");
        r->http_version = NGX_HTTP_VERSION_9;
        u->state->status = NGX_HTTP_OK;
    }

    /* 以下表示解析到完整的HTTP响应行,会将解析到的信息设置到
     * r->upstream->headers-in结构体中。当upstream解析完所有的包
     * 头时,会把headers_in中的成员设置到将要向下游发送的r->headers_out
     * 结构体中,也就是说,现在用户向headers_in中设置的信息,最终
     * 都会发往下游客户端。不直接设置r->headers_out?因为upstream希望
     * 能够按照ngx_http_upstream_conf_t配置结构体中的hide_headers等
     * 成员对发往下游的响应头部做统一处理 */
    if (u->state) {
        u->state->status = ctx->status.code;
    }

    u->headers_in.status_n = ctx->status.code;

    len = ctx->status.end - ctx->status.start;
    u->headers_in.status_line.len = len;

    u->headers_in.status_line.data = ngx_pnalloc(r->pool, len);
    if (u->headers_in.status_line.data == NULL) {
        return NGX_ERROR;
    }

    ngx_memcpy(u->headers_in.status_line.data, ctx->status.start, len);

    /* 下一步将开始解析HTTP头部。设置process_header回调方法为 
     * mytest_upstream_process_header,之后再收到新的字节流将由
     * mytest_upstream_process_header解析 */
    u->process_header = mytest_upstream_process_header;

    /* 如果本次接收到的字节流除了HTTP响应行外,还有多余的字符,那么将由
     * mytest_upstream_process_header解析 */
    return mytest_upstream_process_header(r);    
}

/* process_header 负责解析上游服务器发来的基于TCP的包头 */
static ngx_int_t mytest_upstream_process_header(ngx_http_request_t *r)
{
    ngx_int_t                       rc;
    ngx_table_elt_t                *h;
    ngx_http_upstream_header_t     *hh;
    ngx_http_upstream_main_conf_t  *umcf;

    /* 将upstream模块配置项ngx_http_upstream_main_conf_t取出来,目的是为了
     * 对将要转发给下游客户端的HTTP响应头部进行统一处理。该结构体中存储了
     * 需要进行统一处理的HTTP头部名称和回调方法 */
    umcf = ngx_http_get_module_main_conf(r, ngx_http_upstream_module);

    /* 循环解析所有的HTTP头部 */
    for ( ;; ) {
        /* HTTP框架提供了基础性的ngx_http_parse_header_line方法,用于解析HTTP头部 */
        rc = ngx_http_parse_header_line(r, &r->upstream->buffer, 1);
        /* 返回NGX_OK时,表示解析出一行HTTP头部 */
        if (rc == NGX_OK) {
            /* 向headers_in.headers链表中添加HTTP头部 */
            h = ngx_list_push(&r->upstream->headers_in.headers);
            if (h == NULL) {
                return NGX_ERROR;
            }
            /* 下面构造添加到headers链表中的HTTP头部 */
            h->hash = r->header_hash;

            h->key.len = r->header_name_end - r->header_name_start;
            h->value.len = r->header_end - r->header_start;
            /* 在内存池中分配存放HTTP头部的内存空间 */
            h->key.data = ngx_pnalloc(r->pool, 
                h->key.len + 1 + h->value.len + 1 + h->key.len);
            if (h->key.data == NULL) {
                return NGX_ERROR;
            }

            h->value.data = h->key.data + h->key.len + 1;
            h->lowcase_key = h->key.data + h->key.len + 1 + h->value.len + 1;

            ngx_memcpy(h->key.data, r->header_name_start, h->key.len);
            h->key.data[h->key.len] = '\0';
            ngx_memcpy(h->value.data, r->header_start, h->value.len);
            h->value.data[h->value.len] = '\0';

            if (h->key.len == r->lowcase_index) {
                ngx_memcpy(h->lowcase_key, r->lowcase_header, h->key.len);
            } else {
                ngx_strlow(h->lowcase_key, h->key.data, h->key.len);
            }

            /* upstream模块会对一些HTTP头部做特殊处理 */
            hh = ngx_hash_find(&umcf->headers_in_hash, h->hash, h->lowcase_key, h->key.len);

            if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
                return NGX_ERROR;
            }

            continue;
        }

        /* 返回NGX_HTTP_PARSE_HEADER_DONE时,表示响应中所有的HTTP头部都解析完毕,
         * 接下来再接收到的都将是HTTP包体 */
        if (rc == NGX_HTTP_PARSE_HEADER_DONE) {
            /* 如果之前解析HTTP头部时没有发现server和date头部,
             * 那么下面会根据HTTP协议规范添加这两个头部 */
            if (r->upstream->headers_in.server == NULL) {
                h = ngx_list_push(&r->upstream->headers_in.headers);
                if (h == NULL) {
                    return NGX_ERROR;
                }

                h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash(
                                   ngx_hash('s', 'e'), 'r'), 'v'), 'e'), 'r');

                ngx_str_set(&h->key, "Server");
                ngx_str_null(&h->value);
                h->lowcase_key = (u_char*) "server";
            }

            if (r->upstream->headers_in.date == NULL) {
                h = ngx_list_push(&r->upstream->headers_in.headers);
                if (h == NULL) {
                    return NGX_ERROR;
                }

                h->hash = ngx_hash(ngx_hash(ngx_hash('d', 'a'), 't'), 'e');

                ngx_str_set(&h->key, "Date");
                ngx_str_null(&h->value);
                h->lowcase_key = (u_char*) "date";
            }

            return NGX_OK;
        }

        /* 如果返回NGX_AGAIN,则表示状态机还没有解析到完整的HTTP头部,此时
         * 要求upstream模块继续接收新的字节流,然后交由process_header回调
         * 方法解析 */
        if (rc == NGX_AGAIN) {
            return NGX_AGAIN;
        }

        /* 其他返回值都是非法 */
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 
                      "upstream sent invalid header");

        return NGX_HTTP_UPSTREAM_INVALID_HEADER;
    }
}

static void mytest_upstream_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
    ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "mytest_upstream_finalize_request");
}

9.2. subrequest 的使用方式

subrequest 是由 HTTP 框架提供的一种分解复杂请求的设计模式,它可以把原始请求分解为许多子请求,使得诸多请求协同完成一个用户请求,并且每个请求只关注于一个功能。

subrequest 与访问第三方服务及 upstream 机制关系:

  • 只要不是完全将上游服务器的响应包体转发到下游客户端,基本上都会使用 subrequest 创建子请求,并由子请求使用 upstream 机制访问上游服务器,然后由父请求根据上游响应重新构造返回给下游客户端的响应。
  • HTTP 请求的 ngx_http_request_t 结构体中有一个标志位 subrequest_in_memory,它决定 upstream 对待上游响应包体的行为。但是,从名字上可以看出,它是与 subrequest 有关的,实际上,在创建子请求的方法中就可以设置 subrequest_in_memory。

subrequest 设计的基础是生成一个子请求的代价要非常小,消耗的内存也要很少,并且不会一直占用进程资源。

使用 subrequest 的步骤:

  1. 在 nginx.conf 文件中配置好子请求的处理方式
  2. 启动 subrequest 子请求
  3. 实现子请求执行结束时的回调方法
  4. 实现父请求被激活时的回调方法

9.2.1 配置子请求的处理方式

子请求的处理过程与普通请求完全相同,需要在 nginx.conf 中配置相应的模块处理。子请求与普通请求的不同之处在于,子请求是由父请求生成的,不是接收客户端发来的网络包再由HTTP框架解析出的。

假设生成子请求是以 URI 为 /list 开头的请求,使用 ngx_http_proxy_module 模块让子请求访问新浪的 hq.sinajs.cn 股票服务器,在 nginx.conf 中可如下设置:

location /list {
    proxy_pass http://hq.sinajs.cn;
    /* 不希望第三方服务发来的HTTP包体做过gzip压缩,因为不想在子请求结束时再对
     * 响应做gzip解压缩操作 */
    proxy_set_header Accept-Encoding "";
}

9.2.2 实现子请求处理完毕时的回调方法

Nginx 在子请求正常结束或异常结束时,都会调用 ngx_http_post_subrequest_pt 回调方法:

typedef ngx_int_t (*ngx_http_post_subrequest_pt) (ngx_http_request_t *r, void *data, ngx_int_t rc);

通过建立 ngx_http_post_subrequest_t 结构体将这个回调方法传递给 subrequest 子请求:

typedef struct {
    ngx_http_post_subrequest_pt handler;
    /* handler 函数中 data 参数就是该 data */
    void *data;
}ngx_http_post_subrequest_t;

ngx_http_post_subrequest_pt 回调方法中的 rc 参数是子请求在结束时的状态,它的取值则是执行 ngx_http_finalize_request 销毁请求时所传递的 rc 参数,相应源码如下

void ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc)
{
    ...
    /* 如果当前请求属于某个原始请求的子请求 */
    if (r != r->main && r->post_subrequest) {
        rc = r->post_subrequest->handler(r, r->post_subrequest->data, rc);
    }
    ...
}

在 ngx_http_post_subrequest_pt 回调方法内必须设置父请求激活后的处理方法,首先找出父请求:

ngx_http_request_t *pr = r->parent;

然后将实现好的 ngx_http_post_subrequest_pt 回调方法赋给父请求的 write_event_handler 指针(因为父请求正处于等待发送响应的阶段):

pr->write_event_handler = mytest_post_handler;

9.2.3 处理父请求被重新激活后的回调方法

mytest_post_handler 是父请求重新激活后的回调方法,如下:

typedef void (*ngx_http_event_handler_pt)(ngx_http_request_t *r);

struct ngx_http_request_s {
    ...
    ngx_http_event_handler_pt write_event_handler;
    ...
};

这个方法负责发送响应包给用户.

9.2.4 启动 subrequest 子请求

在 ngx_http_mytest_handler 处理方法中,可以启动 subrequest 子请求。首先调用 ngx_http_subrequest 方法建立 subreuest 子请求,在 ngx_http_mytest_handler 返回后,HTTP 框架会自动执行子请求。如下为 ngx_http_subrequest 的定义:

ngx_int_t
ngx_http_subrequest(ngx_http_request_t *r,
    ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **psr,
    ngx_http_post_subrequest_t *ps, ngx_uint_t flags);
  • r:当前的请求,即父请求
  • uri:子请求的 URI,它对究竟选用 nginx.conf 配置文件中的哪个模块来处理子请求起决定性作用。
  • args:子请求的 URI 参数,如果没有参数,可以传送 NULL 空指针
  • psr:输出参数,将 ngx_http_subrequest 生成的子请求传出来。一般,先建立一个子请求的空指针 ngx_http_request_t *pst,再把它的地址 &psr 传入到 ngx_http_request 方法中,如果 ngx_http_subrequest 返回成功,psr 就指向建立好的子请求
  • ps:传入 ngx_http_post_subrequest_t 结构体指针,它指出子请求结束时必须回调的处理方法
  • flags:flag 的取值范围:
    • 0:没有特殊需求的情况下都应该填写它;
    • NGX_HTTP_SUBREQUEST_IN_MEMORY:这个宏会将子请求的 subrequest_in_memory 标志位置为 1,这意味着如果子请求使用 upstream 访问上游服务器,那么上游服务器的响应将会在内存中处理;
    • NGX_HTTP_SUBREQUEST_WAITED:这个宏会将子请求的 waited 标志位置为 1,当子请求提前结束时,有个 done 标志位会置为 1
  • 返回值:NGX_OK 表示成功建立子请求;返回 NGX_ERROR 表示建立子请求失败

9.2.5 subrequest 使用示例

当使用浏览器方位 /query?s_sh000001 时(s_sh000001 是新浪服务器上的A股上证指数),Nginx 由 mytest 模块处理,它会生成一个子请求,由反向代理处理这个子请求,访问新浪的 http://hq.sinajs.cn 服务器,这时子请求得到的响应包是上证指数的当天价格交易量等信息,而 mytest 模块会解析这个响应,重新构造发往客户端浏览器的 HTTP 响应。浏览器得到的返回值格式为: stock[上证指数], Today current price: 2373.436, volumn: 770.

配置文件中子请求的设置

若访问新浪服务器的 URL 为 /list=s_sh000001, 则可以这样设置:

location /list {
    /* 决定访问的上游服务器地址是hq.sinajs.cn */
    proxy_pass http://hq.sinajs.cn;
    /* 不希望第三方服务发来的HTTP包体进行过gzip压缩 */
    proxy_set_header Accept-Encoding "";
}

此外,处理以 /query 开头的 URI 用户请求还需选用 mytest 模块:

location /query {
    mytest;
}

请求上下文

这里的上下文仅用于保存子请求回调方法中解析出来的股票数据:

typedef struct {
    ngx_str_t       stock[6];
}ngx_http_mytest_ctx_t;

新浪服务器的返回大致如下:

var hq_str_s_sh000009="上证 380,3356.356,-5.725,-0.17,266505,251997";

子请求结束时的处理方法

如下定义 mytest_subrequest_post_handler 作为子请求结束时的回调方法:

static ngx_int_t mytest_subrequest_post_handler(ngx_http_request_t *r, void *data, ngx_int_t rc) 
{
    /* 当前请求 r 是子请求,它的parent成员指向父请求 */
    ngx_http_request_t *pr = r->parent;
    /* 由于上下文是保存在父请求中的,所有要由pr取上下文。其实参数data就是上下文,初始化subrequest
     * 时就对其进行设置。这里仅是为了说明如何获取到父请求的上下文 */
    ngx_http_mytest_ctx_t * myctx = ngx_http_get_module_ctx(pr, ngx_http_mytest_module);
    
    pr->headers_out.status = r->headers_out.status;
    /* 如果返回NGX_HTTP_OK(即200),则意味着访问新浪服务器成功,接着将开始解析
     * HTTP包体 */
    if (r->headers_out.status == NGX_HTTP_OK) {
        int flag = 0;
        
        /* 在不转发响应时,buffer中会保存上游服务器的响应。特别是在使用反向代理模块访问上游
         * 服务器时,如果它使用upstream机制时没有重定义input_filter方法,upstream机制默认
         * 的input_filter方法会试图把所有的上游响应全部保存到buffer缓冲区中 */
        ngx_buf_t *pRecvBuf = &r->upstream->buffer;
        
        /* 以下开始解析上游服务器的响应,并将解析出的值赋到上下文结构体myctx->stock数组中 */
        for (; pRecvBuf->pos != pRecvBuf->last; pRecvBuf->pos++) {
            if (*pRecvBuf->pos == ',' || *pRecvBuf->pos == '\"') {
                if (flag > 0) {
                    myctx->stock[flag - 1].len = pRecvBuf->pos - myctx->stock[flag - 1].data;
                }
                flag++;
                myctx->stock[flag - 1].data = pRecvBuf->pos + 1;
            }
            
            if (flag > 6) {
                break;
            }
        }
    }
    
    /* 设置接下来父请求的回调方法 */
    pr->write_event_handler = mytest_post_handler;
    
    return NGX_OK;
}

父请求的回调方法

将父请求的回调方法定义为 mytest_post_handler:

static void mytest_post_handler(ngx_http_request_t *r)
{
    /* 如果没有返回200,则直接把错误码发回用户 */
    if (r->headers_out.status != NGX_HTTP_OK) {
        ngx_http_finalize_request(r, r->headers_out.status);
        return;
    }
    
    /* 当前请求是父请求,直接取其上下文 */
    ngx_http_mytest_ctx_t *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    
    /* 定义发给用户的HTTP包体内存,格式为:stock[...],Today current price: ..., volumn:... */
    ngx_str_t output_format = ngx_string("stock[%V],Today current price: %V, volumn: %V");
    
    /* 计算待发送包体的长度 */
    int bodylen = output_format.len + myctx->stock[0].len + 
        myctx->stock[1].len + myctx->stock[4].len - 6;
    r->headers_out.content_length_n = bodylen;
    
    /* 在内存池上分配内存以保存将要发送的包体 */
    ngx_buf_t *b = ngx_create_temp_buf(r->pool, bodylen);
    ngx_snprintf(b->pos, bodylen, (char *)output_format.data, 
                 &myctx->stock[0], &myctx->stock[1], &myctx->stock[4]);
    b->last = b->pos + bodylen;
    b->last_buf = 1;
    
    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;
    /* 设置Content-Type,注意,在汉字编码方面,新浪服务器使用了GBK */
    static ngx_str_t type = ngx_string("text/plain; charset=GBK");
    r->headers_out.content_type = type;
    r->headers_out.status = NGX_HTTP_OK;
    
    r->connection->buffered |= NGX_HTTP_WRITE_BUFFERED;
    ngx_int_t ret = ngx_http_send_header(r);
    ret = ngx_http_output_filter(r, &out);
    
    /* 注意,这里发送完响应后必须手动调用ngx_http_finalize_request结束请求,
     * 因为这时HTTP框架不会再帮忙调用它 */
    ngx_http_finalize_request(r, ret);
} 

启动 subrequest

在处理用户请求的 ngx_http_mytest_handler 方法中,开始创建 subrequest 子请求。

static ngx_int_t 
ngx_http_mytest_handler(ngx_http_request_t *r)
{
    /* 创建HTTP上下文 */
    ngx_http_mytest_ctx *myctx = ngx_http_get_module_ctx(r, ngx_http_mytest_module);
    if (myctx == NULL) {
        myctx = ngx_palloc(r->pool, sizeof(ngx_http_mytest_ctx_t));
        if (myctx == NULL) {
            return NGX_ERROR;
        }
        
        /* 将上下文设置到原始请求r中 */
        ngx_http_set_ctx(r, myctx, ngx_http_mytest_module);
    }
    
    /* ngx_http_post_subrequest_t 结构体会决定子请求的回调方法 */
    ngx_http_post_subrequest_t *pst = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
    if (psr == NULL) {
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    
    /* 设置子请求回调方法为mytest_subrequest_post_handler */
    psr->handler = mytest_subrequest_post_handler;
    
    /* 将data设为myctx上下文,这样回调mytest_subrequest_post_handler时传入的
     * data参数就是myctx */
    psr->data = myctx;
    
    /* 子请求的URI前缀是/list,这是因为访问新浪服务器的请求必须时类似/list=s_sh000001的
     * URI,这与在nginx.conf中配置的子请求location的URI是一致的 */
    ngx_str_t sub_prefix = ngx_string("/list=");
    ngx_str_t sub_location;
    sub_location.len = sub_prefix.len + r->args.len;
    sub_location.data = ngx_palloc(r->pool, sub_location.len);
    ngx_snprintf(sub_location.data, sub_location.len, 
                 "%V%V", &sub_prefix, &r->args);
    
    /* sr就是子请求 */
    ngx_http_request_t *sr;
    /*
     * 调用ngx_http_subrequest创建子请求,它只会返回NGX_OK或者NGX_ERROR。返回
     * NGX_OK时,sr已经是合法的子请求。注意,这里的NGX_HTTP_SUBREQUEST_IN_MEMORY
     * 参数将告诉upstream模块把上游服务器的响应全部保存在子请求的sr->upstream->buffer
     * 内存缓冲区中 
     */
    ngx_int_t rc = ngx_http_subrequest(r, &sub_location, NULL, &r, 
                        psr, NGX_HTTP_SUBREQUEST_IN_MEMORY);
    if (rc != NGX_OK) {
        return NGX_ERROR;
    }
    
    /* 必须返回NGX_DONE */
    return NGX_DONE;
}
posted @ 2018-06-26 21:39  季末的天堂  阅读(704)  评论(0编辑  收藏  举报