本章,我们实现一个nginx的访问统计模块,用来统计资源被被某个web访问的次数。
创建访问统计模块
Nginx 提供了很简单的方法来帮助把自己的模块编译到 nginx,方法是把名为 config 的文件放在与自定义模块代码的同一目录下。
我们在 /usr 目录下创建一个 ngx_http_location_count_module 文件夹,文件夹里面创建一个 ngx_http_location_count_module.c 文件和 config 配置文件,目录结构如下:
ngx_http_location_count_module/
├── config
└── ngx_http_location_count_module.c
config 配置文件
config 配置文件需要3个参数:
- ngx_addon_name :一般设置为模块名,执行 configure 时调用
- HTTP_MODULES:保存所有模块内容的变量,相当于源码中的 ngx_modules[] 数组。
- NGX_ADDON_SRCS:自定义模块源码的路径,配置命令中会设置该值
内容如下:
ngx_addon_name=ngx_http_location_count_module
HTTP_MODULES="$HTTP_MODULES ngx_http_location_count_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_location_count_module.c"
ngx_http_location_count_module.c 文件
我们先简单定义下访问统计模块的关键信息,然后简单的测试该模块能够被成功加入ngxin。ngx_http_location_count_module.c 的内容如下:
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
static void* ngx_http_location_count_create_loc_conf(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static char* ngx_http_location_count_create_cmd_set(ngx_conf_t *cf);
static ngx_command_t ngx_http_location_count_cmd[] = {
{
ngx_string("count"),
NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
ngx_http_location_count_create_cmd_set,
NGX_HTTP_LOC_CONF_OFFSET,
0, NULL
},
ngx_null_command
};
static ngx_http_module_t ngx_http_location_count_ctx = {
NULL, //preconfigure
NULL, //postconfigure
NULL, //create main
NULL, //init main
NULL, //create server
NULL, //merge server
ngx_http_location_count_create_loc_conf, //create loc
NULL
};
//ngx_http_location_count_module
ngx_module_t ngx_http_location_count_module = {
NGX_MODULE_V1,
&ngx_http_location_count_ctx,
ngx_http_location_count_cmd,
NGX_HTTP_MODULE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
static void* ngx_http_location_count_create_loc_conf(ngx_conf_t *cf)
{
}
static char* ngx_http_location_count_create_cmd_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
}
将统计模块加入nginx
接下来,我们将编译nginx所依赖的几个公共库放到指定位置。在这里,我在 /usr 目录下放置以下几个开源软件:openssl-1.1.0、pcre-8.41、zlib-1.2.11
然后进入到ngxin源码目录 /usr/nginx-1.14.1 下,执行如下命令进行nginx配置命令,命令中会指定统计模块源码的路径:
[root@localhost nginx]# cd /usr/nginx-1.14.1/
[root@localhost nginx-1.14.1]# ./configure --prefix=/usr/local/nginx --with-http_realip_module --with-http_addition_module --with-http_gzip_static_module --with-http_secure_link_module --with-http_stub_status_module --with-stream --with-pcre=/usr/pcre-8.41 --with-zlib=/usr/zlib-1.2.11 --with-openssl=/usr/openssl-1.1.0 --add-module=/usr/ngx_http_location_count_module
配置成功之后,会看下统计模块 ngx_http_location_count_module 已被成功加入到nginx:
我们查看 /usr/nginx-1.14.1/objs/ngx_modules.c,搜索 ngx_http_location_count_module,也可以看到改模块被加入到代码中了:
接着,我们执行make编译nginx源码,将我们的过滤模块也一起编译:
[root@localhost nginx-1.14.1]# cd /usr/nginx-1.14.1/
[root@localhost nginx-1.14.1]# make
访问统计模块的设计
Nginx 的模块化是将各个模块串成一个链表,在每次请求到来的时候依次遍历链表上的所有模块,调用所有的处理函数。比如 upstream模块、事件模块、HTTP 模块等等。其中 HTTP 模块是实现了 HTTP 协议。
模块的定义
在编写 HTTP 模块之前,首先应该考虑的一点是自己的模块应该介入 HTTP 模块的11个阶段中的哪一个阶段。由于我们要编写的是页面访问次数的统计,意味着我们在 HTTP 请求寻找到相应的 location 配置之后就可以介入,因为我们只需要知道请求的 IP 地址。
下面是访问统计模块的定义:
ngx_module_t ngx_http_location_count_module = {
//宏定义,初始化模块数据结构中的某些变量的值
NGX_MODULE_V1,
//模块的上下文,来使得不同模块有自己的特定行为。
&ngx_http_location_count_ctx,
//定义模块配置项,来处理 nginx.conf 中相应内容
ngx_http_location_count_cmd,
//定义模块配置项,来处理 nginx.conf 中相应内容。
NGX_HTTP_MODULE,
//剩下的内容包括初始化和销毁的函数回调,我们都不需要处理,所以为 NULL
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
模块初始化的定义
HTTP模块的初始化由 ngx_http_location_count_module 的成员 ngx_http_location_count_ctx 来完成。该成员是一个 ngx_http_module_t 类型的结构体变量,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 配置项前需要初始化,因此 ngx_http_location_count_ctx 的初始化如下:
static ngx_http_module_t ngx_http_location_count_ctx = {
NULL, //preconfigure
NULL, //postconfigure
NULL, //create main
NULL, //init main
NULL, //create server
NULL, //merge server
ngx_http_location_count_create_loc_conf, //create loc
NULL
};
ngx_http_location_count_create_loc_conf() 函数的定义如下:
static void* ngx_http_location_count_create_loc_conf(ngx_conf_t *cf)
{
//初始化计自定义模块的全局配置conf
ngx_http_location_count_conf_t *conf = ngx_palloc(cf->pool, sizeof(ngx_http_location_count_conf_t));
if (NULL == conf)
{
return NULL;
}
//打印调试信息
ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno, "ngx_http_location_count_create_loc_conf");
return conf;
}
可见,该函数只是对结构体 ngx_http_location_count_conf_t 进行内存分配,作用很简单。
下面是跟 ngx_http_location_count_create_loc_conf() 函数有关的结构体解释。
typedef struct {
ngx_rbtree_t rbtree; //保存访问者的IP和访问次数
ngx_rbtree_node_t sentinel; //红黑树叶子节点
} ngx_http_location_count_shm_t;
typedef struct {
ngx_slab_pool_t *sbpool; //共享内存池对象
ssize_t shmsize; //分配的共享内存的大小
ngx_http_location_count_shm_t *shm; //自定义模块的共享内存
//ngx_uint_t interval;
} ngx_http_location_count_conf_t;
这样定义结构体的原因是:为统计模块申请一块共享内存,然后在共享内存里保存一颗红黑树。红黑树保存所有的访问记录,用IP作为key,访问次数作为value。
模块配置
真正实现模块功能的地方就是模块的配置,对于本模块来说,就是模块成员 ngx_http_location_count_cmd ,其定义如下:
static ngx_command_t ngx_http_location_count_cmd[] = {
{
ngx_string("count"),
//表示该配置处于 location 并且无参数
NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
//模块配置的回调函数
ngx_http_location_count_create_cmd_set,
//模块配置的位置处于 http 中的 location,后面的都是填充
NGX_HTTP_LOC_CONF_OFFSET,
0, NULL
},
ngx_null_command
};
其中 ngx_string("count")
表示本模块的配置名,ngx_http_location_count_create_cmd_set
是对本模块配置的回调函数,也就是解析配置文件解析到 count 时,会调用的函数。
配置定义完之后,我们就知道了如何在 nginx.conf 中配置本模块,内容如下:
http {
...
server {
...
location /test {
count;
}
...
}
...
}
该配置表明,当URL请求为 /test 时,会调用本模块的 handler。ngx_http_pagecount_set
定义如下:
static char* ngx_http_location_count_create_cmd_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_shm_zone_t *shm_zone;
ngx_str_t name = ngx_string("http_location_count_slab");
ngx_http_core_loc_conf_t *corecf;
ngx_http_location_count_conf_t *lconf = (ngx_http_location_count_conf_t*)conf;
lconf->shmsize = 1024 * 1024;
//分配共享内存空间,获取 ngx_shm_zone_t
shm_zone = ngx_shared_memory_add(cf, &name, lconf->shmsize, &ngx_http_location_count_module);
if (shm_zone == NULL)
{
return NGX_CONF_ERROR;
}
shm_zone->init = ngx_http_location_count_shm_zone_init;
shm_zone->data = lconf;
//获取HTTP模块的loc级别的配置
corecf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
//注册 handler
corecf->handler = ngx_http_location_count_handler;
ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno, "ngx_http_location_count_create_cmd_conf");
return NGX_CONF_OK;
}
该函数中,需要注意以下2点:
- 为模块分配共享内存空间
- 注册handler,在handler中实现业务逻辑
为模块分配共享内存空间
为什么要分配共享内存呢?由于ngxin是多进程的,多个访问请求可能会被多个进程处理。因此需要有一个公共区域来存储访问量,因此就会用到 Nginx 提供的共享内存。
首先使用 Nginx 提供的接口 ngx_shared_memory_add
,该函数会返回 ngx_shm_zone_t
类型变量,其内容大部分参数都由 ngx_shared_memory_add
填充完毕,只有 成员 data 和 init 需要我们手动填入。其中 init 是函数指针,用来初始化刚分配的共享内存,在本模块中就是ngx_http_location_count_shm_zone_init();data 是 init 函数的参数。本模块的 init 定义如下:
static ngx_int_t ngx_http_location_count_shm_zone_init(ngx_shm_zone_t *zone, void *data)
{
ngx_http_location_count_conf_t *conf;
ngx_http_location_count_conf_t *oconf = data;
conf = (ngx_http_location_count_conf_t*)zone->data;
// 若是 nginx -s reload 情况,则不需重新分配内存
if (oconf)
{
conf->shm = oconf->shm;
conf->sbpool = oconf->sbpool;
return NGX_OK;
}
//获取共享内存池的地址
conf->sbpool = (ngx_slab_pool_t*)zone->shm.addr;
//分配共享内存,用来保存红黑树
conf->shm = ngx_slab_alloc(conf->sbpool, sizeof(ngx_http_location_count_shm_t));
if (conf->shm == NULL)
{
return NGX_ERROR;
}
conf->sbpool->data = conf->shm;
//初始化红黑树对象,使用自定义的节点插入函数
ngx_rbtree_init(&conf->shm->rbtree, &conf->shm->sentinel, ngx_http_location_count_rbtree_insert_value);
return NGX_OK;
}
初始化红黑树
初始化红黑树中我们需要注册一个回调函数 ngx_http_location_count_rbtree_insert_value(),为什么要自定义插入函数?因为默认的红黑树插入方法是以 IP 地址的哈希值为 红黑树节点的key,我们需要直接以 IP 作为 key,访问次数为 value。其定义如下:
//自定义红黑树节点插入函数. node为待插入节点
static void ngx_http_location_count_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);
}
业务逻辑
整个模块的大体框架前面已经搭建好了,剩下的就是业务逻辑了,也就是实现访问统计。
注册 handler 是为了在每次请求到来之后,都能够执行该 handler,本模块的核心功能都在 handler 中。handler 定义如下:
//业务处理:统计访问次数并返回结果到客户端
static ngx_int_t ngx_http_location_count_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_location_count_conf_t *conf = ngx_http_get_module_loc_conf(r, ngx_http_location_count_module);
// 记录访问量.需要先对共享内存加锁,防止多进程错误写入
ngx_shmtx_lock(&conf->sbpool->mutex);
ngx_http_location_count_rbtree_lookup(r, conf, key);
ngx_shmtx_unlock(&conf->sbpool->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);
}
业务逻辑分为3个小部分,分别是:
- 统计访问次数
- 构造 HTML 页面
- 发送 HTML 响应
统计访问次数
首先通过 ngx_http_location_count_rbtree_lookup() 函数实现统计访问次数,该函数先查找红黑树中是否有该请求 IP 地址的记录,如果没有,则通过 ngx_rbtree_insert() 将该记录作为新的结点插入到红黑树中;否则在原记录的基础上加1。
需要注意的一点是,该共享内存是临界资源,存在竞争的情况,因此在内存分配的时候需要上锁,通过使用 Nginx 提供的函数 ngx_shmtx_lock 、ngx_shmtx_unlock 。
//自定义红黑树查找函数。key为待查找节点的key
static ngx_uint_t ngx_http_location_count_rbtree_lookup(ngx_http_request_t *r, ngx_http_location_count_conf_t *conf, ngx_uint_t key)
{
ngx_rbtree_node_t *node, *sentinel;
node = conf->shm->rbtree.root;
sentinel = conf->shm->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->sbpool, sizeof(ngx_rbtree_node_t));
if (node == NULL) {
return NGX_ERROR;
}
// 插入结点
node->key = key;
node->data = 1;
//会调用ngx_http_location_count_rbtree_insert_value()
ngx_rbtree_insert(&conf->shm->rbtree, node);
return NGX_OK;
}
构造 HTML 页面
构造页面比较简单,取出的红黑树中的数据构造 HTML 即可:
//构造ngx返回客户端的html页面
static int ngx_encode_http_page_rb(ngx_http_location_count_conf_t *conf, char *html)
{
sprintf(html, "<h1>Http_Location_Count</h1>");
strcat(html, "<h2>");
// 从最小值开始
ngx_rbtree_node_t *node = ngx_rbtree_min(conf->shm->rbtree.root, conf->shm->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->shm->rbtree, node);
} while(node);
strcat(html, "</h2>");
return NGX_OK;
}
这里通过 ngx_rbtree_min() 函数取到最小值,然后依次遍历整个红黑树,生成相应的 HTML,其格式大致如下:
<h1>Http_Location_Count</h1>
<h2>
...
req from 0.0.0.0, count 1
req from 10.10.10.10, count 1
...
</h2>
发送 HTML 响应
最后发送 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 会发送包体出去。
访问统计模块的运行
完整代码
ngx_http_location_count_module.c
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
static void* ngx_http_location_count_create_loc_conf(ngx_conf_t *cf);
static char* ngx_http_location_count_create_cmd_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static ngx_int_t ngx_http_location_count_handler(ngx_http_request_t *r);
static void ngx_http_location_count_rbtree_insert_value(ngx_rbtree_node_t *tmp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
static ngx_int_t ngx_http_location_count_shm_zone_init(ngx_shm_zone_t *zone, void *data);
typedef struct {
ngx_rbtree_t rbtree; //保存访问者的IP和访问次数
ngx_rbtree_node_t sentinel; //红黑树叶子节点
} ngx_http_location_count_shm_t;
typedef struct {
ngx_slab_pool_t *sbpool; //共享内存池对象
ssize_t shmsize; //分配的共享内存的大小
ngx_http_location_count_shm_t *shm; //自定义模块的共享内存
//ngx_uint_t interval;
} ngx_http_location_count_conf_t;
static ngx_command_t ngx_http_location_count_cmd[] = {
{
ngx_string("count"),
NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
ngx_http_location_count_create_cmd_set,
NGX_HTTP_LOC_CONF_OFFSET,
0, NULL
},
ngx_null_command
};
static ngx_http_module_t ngx_http_location_count_ctx = {
NULL, //preconfigure
NULL, //postconfigure
NULL, //create main
NULL, //init main
NULL, //create server
NULL, //merge server
ngx_http_location_count_create_loc_conf, //create loc
NULL
};
//ngx_http_location_count_module
ngx_module_t ngx_http_location_count_module = {
//宏定义,初始化模块数据结构中的某些变量的值
NGX_MODULE_V1,
//模块的上下文,来使得不同模块有自己的特定行为。
&ngx_http_location_count_ctx,
//定义模块配置项,来处理 nginx.conf 中相应内容
ngx_http_location_count_cmd,
//定义模块配置项,来处理 nginx.conf 中相应内容。
NGX_HTTP_MODULE,
//剩下的内容包括初始化和销毁的函数回调,我们都不需要处理,所以为 NULL
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NGX_MODULE_V1_PADDING
};
static int ngx_encode_http_page_rb(ngx_http_location_count_conf_t *conf, char *html);
static void* ngx_http_location_count_create_loc_conf(ngx_conf_t *cf)
{
//初始化计自定义模块的全局配置conf
ngx_http_location_count_conf_t *conf = ngx_palloc(cf->pool, sizeof(ngx_http_location_count_conf_t));
if (NULL == conf)
{
return NULL;
}
//打印调试信息
ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno, "ngx_http_location_count_create_loc_conf");
return conf;
}
static char* ngx_http_location_count_create_cmd_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_shm_zone_t *shm_zone;
ngx_str_t name = ngx_string("http_location_count_slab");
ngx_http_core_loc_conf_t *corecf;
ngx_http_location_count_conf_t *lconf = (ngx_http_location_count_conf_t*)conf;
lconf->shmsize = 1024 * 1024;
//获取 ngx_shm_zone_t
shm_zone = ngx_shared_memory_add(cf, &name, lconf->shmsize, &ngx_http_location_count_module);
if (shm_zone == NULL)
{
return NGX_CONF_ERROR;
}
shm_zone->init = ngx_http_location_count_shm_zone_init;
shm_zone->data = lconf;
//获取HTTP模块的loc级别的配置
corecf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
//注册 handler
corecf->handler = ngx_http_location_count_handler;
ngx_log_error(NGX_LOG_EMERG, cf->log, ngx_errno, "ngx_http_location_count_create_cmd_conf");
return NGX_CONF_OK;
}
static ngx_int_t ngx_http_location_count_shm_zone_init(ngx_shm_zone_t *zone, void *data)
{
ngx_http_location_count_conf_t *conf;
ngx_http_location_count_conf_t *oconf = data;
conf = (ngx_http_location_count_conf_t*)zone->data;
// 处理 nginx -s reload 情况
if (oconf)
{
conf->shm = oconf->shm;
conf->sbpool = oconf->sbpool;
return NGX_OK;
}
//分配共享内存
conf->sbpool = (ngx_slab_pool_t*)zone->shm.addr;
conf->shm = ngx_slab_alloc(conf->sbpool, sizeof(ngx_http_location_count_shm_t));
if (conf->shm == NULL)
{
return NGX_ERROR;
}
conf->sbpool->data = conf->shm;
//初始化红黑树对象,使用自定义的节点插入函数
ngx_rbtree_init(&conf->shm->rbtree, &conf->shm->sentinel, ngx_http_location_count_rbtree_insert_value);
return NGX_OK;
}
//自定义红黑树节点插入函数. node为待插入节点
static void ngx_http_location_count_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);
}
//自定义红黑树查找函数。key为待查找节点的key
static ngx_uint_t ngx_http_location_count_rbtree_lookup(ngx_http_request_t *r, ngx_http_location_count_conf_t *conf, ngx_uint_t key)
{
ngx_rbtree_node_t *node, *sentinel;
node = conf->shm->rbtree.root;
sentinel = conf->shm->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->sbpool, sizeof(ngx_rbtree_node_t));
if (node == NULL) {
return NGX_ERROR;
}
// 插入结点
node->key = key;
node->data = 1;
//会调用ngx_http_location_count_rbtree_insert_value()
ngx_rbtree_insert(&conf->shm->rbtree, node);
return NGX_OK;
}
//业务处理:统计访问次数并返回结果到客户端
static ngx_int_t ngx_http_location_count_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_location_count_conf_t *conf = ngx_http_get_module_loc_conf(r, ngx_http_location_count_module);
// 记录访问量.需要先对共享内存加锁,防止多进程错误写入
ngx_shmtx_lock(&conf->sbpool->mutex);
ngx_http_location_count_rbtree_lookup(r, conf, key);
ngx_shmtx_unlock(&conf->sbpool->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);
}
//构造ngx返回客户端的html页面
static int ngx_encode_http_page_rb(ngx_http_location_count_conf_t *conf, char *html)
{
sprintf(html, "<h1>Http_Location_Count</h1>");
strcat(html, "<h2>");
// 从最小值开始
ngx_rbtree_node_t *node = ngx_rbtree_min(conf->shm->rbtree.root, conf->shm->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->shm->rbtree, node);
} while(node);
strcat(html, "</h2>");
return NGX_OK;
}
执行模块
编译模块
[root@localhost nginx-1.14.1]# cd /usr/nginx-1.14.1/
[root@localhost nginx-1.14.1]# make && make install
启动nginx
在执行下面的启动命令前,确保nginx尚未启动。
[root@localhost nginx]# cd /usr/local/nginx/
[root@localhost nginx]# ./sbin/nginx -c ./conf/nginx.conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
nginx: [emerg] ngx_http_location_count_create_cmd_conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
nginx: [emerg] ngx_http_location_count_create_loc_conf
[root@localhost nginx]#
执行结果
在浏览器中输入配置好的IP,并加上/test。效果如下:
参考文献: