零基礎開發 nginx 模塊

推薦學習資料:

本文大綱:

  • 簡要介紹 Nginx 動態模塊 。
  • 快速搭建簡單 開發環境 ,拉取源碼並編譯 nginx 。
  • 簡要介紹 nginx 模塊 源碼配置與目錄結構 ,建立工程框架。
  • 簡要介紹 nginx HTTP 模塊結構,建立 一個 HTTP 空模塊 框架代碼。
  • 編寫一個簡單配置文件,支持以普通用戶 測試運行 nginx ,方便後續開發測試。
  • 通過一個 hello world 示例簡要介紹 Nginx 配置指令 。
  • 簡要介紹 Nginx HTTP 請求處理器 。
  • 簡要介紹 Nginx 熱更新 (reload) 高級功能。
  • 吐槽與閒聊 。

Nginx 動態模塊

早期版本的 nginx 如果要擴展功能,新增代碼必須和 nginx 主體代碼一起編譯成一個二進制文件,這顯然非常不方便。2016 年 nginx 1.9.11 終於開始支持動態模塊 (Linux 下動態模塊即 so 文件),nginx 1.11.5 起支持單獨編譯動態模塊 (而不必同時編譯 nginx 自身),同時引入支持開源版本 nginx 與 nginx plus 的二進制兼容性。下圖清晰展示了這種結構。

image

nginx 使用 C 語言開發,C/C++ 構建工具衆多,如手寫 Makefile, GNU Autoconf, cmake 等,一些項目甚至專門爲自己開發了構建工具,如 boost 庫等。nginx 使用哪種構建工具呢?很遺憾,最後一種,自己開發。nginx 使用 shell 腳本維護了一套自動生成 Makefile 的構建腳本,類似簡化定製版的 Autoconf 。構建腳本位於代碼庫 auto/ 目錄下,C 源碼則位於 src/ 目錄下。

nginx 構建腳本同時也用來編譯附加模塊。

顯然,在 nginx 模塊中可以自由使用 nginx 主體代碼提供的 API 。需要注意的是, 構建時的 nginx 版本必須與運行時的 nginx 版本精確匹配 ,否則 nginx 將拒絕加載。這大概是 nginx 作者懶得精心維護 API 二進制兼容性。不過 模塊源碼通常是兼容的 ,與不同版本 nginx 源碼一起編譯即可得到對應版本的動態模塊 so 文件。

開發環境

nginx 所需開發環境非常簡單,我使用 Ubuntu 18.04 ,使用下列命令即可安裝所需最小依賴。

sudo apt-get update
sudo apt-get install build-essential libpcre3-dev zlib1g-dev -y

接下來確定目標 nginx 版本,可使用 nginx -v 查看 nginx 版本,如 Ubuntu 18.04 自帶 nginx 版本爲 1.14.0 。

$ nginx -v
nginx version: nginx/1.14.0 (Ubuntu)

獲取目標 nginx 版本源碼,可從 github 拉取。使用 -b 指定拉取版本,--depth 1 表示僅拉取 1 個提交,不要提交歷史,這樣可以快速完成拉取。

git clone -b release-1.14.0 --depth 1 https://github.com/nginx/nginx.git

在 nginx 代碼倉庫目錄下執行如下命令即可構建生成 nginx 可執行文件。

auto/configure && make
  • auto/configure 腳本檢查開發環境和所需依賴,生成 Makefile 腳本,如果有報錯按提示修復後重試即可。
  • make 命令使用 Makefile 構建生成 nginx 可執行文件。
  • 默認在代碼倉庫目錄下新建一個名爲 objs/ 的目錄作爲構建目錄,構建腳本自動生成的相關文件和最終編譯生成的 nginx 可執行文件也在該目錄下。

測試運行剛剛生成的 objs/nginx 可執行文件,結果如下。

$ objs/nginx -v
nginx version: nginx/1.14.0

至此,最簡 nginx 開發環境準備就緒。

注意: 此 nginx 版本僅用最小依賴和最簡配置構建,僅供開發測試動態模塊時使用,不可替代生產環境的 nginx 版本。

源碼配置與目錄結構

模塊源碼在獨立的文件夾下維護 (又稱之爲插件 addon)。模塊源碼目錄下需提供一個名爲 config 的 shell 配置腳本,提供模塊信息。nginx 構建腳本將 ngx_addon_dir 變量設置爲模塊源碼路徑,並執行 config 腳本獲取模塊信息。

在 nginx 代碼倉庫旁邊新建一個名爲 nginx-hello-module 的模塊文件夾,創建一個 config 腳本文件和一個 C 語言源碼文件 hello_module.c,即得到一個最簡單的模塊示例,目錄結構如下。

nginx/       # nginx 代碼倉庫
├── auto/    # nginx 構建腳本目錄
└── src/     # nginx 源碼目錄, 其他文件夾暫未列出。
nginx-hello-module/    # 模塊源碼目錄
├── config             # 模塊配置腳本, shell 腳本
└── hello_module.c     # 模塊源碼文件

編寫 config 配置腳本內容如下:

# vim: set ft=sh et:
ngx_addon_name=ngx_http_hello_module

ngx_module_type=HTTP
ngx_module_name="$ngx_addon_name"
ngx_module_srcs="$ngx_addon_dir/hello_module.c"

. auto/module
  • 插件名 ngx_addon_name 和模塊名 ngx_module_name 設置爲 ngx_http_hello_module 。
  • 模塊類型 ngx_module_type 設置爲 HTTP 。
  • 源碼文件列表 ngx_module_srcs 設置爲 $ngx_addon_dir/hello_module.c。注意: 源碼路徑必須添加 $ngx_addon_dir/ 前綴,構建腳本才能正確找到源碼文件。
  • 語句 . auto/module 調用 nginx 提供的模塊配置腳本,這條語句固定添加到 config 文件最後。

模塊代碼開發我們稍後再說,現在可以先建一個空源碼文件 hello_module.c 。

在 nginx 代碼倉庫下執行如下命令,增加配置上述 nginx-hello-module 模塊。

auto/configure --add-dynamic-module=../nginx-hello-module/

在 nginx 代碼倉庫下執行如下命令編譯模塊。

make modules

竟然編譯成功了!得到動態模塊文件 objs/ngx_http_hello_module.so 。但此時模塊還不可用 (嘗試加載此模塊將報錯),因爲我們還沒有寫任何代碼。

一個空模塊

我們知道,一個 C 程序的入口是 main() 函數。而一個 nginx 動態模塊的入口是一個 ngx_module_t 對象,其結構定義如下。

typedef struct ngx_module_s          ngx_module_t;

struct ngx_module_s {
    /* 私有字段 ... ... */

    void                 *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;

    ngx_int_t           (*init_master)(ngx_log_t *log);

    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);

    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
    void                (*exit_thread)(ngx_cycle_t *cycle);
    void                (*exit_process)(ngx_cycle_t *cycle);

    void                (*exit_master)(ngx_cycle_t *cycle);

    /* 擴展備用字段 ... ... */
};

除去私有字段和擴展備用字段,用戶相關的字段可分爲 3 個部分:

  • 模塊類型 ngx_uint_t type 和模塊類型特定的信息 void *ctx 。模塊類型必須與 config 腳本配置的類型一致,本例即爲 HTTP ,源碼中用 NGX_HTTP_MODULE 表示。
  • 模塊提供的指令列表 ngx_command_t *commands 。列表以 ngx_null_command 結尾,列表可以爲空 (僅包含一個 ngx_null_command 結尾標記) 。
  • 其餘爲模塊生命週期管理函數,可全部設置爲 NULL 。

HTTP 模塊對應的模塊信息 (void *ctx 字段) 爲 ngx_http_module_t 類型,可註冊若干 HTTP 模塊處理函數,可全部設置爲 NULL 。

#define NGX_HTTP_MODULE           0x50545448   /* "HTTP" */

typedef struct {
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    void       *(*create_main_conf)(ngx_conf_t *cf);
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    void       *(*create_srv_conf)(ngx_conf_t *cf);
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    void       *(*create_loc_conf)(ngx_conf_t *cf);
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

下面來編寫 hello_module.c 源碼,爲簡單起見,首先開發一個空模塊吧。

首先引入 nginx 頭文件,聲明模塊入口 ngx_module_t 對象,變量名必須爲 config 腳本中配置的模塊名,本例中即爲

ngx_http_hello_module 。

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
extern ngx_module_t ngx_http_hello_module;

接下來設置 HTTP 模塊信息 ngx_http_module_t ,相關處理函數全部設置爲 NULL 。

static ngx_http_module_t ngx_http_hello_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 configuration */
    NULL        /* merge location configuration */
};

指令列表 ngx_command_t[] 設置爲一個空列表,僅包含 ngx_null_command 結尾標記。

static ngx_command_t ngx_http_hello_commands[] = {
    ngx_null_command
};

最後,定義模塊入口對象 ngx_module_t 。開頭私有字段使用 NGX_MODULE_V1 表示,結尾擴展備用字段使用 NGX_MODULE_V1_PADDING 表示。設置上述定義的 HTTP 模塊信息 ngx_http_hello_module_ctx 和指令列表 ngx_http_hello_commands ,生命週期管理函數全部設置爲 NULL 。

ngx_module_t ngx_http_hello_module = {
    NGX_MODULE_V1,
    &ngx_http_hello_module_ctx,            /* module context */
    ngx_http_hello_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
};

至此,一個空模塊開發完成。這可以作爲開發 HTTP 模塊的初始模板,我們將在此基礎上逐漸增加功能。

在 nginx 代碼倉庫目錄下執行 make modules ,即可重新編譯生成動態模塊文件 objs/ngx_http_hello_module.so 。因爲我們沒有修改模塊配置,沒有添加或刪除源碼文件,所以不需要重新執行 auto/configure 配置腳本,直接執行 make modules 即可。

測試運行 nginx

在 nginx 代碼倉庫目錄下新建一個測試配置文件 objs/nginx.conf ,內容如下:

# vim: set ft=nginx et:
daemon off;  # default on

pid objs/nginx.pid;
error_log stderr notice;

load_module objs/ngx_http_hello_module.so;

events {
}

http {

    access_log objs/access.log;

    server {
        listen 8080 default_server;
        return 200 "test\n";
    }

}
  • daemon off; 設置 nginx 進程不要後臺化,保持前臺運行,按 Ctrl+C 即可退出 nginx 。
  • error_log stderr notice; 錯誤日誌直接輸出到終端,方便測試運行時查看錯誤日誌,設置日誌級別爲 notice 。
  • load_module objs/ngx_http_hello_module.so; 加載我們開發的動態模塊 ngx_http_hello_module.so 。
  • listen 8080 default_server; HTTP 服務器監聽 8080 端口,這樣使用普通用戶即可運行測試。
  • return 200 "testn"; HTTP 請求直接返回 "test" 字符串。

在 nginx 代碼倉庫目錄下使用如下命令測試運行 nginx 。

objs/nginx -p "$PWD" -c objs/nginx.conf
  • -p "$PWD" 設置 nginx prefix 爲當前目錄。配置文件路徑和配置文件中使用的相對路徑使用相對於 prefix 的路徑。
  • -c objs/nginx.conf 設置配置文件路徑。

可看到 nginx 啓動並打印日誌,按 Ctrl+C 後 nginx 退出。此時我們的模塊還是空模塊,沒有發揮任何作用。

Nginx 配置指令 - 世界你好

當我們學習一種新的開發技術時,第一個程序通常是 "hello world": 打印一條 "hello world" 語句,向世界問聲好。第一次接觸 nginx 開發時,我們不得不花時間做一些準備工作。現在,終於是時候張開雙臂,說一聲 "世界你好" 了。

我最早學習使用的是 Apache HTTP 服務器,其至今仍然是一款優秀強大的開源軟件。一些團隊因爲特殊原因開始嘗試新產品,俄羅斯程序員 Igor Sysoev 開發的 nginx 很快因其穩定性和高性能而聲名鵲起。

最初學習使用 nginx 的感受是,nginx 的配置文件似乎比 apache 要簡單友好一些 (在我對兩者都不熟悉的情況下) 。nginx 的配置文件好像是一種腳本,所以 nginx 配置項被稱作指令 (directive) 。沒錯,nginx 不只是一個 HTTP 服務器,還是一個被設計得簡單小巧的腳本語言解釋器,並支持開發添加新的指令。nginx 指令通常用於配置,我們稱之爲配置指令,換一種唬人的說法,叫做聲明式指令。

現在我們設計一個 hello 指令輸出 "hello world" 語句。

創建配置存儲結構體

HTTP 配置分爲 http/server/location 3 層結構。我們設計 hello 指令僅在最頂層 http {} 主區塊 (block) 下使用和生效。HTTP 模塊默認無配置存儲空間,可設置 ngx_http_module_t::create_main_conf 函數創建主區塊配置結構體。

我們設計本模塊僅包含一個字符串參數,即要輸出的語句。nginx 字符串類型爲 ngx_str_t ,編寫創建主配置結構體的函數 hello_create_main_conf() 如下:

static void*
hello_create_main_conf(ngx_conf_t *cf)
{
    ngx_str_t *conf;
    conf = ngx_pcalloc(cf->pool, sizeof(ngx_str_t));
    if (conf == NULL) {
        return NULL;
    }
    return conf;
}
  • 從配置內存池 cf->pool 分配一個字符串 ngx_str_t, 分配結構體將初始化爲 0, 對 ngx_str_t 即空字符串。
  • 如果函數返回 NULL 則表示分配失敗, nginx 將報錯退出。

更新 ngx_http_module_t ngx_http_hello_module_ctx ,設置 create_main_conf 爲 hello_create_main_conf() 函數。

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

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

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

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

創建指令

一個指令用一個 ngx_command_t 類型的數據結構表示。

typedef struct ngx_command_s         ngx_command_t;

struct ngx_command_s {
    ngx_str_t             name;
    ngx_uint_t            type;
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;
    ngx_uint_t            offset;
    void                 *post;
};

#define ngx_null_command  { ngx_null_string, 0, NULL, 0, 0, NULL }
  • name 指定指令名,如 hello 。
  • type 是一個混合結構,包含指令類型、指令使用位置、指令參數個數等多種特性信息。使用 NGX_HTTP_MAIN_CONF 表示指令可在 http 主配置使用,NGX_CONF_TAKE1 表示指令接受 1 個參數。
  • set 爲指令處理函數,即 nginx 配置設置函數。
  • conf 指示保存配置結構體的位置。使用 NGX_HTTP_MAIN_CONF_OFFSET 表示指令配置在 http 主配置下存儲生效。
  • offset 指示指令配置字段的位置。通常一個模塊的配置是一個結構體,而一個指令的配置是其中一個字段,set 函數通過 offset 訪問字段,這樣不需要知道結構體的類型 (結構),就可以讀寫配置字段。模塊只有一個配置項時,設置爲 0 即可。
  • post 對特定處理函數可增加後置處理函數,或增加傳入參數。通常不使用,設爲 NULL 。

聲明指令處理函數 hello() :

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

創建 hello 指令如下:

static ngx_command_t ngx_http_hello_commands[] = {

    { ngx_string("hello"),
      NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1,
      hello,
      NGX_HTTP_MAIN_CONF_OFFSET,
      0,
      NULL },

    ngx_null_command
};

編寫指令處理函數

指令執行處理:

  • nginx 根據指令 type 字段設置的特性自動校驗指令位置,參數個數等信息,並將指令語句解析爲字符串數組 (類似 shell 命令行) ,保存到 cf->args ,再調用指令處理函數。
  • 指令處理函數執行成功時返回 NGX_CONF_OK ,發生錯誤時返回錯誤消息。
  • 爲了簡化和統一指令處理, nginx 預定義了許多標準指令處理函數,如 ngx_conf_set_str_slot() 將一個字符串參數解析保存爲一個 ngx_str_t 配置項。
  • hello 指令可複用 ngx_conf_set_str_slot() 函數獲取參數值,再添加額外邏輯打印 hello 語句。

編寫指令處理函數 hello() 如下:

static char*
hello(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_str_t *str = conf;

    char *rv;

    rv = ngx_conf_set_str_slot(cf, cmd, str);
    if (rv != NGX_CONF_OK) {
        return rv;
    }

    ngx_log_error(NGX_LOG_NOTICE, cf->log, 0, "HELLO %V", str);

    return NGX_CONF_OK;
}
  • ngx_log_error() 是一個宏,最終將調用 ngx_log_error_core() 函數。
  • ngx_log_error() 第 3 個參數 err 表示系統錯誤碼,無對應錯誤碼時使用 0 。
  • nginx 未使用 C 標準庫的 snprintf() 字符串格式化函數,而是自己實現了 ngx_snprintf() 函數,並自定義了類似的格式化字符串,其中 %V 表示輸出 ngx_str_t * 指針指向的字符串。

至此,代碼開發完成。在 nginx 代碼倉庫目錄下執行 make modules 重新編譯生成動態模塊文件。

在配置文件 objs/nginx.conf http 配置下添加如下配置:

hello Nginx;

在 nginx 代碼倉庫目錄下執行如下命令,nginx 日誌將輸出 "HELLO Nginx" 語句,按 Ctrl-C 退出 nginx 。

objs/nginx -p "$PWD" -c objs/nginx.conf

HTTP 請求處理器

nginx 定義了多個 HTTP 請求處理階段 (phase) ,如讀取完 HTTP 請求頭後即進入 NGX_HTTP_POST_READ_PHASE 階段。可在 HTTP 請求處理的各個階段添加處理器函數,類似於 Java Servlet 中的 HTTP 過濾器 (Filter) 。

HTTP 處理器函數簽名 (函數類型) 如下:

typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);
  • 參數 r 爲 HTTP 請求結構體。
  • 返回值爲 NGX_DECLINED 時,表示繼續執行下一個處理器。
  • 發生錯誤時,返回 HTTP 錯誤碼,如服務器錯誤 500 NGX_HTTP_INTERNAL_SERVER_ERROR ,nginx 將立即返回請求。

編寫 HTTP 請求處理器 hello_handler() 如下,對每個 HTTP 請求打印一次 hello 語句,同時打印解析後的請求 uri 。使用 ngx_http_get_module_main_conf() 從 HTTP 請求對象獲取 ngx_http_hello_module 模塊關聯的配置數據。

static ngx_int_t
hello_handler(ngx_http_request_t *r)
{
    ngx_str_t * str = ngx_http_get_module_main_conf(r, ngx_http_hello_module);
    ngx_log_error(NGX_LOG_NOTICE, r->connection->log, 0, "HELLO %V, uri: %V", str, &r->uri);
    return NGX_DECLINED;
}

爲 HTTP 模塊編寫一個 postconfiguration 函數 hello_init() ,將 HTTP 處理器 hello_handler() 註冊到 NGX_HTTP_POST_READ_PHASE 階段。nginx 將在完成配置解析 (執行完配置指令) 後執行 HTTP 模塊的 postconfiguration 函數,以完成模塊初始化。

static ngx_int_t
hello_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_POST_READ_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = hello_handler;

    return NGX_OK;
}

更新 ngx_http_module_t ngx_http_hello_module_ctx ,設置 postconfiguration 爲 hello_init() 函數。

static ngx_http_module_t ngx_http_hello_module_ctx = {
    NULL,                       /* preconfiguration */
    hello_init,                 /* postconfiguration */

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

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

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

至此,開發完成。在 nginx 代碼倉庫目錄下執行 make modules 重新編譯生成動態模塊文件,然後執行如下命令啓動 nginx 。

objs/nginx -p "$PWD" -c objs/nginx.conf

使用瀏覽器或 curl 命令訪問 http://localhost:8080/ ,每訪問一次將看到 nginx 打印一次 hello 語句,及當前請求 uri 。類似如下輸出:

2020/05/16 22:46:26 [notice] 7279#0: *1 HELLO Nginx, uri: /, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8080"
2020/05/16 22:46:27 [notice] 7279#0: *1 HELLO Nginx, uri: /, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8080"

熱更新 (reload)

nginx 還支持熱更新 (reload) ,這是一個很有用的高級特性。在不停止 nginx 的情況下將配置文件中的 hello 指令修改如下:

hello "阿泉";

在 nginx 代碼倉庫目錄下執行如下 reload 命令:

objs/nginx -p "$PWD" -c objs/nginx.conf -s reload

reload 命令將看到如下輸出:

2020/05/16 23:09:31 [notice] 9617#0: HELLO 阿泉
2020/05/16 23:09:31 [notice] 9617#0: signal process started

原 nginx 進程將看到如下輸出。nginx 將重新進行配置初始化,創建新 worker 進程,並優雅退出舊 worker 進程。

2020/05/16 23:09:31 [notice] 9384#0: signal 1 (SIGHUP) received from 9617, reconfiguring
2020/05/16 23:09:31 [notice] 9384#0: reconfiguring
2020/05/16 23:09:31 [notice] 9384#0: HELLO 阿泉
# ... ...
2020/05/16 23:09:31 [notice] 9384#0: start worker process 9623
2020/05/16 23:09:31 [notice] 9385#0: gracefully shutting down

再次訪問 http://localhost:8080/ 時,可看到 nginx 日誌打印的 hello 語句也隨之變成了新配置的 hello 語句。

2020/05/16 23:09:49 [notice] 9623#0: *3 HELLO 阿泉, uri: /, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8080"

熱更新 (reload) 功能非常有用,但在生產使用時一定要非常小心以避免故障。實際使用中可能用的並不多。

教程到此結束,下面扯些題外話。

吐槽與閒聊

Nginx 文檔還算完善,代碼還算優雅,閱讀 Nginx 源碼對提升開發水平頗有裨益,但其過程實在是燒腦和痛苦 (對我而言) 。Nginx 源碼幾乎攢齊了傳統 C 語言編程的所有缺點。比如使用整數定義錯誤碼和枚舉類型,使用了迪傑斯特拉 (Dijkstra) 先生不建議使用的 goto 語句,一個整數字段 (如 cmd->type) 整合了多種枚舉類型信息,許多地方使用了動態類型 void* 等。這些用法不受工具 (靜態) 檢查和約束 (原作者的腦中可能有一幅清晰的場景圖表?),對不熟悉的開發者來說不僅難以理解,而且非常危險!但其背後往往又是出於性能 (和某種簡潔性) 的考慮,大概是使用 C 語言的情況下所能做出的最大努力。換句話說,(很多時候) 這是 C 語言的侷限性,而不是 Nginx 的問題。錯誤處理的正確解法應該是 Java 受檢查的異常,但 C 語言缺少異常 (Exception) 等高級特性,合理使用 (無效業務值) 錯誤碼和 goto 語句是優雅且高效的最佳實踐之一。

語言之爭

本段內容容易引起不適,建議跳過。

有時候想,Nginx 爲什麼不使用更高級的開發語言 (比如 C++) 編寫,或者至少可以複用 Apache 基礎庫 APR 吧 ?其實又何止 Apache 基礎庫, Apache HTTP 服務器應該有很多組件都可以複用。但如果這樣的話,Nginx 又怎麼能叫 Nginx 呢 ? 大概只能是一個特殊版本的 Apache HTTP 服務器,影響力和競爭力都很難超越官方正版(就像許多 Nginx 修改版很難超越 Nginx 一樣) 。不止是 3 方基礎庫,Nginx 連 C 語言標準庫都試圖避免直接使用,比如自己開發了 ngx_snprintf() (但 Nginx 也不是全都自己來,比如合理使用了 pcre, zlib, openssl 等 3 方庫) 。很多 C 語言項目其實都在使用自己特殊定製版的 C 語言 (又一個典型缺點) 。這讓我想起《黑客與畫家》文集上提到的 迎難而生 的問題 (值得另外開貼討論) ,如果一個問題太容易,誰都可以複製 (抄襲),那麼它的核心競爭力在哪裏?

Nginx 及其模塊開發本身是有一定門檻的,甚至 Nginx 本身建議不要濫用模塊開發 (而儘量用 nginx 配置或內置的 perl/njs 腳本) 。

有 nodejs 粉說用 nodejs 幾條語句就可以寫出一個高性能 HTTP 服務器,如果 nginx 這樣寫成,結果會怎樣 ?在大家都在喊着 nodejs/python/php/golang/kotlin 天下第一的時候,老態龍鍾的 C 語言榮獲 TIOBE 編程語言排行榜 2019 年度語言,最近 (2020 年 5 月) 又重奪排行榜第一。我不是針對誰,我是說 javascript/php/golang 等都是垃圾語言 (python 和 kotlin 還算能用?)。我也不推薦 C 語言,C 語言顯然有很多缺點 (過於底層),如果能夠加上一些 C++ 特性 (特別是類和 RAII) 那肯定會好很多。但是 C++ 特性太多,簡直是一團漿糊,所以許多團隊和項目不得不精心控制一些邊界,設計一個定製版的 C++ 語言 (與 C 語言類似)。這導致 C++ 語言分裂,是個不好的信號,也是這個原因導致許多聲稱解決這些問題的新語言不斷出現。

結論: 貼近系統和硬件編程,C/C++ 是不錯的選擇,高級語言首選 Java ,其他一些快速粗糙 (quick and dirty) 的場景可適當選用其他語言。但一定要小心避免垃圾語言 (不再一一點名了) 和所謂的領域專用語言 (DSL) 。

代碼風格

首選吐槽一下。Nginx 只使用 C 風格的註釋 / / (不使用 C++ 的雙斜槓 // 註釋) 。使用 4 個空格縮進 (而不是 tab) 。變量名常常太短 (導致含義不直觀) 。單行源碼不超過 80 個字符 (可能也是導致變量名過短的原因)。這幾點個人不太喜歡。

聽說 nginx 作者有代碼潔癖,要求字段名 (變量名) 排版對齊。我也有代碼潔癖,我反對這種對齊,表面上視覺整齊了,實際上維護跟蹤很麻煩 (特別是沒有工具支持的情況下) 。再看 nginx 代碼,不僅要求對齊,而且是拋開修飾符後的單詞對齊 (嗯,奇怪的排版) 。如 struct ngx_command_s 定義如下。

struct ngx_command_s {
    ngx_str_t             name;
    ngx_uint_t            type;
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;
    ngx_uint_t            offset;
    void                 *post;
};

nginx 代碼是很吝惜註釋的,但並非沒有註釋,恰當的時候會有註釋,而更多的時候讓代碼自己說話。如 ngx_http_core_generic_phase() 函數的這段代碼,結合註釋可知這裏已經考慮枚舉了 rc 的所有可能取值。這點我是比較讚賞的,不過個人建議可以適當添加更多註釋 (特別是邏輯複雜的地方) 。

    if (rc == NGX_DECLINED) {
        r->phase_handler++;
        return NGX_AGAIN;
    }

    if (rc == NGX_AGAIN || rc == NGX_DONE) {
        return NGX_OK;
    }

    /* rc == NGX_ERROR || rc == NGX_HTTP_...  */

    ngx_http_finalize_request(r, rc);

    return NGX_OK;

另外,nginx 代碼鼓勵用空行分割語義塊 (哪怕只有一行) ,如 ngx_conf_handler() 函數包含如下代碼塊:

            /* set up the directive's configuration context */

            conf = NULL;

            if (cmd->type & NGX_DIRECT_CONF) {
                conf = ((void **) cf->ctx)[cf->cycle->modules[i]->index];

            } else if (cmd->type & NGX_MAIN_CONF) {
                conf = &(((void **) cf->ctx)[cf->cycle->modules[i]->index]);

            } else if (cf->ctx) {
                confp = *(void **) ((char *) cf->ctx + cmd->conf);

                if (confp) {
                    conf = confp[cf->cycle->modules[i]->ctx_index];
                }
            }

if 子句和 else 子句執行不同的邏輯,用一個空行分開,結構更加清晰,這一點值得學習。順便說句,這段代碼較難讀懂,也許可以再適當添加部分註釋。

最後,很多人可能聽過類似 "單個函數不要超過 100 行" (更有嚴格的說 50 行, 20 行) 這樣的最佳實踐。但如果我們看許多優秀開源項目的代碼,大佬們寫起代碼來根本停不下來,洋洋灑灑幾百行的核心函數純屬正常。儘量保持函數功能單一和簡短當然是最近實踐,但是 不用死守規則 。規則往往是由強者制定來約束弱者,黑客從來不應該受任何具體規則的束縛,唯一的規則就是正確、簡短、健壯,然後越快越好。別給我說那些婆婆媽媽的編程規範。

我的代碼又快又穩定,然後你跑來說我排版不好看 (是的我說了) ?滾一邊去!

後浪

初次接觸一種開發技術,好像來到一座花園,想要到某個目的地取採摘一朵花 (開發需求)。陌生的花園猶如迷宮,一開始我們跌跌撞撞,可能被荊棘扎手,可能走錯方向,但最終來到玫瑰花欄,摘下一朵花。於是我沿途做下記號,小心避開荊棘和彎路,就成了這篇文章。

所有本文更適合作爲簡單的快速參考 (沿路記號),而讀者可能會充滿 “這裏爲什麼要這樣?” 的疑問。許多疑問都可以在 Nginx 官方 開發指南 和 源碼 裏找到答案,那纔是真正的藏寶圖。只有我們親自摸索熟悉了這座花園,纔會發現許許多多的寶藏,你也許會發現,旁邊花欄有更美麗的鬱金香和清香的茉莉花。 先讀代碼,後浪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章