鍍金池/ 教程/ Linux/ 過濾模塊的分析
示例: hello handler 模塊
什么是 Nginx
handler 模塊的掛載
Nginx 特點
handler 模塊簡介
初探 Nginx 架構(gòu)
Nginx 的模塊化體系結(jié)構(gòu)
更多 handler 模塊示例分析
Nginx 基礎概念
upstream 模塊簡介
Nginx 的請求處理
過濾模塊簡介
基本數(shù)據(jù)結(jié)構(gòu)
模塊的基本結(jié)構(gòu)
負載均衡模塊
過濾模塊的分析
core 模塊
handler 模塊的基本結(jié)構(gòu)
Nginx 的配置系統(tǒng)
handler 的編寫步驟
handler 模塊的編譯和使用
event 模塊

過濾模塊的分析

相關結(jié)構(gòu)體

ngx_chain_t 結(jié)構(gòu)非常簡單,是一個單向鏈表:

        typedef struct ngx_chain_s ngx_chain_t;

        struct ngx_chain_s {
            ngx_buf_t    *buf;
            ngx_chain_t  *next;
        };

在過濾模塊中,所有輸出的內(nèi)容都是通過一條單向鏈表所組成。這種單向鏈表的設計,正好應和了 Nginx 流式的輸出模式。每次 Nginx 都是讀到一部分的內(nèi)容,就放到鏈表,然后輸出出去。這種設計的好處是簡單,非阻塞,但是相應的問題就是跨鏈表的內(nèi)容操作非常麻煩,如果需要跨鏈表,很多時候都只能緩存鏈表的內(nèi)容。

單鏈表負載的就是 ngx_buf_t,這個結(jié)構(gòu)體使用非常廣泛,先讓我們看下該結(jié)構(gòu)體的代碼:

        struct ngx_buf_s {
            u_char          *pos;       /* 當前buffer真實內(nèi)容的起始位置 */
            u_char          *last;      /* 當前buffer真實內(nèi)容的結(jié)束位置 */
            off_t            file_pos;  /* 在文件中真實內(nèi)容的起始位置   */
            off_t            file_last; /* 在文件中真實內(nèi)容的結(jié)束位置   */

            u_char          *start;    /* buffer內(nèi)存的開始分配的位置 */
            u_char          *end;      /* buffer內(nèi)存的結(jié)束分配的位置 */
            ngx_buf_tag_t    tag;      /* buffer屬于哪個模塊的標志 */
            ngx_file_t      *file;     /* buffer所引用的文件 */

            /* 用來引用替換過后的buffer,以便當所有buffer輸出以后,
             * 這個影子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;
            /* 基本上是一段輸出鏈的最后一個buffer帶的標志,標示可以輸出,
             * 有些零長度的buffer也可以置該標志
             */
            unsigned         sync:1;
            /* 所有請求里面最后一塊buffer,包含子請求 */
            unsigned         last_buf:1;
            /* 當前請求輸出鏈的最后一塊buffer         */
            unsigned         last_in_chain:1;
            /* shadow鏈里面的最后buffer,可以釋放buffer了 */
            unsigned         last_shadow:1;
            /* 是否是暫存文件 */
            unsigned         temp_file:1;

            /* 統(tǒng)計用,表示使用次數(shù) */
            /* STUB */ int   num;
        };

一般 buffer 結(jié)構(gòu)體可以表示一塊內(nèi)存,內(nèi)存的起始和結(jié)束地址分別用 start 和 end 表示,pos 和 last 表示實際的內(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)處理完了。下面是一個簡單的示意圖,說明 buffer 中指針的用法:

http://wiki.jikexueyuan.com/project/nginx/images/chapter-4-2.png" alt="" />

響應頭過濾函數(shù)

響應頭過濾函數(shù)主要的用處就是處理 HTTP 響應的頭,可以根據(jù)實際情況對于響應頭進行修改或者添加刪除。響應頭過濾函數(shù)先于響應體過濾函數(shù),而且只調(diào)用一次,所以一般可作過濾模塊的初始化工作。

響應頭過濾函數(shù)的入口只有一個:

        ngx_int_t
        ngx_http_send_header(ngx_http_request_t *r)
        {
            ...

            return ngx_http_top_header_filter(r);
        }

該函數(shù)向客戶端發(fā)送回復的時候調(diào)用,然后按前一節(jié)所述的執(zhí)行順序。該函數(shù)的返回值一般是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分別表示處理成功,失敗和未完成。

你可以把 HTTP 響應頭的存儲方式想象成一個 hash 表,在 Nginx 內(nèi)部可以很方便地查找和修改各個響應頭部,ngx_http_header_filter_module 過濾模塊把所有的 HTTP 頭組合成一個完整的 buffer,最終 ngx_http_write_filter_module 過濾模塊把 buffer 輸出。

按照前一節(jié)過濾模塊的順序,依次講解如下:

filter module description
ngx_http_not_modified_filter_module 默認打開,如果請求的 if-modified-since 等于回復的 last-modified 間值,說明回復沒有變化,清空所有回復的內(nèi)容,返回 304。
ngx_http_range_body_filter_module 默認打開,只是響應體過濾函數(shù),支持 range 功能,如果請求包含range請求,那就只發(fā)送range請求的一段內(nèi)容。
ngx_http_copy_filter_module 始終打開,只是響應體過濾函數(shù), 主要工作是把文件中內(nèi)容讀到內(nèi)存中,以便進行處理。
ngx_http_headers_filter_module 始終打開,可以設置 expire 和 Cache-control 頭,可以添加任意名稱的頭
ngx_http_userid_filter_module 默認關閉,可以添加統(tǒng)計用的識別用戶的 cookie。
ngx_http_charset_filter_module 默認關閉,可以添加 charset,也可以將內(nèi)容從一種字符集轉(zhuǎn)換到另外一種字符集,不支持多字節(jié)字符集。
ngx_http_ssi_filter_module 默認關閉,過濾 SSI 請求,可以發(fā)起子請求,去獲取include進來的文件
ngx_http_postpone_filter_module 始終打開,用來將子請求和主請求的輸出鏈合并
ngx_http_gzip_filter_module 默認關閉,支持流式的壓縮內(nèi)容
ngx_http_range_header_filter_module 默認打開,只是響應頭過濾函數(shù),用來解析range頭,并產(chǎn)生range響應的頭。
ngx_http_chunked_filter_module 默認打開,對于 HTTP/1.1 和缺少 content-length 的回復自動打開。
ngx_http_header_filter_module 始終打開,用來將所有 header 組成一個完整的 HTTP 頭。
ngx_http_write_filter_module 始終打開,將輸出鏈拷貝到 r->out中,然后輸出內(nèi)容。

響應體過濾函數(shù)

響應體過濾函數(shù)是過濾響應主體的函數(shù)。ngx_http_top_body_filter 這個函數(shù)每個請求可能會被執(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)用,對于整個請求的處理階段來說,他們處于的用處都是一樣的,就是把響應內(nèi)容過濾,然后發(fā)給客戶端。

具體模塊的響應體過濾函數(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,分別表示處理成功,失敗和未完成。

主要功能介紹

響應的主體內(nèi)容就存于單鏈表 in,鏈表一般不會太長,有時 in 參數(shù)可能為 NULL。in中存有buf結(jié)構(gòu)體中,對于靜態(tài)文件,這個buf大小默認是 32K;對于反向代理的應用,這個buf可能是4k或者8k。為了保持內(nèi)存的低消耗,Nginx一般不會分配過大的內(nèi)存,處理的原則是收到一定的數(shù)據(jù),就發(fā)送出去。一個簡單的例子,可以看看Nginx的chunked_filter模塊,在沒有 content-length 的情況下,chunk 模塊可以流式(stream)的加上長度,方便瀏覽器接收和顯示內(nèi)容。

在響應體過濾模塊中,尤其要注意的是 buf 的標志位,完整描述可以在“相關結(jié)構(gòu)體”這個節(jié)中看到。如果 buf 中包含 last 標志,說明是最后一塊 buf,可以直接輸出并結(jié)束請求了。如果有 flush 標志,說明這塊 buf 需要馬上輸出,不能緩存。如果整塊 buffer 經(jīng)過處理完以后,沒有數(shù)據(jù)了,你可以把 buffer 的 sync 標志置上,表示只是同步的用處。

當所有的過濾模塊都處理完畢時,在最后的 write_fitler 模塊中,Nginx 會將 in 輸出鏈拷貝到 r->out 輸出鏈的末尾,然后調(diào)用 sendfile 或者 writev 接口輸出。由于 Nginx 是非阻塞的 socket 接口,寫操作并不一定會成功,可能會有部分數(shù)據(jù)還殘存在 r->out。在下次的調(diào)用中,Nginx 會繼續(xù)嘗試發(fā)送,直至成功。

發(fā)出子請求

Nginx 過濾模塊一大特色就是可以發(fā)出子請求,也就是在過濾響應內(nèi)容的時候,你可以發(fā)送新的請求,Nginx 會根據(jù)你調(diào)用的先后順序,將多個回復的內(nèi)容拼接成正常的響應主體。一個簡單的例子可以參考 addition 模塊。

Nginx 是如何保證父請求和子請求的順序呢?當 Nginx 發(fā)出子請求時,就會調(diào)用 ngx_http_subrequest 函數(shù),將子請求插入父請求的 r->postponed 鏈表中。子請求會在主請求執(zhí)行完畢時獲得依次調(diào)用。子請求同樣會有一個請求所有的生存期和處理過程,也會進入過濾模塊流程。

關鍵點是在 postpone_filter 模塊中,它會拼接主請求和子請求的響應內(nèi)容。r->postponed 按次序保存有父請求和子請求,它是一個鏈表,如果前面一個請求未完成,那后一個請求內(nèi)容就不會輸出。當前一個請求完成時并輸出時,后一個請求才可輸出,當所有的子請求都完成時,所有的響應內(nèi)容也就輸出完畢了。

一些優(yōu)化措施

Nginx 過濾模塊涉及到的結(jié)構(gòu)體,主要就是 chain 和 buf,非常簡單。在日常的過濾模塊中,這兩類結(jié)構(gòu)使用非常頻繁,Nginx采用類似 freelist 重復利用的原則,將使用完畢的 chain 或者 buf 結(jié)構(gòu)體,放置到一個固定的空閑鏈表里,以待下次使用。

比如,在通用內(nèi)存池結(jié)構(gòu)體中,pool->chain 變量里面就保存著釋放的 chain。而一般的 buf 結(jié)構(gòu)體,沒有模塊間公用的空閑鏈表池,都是保存在各模塊的緩存空閑鏈表池里面。對于 buf 結(jié)構(gòu)體,還有一種 busy 鏈表,表示該鏈表中的 buf 都處于輸出狀態(tài),如果 buf 輸出完畢,這些 buf 就可以釋放并重復利用了。

功能 函數(shù)名
chain 分配 ngx_alloc_chain_link
chain 釋放 ngx_free_chain
buf 分配 ngx_chain_get_free_buf
buf 釋放 ngx_chain_update_chains

過濾內(nèi)容的緩存

由于 Nginx 設計流式的輸出結(jié)構(gòu),當我們需要對響應內(nèi)容作全文過濾的時候,必須緩存部分的 buf 內(nèi)容。該類過濾模塊往往比較復雜,比如 sub,ssi,gzip 等模塊。這類模塊的設計非常靈活,我簡單講一下設計原則:

  1. 輸入鏈 in 需要拷貝操作,經(jīng)過緩存的過濾模塊,輸入輸出鏈往往已經(jīng)完全不一樣了,所以需要拷貝,通過 ngx_chain_add_copy 函數(shù)完成。

  2. 一般有自己的 free 和 busy 緩存鏈表池,可以提高 buf 分配效率。

  3. 如果需要分配大塊內(nèi)容,一般分配固定大小的內(nèi)存卡,并設置 recycled 標志,表示可以重復利用。

  4. 原有的輸入 buf 被替換緩存時,必須將其 buf->pos 設為 buf->last,表明原有的 buf 已經(jīng)被輸出完畢?;蛘咴谛陆⒌?buf,將 buf->shadow 指向舊的 buf,以便輸出完畢時及時釋放舊的 buf。