|零基础开发 nginx 模块


本文大纲:

  1. 简要介绍Nginx 动态模块。
  2. 快速搭建简单开发环境, 拉取源码并编译 nginx。
  3. 简要介绍 nginx 模块源码配置与目录结构, 建立工程框架 。
  4. 简要介绍 nginx HTTP 模块结构 , 建立一个 HTTP 空模块 框架代码 。
  5. 编写一个简单配置文件 , 支持以普通用户测试运行 nginx, 方便后续开发测试 。
  6. 通过一个 hello world 示例简要介绍Nginx 配置指令。
  7. 简要介绍 NginxHTTP 请求处理器。
  8. 简要介绍 Nginx热更新 (reload) 高级功能 。
  9. 吐槽与闲聊。
Nginx 动态模块
早期版本的 nginx 如果要扩展功能 , 新增代码必须和 nginx 主体代码一起编译成一个二进制文件 , 这显然非常不方便 。 2016 年 nginx 1.9.11 终于开始支持动态模块 (Linux 下动态模块即 so 文件) , nginx 1.11.5 起支持单独编译动态模块 (而不必同时编译 nginx 自身) , 同时引入支持开源版本 nginx 与 nginx plus 的二进制兼容性 。 下图清晰展示了这种结构 。
|零基础开发 nginx 模块
本文插图

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【|零基础开发 nginx 模块】竟然编译成功了!得到动态模块文件 objs/ngx_http_hello_module.so。 但此时模块还不可用 (尝试加载此模块将报错) , 因为我们还没有写任何代码 。
一个空模块
我们知道 , 一个 C 程序的入口是 main() 函数 。 而一个 nginx 动态模块的入口是一个 ngx_module_t 对象 , 其结构定义如下 。
typedef struct ngx_module_sngx_module_t; struct ngx_module_s { /* 私有字段 ... ... */ void*ctx; ngx_command_t*commands; ngx_uint_ttype; 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_MODULE0x50545448/* ''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 #include #include 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 ''testn''; } }
  • 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_sngx_command_t; struct ngx_command_s { ngx_str_tname; ngx_uint_ttype; char*(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_tconf; ngx_uint_toffset; 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.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_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 reloadreload 命令将看到如下输出:
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_tname; ngx_uint_ttype; char*(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_tconf; ngx_uint_toffset; 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 官方 开发指南 和 源码 里找到答案 , 那才是真正的藏宝图 。 只有我们亲自摸索熟悉了这座花园 , 才会发现许许多多的宝藏 , 你也许会发现 , 旁边花栏有更美丽的郁金香和清香的茉莉花 。 先读代码 , 后浪 。
本文为阿里云原创内容 , 未经允许不得转载 。


    推荐阅读