Nginx 不僅是一個網頁伺服器,它更是一個可擴充套件的平台,讓你能夠透過自定義模組擴充其功能。在我多年的伺服器開發經驗中,發現當標準功能無法滿足特定需求時,開發自定義模組能夠讓你掌握更多對請求處理的控制權,實作客製化的邏輯、最佳化路由、過濾內容,甚至建立非標準的驗證機制。

為什麼要開發 Nginx 模組?

在深入技術細節前,我想分享為何自定義模組對某些專案至關重要。在一次為金融科技公司開發高安全性 API 閘道時,我們需要一種特殊的請求驗證方式,標準的 Nginx 設定無法實作。透過開發自定義模組,我們不僅解決了這個問題,還提升了整體效能,因為邏輯直接在 Nginx 層執行,無需轉發到應用伺服器。

從最簡單的模組開始

開發 Nginx 模組的最佳方式是從簡單開始。我總是建議先建立一個基本的「Hello World」模組,這有助於理解 Nginx 的模組架構和記憶體管理方式。以下是一個簡單模組的實作,它會回應「Hello World from custom Nginx module!」:

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

// 請求處理函式:回傳歡迎訊息
static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r) {
    ngx_int_t rc;
    ngx_buf_t *b;
    ngx_chain_t out;

    // 設定內容型別
    r->headers_out.content_type.len = sizeof("text/plain") - 1;
    r->headers_out.content_type.data = (u_char *)"text/plain";

    // 從記憶體池分配緩衝區
    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (b == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    // 準備回應文字
    u_char *response = (u_char *)"Hello World from custom Nginx module!";
    size_t response_len = ngx_strlen(response);

    // 填充緩衝區
    b->pos = response;
    b->last = response + response_len;
    b->memory = 1;    // 資料位於記憶體中(非磁碟)
    b->last_buf = 1;  // 這是鏈中的最後一個緩衝區

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

    // 設定回應標頭
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = response_len;

    rc = ngx_http_send_header(r);
    if (rc != NGX_OK) {
        return rc;
    }
    
    return ngx_http_output_filter(r, &out);
}

// 透過 "hello" 指令設定處理程式的函式
static char *ngx_http_hello(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {
    // 取得當前位置的設定並指派我們的處理程式
    ngx_http_core_loc_conf_t *clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_hello_handler;
    
    return NGX_CONF_OK;
}

static ngx_command_t ngx_http_hello_commands[] = {
    { ngx_string("hello"),
      NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
      ngx_http_hello,
      0,
      0,
      NULL },
      
    ngx_null_command
};

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

ngx_module_t ngx_http_hello_module = {
    NGX_MODULE_V1,
    &ngx_http_hello_module_ctx,
    ngx_http_hello_commands,
    NGX_HTTP_MODULE,
    NULL, NULL, NULL, NULL, NULL, NULL, NULL,
    NGX_MODULE_V1_PADDING
};

理解關鍵元素

讓我們分析這個模組的關鍵部分:

  1. 處理函式 (ngx_http_hello_handler):負責生成回應內容,設定 HTTP 標頭,並將資料傳送給使用者端。

  2. 設定處理函式 (ngx_http_hello):當在 Nginx 設定中使用 hello 指令時,此函式會被呼叫,它將我們的處理函式與特定位置關聯。

  3. 指令定義 (ngx_http_hello_commands):定義模組支援的指令,包括名稱、使用位置和引數要求。

  4. 模組上下文 (ngx_http_hello_module_ctx):定義模組的各種回呼函式。

  5. 模組定義 (ngx_http_hello_module):模組的主要結構,包含版本資訊、上下文和指令列表。

在這個例子中,Nginx 的記憶體管理非常謹慎。我們不使用標準的 malloc() 函式,而是從請求的記憶體池中分配資源,這確保了資源會在請求結束時自動釋放,防止記憶體洩漏。

進階:支援自定義設定的模組

基本模組運作順利後,下一步是讓它更具彈性。假設我們希望能透過 Nginx 設定設定歡迎訊息,而不是在每次修改訊息時都重新編譯模組。

以下是一個支援自定義設定的進階模組範例:

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

typedef struct {
    ngx_str_t greeting;
} ngx_http_custom_conf_t;

// 為位置建立設定
static void *ngx_http_custom_create_loc_conf(ngx_conf_t *cf) {
    ngx_http_custom_conf_t *conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_custom_conf_t));
    if (conf == NULL) {
        return NGX_CONF_ERROR;
    }
    
    // 預設情況下未設定歡迎訊息
    conf->greeting.len = 0;
    conf->greeting.data = NULL;
    
    return conf;
}

// 使用自定義訊息的請求處理函式
static ngx_int_t ngx_http_custom_handler(ngx_http_request_t *r) {
    ngx_int_t rc;
    ngx_buf_t *b;
    ngx_chain_t out;
    ngx_http_custom_conf_t *conf;

    conf = ngx_http_get_module_loc_conf(r, ngx_http_custom_module);

    if (conf->greeting.len == 0) {
        // 如果未設定訊息,使用預設訊息
        conf->greeting.data = (u_char *)"Default custom greeting!";
        conf->greeting.len = ngx_strlen(conf->greeting.data);
    }

    r->headers_out.content_type.len = sizeof("text/plain") - 1;
    r->headers_out.content_type.data = (u_char *)"text/plain";

    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (b == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    b->pos = conf->greeting.data;
    b->last = conf->greeting.data + conf->greeting.len;
    b->memory = 1;
    b->last_buf = 1;

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

    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = conf->greeting.len;

    rc = ngx_http_send_header(r);
    if (rc != NGX_OK) {
        return rc;
    }
    
    return ngx_http_output_filter(r, &out);
}

// 設定 "hello_msg" 指令的函式
static char *ngx_http_custom_set_greeting(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {
    ngx_http_custom_conf_t *custom_conf = conf;
    ngx_str_t *value = cf->args->elts;
    
    custom_conf->greeting = value[1];
    
    return NGX_CONF_OK;
}

static ngx_command_t ngx_http_custom_commands[] = {
    { ngx_string("hello"),
      NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
      ngx_http_custom_handler,
      0,
      0,
      NULL },
      
    { ngx_string("hello_msg"),
      NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1,
      ngx_http_custom_set_greeting,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_custom_conf_t, greeting),
      NULL },
      
    ngx_null_command
};

static ngx_http_module_t ngx_http_custom_module_ctx = {
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    ngx_http_custom_create_loc_conf,
    NULL
};

ngx_module_t ngx_http_custom_module = {
    NGX_MODULE_V1,
    &ngx_http_custom_module_ctx,
    ngx_http_custom_commands,
    NGX_HTTP_MODULE,
    NULL, NULL, NULL, NULL, NULL, NULL, NULL,
    NGX_MODULE_V1_PADDING
};

進階模組的關鍵元素

這個進階模組引入了幾個新概念:

  1. 設定結構體:我們定義了 ngx_http_custom_conf_t 來儲存模組的設定資料。

  2. 設定建立函式ngx_http_custom_create_loc_conf 負責初始化設定結構體。

  3. 設定設定函式ngx_http_custom_set_greeting 處理從設定讀取的值並更新設定結構體。

  4. 新增指令:我們增加了 hello_msg 指令,它接受一個引數作為自定義歡迎訊息。

在 Nginx 設定中使用自定義模組

模組編譯完成後,可以在 Nginx 設定中這樣使用:

http {
    server {
        listen 80;
        server_name example.com;

        location /hello {
            hello;
        }

        location /custom-hello {
            hello;
            hello_msg "Welcome to my custom Nginx module!";
        }
    }
}

這樣,存取 /hello 將顯示預設訊息,而 /custom-hello 將顯示我們在設定中設定的自定義訊息。

編譯和載入模組

有兩種方式將模組整合到 Nginx 中:

  1. 靜態編譯:在編譯 Nginx 時包含模組

    ./configure --add-module=/path/to/module
    make
    make install
    
  2. 動態載入(Nginx 1.9.11+):

    ./configure --add-dynamic-module=/path/to/module
    make modules
    

    然後在設定中載入:

    load_module modules/ngx_http_custom_module.so;
    

進階模組開發技巧

隨著對 Nginx 模組架構的深入理解,玄貓建議考慮以下進階技術:

  1. 過濾模組:這類別模組可以修改請求或回應內容,例如壓縮、加密或內容轉換。

  2. 變數處理:建立自定義變數,豐富 Nginx 設定的靈活性。

  3. 分享記憶體:在多個工作程式間分享資料,實作更複雜的功能。

  4. 非同步處理:利用 Nginx 的事件驅動模型處理長時間執行的任務。

常見陷阱與最佳實踐

在開發 Nginx 模組時,我遇到過一些常見問題,分享給大家:

  1. 記憶體管理:始終使用 Nginx 的記憶體池而非直接分配記憶體,這可以防止記憶體洩漏。

  2. 並發考量:記住 Nginx 使用多個工作程式,避免依賴程式間分享的全域變數。

  3. 效能優先:模組應該高效執行,因為它們位於請求處理的關鍵路徑上。

  4. 錯誤處理:妥善處理所有可能的錯誤情況,確保模組不會導致 Nginx 當機。

  5. 設定合併:當使用多層級設定時,確保正確實作設定合併函式。

模組開發實戰案例

在一個實際專案中,我曾經開發過一個 Nginx 模組來實作 API 請求節流和速率限制。這個模組使用分享記憶體來追蹤請求頻率,並根據預先定義的規則允許或拒絕請求。

關鍵在於理解 Nginx 的事件處理模型,以及如何高效地在工作程式間分享資料。這個模組最終成為我們 API 閘道架構的核心元件,大幅減少了後端服務的負載。

Nginx 模組開發雖然複雜,但掌握了核心概念後,你將能夠極大地擴充套件 Nginx 的功能,實作幾乎任何你能想到的網頁伺服器功能。從簡單的內容修改到複雜的請求處理邏輯,Nginx 模組都能勝任。

開發 Nginx 模組是深入理解網頁伺服器架構和 C 語言系統程式設計的絕佳方式。透過實踐,你不僅能解決特定需求,還能提升整體系統的效能和可靠性。

Nginx 模組開發的魅力在於它結合了低階系統程式設計和高階網頁伺服器概念,讓開發者能夠在高效能伺服器平台上實作自己的創意。無論是為瞭解決特定問題,還是為了學習和探索,開發 Nginx 模組都是一項值得投入的技術挑戰。

理解 Nginx 模組:過濾器與認證模組實作

設定結構與指令設定

在前一部分中,我們定義了 ngx_http_custom_conf_t 結構來儲存問候訊息。透過這種設計,使用者可以在 Nginx 設定中自訂問候訊息,而不需要重新編譯模組:

location /custom {
    hello;
    hello_msg "您好,這是我的自訂問候語!";
}

這種方式讓模組的行為變得更加靈活,可以根據不同需求進行調整。接下來,讓我們探討更進階的模組型別。

實作過濾器模組

過濾器模組是 Nginx 中非常強大的功能,它們允許我們在回應形成的過程中進行干預。例如,我們可以實作一個在 HTML 頁面尾部增加自訂註解的過濾器:

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

// 宣告過濾器函式
static ngx_int_t ngx_http_filter_header_filter(ngx_http_request_t *r);
static ngx_int_t ngx_http_filter_body_filter(ngx_http_request_t *r, ngx_chain_t *in);

// 儲存下一個過濾器的指標
static ngx_http_output_header_filter_pt  next_header_filter;
static ngx_http_output_body_filter_pt    next_body_filter;

// 要增加到回應尾部的自訂註解
static ngx_str_t custom_footer = ngx_string("<!-- 由自訂過濾器提供支援 -->");

// 標頭過濾器:不做修改,直接傳遞給下一個過濾器
static ngx_int_t ngx_http_filter_header_filter(ngx_http_request_t *r) {
    return next_header_filter(r);
}

// 內容過濾器:在最後一個緩衝區增加自訂註解
static ngx_int_t ngx_http_filter_body_filter(ngx_http_request_t *r, ngx_chain_t *in) {
    ngx_chain_t *cl;
    
    if (in == NULL) {
        return next_body_filter(r, in);
    }
    
    // 尋找鏈中的最後一個緩衝區
    for (cl = in; cl; cl = cl->next) {
        if (cl->buf->last_buf) {
            size_t footer_size = custom_footer.len;
            
            // 為註解建立新的臨時緩衝區
            ngx_buf_t *b = ngx_create_temp_buf(r->pool, footer_size);
            if (b == NULL) {
                return NGX_ERROR;
            }
            
            ngx_memcpy(b->pos, custom_footer.data, footer_size);
            b->last = b->pos + footer_size;
            b->last_buf = 1;
            
            // 移除原緩衝區的最後標記
            cl->buf->last_buf = 0;
            
            // 為註解建立新的鏈結元素
            ngx_chain_t *new_cl = ngx_alloc_chain_link(r->pool);
            if (new_cl == NULL) {
                return NGX_ERROR;
            }
            
            new_cl->buf = b;
            new_cl->next = NULL;
            cl->next = new_cl;
            break;
        }
    }
    
    return next_body_filter(r, in);
}

// 初始化函式:設定過濾器鏈
static ngx_int_t ngx_http_filter_init(ngx_conf_t *cf) {
    // 儲存現有過濾器的指標並用我們的函式替換它們
    next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter = ngx_http_filter_header_filter;
    
    next_body_filter = ngx_http_top_body_filter;
    ngx_http_top_body_filter = ngx_http_filter_body_filter;
    
    return NGX_OK;
}

// 模組上下文
static ngx_http_module_t ngx_http_filter_module_ctx = {
    NULL,                          // preconfiguration
    ngx_http_filter_init,          // postconfiguration
    NULL,                          // create main configuration
    NULL,                          // init main configuration
    NULL,                          // create server configuration
    NULL,                          // merge server configuration
    NULL,                          // create location configuration
    NULL                           // merge location configuration
};

// 模組定義
ngx_module_t ngx_http_filter_module = {
    NGX_MODULE_V1,
    &ngx_http_filter_module_ctx,   // module context
    NULL,                          // 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
};

這個過濾器模組的工作原理是:當檢測到回應中的最後一個緩衝區時,它會建立一個新的緩衝區來存放我們的自訂註解,然後將這個新緩衝區附加到原始緩衝區鏈的尾部。這樣,每個回應都會在末尾增加我們定義的註解文字。

實作認證模組

接下來,讓我們實作一個更實用的模組 - 根據 HTTP 標頭的認證模組。這個模組會檢查請求中是否包含特定的標頭(例如 X-Auth-Token),如果沒有,則直接回傳 403 Forbidden 狀態碼:

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

// 認證處理函式
static ngx_int_t ngx_http_auth_handler(ngx_http_request_t *r) {
    ngx_table_elt_t *h;
    ngx_list_part_t *part;
    ngx_uint_t i;
    ngx_str_t header_name = ngx_string("X-Auth-Token");
    
    part = &r->headers_in.headers.part;
    h = part->elts;
    
    // 遍歷所有標頭,尋找認證標頭
    for (i = 0;; i++) {
        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }
            
            part = part->next;
            h = part->elts;
            i = 0;
        }
        
        if (ngx_strcasecmp(h[i].key.data, header_name.data) == 0) {
            // 找到認證標頭,允許請求繼續處理
            return NGX_DECLINED;
        }
    }
    
    // 未找到認證標頭,拒絕存取
    return NGX_HTTP_FORBIDDEN;
}

// 初始化函式
static ngx_int_t ngx_http_auth_init(ngx_conf_t *cf) {
    ngx_http_handler_pt *h;
    ngx_http_core_main_conf_t *cmcf;
    
    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
    
    // 將處理函式增加到預存取階段
    h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }
    
    *h = ngx_http_auth_handler;
    return NGX_OK;
}

// 模組上下文
static ngx_http_module_t ngx_http_auth_module_ctx = {
    NULL,                          // preconfiguration
    ngx_http_auth_init,            // postconfiguration
    NULL,                          // create main configuration
    NULL,                          // init main configuration
    NULL,                          // create server configuration
    NULL,                          // merge server configuration
    NULL,                          // create location configuration
    NULL                           // merge location configuration
};

// 模組定義
ngx_module_t ngx_http_auth_module = {
    NGX_MODULE_V1,
    &ngx_http_auth_module_ctx,     // module context
    NULL,                          // 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
};

這個認證模組的工作方式相對簡單:它在請求處理的預存取階段檢查是否存在 X-Auth-Token 標頭。如果找到該標頭,則允許請求繼續處理;如果沒有找到,則立即回傳 403 Forbidden 狀態碼,拒絕存取。

模組的編譯與整合

在實作完這些模組後,我們需要將它們編譯並整合到 Nginx 中。這通常涉及以下步驟:

  1. 將模組原始碼放置在適當的目錄中
  2. 使用 Nginx 的 configure 指令碼時,透過 --add-module=path/to/module 引數指定模組路徑
  3. 執行 makemake install 命令編譯並安裝 Nginx

例如,如果我們的過濾器模組位於 /path/to/filter_module,則可以使用以下命令編譯 Nginx:

./configure --prefix=/usr/local/nginx --add-module=/path/to/filter_module
make
make install

這樣,我們的自訂模組就會被編譯到 Nginx 中,並在 Nginx 啟動時載入。

實際應用與效能考量

在開發 Nginx 模組時,有幾個重要的效能考量:

  1. 記憶體使用:Nginx 以其低記憶體佔用著稱,因此模組應該盡量避免過度分配記憶體。

  2. 計算複雜度:由於 Nginx 處理大量並發請求,模組中的任何複雜計算都可能成為效能瓶頸。

  3. 非阻塞設計:Nginx 使用事件驅動的非阻塞架構,模組應該遵循這一設計理念,避免阻塞操作。

在實際應用中,這些模組可以根據特定需求進行擴充套件。例如,認證模組可以改進為不只檢查標頭是否存在,還可以驗證標頭中的令牌是否有效;過濾器模組可以用於實作更複雜的內容轉換,如壓縮、加密或格式轉換。

透過深入理解 Nginx 的模組架構,我們可以開發出各種功能強大與效能優異的擴充套件,使 Nginx 能夠滿足更多特定的應用需求。

動態與靜態模組編譯:Nginx 模組整合方案

靜態模組編譯

若要將自定義模組直接編譯進 Nginx 核心,需要透過靜態編譯方式進行。這種方法會將模組直接整合至 Nginx 執行檔中,成為其不可分離的一部分。首先,我們需要下載 Nginx 的原始碼,然後將我們的模組放置在適當的目錄中,接著進行設定與編譯:

./configure --add-module=/path/to/your/module
make
sudo make install

這種編譯方式的優點是執行效率較高,因為模組已經直接整合進 Nginx 核心。然而,缺點是每次更新或修改模組時,都需要重新編譯整個 Nginx 伺服器,這在生產環境中可能造成不便。

動態模組編譯

從 Nginx 1.9.11 版本開始,Nginx 開始支援動態模組功能,這讓我們能夠在不重新編譯整個伺服器的情況下更新或替換模組。動態編譯的步驟如下:

./configure --add-dynamic-module=/path/to/your/module
make modules

編譯完成後,系統會在 Nginx 模組目錄中生成一個 .so 檔案。接下來,我們需要在 Nginx 的主設定檔 nginx.conf 中加入以下指令來載入該模組:

load_module modules/ngx_http_your_module.so;

透過這種方式,Nginx 伺服器會在啟動時動態載入指定的模組。

兩種方式的比較與選擇

在開發過程中,玄貓發現動態模組編譯方式特別適合開發與測試階段,因為它允許我們快速迭代模組的開發而無需重新編譯整個 Nginx。這大幅提高了開發效率,尤其在處理複雜模組時。

然而,在生產環境中,靜態編譯可能會有輕微的效能優勢,因為它避免了動態載入模組的額外步驟。不過,這種效能差異在大多數情況下並不明顯,而動態模組的維護便利性往往更為重要。

模組開發實務考量

在開發 Nginx 模組時,玄貓建議先使用動態編譯方式進行開發與測試,待模組穩定後,再決定是否轉為靜態編譯。這種策略在大型專案中特別有效,因為它平衡了開發效率與佈署穩定性。

此外,在開發自定義模組時,務必充分了解 Nginx 的事件處理模型與模組架構。Nginx 採用非阻塞 I/O 和事件驅動的設計,這意味著模組必須以非阻塞方式實作,避免影響 Nginx 的高效能特性。

值得注意的是,模組開發需要謹慎處理記憶體管理,因為 Nginx 在高併發環境下執行,任何記憶體洩漏都可能導致嚴重問題。建議使用 Nginx 提供的記憶體池(memory pool)機制進行記憶體管理,這樣可以顯著減少記憶體碎片和洩漏風險。

從實務經驗來看,開發一個優質的 Nginx 模組需要深入理解 HTTP 協定和 Nginx 內部架構,同時還需要具備良好的 C 語言程式設計能力。雖然學習曲線較陡,但掌握這項技能後,你將能夠根據特定需求定製 Nginx 的行為,大幅提升網站效能和功能。

在開發過程中遇到問題時,Nginx 的官方檔案和社群資源是非常寶貴的參考。此外,研究現有的開放原始碼模組也是學習 Nginx 模組開發的有效途徑,能夠幫助你更快地理解模組的結構和最佳實踐。

Nginx 模組開發雖然挑戰性較高,但它提供了極大的靈活性,讓我們能夠根據特定需求擴充套件 Nginx 的功能,實作高效與可靠的網路服務。