typedef struct { /* * 字符串的有效長度 */ size_t len; /* * 有效字符串的起始地址,該字符串一般並不以'\0'結尾. */ u_char *data; } ngx_str_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;
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;
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; };
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
在 configure 腳本執行時加入參數:--add-module=<PATH>
。設計模式
開發一個 HTTP 模塊,則 config 文件中需定義如下 3 個變量:數組
"$HTTP_MODULES ngx_http_mytest_module"
。所以,對於一個第三方 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"
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; };
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;
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; };
自定義的 HTTP 模塊介入 Nginx 的方式:緩存
在這種方式下,mytest 處理請求是固定在 NGX_HTTP_CONTENT_PAHSE 階段開始處理請求。服務器
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; }
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 接口。網絡
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 };
當出現 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);
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 如何起做用。
四個通用的返回碼:
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); }
處理 HTTP 配置項能夠分爲下面 4 個步驟:
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; }
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; }
Nginx 提供了兩種全異步方式來與第三方服務器通訊:upstream 和 subrequest。
upstreasm 能夠保證與第三方服務器交互時(包括三次握手創建 TCP 鏈接、發送請求、接收響應、四次握手關閉 TCP 鏈接等)不會阻塞 Nginx 進程處理其餘請求。
subrequest 是分解複雜請求的一種設計模式,它本質上與訪問第三方服務沒有任何關係,subrequest 訪問第三方服務最終也是基於 upstream 實現的。
若但願把第三方服務的內容幾乎原封不動地返回給用戶時,通常使用 upstream 方式,它能夠很是高效地透傳 HTTP。若是訪問第三方服務只是爲了獲取某些信息,再依據這些信息來構造響應併發送給用戶,這時應該用 subrequest 方式。
HTTP 模塊啓用 upstream 機制流程:
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; };
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; ... }
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;
三個必須實現的回調方法爲 create_request、process_header、finalize_request。
該示例 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"); }
subrequest 是由 HTTP 框架提供的一種分解複雜請求的設計模式,它能夠把原始請求分解爲許多子請求,使得諸多請求協同完成一個用戶請求,而且每一個請求只關注於一個功能。
subrequest 與訪問第三方服務及 upstream 機制關係:
subrequest 設計的基礎是生成一個子請求的代價要很是小,消耗的內存也要不多,而且不會一直佔用進程資源。
使用 subrequest 的步驟:
子請求的處理過程與普通請求徹底相同,須要在 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 ""; }
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;
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; ... };
這個方法負責發送響應包給用戶.
在 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);
當使用瀏覽器方位 /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); }
在處理用戶請求的 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; }