引言:Nginx,你究竟还有多少副面孔?
兄弟们,姐妹们,各位在代码海洋里冲浪的弄潮儿们!提到Nginx,你脑子里第一反应是啥?
“哦,那个反向代理,老快了!”
“对对对,负载均衡一把好手!”
“静态资源服务器,YYDS!”
停!打住!如果Nginx在你心中还只是个“傻快”的流量搬运工,那今天这篇文章,就是来给你“洗脑”的。
想象一下,你是一家网红火锅店的老板。Nginx就是你那个腿脚麻利、永不犯错的金牌服务员。客人来了(请求),他看一眼就能精准地分到空闲的桌子(上游服务器)。客人要杯凉白开(静态文件),他秒速送到。
但问题是,如果客人想办张会员卡,查询积分,或者想玩个“掷骰子免单”的小游戏呢?这个服务员就懵了,他得跑回后厨(你的应用服务器,比如Java/Python/PHP那边)去问,一来一回,再麻利的腿脚也耽误工夫。
那么,灵魂拷问来了:能不能让这个金牌服务员,不仅腿脚麻利,还能当场算账、办理会员、甚至陪你掷骰子?
答案是:能!必须能! 而实现这一切的魔法,就是——用C++为Nginx开发模块。
今天,我们不满足于让Nginx当跑堂,我们要亲手给他植入“最强大脑”,让他成为能文能武、业务通吃的“全能店长”!
第一章:手术前的“知情同意书”——我们到底在干啥?
在开始这场“外科手术”前,咱们得先统一思想,明白我们即将捣鼓的是什么。
1.1 Nginx的“可扩展性”基石
Nginx之所以强大,除了它那经典的事件驱动、异步非阻塞模型,另一个核心就是其高度模块化的设计。它的每一个功能,比如处理HTTP请求、解析Headers、提供Gzip压缩,本质上都是一个模块。
我们这些开发者要做的,就是遵循它的游戏规则,编写一个符合规范的“插件”(也就是模块),然后把它“插入”到Nginx的身体里。从此,Nginx就拥有了你赋予它的超能力。
1.2 为什么是C++?不是Lua?不是JavaScript?
好问题!Nginx社区确实有像lua-nginx-module这样的神器,用脚本语言开发又快又方便。但C/C++的优势在于:
- 性能巅峰:与Nginx自身同为C语言开发,无缝衔接,性能损耗极低。对于CPU密集型的复杂业务逻辑,C++是终极选择。
- 深度集成:你可以深入到Nginx的任何一个生命周期,实现任何你想实现的功能,限制你的只有你的C++水平。
- “系统级”能力:直接操作内存、进行底层网络通信等,自由度拉满。
简单说,Lua像是给Nginx配了个便捷的对讲机,而C++是直接给他做了个脑机接口。
1.3 我们的“小目标”
今天,我们不搞那么复杂的。我们来实现一个看起来简单,但“五脏俱全”的模块,名叫 nd_hello_world。
它的功能是:当用户访问 http://你的域名/hello 时,Nginx不会去代理别的服务,也不会找磁盘文件,而是直接由我们的C++代码,返回一个经典的 "Hello, World! The current time is {当前时间}"。
别看功能简单,它将带你走完Nginx模块开发的全流程:配置、编译、安装、测试。
第二章:搭建你的“手术台”——开发环境准备
工欲善其事,必先利其器。我们先得把“手术台”搭起来。
你需要准备:
- 一台Linux机器(Ubuntu/CentOS都行,我这里以Ubuntu 20.04为例)。
- Nginx源码:对,不是直接用
apt-get安装的那个,我们要从源码编译。 - C++编译工具链:g++, make, cmake等。
- 一颗不怕报错的心。
步骤一:安装依赖
sudo apt-get update
sudo apt-get install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev git -y
步骤二:获取Nginx源码
我们去官网下个稳定版,比如1.24.0。
wget https://nginx.org/download/nginx-1.24.0.tar.gz
tar -zxvf nginx-1.24.0.tar.gz
cd nginx-1.24.0
现在,你进入了Nginx的“心脏”地带。
第三章:解剖Nginx模块的“基因序列”
在动刀之前,得先了解Nginx模块长啥样。一个Nginx模块,主要包含两部分:
- 配置结构:告诉Nginx你这个模块需要什么配置参数。
- 模块定义:向Nginx系统“注册”你自己,声明你的“姓名”、“身份证号”(唯一标识)以及“技能”(处理函数)。
最关键的是那个处理函数。Nginx在处理一个请求时,会把请求分成很多阶段(Phase),比如NGX_HTTP_POST_READ_PHASE, NGX_HTTP_CONTENT_PHASE等。我们的模块通常挂在 NGX_HTTP_CONTENT_PHASE 阶段,因为这个阶段是用来生成内容的。
我们的C++代码,就是要提供一个在这个阶段被调用的函数,在这个函数里,我们生成 "Hello, World" 并发送给客户端。
第四章:上才艺!手搓你的第一个C++模块
激动人心的时刻到了!我们开始写代码。
第一步:创建我们的模块源码文件
在 nginx-1.24.0/ 目录下,我们创建一个新文件 src/nd_hello_world_module.cpp。用src文件夹来管理我们的代码是个好习惯。
// src/nd_hello_world_module.cpp
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <ctime>
#include <cstring>
// 声明我们的处理函数
static ngx_int_t ngx_http_nd_hello_world_handler(ngx_http_request_t *r);
// 1. 模块配置结构(虽然我们这个简单模块不需要,但结构得有)
static ngx_http_module_t ngx_http_nd_hello_world_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
};
// 2. 模块指令定义 (这是我们这个demo的核心!)
// 这个结构告诉Nginx,当在配置文件中遇到 `nd_hello_world` 这个指令时,该干嘛。
static ngx_command_t ngx_http_nd_hello_world_commands[] = {
{
ngx_string("nd_hello_world"), // 指令名字
NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS, // 使用在location块,且不接受参数
ngx_http_nd_hello_world_handler, // 当遇到这个指令时,调用的函数!
0, // 指令相关的配置结构偏移,我们不用
0, // 同上
NULL
},
ngx_null_command // 数组结束标志
};
// 3. 核心处理函数!
// 当有人请求配置了 `nd_hello_world on;` 的location时,这个函数就会被调用。
static ngx_int_t ngx_http_nd_hello_world_handler(ngx_http_request_t *r) {
// 我们只处理GET和HEAD方法,像个正经的HTTP处理器
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;
}
// 获取当前时间,让我们的响应更“动态”一点
std::time_t now = std::time(nullptr);
std::tm* tm_now = std::localtime(&now);
char time_str[100];
std::strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_now);
// 构造响应内容
std::string response = "Hello, World! The current time is ";
response += time_str;
response += "\\n";
// 设置响应头
ngx_str_t content_type = ngx_string("text/plain; charset=utf-8");
r->headers_out.content_type = content_type;
r->headers_out.content_type_len = content_type.len;
r->headers_out.status = NGX_HTTP_OK; // 200 OK
r->headers_out.content_length_n = response.length();
// 发送响应头
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK) {
return rc;
}
// 构造一个缓冲区来存放我们的响应体
ngx_buf_t *b;
b = ngx_create_temp_buf(r->pool, response.length());
if (b == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
// 把我们的字符串拷贝到缓冲区
ngx_memcpy(b->pos, response.c_str(), response.length());
b->last = b->pos + response.length();
b->last_buf = 1; // 这是最后一个缓冲区
// 构造输出链
ngx_chain_t out;
out.buf = b;
out.next = NULL;
// 最终,发送响应体!
return ngx_http_output_filter(r, &out);
}
// 4. 模块定义 (向Nginx“自我介绍”)
ngx_module_t ngx_http_nd_hello_world_module = {
NGX_MODULE_V1, // 一个宏,填充了模块结构的版本信息等
&ngx_http_nd_hello_world_module_ctx, // 模块上下文
ngx_http_nd_hello_world_commands, // 模块指令
NGX_HTTP_MODULE, // 模块类型(我们是HTTP模块)
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 // 一个宏,用于填充结构体剩余部分
};
代码精讲:
ngx_http_nd_hello_world_commands:这是模块与Nginx配置文件的桥梁。它定义了一个指令nd_hello_world,并绑定了处理函数ngx_http_nd_hello_world_handler。ngx_http_nd_hello_world_handler:这是模块的灵魂。它接收一个ngx_http_request_t *r指针,这个指针包含了当前请求的所有信息。
-
- 它先检查HTTP方法。
- 然后丢弃不需要的请求体。
- 接着,它用C++标准库生成带时间的响应字符串。
- 之后,它设置响应头(状态码200,内容类型为文本)。
- 最后,它创建缓冲区,填充数据,并通过Nginx的过滤链将响应发送出去。
第二步:修改Nginx的编译配置
现在要告诉Nginx:“兄弟,编译的时候,把咱这个新模块也带上。”
编辑 nginx-1.24.0/ 目录下的 auto/config 文件?不,我们有更标准的方法。在源码根目录执行:
./configure --add-module=./src
这个命令的意思是:在原有的标准模块基础上,额外添加 ./src 目录下的模块。Nginx的构建系统会自动在 src 目录里寻找 config 文件来指导编译。
第三步:创建模块的 config 文件
在 src/ 目录下创建一个 config 文件,内容如下:
# src/config
# 告诉Nginx构建系统,这是一个HTTP模块
ngx_addon_name=ngx_http_nd_hello_world_module
# 指定我们的源码文件
HTTP_MODULES="$HTTP_MODULES ngx_http_nd_hello_world_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/nd_hello_world_module.cpp"
# 告诉编译器使用C++,并链接C++标准库
CXXFLAGS="$CXXFLAGS -std=c++11"
NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_addon_dir/nd_hello_world_module.cpp"
CORE_LIBS="$CORE_LIBS -lstdc++"
这个 config 文件是打通C++模块和Nginx C语言世界的关键。
第五章:“开机!启动!”——编译与测试
步骤一:编译并安装
回到 nginx-1.24.0/ 根目录,执行三部曲:
# 1. 配置
./configure --add-module=./src
# 2. 编译 (用-j参数加速,数字看你CPU核心数)
make -j4
# 3. 安装 (默认会安装到 /usr/local/nginx/)
sudo make install
如果一切顺利,你会看到编译成功的信息。恭喜你,你的Nginx已经内置了你的C++大脑!
步骤二:配置Nginx
编辑Nginx的配置文件,通常是 /usr/local/nginx/conf/nginx.conf。
在 http 块内的某个 server 块里,添加一个 location:
# 在 /usr/local/nginx/conf/nginx.conf 的 http -> server 块内
server {
listen 80;
server_name localhost;
# 你的其他location...
# 这是我们新模块的location!
location /hello {
# 启用我们的C++模块!
nd_hello_world;
}
}
看,我们使用了在C++代码里定义的指令 nd_hello_world。
步骤三:启动Nginx并测试
# 启动Nginx
sudo /usr/local/nginx/sbin/nginx
# 使用curl测试我们的模块
curl http://localhost/hello
奇迹发生时刻! 你应该会看到类似下面的输出:
Hello, World! The current time is 2023-10-27 14:30:25
每刷新一次,时间都会变!这说明,响应是完全由你的C++代码动态生成的,Nginx没有去代理任何其他服务,也没有读取任何磁盘文件。
第六章:从“Hello World”到“改变世界”——还能怎么玩?
成功了?别急,这只是万里长征第一步。我们这个模块虽然简单,但已经包含了所有核心流程。在此基础上,你可以放飞自我:
- 读取GET/POST参数:从
r->args(GET参数) 和解析请求体 (POST参数) 中获取用户输入。 - 连接数据库:在模块初始化时创建数据库连接池,在处理函数中执行查询,返回JSON格式的数据(实现一个高性能的API接口)。
- 实现复杂业务逻辑:做鉴权、做限流、做A/B测试,只要你C++够溜,没有实现不了的功能。
- 玩转Nginx变量:创建和使用Nginx内置变量,实现更灵活的配置。
第七章:填坑指南——那些你可能遇到的“坑”
- 编译错误
undefined reference to ‘std::cout‘:确保你的config文件正确链接了-lstdc++。 - Nginx启动失败
module is not binary compatible:确保你使用的Nginx源码版本和之前安装的版本一致。最干净的做法是,每次都从同一份源码编译和安装。 - 内存泄漏:Nginx有自己的内存池(
r->pool和cycle->pool),尽量使用ngx_palloc等函数在其上分配内存,请求结束时Nginx会自动帮你清理,非常省心。 - 段错误 (Segmentation Fault):使用GDB调试Nginx Worker进程。
sudo gdb -p <nginx-worker-pid>。记住,Nginx是多进程的,要attach到Worker进程上。
结语:你已经是能给Nginx“动手术”的医生了
朋友们,走到这一步,你已经不再是那个只会配置 proxy_pass 的Nginx用户了。你已经成为了一名能够深入其内核,用C++为其赋予全新生命的“Nginx医生”。
你亲手完成了一次从源码编译、模块开发、到集成测试的完整流程。这个世界上的高性能Web服务器,对你来说不再是一个黑盒子。
从此,你的技术武器库里,又多了一件可以屠龙的神兵利器。当别人还在为网关的性能瓶颈发愁时,你可以轻描淡写地说:“没事,我用C++写个Nginx模块搞定它。”
这,就是技术的深度带来的魅力与力量。
去吧,用你的C++,让Nginx为你创造奇迹!
(全文约4200字)

被折叠的 条评论
为什么被折叠?



