nginx學習筆記(2):開發一個簡單的HTTP模組

li27z發表於2016-09-12

準備工作

nginx模組需要用C(或者C++)語言來編碼實現,每個模組都要有自己的名字。我們這裡把編寫的HTTP模組命名為ngx_http_mytest_module,原始碼檔案命名為ngx_http_mytest_module.c。

定義自己的HTTP模組

定義HTTP模組的方式很簡單,例如:

ngx_module_t ngx_http_mytest_module;

ngx_module_t是一個nginx模組的資料結構,如下所示:


typedef struct ngx_module_s ngx_module_t;
struct ngx_module_s {   
    // 分類的模組計數器
    // nginx模組可以分為四種:core、event、http和mail
    // 每個模組都會各自計數,ctx_index就是每個模組在其所屬類組的計數
    ngx_uint_t ctx_index;

    // 模組計數器,按照每個模組在ngx_modules[]陣列中的宣告順序
    // 從0開始依次給每個模組賦值
    ngx_uint_t index;

    // 保留變數,暫未使用
    ngx_uint_t spare0;
    ngx_uint_t spare1;
    ngx_uint_t spare2;
    ngx_uint_t spare3;

    ngx_uint_t version;  // nginx模組版本

    // 模組的上下文,不同種類的模組有不同的上下文,因此實現了四種結構體
    void *ctx;

    ngx_command_t *commands;  // 處理模組中的配置項

    ngx_uint_t type;  // 模組型別,用於區分core,event,http和mail

    ngx_int_t  (*init_master)(ngx_log_t *log);  // 初始化master時執行
    ngx_int_t  (*init_module)(ngx_cycle_t *cycle);  // 初始化module時執行
    ngx_int_t  (*init_process)(ngx_cycle_t *cycle);  // 初始化process時執行
    ngx_int_t  (*init_thread)(ngx_cycle_t *cycle);   // 初始化thread時執行
    void (*exit_thread)(ngx_cycle_t *cycle);  // 退出thread時執行
    void (*exit_process)(ngx_cycle_t *cycle);  // 退出process時執行
    void (*exit_master)(ngx_cycle_t *cycle);  // 退出master時執行

    // 以下變數是保留欄位,暫未使用
    uintptr_t  spare_hook0;
    uintptr_t  spare_hook1;
    uintptr_t  spare_hook2;
    uintptr_t  spare_hook3;
    uintptr_t  spare_hook4;
    uintptr_t  spare_hook5;
    uintptr_t  spare_hook6;
    uintptr_t  spare_hook7;
};

該結構體在初始化時,需要用到兩個巨集定義:
#define NGX_MODULE_V1    0, 0, 0, 0, 0, 0, 1                // 初始化前七個成員  
#define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0       // 初始化後八個成員  

定義HTTP模組時,最重要的是要設定ctxcommands這兩個成員:

(1)我們首先來定義mytest配置項的處理,並設定在出現mytest配置後的解析方法ngx_http_mytest,如下所示:

static ngx_command_t ngx_http_mytest_commands[] = {
    {
        ngx_string("mytest"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS,
        ngx_http_mytest,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};

static char *ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t *clcf;
    // 找到mytest配置項所屬的配置塊
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http-core_module);
    // ngx_http_mytest_handler方法處理請求
    clcf->handler = ngx_http_mytest_handler;
    return NGX_CONF_OK;
}

(2)對於HTTP型別的模組來說,ngx_module_t中的ctx指標必須指向ngx_http_module_t介面。我們當前只是開發一個簡單的模組,如果沒有什麼工作是必須在HTTP框架初始化時完成的,那就不必實現ngx_http_module_t中的8個回撥方法,可以像下面這樣定義ngx_http_module_t介面:

static ngx_http_module_t ngx_http_mytest_module_ctx = {
    NULL,  /* preconfiguration */
    NULL,  /* postconfiguration */

    NULL,  /* create main configuration */
    NULL,  /* init main configuration */

    NULL,  /* create server configuration */
    NULL,  /* merge server configuration */

    NULL,  /* create location configration */
    NULL,  /* merge location configration */
};

最後,我們來定義mytest模組:

ngx_module_t  ngx_http_mytest_module = {
    NGX_MODULE_V1,
    &ngx_http_mytest_module_ctx,   /* module context */
    ngx_http_mytest_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
};

這樣,mytest模組在編譯時就會被加入到ngx_modules全域性陣列中。

完整的ngx_http_mytest_handler處理方法

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
    // 必須是GET或HEAD方法,否則返回405 not allowed 
    if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD))) {  
        return NGX_HTTP_NOT_ALLOWED;  
    }  

    // 丟棄請求中的包體(如果不想處理就丟棄掉)
    // 它的意義在於:有些客戶端可能會一直試圖傳送包體,而如果HTTP模組
    // 不接收發來的TCP流,有可能造成客戶端傳送超時
    ngx_int_t rc = ngx_http_discard_request_body(r);  
    if (rc != NGX_OK) {  
        return rc;  
    }  

    // 設定返回的Content-Type
    ngx_str_t type = ngx_string("text/plain");  
    // 返回的包體內容
    ngx_str_t response = ngx_string("Hello World");  
    // 設定返回狀態碼
    r->headers_out.status = NGX_HTTP_OK;  
    // 響應包是有包體內容的,需要設定Content-Length長度
    r->headers_out.content_length_n = response.len;  
    // 設定Content-Type
    r->headers_out.content_type = type;  

    // 傳送HTTP頭部
    rc = ngx_http_send_header(r);  
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {  
        return rc;  
    }  

    // 構造ngx_buf_t結構體準備傳送包體
    ngx_buf_t *b;  
    b = ngx_create_temp_buf(r->pool, response.len);  
    if (b == NULL) {  
        return NGX_HTTP_INTERNAL_SERVER_ERROR;  
    }  

    // 將包體內容即Hello World複製到ngx_buf_t所指向的記憶體
    ngx_memcpy(b->pos, response.data, response.len); 
    // 一定要設定好last指標 
    b->last = b->pos + response.len; 
    // 宣告這是最後一塊緩衝區 
    b->last_buf = 1;  

    // 構造傳送時的ngx_chain_t結構體
    ngx_chain_t out;  
    // 賦值ngx_buf_t
    out.buf = b;  
    // 設定next為NULL
    out.next = NULL;

    // 最後一步為傳送包體
    // 傳送結束後HTTP框架會呼叫ngx_http_finalize_request方法結束請求
    return ngx_http_output_filter(r, &out);  
}

將編寫的HTTP模組編譯進nginx

為了讓HTTP模組正常工作,首先需要把它編譯進nginx。

nginx提供的一種簡單的編譯方式是:首先把原始碼檔案全部放到一個目錄下,同時在該目錄中編寫一個檔案用於通知nginx如何編譯本模組,這個檔名必須為config。

config檔案的寫法:

如果只想開發一個HTTP模組,那麼config檔案中需要定義以下三個變數:
(1)ngx_addon_name:僅在configure執行時使用,一般設定為模組名稱;
(2)HTTP_MODULES:儲存所有的HTTP模組名稱,每個模組間由空格相連;
(3)NGX_ADDON_SRCS:用於指定新模組的原始碼,多個待編譯的原始碼之間可以用空格相連。注意,在設定這個變數時可以使用$ngx_addon_dir變數,它等價於configure執行時--add-module=PATH的PATH引數。

對於我們的mytest模組,可以這樣編寫config檔案:

ngx_addon_name=ngx_http_mytest_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c"

這樣,只要在configure指令碼執行時加入引數–add-module=PATH(PATH就是我們的原始碼和config檔案的儲存目錄),就可以在執行正常編譯安裝流程時完成nginx編譯工作。

上述方法是最方便的,但有時我們需要更靈活的方式,比如重新決定各個模組的順序或者在編譯原始碼時加入一些獨特的編譯選項,我們可以在執行configure後,對生成的objs/ngx_modules.c和objs/Makefile檔案直接進行修改。(此種方法需慎用,不正確的修改可能導致nginx運作不正常,此處不詳述)

完整程式碼

config檔案:

ngx_addon_name=ngx_http_mytest_module
HTTP_MODULES="$HTTP_MODULES ngx_http_mytest_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_mytest_module.c"

原始碼 ngx_http_mytest_module.c

#include <ngx_config.h>  
#include <ngx_core.h>  
#include <ngx_http.h>  

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r);  

static char *   
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

static ngx_command_t ngx_http_mytest_commands[] = {  
    {  
        ngx_string("mytest"),         NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS,  
        ngx_http_mytest,  
        NGX_HTTP_LOC_CONF_OFFSET,  
        0,  
        NULL  
    },  
    ngx_null_command  
};  

static ngx_http_module_t ngx_http_mytest_module_ctx = {  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NULL  
};  

ngx_module_t ngx_http_mytest_module = {  
    NGX_MODULE_V1,  
    &ngx_http_mytest_module_ctx,  
    ngx_http_mytest_commands,  
    NGX_HTTP_MODULE,  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NULL,  
    NGX_MODULE_V1_PADDING  
};  

static char *   
ngx_http_mytest(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)  
{  
    ngx_http_core_loc_conf_t *clcf;  

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);  

    clcf->handler = ngx_http_mytest_handler;  

    return NGX_CONF_OK;  
}  

static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)  
{  
    if (!(r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD))) {  
        return NGX_HTTP_NOT_ALLOWED;  
    }  

    ngx_int_t rc = ngx_http_discard_request_body(r);  
    if (rc != NGX_OK) {  
        return rc;  
    }  

    ngx_str_t type = ngx_string("text/plain");  
    ngx_str_t response = ngx_string("Hello World");  
    r->headers_out.status = NGX_HTTP_OK;  
    r->headers_out.content_length_n = response.len;  
    r->headers_out.content_type = type;  

    rc = ngx_http_send_header(r);  
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {  
        return rc;  
    }  

    ngx_buf_t *b;  
    b = ngx_create_temp_buf(r->pool, response.len);  
    if (b == NULL) {  
        return NGX_HTTP_INTERNAL_SERVER_ERROR;  
    }  

    ngx_memcpy(b->pos, response.data, response.len);  
    b->last = b->pos + response.len;  
    b->last_buf = 1;  

    ngx_chain_t out;  
    out.buf = b;  
    out.next = NULL;  

    return ngx_http_output_filter(r, &out);  
}  

這裡寫圖片描述

編譯及測試結果

編譯:

./configure --prefix=/usr/local/nginx(指定安裝部署後的根目錄) --add-module=/home/test/testmodule(新模組存放目錄)  

make  

make install  

這裡寫圖片描述

修改配置檔案:
配置檔案位於/usr/local/nginx/conf/nginx.conf,做如下修改:

location /test {
    mytest;
}

這裡寫圖片描述

測試結果:
這裡寫圖片描述


參考資料:
陶輝.深入理解Nginx 模組開發與架構解析.北京:機械工業出版社,2013

相關文章