1. 程式人生 > >Nginx開發一個簡單的HTTP過濾模組

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_filternot_modified_filter,模組的執行順序是反向的。也就是說最早執行的是not_modified_filter,然後各個模組依次執行。所有第三方的模組只能加入到copy_filterheaders_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-Typetext/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-Typetext/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

可以看到返回的包體添加了字首:

這裡寫圖片描述
(四)參考