1. 程式人生 > >【Nginx】開發一個HTTP過濾模塊

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

返回 mil http_core 應該 already eas srv 分配內存 alert

與HTTP處理模塊不同。HTTP過濾模塊的工作是對發送給用戶的HTTP響應做一些加工。

server返回的一個響應能夠被隨意多個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_t
ngx_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_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;
 
    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_t
ngx_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


配置文件改動後例如以下圖所看到的: 技術分享

當client輸入的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》第六章。

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