前言
Nginx 是一款强大的web服务器,是由核心模块和其他的一系列模块一起组成,其高度模块化的设计使得Nginx的扩展和开发十分方便。
Nginx提供了web反向代理服务、email邮件服务、负载均衡、web服务器等功能,其核心模块Nginx core提供了通讯、运行环境,是所有其他模块协作的基石。
本文将会手撸一个访问统计模块,统计每一个 IP 的页面访问次数,业务并不复杂,只是用来学习 Nginx 模块的编写方法。在这之前,先简单介绍下 Nginx 模块的基本理论。
Nginx 模块概述
Nginx 的模块化是非常值得品味的一点,其将各个模块串成一个链表,在每次请求到来的时候依次遍历链表上的所有模块,调用所有的处理函数。比如 upstream模块、事件模块、HTTP 模块等等。其中 HTTP 模块是实现了 HTTP 协议,是 Nginx 中的”大户“,代码规模远超其他模块,其中对 HTTP 优秀的实现值得借鉴。
HTTP 模块概述
HTTP 请求过程主要是以下几个阶段:
- 初始化 HTTP Request;
- 处理 HTTP Header;
- 处理 HTTP Body;
- 处理请求相关 Handler;
- 处理 phase handler。
其中 HTTP 模块在 Nginx 的实现中分了11个 phase,大致如下:
- NGX_HTTP_POST_READ_PHASE 读取请求阶段
- NGX_HTTP_SERVER_REWRITE_PHASE 请求地址重写阶段
- NGX_HTTP_FIND_CONFIG_PHASE 配置查找阶段
- NGX_HTTP_REWRITE_PHASE 请求地址重写阶段
- NGX_HTTP_POST_REWRITE_PHASE 请求地址重写提交阶段
- NGX_HTTP_PREACCESS_PHASE 访问权限检查准备阶段
- NGX_HTTP_ACCESS_PHASE 访问权限检查阶段
- NGX_HTTP_POST_ACCESS_PHASE 访问权限检查提交阶段
- NGX_HTTP_TRY_FILES_PHASE 配置项 try_files 处理阶段
- NGX_HTTP_CONTENT_PHASE 内容产生阶段
- NGX_HTTP_LOG_PHASE 日志模块处理阶段
在 Nginx 收到 HTTP Header 之后,Nginx会找到这个请求关联的虚拟主机(server)的配置,然后就进行以上的11个阶段的处理(phase handles)。
这些阶段的 handler 可以到对应配置文件的 location 中去查找到,然后依次执行。
其中内容产生阶段(NGX_HTTP_CONTENT_PHASE)是 HTTP 核心的阶段,原因有:
第一,之前的9个阶段只是做一些基础工作,比如重写 URL、找到 location 配置块、判断请求的访问权限等等,能够适用绝大多数的工作,因此大多数 HTTP 模块希望共享这些功能。
第二,内容产生阶段(NGX_HTTP_CONTENT_PHASE)不同于其他阶段,如果未在配置中找到相应的 handler,则会依次执行:
- 如果 location 中有配置 random_index_one,随选择一个文件发送到客户端;
- 如果 location 中有配置 index,发送 index 指向的文件到客户端;
- 如果 location 中有配置 autoindex on,随选择一个文件发送到客户端;
- 如果这个 request 对应的 location 上有设置 gzip_static on,那么就查找是否有对应的.gz 文件存在,有的话,就发送这个给客户端(客户端支持 gzip 的情况下)。
- 请求的 URI 如果对应一个静态文件,static module 就发送静态文件的内容到客户端。
因此大部分 HTTP 模块都只在内容产生阶段来处理请求。
编译准备
在编写自己的 HTTP 模块前,首先需要了解如何把自己的模块编译进 Nginx 项目中。Nginx 提供了很简单的方法来帮助编译,方法是把名为 config 的文件放在与自定义模块的同一目录下,所有的源码必须在同一目录下。依照 Nginx HTTP 模块命名风格,本次编写的自定义模块命名为 ngx_http_pagecount_module
,目录结构如下:
ngx_http_pagecount_module/
├── config
└── ngx_http_pagecount_module.c
config 文件写法
config 文件需要三个参数:
- ngx_addon_name: 一般设置为模块名,执行 configure 时调用
- HTTP_MODULES: 保存所有模块内容的变量
- NGX_ADDON_SRCS: 新增模块源码的路径
配置如下:
ngx_addon_name=ngx_http_pagecount_module
# 需要加上 $HTTP_MODULES
HTTP_MODULES="$HTTP_MODULES ngx_http_pagecount_module"
# $ngx_addon_dir 为 configure 时 --add-module=PATH 的 PATH
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_pagecount_module.c"
如果不是开发 HTTP 模块,而是开发 HTTP 过滤模块,那么 HTTP_MODULES
需要替换为 HTTP_FILTER_MODULES
,其他类型的模块同样有自己独有的变量名。
预编译测试
虽然我们还没有写模块,但可以先 configure 试试,进入到 Nginx 源码目录并执行命令:
$ cd /usr/local/src/nginx-1.16.1/ # 我的nginx源码在/usr/local/src/下
$ sudo ./configure --prefix=/usr/local/nginx --with-http_realip_module --with-http_addition_module --with-http_ssl_module --with-http_gzip_static_module --with-http_secure_link_module --with-http_stub_status_module --with-stream --with-pcre=/usr/local/src/pcre-8.44 --with-zlib=/usr/local/src/zlib-1.2.11 --with-openssl=/usr/local/src/openssl-1.1.1g --add-module=/home/test/project/ngx_http_pagecount_module # 我的自定义模块在 /home/test/project/ngx_http_pagecount_module 下
这里有一点注意的是,我在 configure 时加入了 pcre、zlib 和 openssl,如果没有这几个软件的目录会配置失败,如果不想安装可以在 configure 的参数中去掉这几个目录。
--add-module=
后接的是自定义模块文件夹的路径,需要根据自己的实际路径来修改。
结果如下:
checking for OS
+ Linux 5.4.72-microsoft-standard-WSL2 x86_64
checking for C compiler ... found
+ using GNU C compiler
+ gcc version: 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
checking for gcc -pipe switch ... found
...省略很多行
checking for getaddrinfo() ... found
configuring additional modules
adding module in /home/test/project/ngx_http_pagecount_module
+ ngx_http_pagecount_module was configured
creating objs/Makefile
Configuration summary
+ using PCRE library: /usr/local/src/pcre-8.44
+ using OpenSSL library: /usr/local/src/openssl-1.1.1g
+ using zlib library: /usr/local/src/zlib-1.2.11
nginx path prefix: "/usr/local/nginx"
nginx binary file: "/usr/local/nginx/sbin/nginx"
nginx modules path: "/usr/local/nginx/modules"
nginx configuration prefix: "/usr/local/nginx/conf"
nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
nginx pid file: "/usr/local/nginx/logs/nginx.pid"
nginx error log file: "/usr/local/nginx/logs/error.log"
nginx http access log file: "/usr/local/nginx/logs/access.log"
nginx http client request body temporary files: "client_body_temp"
nginx http proxy temporary files: "proxy_temp"
nginx http fastcgi temporary files: "fastcgi_temp"
nginx http uwsgi temporary files: "uwsgi_temp"
nginx http scgi temporary files: "scgi_temp"
可以在其中找到这几行:
configuring additional modules
adding module in /home/test/project/ngx_http_pagecount_module
+ ngx_http_pagecount_module was configured
表示已经成功配置自定义模块的相关参数,在以下目录可以找到相应的模块:
$ cat /usr/local/src/nginx-1.16.1/objs/ngx_modules.c
...省略
extern ngx_module_t ngx_http_pagecount_module;
...省略
ngx_module_t *ngx_modules[] = {
...省略
&ngx_http_pagecount_module,
...省略
}
char *ngx_module_names[] = {
...省略省略
"ngx_http_pagecount_module",
...省略
}
自定义模块
我们准备好编译环境后,就可以开始定义自己的 HTTP 模块。在编写 HTTP 模块之前,首先应该考虑的一点是自己的模块应该介入 HTTP 模块的哪一个阶段,我们要编写的是页面访问次数的统计,意味着我们在 HTTP 请求寻找到相应的 location 配置之后就可以介入,因为我们只需要知道请求的 IP 地址。
我们会有以下的几个部分来依次编写访问统计模块:
- 模块定义
- HTTP 初始化
- 模块配置
模块定义
首先 Nginx 对于模块的定义有统一的写法,模块类型对象名为 ngx_module_t
,是 ngx_module_s
的宏定义,其原型为:
struct ngx_module_s {
ngx_uint_t ctx_index;
ngx_uint_t index;
char *name;
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t version;
const char *signature;
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);
uintptr_t spare_hook0;
uintptr_t spare_hook1;
uintptr_t spare_hook2;
uintptr_t spare_hook3;
uintptr_t spare_hook4;
uintptr_t spare_hook5;
uintptr_t spare_hook6;
uintptr_t spare_hook7;
};
本模块的模块定义如下:
ngx_module_t ngx_http_pagecount_module = {
NGX_MODULE_V1,
&ngx_http_pagecount_ctx, /* module context */
ngx_http_pagecount_cmd, /* module type */
NGX_HTTP_MODULE, /* module directives */
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_MODULE_V1
是一个宏定义,初始化模块数据结构中的某些变量的值。
ngx_http_pagecount_ctx
作为模块的上下文,来使得不同模块有自己的特定行为。
ngx_http_pagecount_cmd
是用来定义模块配置项,来处理 nginx.conf
中相应内容。
NGX_HTTP_MODULE
表示该模块的类型,页面访问统计模块是 HTTP 模块。
剩下的内容包括初始化和销毁的函数回调,我们都不需要处理,所以为 NULL。
HTTP 模块初始化
HTTP模块初始化是由一个结构体 ngx_http_module_t
来表示,原型为
typedef struct {
// 解析配置文件前
ngx_int_t (*preconfiguration)(ngx_conf_t *cf);
// 完成解析配置文件后
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);
// 创建存储main级别的配置项时的结构体
void *(*create_main_conf)(ngx_conf_t *cf);
// 初始化main级别的配置项
char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
// 创建存储srv级别的配置项时的结构体
void *(*create_srv_conf)(ngx_conf_t *cf);
// 合并main级别和srv级别的同名配置项
char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);
// 创建存储loc级别的配置项时的结构体
void *(*create_loc_conf)(ngx_conf_t *cf);
// 合并srv级别和loc级别的同名配置项
char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;
可以看出该结构体由8个回调函数(函数指针)组成,执行顺序分别是:
create_main_conf、create_srv_conf、create_loc_conf、preconfiguration、init_main_conf、merge_srv_conf、merge_loc_conf、postconfiguration
本模块只使用到 create_loc_conf
回调,也就是在创建 location 配置项前需要初始化。初始化配置具体如下:
static ngx_http_module_t ngx_http_pagecount_ctx = {
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
ngx_http_pagecount_create_loc_conf,
NULL,
};
我们需要定义 ngx_http_pagecount_create_loc_conf
函数,其定义如下:
static void* ngx_http_pagecount_create_loc_conf(ngx_conf_t *cf)
{
ngx_http_pagecount_conf_t *conf;
conf = (ngx_http_pagecount_conf_t *)ngx_pcalloc(cf->pool, sizeof(ngx_http_pagecount_conf_t));
if (conf == NULL) {
return NULL;
}
return conf;
}
我们可以看到,在该函数中我们只是对一个结构体 ngx_http_pagecount_conf_t
进行了内存分配,因此这个函数做的事情很简单,就是初始化自定义模块的全局配置。全局配置 ngx_http_pagecount_conf_t
定义如下:
typedef struct {
ssize_t shmsize;
ngx_slab_pool_t *shpool;
ngx_http_pagecount_shm_t *sh;
} ngx_http_pagecount_conf_t;
其中 shmsize 是分配的共享内存大小,shpool 是 Nginx 的 slab 对象 ngx_slab_pool_t
,是 Nginx 的 slab 共享内存池对象,sh 是自定义模块的共享内存对象,其定义如下:
typedef struct {
ngx_rbtree_t rbtree;
ngx_rbtree_node_t sentinel;
} ngx_http_pagecount_shm_t;
其中 rbtree 是 Nginx 中的红黑树结构,sentinel是红黑树的叶子结点。为什么用到红黑树对象?是由业务决定的,本模块是实现访问统计,根据 IP 来作为 key,value 作为访问次数的这样的一个数据结构。
所以回头再看 ngx_http_pagecount_create_loc_conf
函数,其使用 ngx_pcalloc
为模块全局配置对象 ngx_http_pagecount_conf_t
在 Nginx 内存池分配了内存,并返回该配置对象。
模块配置
最后是真正实现模块功能的地方就是模块的配置,Nginx 专门有一个结构体来定义模块的配置文件参数:
struct ngx_command_s {
// 配置项名称
ngx_str_t name;
// 配置项类型,比如出现在loc还是srv中,参数多少个
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;
};
本模块的配置定义如下:
static ngx_command_t ngx_http_pagecount_cmd[] = {
{
ngx_string("pagecount"),
NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
ngx_http_pagecount_set,
NGX_HTTP_LOC_CONF_OFFSET,
0, NULL
},
ngx_null_command
};
其中 ngx_string("pagecount")
表示本模块的配置名,NGX_HTTP_LOC_CONF
和 NGX_CONF_NOARGS
表示该配置处于 location 并且无参数。ngx_http_pagecount_set
是对本模块配置的回调函数,也就是解析配置文件解析到 count 时,会调用的函数。NGX_HTTP_LOC_CONF_OFFSET
是表明本模块配置的位置处于 http 中的 location,后面的都是填充。配置定义完之后,我们就知道了本模块如何在 nginx.conf 中配置,内容如下(省略其他配置):
http {
...
server {
...
location /test {
pagecount;
}
...
}
...
}
该配置表明了当URL请求为/test时,会调用本模块的 handler。ngx_http_pagecount_set
定义如下:
static char *ngx_http_pagecount_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_shm_zone_t *shm_zone;
ngx_str_t name = ngx_string("pagecount_slab");
ngx_http_core_loc_conf_t *corecf;
ngx_http_pagecount_conf_t *mconf = (ngx_http_pagecount_conf_t*)conf;
mconf->shmsize = 1024*1024;
// 获取 ngx_shm_zone_t
shm_zone = ngx_shared_memory_add(cf, &name, mconf->shmsize, &ngx_http_pagecount_module);
if (shm_zone == NULL) {
return NGX_CONF_ERROR;
}
shm_zone->init = ngx_http_pagecount_shm_zone_init;
shm_zone->data = mconf;
// 注册 handler
corecf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
corecf->handler = ngx_http_pagecount_handler;
return NGX_CONF_OK;
}
在该函数中,总共做了两件事:
- 分配共享内存空间
- 注册 handler,在handler中实现业务逻辑
分配共享内存
为什么要分配共享内存,因为 Nginx 是多进程模型,我们需要统计所有请求的访问,而这些访问会分散到各个不同的进程。因此我们需要有一个公共区域来存储访问量,因此就会用到 Nginx 提供的共享内存。
首先使用 Nginx 提供的接口 ngx_shared_memory_add
,该函数会返回 Nginx 对象 ngx_shm_zone_t
,其原型如下:
struct ngx_shm_zone_s {
void *data;
ngx_shm_t shm;
ngx_shm_zone_init_pt init;
void *tag;
void *sync;
ngx_uint_t noreuse; /* unsigned noreuse:1; */
};
其内容大部分参数都由 ngx_shared_memory_add
填充完毕,只有 data 和 init 需要我们手动填入。其中 init 是共享内存分配时的初始化函数,data 是作为该初始化函数的参数传入 init。本模块的 init 定义如下:
static ngx_int_t ngx_http_pagecount_shm_zone_init(ngx_shm_zone_t *zone, void *data)
{
ngx_http_pagecount_conf_t *conf;
ngx_http_pagecount_conf_t *oconf = data;
conf = (ngx_http_pagecount_conf_t *)zone->data;
// 处理nginx -s reload的情况
if (oconf) {
conf->sh = oconf->sh;
conf->shpool = oconf->shpool;
return NGX_OK;
}
// 分配共享内存
conf->shpool = (ngx_slab_pool_t *)zone->shm.addr;
conf->sh = ngx_slab_alloc(conf->shpool, sizeof(ngx_http_pagecount_shm_t));
if (conf->sh == NULL) {
return NGX_ERROR;
}
conf->shpool->data = conf->sh;
// 初始化红黑树对象
ngx_rbtree_init(&conf->sh->rbtree, &conf->sh->sentinel, ngx_http_pagecount_rbtree_insert_value);
return NGX_OK;
}
这里主要做了两件事情,第一是分配共享内存,第二是初始化红黑树。
其中分配共享内存的时候需要考虑到 Nginx reload 的情况,这种情况下是不需要重新分配共享内存的。ngx_slab_alloc
是真正分配共享内存的函数,其接受的参数除了size之外,还需要一个 Nginx 共享内存池对象 ngx_slab_pool_t
。我们前面使用ngx_shared_memory_add
函数就是为了获得这个参数,该参数就是ngx_shm_zone_t
对象的 shm.addr 成员,其中 shm 是 Nginx 的共享内存对象。
初始化红黑树中我们需要注册一个回调函数 ngx_http_pagecount_rbtree_insert_value
,为什么要自定义插入函数?因为默认的红黑树插入方法是以 IP 地址的哈希值为key,我们需要直接以 IP 作为 key。其定义如下:
static void ngx_http_pagecount_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
ngx_rbtree_node_t **p;
for (;;) {
if (node->key < temp->key) {
p = &temp->left;
} else if (node->key > temp->key) {
p = &temp->right;
} else {
return;
}
if (*p == sentinel) {
break;
}
temp = *p;
}
*p = node;
node->parent = temp;
node->left = sentinel;
node->right = sentinel;
ngx_rbt_red(node);
}
红黑树的插入逻辑并不复杂,这里就不赘述了。主要就是为了实现以 IP 地址为key,访问次数为 value 的红黑树结构。
业务逻辑
整个模块的大体框架前面已经搭建好了,剩下的就是业务逻辑了,也就是实现访问统计。
注册 handler 是为了在每次请求到来之后,都能够执行该 handler,本模块的核心功能都在 handler 中。handler 定义如下:
static ngx_int_t ngx_http_pagecount_handler(ngx_http_request_t *r)
{
u_char html[1024] = {0};
int len = sizeof(html);
ngx_rbtree_key_t key = 0;
struct sockaddr_in *client_addr = (struct sockaddr_in *)r->connection->sockaddr;
key = (ngx_rbtree_key_t)client_addr->sin_addr.s_addr;
ngx_http_pagecount_conf_t *conf = ngx_http_get_module_loc_conf(r, ngx_http_pagecount_module);
// 记录访问量
ngx_shmtx_lock(&conf->shpool->mutex);
ngx_http_pagecount_rbtree_lookup(r, conf, key);
ngx_shmtx_unlock(&conf->shpool->mutex);
// 构造 HTML
ngx_encode_http_page_rb(conf, (char*)html);
// HTTP header
r->headers_out.status = 200;
ngx_str_set(&r->headers_out.content_type, "text/html");
ngx_http_send_header(r);
// HTTP body
ngx_buf_t *b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
ngx_chain_t out;
out.buf = b;
out.next = NULL;
b->pos = html;
b->last = html + len;
b->memory = 1;
b->last_buf = 1;
return ngx_http_output_filter(r, &out);
}
handler 总共分了三个部分:
- 记录访问量(业务逻辑)
- 构造 HTML 页面
- 发送 HTML 响应
记录访问量
首先是通过 ngx_http_pagecount_rbtree_lookup
来查找红黑树中是否有该请求 IP 地址的记录,如果没有就新增该记录,否则在原纪录的基础上加1。
其实现如下:
static ngx_uint_t ngx_http_pagecount_rbtree_lookup(ngx_http_request_t *r, ngx_http_pagecount_conf_t *conf, ngx_uint_t key)
{
ngx_rbtree_node_t *node, *sentinel;
node = conf->sh->rbtree.root;
sentinel = conf->sh->rbtree.sentinel;
while (node != sentinel) {
if (key < node->key) {
node = node->left;
} else if (key > node->key) {
node = node->right;
} else {
// 找到记录
node->data++;
return NGX_OK;
}
}
// 分配共享内存
node = ngx_slab_alloc_locked(conf->shpool, sizeof(ngx_rbtree_node_t));
if (node == NULL) {
return NGX_ERROR;
}
// 插入结点
node->key = key;
node->data = 1;
ngx_rbtree_insert(&conf->sh->rbtree, node);
return NGX_OK;
}
这里需要注意的一点是,该共享内存是临界资源,存在竞争的情况,因此在内存分配的时候需要上锁,通过使用 Nginx 提供的函数 ngx_shmtx_lock
、ngx_shmtx_unlock
。如果找到 key,value 加一后直接返回,否则通过 ngx_rbtree_insert
将新的结点插入到红黑树中。
构造页面
构造页面比较简单,取出的红黑树中的数据构造 HTML 即可:
static int ngx_encode_http_page_rb(ngx_http_pagecount_conf_t *conf, char *html)
{
sprintf(html, "<h1>PageCount</h1>");
strcat(html, "<h2>");
// 从最小值开始
ngx_rbtree_node_t *node = ngx_rbtree_min(conf->sh->rbtree.root, conf->sh->rbtree.sentinel);
// 遍历红黑树
do {
char str[INET_ADDRSTRLEN] = {0};
char buffer[128] = {0};
sprintf(buffer, "req from %s, count %d<br/>", inet_ntop(AF_INET, &node->key, str, sizeof(str)), node->data);
strcat(html, buffer);
node = ngx_rbtree_next(&conf->sh->rbtree, node);
} while(node);
strcat(html, "</h2>");
return NGX_OK;
}
这里通过 ngx_rbtree_min
函数取到最小值,然后依次遍历整个红黑树,生成相应 HTML,大致如下:
<h1>PageCount</h1>
<h2>
...
req from 0.0.0.0, count 1
req from 10.10.10.10, count 1
...
</h2>
发送 HTTP 响应
最后发送 HTTP 响应,先构造 HTTP 响应头,只需设置状态码 200,类型 text/html 即可。其次构造 HTTP 响应体,Nginx 中 HTTP Body 是由 ngx_buf_t
结构来表示,需要先分配一个 ngx_buf_t b
,该数据结构用来处理大数据,b->pos 指向 html 首指针,b->last 指向 html 尾指针,表面希望 Nginx 处理全部 html 内容,b->memory 置 1,表示这段内存只读,b->last_buf 置 1 表示这是最后一块缓冲区。然后定义 ngx_chain_t
将 b 作为链表结点,通过调用 ngx_http_output_filter
来将其作为 output 过滤器串到过滤器的链表上,Nginx 会发送包体出去。
编译和结果
我们执行make && make install 操作:
$ sudo make && sudo make install
需要耐心等待一段时间,如果没有报错则编译成功,否则需要检查源代码语法和内容的错误。
编译完成后启动 Nginx:
$ /usr/local/nginx/sbin/nginx -c /usr/local/conf/nginx.conf
如果 Nginx 之前已经启动,需要先关闭。
然后输入 Nginx 配置好的地址和端口号,并加上 /test,我的 URL 是 172.17.61.172/test,效果如下:
总结
本次从头到尾手撸了一个 Nginx HTTP 访问统计模块,首先对 Nginx 模块进行了简单的描述,重点介绍了 HTTP 模块和其 11 个阶段。在编写自定义模块之前需要了解 如何将模块编译到 Nginx 中,Nginx 提供了一套简单的方法。自定义模块编写的时候需要首先了解模块如何定义,HTTP 模块的初始化,这些在 Nginx 都有统一的方式,我们照葫芦画瓢即可。进行模块的配置的时候,模块的配置决定我们的配置名、参数等信息,决定了 Nginx 解析到配置后做什么,每次请求后作什么。我们最后使用了共享内存+红黑树的方式来存储访问量,这样能高效地记录到所有进程的访问次数。
参考书籍
《深入理解Nginx》
(全文完)