nginx 访问统计模块的实现

        本章,我们实现一个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 时,会调用本模块的 handlerngx_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。效果如下:

参考文献:

一起写一个 Nginx 访问统计模块_nginx统计模块_良晨的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值