首页 » 软件优化 » 零基础开发 nginx 模块(模块指令配置函数代码)

零基础开发 nginx 模块(模块指令配置函数代码)

雨夜梧桐 2024-10-30 06:43:30 0

扫一扫用手机浏览

文章目录 [+]

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 作者开发环境

nginx 所需开发环境非常简单,我使用 Ubuntu 18.04 ,使用下列命令即可安装所需最小依赖。

零基础开发 nginx 模块(模块指令配置函数代码) 软件优化
(图片来自网络侵删)

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

接下来确定目标 nginx 版本,可使用 nginx -v 查看 nginx 版本,如 Ubuntu 18.04 自带 nginx 版本为 1.14.0 。

$ nginx -vnginx 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 && makeauto/configure 脚本检查开发环境和所需依赖,生成 Makefile 脚本,如果有报错按提示修复后重试即可。
make 命令使用 Makefile 构建生成 nginx 可执行文件。
默认在代码仓库目录下新建一个名为 objs/ 的目录作为构建目录,构建脚本自动生成的相关文件和最终编译生成的 nginx 可执行文件也在该目录下。

测试运行刚刚生成的 objs/nginx 可执行文件,结果如下。

$ objs/nginx -vnginx 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_modulengx_module_type=HTTPngx_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 onpid 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 "test\n"; 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 voidhello_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 charhello(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.confHTTP 请求处理器

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_thello_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_thello_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, reconfiguring2020/05/16 23:09:31 [notice] 9384#0: reconfiguring2020/05/16 23:09:31 [notice] 9384#0: HELLO 阿泉# ... ...2020/05/16 23:09:31 [notice] 9384#0: start worker process 96232020/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 官方 开发指南 和 源码 里找到答案,那才是真正的藏宝图。
只有我们亲自摸索熟悉了这座花园,才会发现许许多多的宝藏,你也许会发现,旁边花栏有更美丽的郁金香和清香的茉莉花。
先读代码,后浪。

本文为阿里云原创内容,未经允许不得转载。

标签:

相关文章