ngx_chain_t 結(jié)構(gòu)非常簡單,是一個(gè)單向鏈表:
typedef struct ngx_chain_s ngx_chain_t;
struct ngx_chain_s {
ngx_buf_t *buf;
ngx_chain_t *next;
};
在過濾模塊中,所有輸出的內(nèi)容都是通過一條單向鏈表所組成。這種單向鏈表的設(shè)計(jì),正好應(yīng)和了 Nginx 流式的輸出模式。每次 Nginx 都是讀到一部分的內(nèi)容,就放到鏈表,然后輸出出去。這種設(shè)計(jì)的好處是簡單,非阻塞,但是相應(yīng)的問題就是跨鏈表的內(nèi)容操作非常麻煩,如果需要跨鏈表,很多時(shí)候都只能緩存鏈表的內(nèi)容。
單鏈表負(fù)載的就是 ngx_buf_t,這個(gè)結(jié)構(gòu)體使用非常廣泛,先讓我們看下該結(jié)構(gòu)體的代碼:
struct ngx_buf_s {
u_char *pos; /* 當(dāng)前buffer真實(shí)內(nèi)容的起始位置 */
u_char *last; /* 當(dāng)前buffer真實(shí)內(nèi)容的結(jié)束位置 */
off_t file_pos; /* 在文件中真實(shí)內(nèi)容的起始位置 */
off_t file_last; /* 在文件中真實(shí)內(nèi)容的結(jié)束位置 */
u_char *start; /* buffer內(nèi)存的開始分配的位置 */
u_char *end; /* buffer內(nèi)存的結(jié)束分配的位置 */
ngx_buf_tag_t tag; /* buffer屬于哪個(gè)模塊的標(biāo)志 */
ngx_file_t *file; /* buffer所引用的文件 */
/* 用來引用替換過后的buffer,以便當(dāng)所有buffer輸出以后,
* 這個(gè)影子buffer可以被釋放。
*/
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;
unsigned recycled:1; /* 內(nèi)存可以被輸出并回收 */
unsigned in_file:1; /* buffer的內(nèi)容在文件中 */
/* 馬上全部輸出buffer的內(nèi)容, gzip模塊里面用得比較多 */
unsigned flush:1;
/* 基本上是一段輸出鏈的最后一個(gè)buffer帶的標(biāo)志,標(biāo)示可以輸出,
* 有些零長度的buffer也可以置該標(biāo)志
*/
unsigned sync:1;
/* 所有請求里面最后一塊buffer,包含子請求 */
unsigned last_buf:1;
/* 當(dāng)前請求輸出鏈的最后一塊buffer */
unsigned last_in_chain:1;
/* shadow鏈里面的最后buffer,可以釋放buffer了 */
unsigned last_shadow:1;
/* 是否是暫存文件 */
unsigned temp_file:1;
/* 統(tǒng)計(jì)用,表示使用次數(shù) */
/* STUB */ int num;
};
一般 buffer 結(jié)構(gòu)體可以表示一塊內(nèi)存,內(nèi)存的起始和結(jié)束地址分別用 start 和 end 表示,pos 和 last 表示實(shí)際的內(nèi)容。如果內(nèi)容已經(jīng)處理過了,pos 的位置就可以往后移動。如果讀取到新的內(nèi)容,last 的位置就會往后移動。所以 buffer 可以在多次調(diào)用過程中使用。如果 last 等于 end,就說明這塊內(nèi)存已經(jīng)用完了。如果 pos 等于 last,說明內(nèi)存已經(jīng)處理完了。下面是一個(gè)簡單的示意圖,說明 buffer 中指針的用法:
響應(yīng)頭過濾函數(shù)主要的用處就是處理 HTTP 響應(yīng)的頭,可以根據(jù)實(shí)際情況對于響應(yīng)頭進(jìn)行修改或者添加刪除。響應(yīng)頭過濾函數(shù)先于響應(yīng)體過濾函數(shù),而且只調(diào)用一次,所以一般可作過濾模塊的初始化工作。
響應(yīng)頭過濾函數(shù)的入口只有一個(gè):
ngx_int_t
ngx_http_send_header(ngx_http_request_t *r)
{
...
return ngx_http_top_header_filter(r);
}
該函數(shù)向客戶端發(fā)送回復(fù)的時(shí)候調(diào)用,然后按前一節(jié)所述的執(zhí)行順序。該函數(shù)的返回值一般是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分別表示處理成功,失敗和未完成。
你可以把 HTTP 響應(yīng)頭的存儲方式想象成一個(gè) hash 表,在 Nginx 內(nèi)部可以很方便地查找和修改各個(gè)響應(yīng)頭部,ngx_http_header_filter_module 過濾模塊把所有的 HTTP 頭組合成一個(gè)完整的 buffer,最終 ngx_http_write_filter_module 過濾模塊把 buffer 輸出。
按照前一節(jié)過濾模塊的順序,依次講解如下:
filter module | description |
---|---|
ngx_http_not_modified_filter_module | 默認(rèn)打開,如果請求的 if-modified-since 等于回復(fù)的 last-modified 間值,說明回復(fù)沒有變化,清空所有回復(fù)的內(nèi)容,返回 304。 |
ngx_http_range_body_filter_module | 默認(rèn)打開,只是響應(yīng)體過濾函數(shù),支持 range 功能,如果請求包含range請求,那就只發(fā)送range請求的一段內(nèi)容。 |
ngx_http_copy_filter_module | 始終打開,只是響應(yīng)體過濾函數(shù), 主要工作是把文件中內(nèi)容讀到內(nèi)存中,以便進(jìn)行處理。 |
ngx_http_headers_filter_module | 始終打開,可以設(shè)置 expire 和 Cache-control 頭,可以添加任意名稱的頭 |
ngx_http_userid_filter_module | 默認(rèn)關(guān)閉,可以添加統(tǒng)計(jì)用的識別用戶的 cookie。 |
ngx_http_charset_filter_module | 默認(rèn)關(guān)閉,可以添加 charset,也可以將內(nèi)容從一種字符集轉(zhuǎn)換到另外一種字符集,不支持多字節(jié)字符集。 |
ngx_http_ssi_filter_module | 默認(rèn)關(guān)閉,過濾 SSI 請求,可以發(fā)起子請求,去獲取include進(jìn)來的文件 |
ngx_http_postpone_filter_module | 始終打開,用來將子請求和主請求的輸出鏈合并 |
ngx_http_gzip_filter_module | 默認(rèn)關(guān)閉,支持流式的壓縮內(nèi)容 |
ngx_http_range_header_filter_module | 默認(rèn)打開,只是響應(yīng)頭過濾函數(shù),用來解析range頭,并產(chǎn)生range響應(yīng)的頭。 |
ngx_http_chunked_filter_module | 默認(rèn)打開,對于 HTTP/1.1 和缺少 content-length 的回復(fù)自動打開。 |
ngx_http_header_filter_module | 始終打開,用來將所有 header 組成一個(gè)完整的 HTTP 頭。 |
ngx_http_write_filter_module | 始終打開,將輸出鏈拷貝到 r->out中,然后輸出內(nèi)容。 |
響應(yīng)體過濾函數(shù)是過濾響應(yīng)主體的函數(shù)。ngx_http_top_body_filter 這個(gè)函數(shù)每個(gè)請求可能會被執(zhí)行多次,它的入口函數(shù)是 ngx_http_output_filter,比如:
ngx_int_t
ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_int_t rc;
ngx_connection_t *c;
c = r->connection;
rc = ngx_http_top_body_filter(r, in);
if (rc == NGX_ERROR) {
/* NGX_ERROR may be returned by any filter */
c->error = 1;
}
return rc;
}
ngx_http_output_filter 可以被一般的靜態(tài)處理模塊調(diào)用,也有可能是在 upstream 模塊里面被調(diào)用,對于整個(gè)請求的處理階段來說,他們處于的用處都是一樣的,就是把響應(yīng)內(nèi)容過濾,然后發(fā)給客戶端。
具體模塊的響應(yīng)體過濾函數(shù)的格式類似這樣:
static int
ngx_http_example_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
...
return ngx_http_next_body_filter(r, in);
}
該函數(shù)的返回值一般是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分別表示處理成功,失敗和未完成。
響應(yīng)的主體內(nèi)容就存于單鏈表 in,鏈表一般不會太長,有時(shí) in 參數(shù)可能為 NULL。in中存有buf結(jié)構(gòu)體中,對于靜態(tài)文件,這個(gè)buf大小默認(rèn)是 32K;對于反向代理的應(yīng)用,這個(gè)buf可能是4k或者8k。為了保持內(nèi)存的低消耗,Nginx一般不會分配過大的內(nèi)存,處理的原則是收到一定的數(shù)據(jù),就發(fā)送出去。一個(gè)簡單的例子,可以看看Nginx的chunked_filter模塊,在沒有 content-length 的情況下,chunk 模塊可以流式(stream)的加上長度,方便瀏覽器接收和顯示內(nèi)容。
在響應(yīng)體過濾模塊中,尤其要注意的是 buf 的標(biāo)志位,完整描述可以在“相關(guān)結(jié)構(gòu)體”這個(gè)節(jié)中看到。如果 buf 中包含 last 標(biāo)志,說明是最后一塊 buf,可以直接輸出并結(jié)束請求了。如果有 flush 標(biāo)志,說明這塊 buf 需要馬上輸出,不能緩存。如果整塊 buffer 經(jīng)過處理完以后,沒有數(shù)據(jù)了,你可以把 buffer 的 sync 標(biāo)志置上,表示只是同步的用處。
當(dāng)所有的過濾模塊都處理完畢時(shí),在最后的 write_fitler 模塊中,Nginx 會將 in 輸出鏈拷貝到 r->out 輸出鏈的末尾,然后調(diào)用 sendfile 或者 writev 接口輸出。由于 Nginx 是非阻塞的 socket 接口,寫操作并不一定會成功,可能會有部分?jǐn)?shù)據(jù)還殘存在 r->out。在下次的調(diào)用中,Nginx 會繼續(xù)嘗試發(fā)送,直至成功。
Nginx 過濾模塊一大特色就是可以發(fā)出子請求,也就是在過濾響應(yīng)內(nèi)容的時(shí)候,你可以發(fā)送新的請求,Nginx 會根據(jù)你調(diào)用的先后順序,將多個(gè)回復(fù)的內(nèi)容拼接成正常的響應(yīng)主體。一個(gè)簡單的例子可以參考 addition 模塊。
Nginx 是如何保證父請求和子請求的順序呢?當(dāng) Nginx 發(fā)出子請求時(shí),就會調(diào)用 ngx_http_subrequest 函數(shù),將子請求插入父請求的 r->postponed 鏈表中。子請求會在主請求執(zhí)行完畢時(shí)獲得依次調(diào)用。子請求同樣會有一個(gè)請求所有的生存期和處理過程,也會進(jìn)入過濾模塊流程。
關(guān)鍵點(diǎn)是在 postpone_filter 模塊中,它會拼接主請求和子請求的響應(yīng)內(nèi)容。r->postponed 按次序保存有父請求和子請求,它是一個(gè)鏈表,如果前面一個(gè)請求未完成,那后一個(gè)請求內(nèi)容就不會輸出。當(dāng)前一個(gè)請求完成時(shí)并輸出時(shí),后一個(gè)請求才可輸出,當(dāng)所有的子請求都完成時(shí),所有的響應(yīng)內(nèi)容也就輸出完畢了。
Nginx 過濾模塊涉及到的結(jié)構(gòu)體,主要就是 chain 和 buf,非常簡單。在日常的過濾模塊中,這兩類結(jié)構(gòu)使用非常頻繁,Nginx采用類似 freelist 重復(fù)利用的原則,將使用完畢的 chain 或者 buf 結(jié)構(gòu)體,放置到一個(gè)固定的空閑鏈表里,以待下次使用。
比如,在通用內(nèi)存池結(jié)構(gòu)體中,pool->chain 變量里面就保存著釋放的 chain。而一般的 buf 結(jié)構(gòu)體,沒有模塊間公用的空閑鏈表池,都是保存在各模塊的緩存空閑鏈表池里面。對于 buf 結(jié)構(gòu)體,還有一種 busy 鏈表,表示該鏈表中的 buf 都處于輸出狀態(tài),如果 buf 輸出完畢,這些 buf 就可以釋放并重復(fù)利用了。
功能 | 函數(shù)名 |
---|---|
chain 分配 | ngx_alloc_chain_link |
chain 釋放 | ngx_free_chain |
buf 分配 | ngx_chain_get_free_buf |
buf 釋放 | ngx_chain_update_chains |
由于 Nginx 設(shè)計(jì)流式的輸出結(jié)構(gòu),當(dāng)我們需要對響應(yīng)內(nèi)容作全文過濾的時(shí)候,必須緩存部分的 buf 內(nèi)容。該類過濾模塊往往比較復(fù)雜,比如 sub,ssi,gzip 等模塊。這類模塊的設(shè)計(jì)非常靈活,我簡單講一下設(shè)計(jì)原則:
輸入鏈 in 需要拷貝操作,經(jīng)過緩存的過濾模塊,輸入輸出鏈往往已經(jīng)完全不一樣了,所以需要拷貝,通過 ngx_chain_add_copy 函數(shù)完成。
一般有自己的 free 和 busy 緩存鏈表池,可以提高 buf 分配效率。
如果需要分配大塊內(nèi)容,一般分配固定大小的內(nèi)存卡,并設(shè)置 recycled 標(biāo)志,表示可以重復(fù)利用。
更多建議: