【Nginx】開發一個HTTP過濾模組

來源:互聯網
上載者:User

標籤:style   blog   http   使用   檔案   os   

與HTTP處理模組不同,HTTP過濾模組的工作是對發送給使用者的HTTP響應做一些加工。伺服器返回的一個響應可以被任意多個HTTP過濾模組以流水線的方式依次處理。HTTP響應分為頭部和包體,ngx_http_send_header和ngx_http_output_filter函數分別負責發送頭部和包體,它們會依次調用各個過濾模組對待發送的響應進行處理。
HTTP過濾模組可以單獨處理響應的頭部或者包體或同時處理二者。處理頭部和包體的方法原型分別如下,它們在HTTP架構模組ngx_http_core_module.h中定義:
// 過濾模組處理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);


上面是兩個函數指標的聲明,HTTP過濾模組就是依賴這種指標串接成一個單鏈表的,每個HTTP過濾模組至少定義一個上述函數指標。入口鏈表如下所示:
// HTTP過濾模組鏈表入口extern ngx_http_output_header_filter_pt  ngx_http_top_header_filter;extern ngx_http_output_body_filter_pt    ngx_http_top_body_filter;


當HTTP模組調用ngx_http_send_header發送頭部時,就從ngx_http_top_header_filter指向的模組開始遍曆所有的HTTP頭部過濾模組並處理;當HTTP模組調用ngx_http_output_filter發送包體時,就從ngx_http_top_body_filter指向的模組開始遍曆所有的HTTP包體過濾模組並處理。函數ngx_http_send_header代碼如下:
ngx_int_tngx_http_send_header(ngx_http_request_t *r){    if (r->header_sent) {        ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0,                      "header already sent");        return NGX_ERROR;    }     if (r->err_status) {        r->headers_out.status = r->err_status;        r->headers_out.status_line.len = 0;    }     // 從頭遍曆HTTP頭部過濾模組    return ngx_http_top_header_filter(r);}


函數ngx_http_top_header_filter代碼如下:
ngx_int_tngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in){    ngx_int_t          rc;    ngx_connection_t  *c;     c = r->connection;     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,                   "http output filter \"%V?%V\"", &r->uri, &r->args);     // 遍曆HTTP包體過濾模組    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;}


在每個HTTP過濾模組中至少存在上述兩種函數指標中的一個,聲明如下:
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過濾模組單鏈表的方法如下:
static ngx_int_tngx_http_addition_filter_init(ngx_conf_t *cf){    // 從頭部加入    ngx_http_next_header_filter = ngx_http_top_header_filter;    ngx_http_top_header_filter = ngx_http_addition_header_filter;     // 從頭部加入    ngx_http_next_body_filter = ngx_http_top_body_filter;    ngx_http_top_body_filter = ngx_http_addition_body_filter;     return NGX_OK;}


當本模組被初始化時,調用上述函數,將自己加入到鏈表頭部。類似上面ngx_http_addition_filter_init這樣的初始化函數在什麼時候被調用呢?答案是依該方法放在ngx_http_module_t結構體的哪個成員而定。一般而言,大多數官方HTTP過濾模組通常放在ngx_http_module_t.postconfiguration函數指標中,讀取完所有配置項後被回調。各個模組初始化的順序是怎麼樣的呢?這由configure命令產生的ngx_modules.c檔案中的ngx_modules數組的排列順序決定。數組中靠前的模組先初始化。由於過濾模組是將自己插入到鏈表頭部,使得ngx_modules數組中過濾模組的排列順序和它們實際執行的順序相反。至於ngx_modules數組中的排列順序,又是由其它指令碼決定的。
下面是開發一個簡單的HTTP過濾模組的過程。首先定義兩個結構體:
typedef struct{    ngx_flag_t  enable;     // 儲存on或者off} ngx_http_myfilter_conf_t; typedef struct{    ngx_int_t  add_prefix;} ngx_http_myfilter_ctx_t;  // HTTP上下文結構體


ngx_http_myfilter_conf_t用於儲存標誌是否開啟過濾功能的配置項,這裡我們使用預設的配置項解析方法對此配置項進行解析。ngx_http_myfilter_ctx_t則用於儲存一個HTTP請求的上下文,因為HTTP包體的處理過程不是一次完成的,也就是說處理包體的函數會被調用多次,所以我們需要一個標誌來記錄當前是否已經由過濾模組進行過處理了。HTTP上下文相當於一張表,表中記錄了該請求當前的處理記錄。這樣,當一個請求被拆分成多次處理時,同一個處理函數就能夠瞭解該請求已經執行到哪裡了,從而接著當前的進度進行處理。
HTTP模組結構ngx_http_module_t定義如下:
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_http_myfilter_init:初始化該過濾模組,也就是將該模組插入到HTTP過濾模組單鏈表中,插入方法已經在上面介紹過了。
  • ngx_http_myfilter_create_conf:給儲存配置項的結構體ngx_http_myfilter_conf_t分配空間並初始化結構體成員
  • ngx_http_myfilter_merge_conf:合并出現在main、srv層級的同名配置項

還有一個非常重要的結構ngx_command_t定義如下:
static ngx_command_t  ngx_http_myfilter_commands[] ={    {        ngx_string("myfilter"),        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG,        ngx_conf_set_flag_slot,     // 內建的解析函數        NGX_HTTP_LOC_CONF_OFFSET,        offsetof(ngx_http_myfilter_conf_t, enable),        NULL    },    ngx_null_command};


我們使用預設的配置項解析函數ngx_conf_set_flag_slot解析配置項myfilter,將解析出的配置項參數存入結構體ngx_http_myfilter_conf_t的enable成員中。配置項myfilter只能是“on”或者“off”。
注意,HTTP過濾模組仍然屬於一個HTTP模組,也就是說:ngx_module_t.type = NGX_HTTP_MODULE
下面是HTTP過濾模組的核心部分,即兩個指標分別指向的函數。首先來看看處理HTTP回應標頭部的函數:
// 處理請求的頭部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)        //該請求的上下文已經存在,這說明該模組已經被調用過1次,直接交由下一個過濾模組處理        return ngx_http_next_header_filter(r);     //擷取儲存配置項的ngx_http_myfilter_conf_t結構體    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);     //構造http上下文結構體ngx_http_myfilter_ctx_t    ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_myfilter_ctx_t));    if (ctx == NULL)        return NGX_ERROR;     //add_prefix為0表示不加首碼    ctx->add_prefix = 0;     //將構造的上下文設定到當前請求中    ngx_http_set_ctx(r, ctx, ngx_http_myfilter_module);     //myfilter過濾模組只處理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;    //1表示需要在http包體前加入首碼         //如果處理模組已經在Content-Length寫入了http包體的長度,由於        //我們加入了前置詞字元串,所以需要把這個字串的長度也加入到        //Content-Length中        if (r->headers_out.content_length_n > 0)            r->headers_out.content_length_n += filter_prefix.len;    }     //交由下一個過濾模組繼續處理    return ngx_http_next_header_filter(r);}


這裡有幾點要注意。當該模組遇到錯誤的響應或模組本身出錯時,不應該退出程式而是應該交由下一個HTTP過濾模組處理,這就是上述代碼中多次調用ngx_http_next_header_filter函數的原因。還有就是函數首先要判斷在設定檔中是否開啟此過濾功能並檢查響應的類型。流程圖如下:

接下來是處理HTTP包體的函數:
//將在包體中添加這個首碼static ngx_str_t filter_prefix = ngx_string("~~~~~~~This is a prefix~~~~~~~\n"); // 處理請求的包體static ngx_int_t ngx_http_myfilter_body_filter(ngx_http_request_t *r, ngx_chain_t *in){    ngx_http_myfilter_ctx_t  *ctx;      // 獲得HTTP上下文    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);    cl->buf = b;    cl->next = in;      //調用下一個模組的http包體處理方法,注意這時傳入的是新產生的cl鏈表    return ngx_http_next_body_filter(r, cl);}


根據這一句cl->next = in可以判斷,我們將filter_prefix包含的字串添加到了HTTP響應包體的前面。以上函數的流程圖如下:

下面是編譯及示範過程。我們在第一個mytest模組的基礎上對發送的資訊添加首碼。為了把兩個模組同時編譯進nginx,我們在config時輸入如下命令:
./configure --add-module="/work/nginx/modules/mytest /work/nginx/modules/myfilter"


然後檢查是否成功包含了兩個模組,這裡我們可以查看儲存所有模組的數組ngx_modules[]:
vim objs/ngx_modules.c


結果如下:

可以看到,HTTP模組mytest和HTTP過濾模組myfilter都被包含進來了。接下來就是make和make install了。安裝成功後,我們還要修改設定檔:
vim /usr/local/nginx/conf/nginx.conf


設定檔修改後如所示:

當用戶端輸入的URI為/nestle時,就會啟動mytest和myfilter兩個模組,運行結果如下所示:

可以看到,HTTP過濾模組成功的在響應包體的內容前面添加了我們預設的字串。
整個HTTP過濾模組的完整代碼如下:
#include <ngx_config.h>#include <ngx_core.h>#include <ngx_http.h> // 配置項結構體typedef struct {ngx_flag_t enable;} ngx_http_myfilter_conf_t;// 上下文結構體typedef struct {ngx_int_t add_prefix;} ngx_http_myfilter_ctx_t;ngx_module_t ngx_http_myfilter_module;// 前向聲明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_str_t filter_prefix = ngx_string("~~~~~~~This is a prefix~~~~~~~\n");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;if (r->headers_out.status != NGX_HTTP_OK)return ngx_http_next_header_filter(r);// 交由下一個過濾模組處理ctx = ngx_http_get_module_ctx(r, ngx_http_myfilter_module);if (ctx)return ngx_http_next_header_filter(r);// 上下文已存在,不再處理conf = ngx_http_get_module_loc_conf(r, ngx_http_myfilter_module);// 擷取配置項結構體if (conf->enable == 0)return ngx_http_next_header_filter(r);// 此過濾模組未開啟ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_myfilter_ctx_t));// 建立上下文結構體if (ctx == NULL)return NGX_ERROR;ctx->add_prefix = 0;// 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;// 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);// 擷取上下文結構體if (ctx == NULL || ctx->add_prefix != 1)return ngx_http_next_body_filter(r, in);// 不添加首碼ctx->add_prefix = 2;// 2表示已添加首碼ngx_buf_t *b = ngx_create_temp_buf(r->pool, filter_prefix.len);b->start = b->pos = filter_prefix.data;b->last = b->pos + filter_prefix.len;// 鏈入待發送包體頭部ngx_chain_t *cl = ngx_alloc_chain_link(r->pool);cl->buf = b;cl->next = in;return ngx_http_next_body_filter(r, cl);// 跳到下一個過濾模組}// 初始化HTTP過濾模組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 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;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_conf_merge_value(conf->enable, prev->enable, 0);// 合并函數return NGX_CONF_OK;}static ngx_command_t ngx_http_myfilter_commands[] = {    {        ngx_string("myfilter"),        NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_FLAG,        ngx_conf_set_flag_slot,        NGX_HTTP_LOC_CONF_OFFSET,        offsetof(ngx_http_myfilter_conf_t, enable),        NULL,    },    ngx_null_command}; // HTTP架構初始化時調用的八個函數static ngx_http_module_t ngx_http_myfilter_module_ctx = {    NULL,    ngx_http_myfilter_init,    NULL,    NULL,    NULL,    NULL,    ngx_http_myfilter_create_conf,    ngx_http_myfilter_merge_conf,}; // 定義一個HTTP模組ngx_module_t ngx_http_myfilter_module = {    NGX_MODULE_V1,  // 0,0,0,0,0,0,1    &ngx_http_myfilter_module_ctx,    ngx_http_myfilter_commands,    NGX_HTTP_MODULE,    NULL,    NULL,    NULL,    NULL,    NULL,    NULL,    NULL,    NGX_MODULE_V1_PADDING,  // 0,0,0,0,0,0,0,0,保留欄位};



參考:《深入理解Nginx》第六章。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.