一起写一个 Nginx 访问统计模块

前言

Nginx 是一款强大的web服务器,是由核心模块和其他的一系列模块一起组成,其高度模块化的设计使得Nginx的扩展和开发十分方便。

Nginx提供了web反向代理服务、email邮件服务、负载均衡、web服务器等功能,其核心模块Nginx core提供了通讯、运行环境,是所有其他模块协作的基石。

本文将会手撸一个访问统计模块,统计每一个 IP 的页面访问次数,业务并不复杂,只是用来学习 Nginx 模块的编写方法。在这之前,先简单介绍下 Nginx 模块的基本理论。

Nginx 模块概述

Nginx 的模块化是非常值得品味的一点,其将各个模块串成一个链表,在每次请求到来的时候依次遍历链表上的所有模块,调用所有的处理函数。比如 upstream模块、事件模块、HTTP 模块等等。其中 HTTP 模块是实现了 HTTP 协议,是 Nginx 中的”大户“,代码规模远超其他模块,其中对 HTTP 优秀的实现值得借鉴。

HTTP 模块概述

HTTP 请求过程主要是以下几个阶段:

  1. 初始化 HTTP Request;
  2. 处理 HTTP Header;
  3. 处理 HTTP Body;
  4. 处理请求相关 Handler;
  5. 处理 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,则会依次执行:

  1. 如果 location 中有配置 random_index_one,随选择一个文件发送到客户端;
  2. 如果 location 中有配置 index,发送 index 指向的文件到客户端;
  3. 如果 location 中有配置 autoindex on,随选择一个文件发送到客户端;
  4. 如果这个 request 对应的 location 上有设置 gzip_static on,那么就查找是否有对应的.gz 文件存在,有的话,就发送这个给客户端(客户端支持 gzip 的情况下)。
  5. 请求的 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_CONFNGX_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_lockngx_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,效果如下:

img

总结

本次从头到尾手撸了一个 Nginx HTTP 访问统计模块,首先对 Nginx 模块进行了简单的描述,重点介绍了 HTTP 模块和其 11 个阶段。在编写自定义模块之前需要了解 如何将模块编译到 Nginx 中,Nginx 提供了一套简单的方法。自定义模块编写的时候需要首先了解模块如何定义,HTTP 模块的初始化,这些在 Nginx 都有统一的方式,我们照葫芦画瓢即可。进行模块的配置的时候,模块的配置决定我们的配置名、参数等信息,决定了 Nginx 解析到配置后做什么,每次请求后作什么。我们最后使用了共享内存+红黑树的方式来存储访问量,这样能高效地记录到所有进程的访问次数。

参考书籍

《深入理解Nginx》


(全文完)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值