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,不然永遠不會發送成功,且這個請求將一直不會結束.nginx

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 方法:cookie

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;
}
相關文章
相關標籤/搜索