Nginx開發一個簡單的HTTP過濾模組
開發一個HTTP過濾模組的步驟和相關知識跟開發一個普通的HTTP模組是類似的,只不過HTTP過濾模組的地位、作用與正常的HTTP過濾模組不同,它的工作是對傳送給使用者的HTTP響應包做一些加工。
本文將學習開發一個簡單的HTTP過濾模組,它能夠對Content-Type為text/plain
的包體前加上字首字串prefix。
(一)過濾模組的呼叫順序 |
過濾模組可以疊加,也就是說一個請求會被所有的HTTP過濾模組依次處理。
過濾模組的呼叫是有順序的,它的順序在編譯的時候就決定了。控制編譯的指令碼位於auto/modules
中,當你編譯完Nginx以後,可以在objs目錄下面看到一個ngx_modules.c的檔案。開啟這個檔案,有類似的程式碼:
ngx_module_t *ngx_modules[] = { ... &ngx_http_write_filter_module, &ngx_http_header_filter_module, &ngx_http_chunked_filter_module, &ngx_http_range_header_filter_module, &ngx_http_gzip_filter_module, &ngx_http_postpone_filter_module, &ngx_http_ssi_filter_module, &ngx_http_charset_filter_module, &ngx_http_userid_filter_module, &ngx_http_headers_filter_module, &ngx_http_copy_filter_module, &ngx_http_range_body_filter_module, &ngx_http_not_modified_filter_module, NULL };
從write_filter
到not_modified_filter
,模組的執行順序是反向的。也就是說最早執行的是not_modified_filter
,然後各個模組依次執行。所有第三方的模組只能加入到copy_filter
和headers_filter
模組之間執行。
在編譯Nginx原始碼時,已經定義了一個由所有HTTP過濾模組組成的單鏈表,這個單鏈表是這樣的:
連結串列的每一個元素都是一個C原始碼檔案,這個C原始碼檔案中有兩個指標,分別指向下一個過濾模組(檔案)的過濾頭部和包體的方法(可理解為連結串列中的next指標)
過濾模組單鏈表示意圖:
這兩個指標的宣告如下:
/*過濾模組處理HTTP頭部的函式指標型別定義,它攜帶一個引數:請求*/
typedef ngx_int_t (*ngx_http_output_header_filter_pt)(ngx_http_request_t *r);
/*過濾模組處理HTTP包體的函式指標型別定義,它攜帶兩個引數:請求、要傳送的包體*/
typedef ngx_int_t (*ngx_http_output_body_filter_pt)
(ngx_http_request_t *r, ngx_chain_t *chain);
在我們定義的第三方模組中則有如下宣告:
/*用static修飾只在本檔案生效,因此允許所有的過濾模組都有自己的這兩個指標*/
static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
那麼怎麼將這個原始檔(節點),插入到HTTP過濾模組組成的單鏈表中去呢?
Nginx採用頭插法的辦法,所有的新節點都插入在連結串列的開頭:
//插入到頭部處理方法連結串列的首部
ngx_http_next_header_filter=ngx_http_top_header_filter;
ngx_http_top_header_filter=ngx_http_myfilter_header_filter;
//插入到包體處理方法連結串列的首部
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_myfilter_body_filter;
其中兩個top指標宣告如下:
extern ngx_http_output_header_filter_pt ngx_http_next_header_filter;
extern ngx_http_output_body_filter_pt ngx_http_next_body_filter;
由於是頭插法,這樣就解釋了,越早插入連結串列的過濾模組,就會越晚執行。
(二)開發一個簡單的過濾模組 |
要開發一個簡單的過濾模組,它的功能是對Content-Type
為text/plain
的響應新增一個字首,類似於開發一個HTTP模組,它應該遵循如下步驟:
1.確定原始碼檔名稱,原始碼所在目錄建立
config
指令碼檔案,config
檔案的編寫方式跟HTTP模組開發基本一致,不同的是需要將HTTP_MODULES
改成HTTP_FILTER_MODULES
。2.定義過濾模組。例項化
ngx_module_t
型別模組結構,因為HTTP過濾模組也是HTTP模組,所以其中的type成員也是NGX_HTTP_MODULE
。3.處理感興趣的配置項,通過設定
ngx_module_t
中的ngx_command_t
陣列來處理感興趣的配置項。4.實現初始化方法。初始化方法就是把本模組中處理HTTP頭部的
ngx_http_output_header_filter_pt
方法和處理HTTP包體的ngx_http_output_body_filter_pt
方法插入到過濾模組連結串列的首部。5.實現4.中提到兩個處理頭部和包體的方法。
接下來按照上述步驟依次來實現:
2.1 確定原始碼檔案目錄,編寫config檔案
config 檔案如下
ngx_addon_name=ngx_http_myfilter_module
HTTP_FILTER_MODULES="$HTTP_FILTER_MODULES ngx_http_myfilter_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_myfilter_module.c"
2.2 定義過濾模組,例項化ngx_module_t
/*定義過濾模組,ngx_module_t結構體例項化*/
ngx_module_t ngx_http_myfilter_module =
{
NGX_MODULE_V1, /*Macro*/
&ngx_http_myfilter_module_ctx, /*module context*/
ngx_http_myfilter_commands, /*module directives*/
NGX_HTTP_MODULE, /* module type */
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 /*Macro*/
};
2.3 處理感興趣的配置項
/*處理感興趣的配置項*/
static ngx_command_t ngx_http_myfilter_commands[]=
{
{
ngx_string("add_prefix"), //配置項名稱
NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG,//配置項只能攜帶一個引數並且是on或者off
ngx_conf_set_flag_slot,//使用nginx自帶方法,引數on/off
NGX_HTTP_LOC_CONF_OFFSET,//使用create_loc_conf方法產生的結構體來儲存
//解析出來的配置項引數
offsetof(ngx_http_myfilter_conf_t, enable),//on/off
NULL
},
ngx_null_command //
};
其中定義結構體ngx_http_myfilter_conf_t
來儲存配置項引數:
typedef struct
{
ngx_flag_t enable;
}ngx_http_myfilter_conf_t;
2.4 實現初始化方法
頭插入法將本過濾模組插入到單鏈表的首部:
/*初始化方法*/
static ngx_int_t
ngx_http_myfilter_init(ngx_conf_t*cf)
{
//插入到頭部處理方法連結串列的首部
ngx_http_next_header_filter=ngx_http_top_header_filter;
ngx_http_top_header_filter=ngx_http_myfilter_header_filter;
//插入到包體處理方法連結串列的首部
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_myfilter_body_filter;
}
2.5 實現頭部和包體過濾方法
2.5.1 函式宣告
/*頭部處理方法*/
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r);
/*包體處理方法*/
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
2.5.2 函式實現
(1)頭部處理方法:最終處理效果頭部資訊Content-Length
的值加上prefix的長度。
/*頭部處理方法*/
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r)
{
ngx_http_myfilter_ctx_t *ctx;
ngx_http_myfilter_conf_t *conf;
//如果不是返回成功,這時是不需要理會是否加字首的,
//直接交由下一個過濾模組
//處理響應碼非200的情形
if (r->headers_out.status != NGX_HTTP_OK)
{
return ngx_http_next_header_filter(r);
}
/*獲取http上下文*/
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
if(ctx)
{
//該請求的上下文已經存在,這說明
// ngx_http_myfilter_header_filter已經被呼叫過1次,
//直接交由下一個過濾模組處理
return ngx_http_next_header_filter(r);
}
//獲取儲存配置項引數的結構體
conf = ngx_http_get_module_loc_conf(r, ngx_http_myfilter_module);
//如果enable成員為0,也就是配置檔案中沒有配置add_prefix配置項,
//或者add_prefix配置項的引數值是off,這時直接交由下一個過濾模組處理
if (conf->enable == 0)
{
return ngx_http_next_header_filter(r);
}
//conf->enable==1
//構造http上下文結構體ngx_http_myfilter_ctx_t
ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_myfilter_ctx_t));
if(NULL==ctx)
{
return NGX_ERROR;
}
ctx->add_prefix=0;
ngx_http_set_ctx(r,ctx,ngx_http_myfilter_module);
//只處理Content-Type是"text/plain"型別的http響應
if (r->headers_out.content_type.len >= sizeof("text/plain") - 1
&& ngx_strncasecmp(r->headers_out.content_type.data, (u_char *) "text/plain", sizeof("text/plain") - 1) == 0)
{
ctx->add_prefix=1;
if(r->headers_out.content_length_n > 0)
{
r->headers_out.content_length_n+=filter_prefix.len;
}
}
//交由下一個過濾模組繼續處理
return ngx_http_next_header_filter(r);
}
(2)響應包體處理方法:最終處理效果在包體前面新增字首。
/*包體處理方法*/
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
//如果獲取不到上下文,或者上下文結構體中的add_prefix為0或者2時,
//都不會新增字首,這時直接交給下一個http過濾模組處理
if (ctx == NULL || ctx->add_prefix != 1)
{
return ngx_http_next_body_filter(r, in);
}
//將add_prefix設定為2,這樣即使ngx_http_myfilter_body_filter
//再次回撥時,也不會重複新增字首
ctx->add_prefix = 2;
//從請求的記憶體池中分配記憶體,用於儲存字串字首
ngx_buf_t* b = ngx_create_temp_buf(r->pool, filter_prefix.len);
//將ngx_buf_t中的指標正確地指向filter_prefix字串
b->start = b->pos = filter_prefix.data;
b->last = b->pos + filter_prefix.len;
//從請求的記憶體池中生成ngx_chain_t連結串列,將剛分配的ngx_buf_t設定到
//其buf成員中,並將它新增到原先待發送的http包體前面
ngx_chain_t *cl = ngx_alloc_chain_link(r->pool);
/*note: in表示原來待發送的包體*/
cl->buf = b;
cl->next = in;
//呼叫下一個模組的http包體處理方法,注意這時傳入的是新生成的cl連結串列
return ngx_http_next_body_filter(r, cl);
}
(三)完整程式碼與測試 |
至此,已經完成大部分的工作,我們還需要為這個過濾模組編寫模組上下文,編寫建立和合並配置項引數結構體的函式等。
3.1 完整程式碼
/*ngx_http_myfilter_module.c*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
/*用static修飾只在本檔案生效,因此允許所有的過濾模組都有自己的這兩個指標*/
static ngx_http_output_header_filter_pt ngx_http_next_header_filter;
static ngx_http_output_body_filter_pt ngx_http_next_body_filter;
/*初始化方法,將過濾模組插入到連結串列頭部*/
static ngx_int_t
ngx_http_myfilter_init(ngx_conf_t *cf);
/*頭部處理方法*/
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r);
/*包體處理方法*/
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
typedef struct
{
ngx_flag_t enable;
}ngx_http_myfilter_conf_t;
/*請求上下文*/
typedef struct
{
ngx_int_t add_prefix;
}ngx_http_myfilter_ctx_t;
/*在包體中新增的字首*/
static ngx_str_t filter_prefix=ngx_string("[my filter prefix]");
/*處理感興趣的配置項*/
static ngx_command_t ngx_http_myfilter_commands[]=
{
{
ngx_string("add_prefix"), //配置項名稱
NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG,//配置項只能攜帶一個引數並且是on或者off
ngx_conf_set_flag_slot,//使用nginx自帶方法,引數on/off
NGX_HTTP_LOC_CONF_OFFSET,//使用create_loc_conf方法產生的結構體來儲存
//解析出來的配置項引數
offsetof(ngx_http_myfilter_conf_t, enable),//on/off
NULL
},
ngx_null_command //
};
static void* ngx_http_myfilter_create_conf(ngx_conf_t *cf);
static char*
ngx_http_myfilter_merge_conf(ngx_conf_t *cf,void*parent,void*child);
/*模組上下文*/
static ngx_http_module_t ngx_http_myfilter_module_ctx=
{
NULL, /* preconfiguration方法 */
ngx_http_myfilter_init, /* postconfiguration方法 */
NULL, /*create_main_conf 方法 */
NULL, /* init_main_conf方法 */
NULL, /* create_srv_conf方法 */
NULL, /* merge_srv_conf方法 */
ngx_http_myfilter_create_conf, /* create_loc_conf方法 */
ngx_http_myfilter_merge_conf /*merge_loc_conf方法*/
};
/*定義過濾模組,ngx_module_t結構體例項化*/
ngx_module_t ngx_http_myfilter_module =
{
NGX_MODULE_V1, /*Macro*/
&ngx_http_myfilter_module_ctx, /*module context*/
ngx_http_myfilter_commands, /*module directives*/
NGX_HTTP_MODULE, /* module type */
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 /*Macro*/
};
static void* ngx_http_myfilter_create_conf(ngx_conf_t *cf)
{
ngx_http_myfilter_conf_t *mycf;
//建立儲存配置項的結構體
mycf = (ngx_http_myfilter_conf_t *)ngx_pcalloc(cf->pool, sizeof(ngx_http_myfilter_conf_t));
if (mycf == NULL)
{
return NULL;
}
//ngx_flat_t型別的變數,如果使用預設函式ngx_conf_set_flag_slot
//解析配置項引數,必須初始化為NGX_CONF_UNSET
mycf->enable = NGX_CONF_UNSET;
return mycf;
}
static char*
ngx_http_myfilter_merge_conf(ngx_conf_t *cf,void*parent,void*child)
{
ngx_http_myfilter_conf_t *prev = (ngx_http_myfilter_conf_t *)parent;
ngx_http_myfilter_conf_t *conf = (ngx_http_myfilter_conf_t *)child;
//合併ngx_flat_t型別的配置項enable
ngx_conf_merge_value(conf->enable, prev->enable, 0);
return NGX_CONF_OK;
}
/*初始化方法*/
static ngx_int_t
ngx_http_myfilter_init(ngx_conf_t*cf)
{
//插入到頭部處理方法連結串列的首部
ngx_http_next_header_filter=ngx_http_top_header_filter;
ngx_http_top_header_filter=ngx_http_myfilter_header_filter;
//插入到包體處理方法連結串列的首部
ngx_http_next_body_filter = ngx_http_top_body_filter;
ngx_http_top_body_filter = ngx_http_myfilter_body_filter;
return NGX_OK;
}
/*頭部處理方法*/
static ngx_int_t
ngx_http_myfilter_header_filter(ngx_http_request_t *r)
{
ngx_http_myfilter_ctx_t *ctx;
ngx_http_myfilter_conf_t *conf;
//如果不是返回成功,這時是不需要理會是否加字首的,
//直接交由下一個過濾模組
//處理響應碼非200的情形
if (r->headers_out.status != NGX_HTTP_OK)
{
return ngx_http_next_header_filter(r);
}
/*獲取http上下文*/
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
if(ctx)
{
//該請求的上下文已經存在,這說明
// ngx_http_myfilter_header_filter已經被呼叫過1次,
//直接交由下一個過濾模組處理
return ngx_http_next_header_filter(r);
}
//獲取儲存配置項引數的結構體
conf = ngx_http_get_module_loc_conf(r, ngx_http_myfilter_module);
//如果enable成員為0,也就是配置檔案中沒有配置add_prefix配置項,
//或者add_prefix配置項的引數值是off,這時直接交由下一個過濾模組處理
if (conf->enable == 0)
{
return ngx_http_next_header_filter(r);
}
//conf->enable==1
//構造http上下文結構體ngx_http_myfilter_ctx_t
ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_myfilter_ctx_t));
if(NULL==ctx)
{
return NGX_ERROR;
}
ctx->add_prefix=0;
ngx_http_set_ctx(r,ctx,ngx_http_myfilter_module);
//只處理Content-Type是"text/plain"型別的http響應
if (r->headers_out.content_type.len >= sizeof("text/plain") - 1
&& ngx_strncasecmp(r->headers_out.content_type.data, (u_char *) "text/plain", sizeof("text/plain") - 1) == 0)
{
ctx->add_prefix=1;
if(r->headers_out.content_length_n > 0)
{
r->headers_out.content_length_n+=filter_prefix.len;
}
}
//交由下一個過濾模組繼續處理
return ngx_http_next_header_filter(r);
}
/*包體處理方法*/
static ngx_int_t
ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_http_myfilter_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);
//如果獲取不到上下文,或者上下文結構體中的add_prefix為0或者2時,
//都不會新增字首,這時直接交給下一個http過濾模組處理
if (ctx == NULL || ctx->add_prefix != 1)
{
return ngx_http_next_body_filter(r, in);
}
//將add_prefix設定為2,這樣即使ngx_http_myfilter_body_filter
//再次回撥時,也不會重複新增字首
ctx->add_prefix = 2;
//從請求的記憶體池中分配記憶體,用於儲存字串字首
ngx_buf_t* b = ngx_create_temp_buf(r->pool, filter_prefix.len);
//將ngx_buf_t中的指標正確地指向filter_prefix字串
b->start = b->pos = filter_prefix.data;
b->last = b->pos + filter_prefix.len;
//從請求的記憶體池中生成ngx_chain_t連結串列,將剛分配的ngx_buf_t設定到
//其buf成員中,並將它新增到原先待發送的http包體前面
ngx_chain_t *cl = ngx_alloc_chain_link(r->pool);
/*note: in表示原來待發送的包體*/
cl->buf = b;
cl->next = in;
//呼叫下一個模組的http包體處理方法,注意這時傳入的是新生成的cl連結串列
return ngx_http_next_body_filter(r, cl);
}
3.2 測試
我們的過濾模組只對Content-Type
為text/plain
的響應有效,檢視Nginx的預設配置中的mime.types
檔案,發現
types{
#...
text/plain txt;
#...
}
也即當請求資源為txt時才會呼叫我們過濾模組,如果想要強制將響應的Content-Type
設定為text/plain
呢?
只需要修改nginx.conf檔案如下即可:
#user root;
worker_processes 1;
error_log logs/error.log debug;
events {
worker_connections 1024;
}
http {
# 註釋掉http塊下的配置
# include mime.types;
# default_type application/octet-stream;
keepalive_timeout 65;
server {
listen 1024;
location / {
#在location塊下將預設型別設定為text/plain
default_type text/plain;
root html;
add_prefix on;
index index.htm index.html;
}
}
}
將自定義的過濾模組編譯進Nginx:
./configure --add-module=/home/zhangxiao/nginx/nginx-1.0.15/src/myHttpFilterModule/
make;sudo make install
重啟nginx,用curl工具進行測試:
curl -v localhost:1024
可以看到返回的包體添加了字首:
(四)參考 |