• Nginx:handler 模块的实现


    Nginx 内部结构是由核心部分和一系列功能模块所组成的,每个模块的功能相对简单,便于开发,同时也便于对系统进行功能扩展。Nginx 将各功能模块组织成一条链,当有请求到达的时候,请求依次经过这条链上的部分或者全部模块,进行处理。每个模块实现特定的功能。

    handler 模块就是接受来自客户端的请求并产生输出的模块,例如 ngx_http_static_module 模块,负责客户端的静态页面请求处理并将对应的磁盘文件准备为响应内容输出。handler 模块的处理结果通常有三种:处理成功,处理失败(发生错误)和拒绝处理。

    1、模块的分类

    nginx 的模块根据功能可分为

    • event module:事件模块,搭建了独立于操作系统的事件处理机制的框架,及提供了各具体事件的处理。包括 ngx_events_module, ngx_event_core_module和ngx_epoll_module 等。Nginx 具体使用何种事件处理模块,这依赖于具体的操作系统和编译选项。
    • phase handler:handler 模块,主要负责处理客户端请求并产生待响应内容,
    • output filter:过滤模块,仅处理服务器发送给客户端的 http 响应。
    • upstream: upstream 模块,实现反向代理的功能,将真正的请求转发到后端服务器上,并从后端服务器上读取响应,发回客户端。upstream 模块是一种特殊的 handler,只不过响应内容不是真正由自己产生的,而是从后端服务器上读取的。
    • load-balancer: 负载均衡模块,实现特定的算法,在众多的后端服务器中,选择一个服务器出来作为某个请求的转发服务器。

    最常用的是 handler,filter,load-balancer。

    2、模块的基本结构

    2.1、模块配置结构

    定义该模块的配置结构来存储配置项(配置命令)。Nginx 的配置信息分为三个作用域 main, server, location,每个模块提供的配置命令需要定义不同的模块配置结构来存储。

    typedef struct {
    	// 定义配置命令 
    }ngx_http_loc_conf_t;
    
    • 1
    • 2
    • 3

    2.2、模块配置命令

    commands 数组用于定义模块的配置文件参数,每一个数组元素都是 ngx_command_t 类型,用于解析一个配置项,数组的结尾以 ngx_null_command 表示。Nginx 在解析配置文件中的一个配置项时会首先遍历所有的模块,即通过遍历 commands 数组,当遍历到 ngx_null_command 时,会停止使用当前模块解析该配置。

    typedef struct ngx_command_s         ngx_command_t;
    // ngx 命令
    struct ngx_command_s {
        // 配置项名称,"listen"
        ngx_str_t             name; 
        // 配置项类型,配置指令属性合集,指定配置项的位置和携带的参数个数
        ngx_uint_t            type; 
        // 配置项参数处理方法
        char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
        // 在配置文件中的偏移量
        ngx_uint_t            conf;
        // 使用预设的解析方法解析配置项
        ngx_uint_t            offset;
        // 配置项读取后的处理方法,必须是 ngx_conf_post_t 结构的指针
        void                 *post;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    当某个配置块出现对应的配置项(配置命令)时,Nginx 回调 set 指向的回调方法,用于解析命令的处理方法。在 set 方法中获取配置信息,设置 handler 回调阶段和回调方法,例如

    static char *ngx_http_test_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {
        ngx_http_core_loc_conf_t *clcf;
        
        // 找到 conf 所属的配置块
        clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
        
        // 在 NGX_HTTP_CONTENT_PHASE 阶段,调用该请求
        clcf->handler = ngx_http_test_handler;
        
    	return NGX_CONF_OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.3、模块上下文结构

    定义 ngx_http_module_t 类型的静态变量,提供一组回调函数指针,指定 8 个阶段的上下文处理方法,也就是该类模块的公共接口。

    // http 模块定义的8个阶段(回调函数),调用顺序与定义顺序不同
    typedef struct {
        // 配置文件
        ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);    // 解析配置文件前调用
        ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);   // 解析配置文件后调用
    
        // main 级别的配置项(直属于 http 块配置的配置项),全局配置项
        // 创建用于存储 main 级别配置项的数据结构
        void       *(*create_main_conf)(ngx_conf_t *cf); 
        // 初始化 main 级别配置项
        char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);  
    
        // srv 级别的配置项(直属于 server 块配置的配置项)
        // 创建用于存储 srv 级别配置项的数据结构
        void       *(*create_srv_conf)(ngx_conf_t *cf);
        // 合并main级别与srv级别下的同名配置项
        char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);  
    
        // loc 级别的配置项(直属于 location 块配置的配置项)
        // 创建用于存储 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2.4、模块的定义

    struct ngx_module_s {
        // 宏定义:预设值 NGX_MODULE_V1
        ngx_uint_t            ctx_index; // 当前模块在该类模块中的序号,优先级和模块位置
        ngx_uint_t            index;     // 当前模块在ngx_modules数组(所有模块)中的序号
     	ngx_uint_t            spare0;	 // 未使用
        ngx_uint_t            spare1;	 // 未使用
        ngx_uint_t            spare2;	 // 未使用
        ngx_uint_t            spare3;	 // 未使用
        ngx_uint_t            version;	 // 版本号
    
        /*  ------ 需要自定义的部分 ------ */ 
        void                 *ctx;      // 指向一类模块的上下文,指向特定模块的公共接口
        ngx_command_t        *commands; // 处理 nginx.conf 中的配置项
        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);
        /*  ------------------------------ */ 
    
        // 宏定义:NGX_MODULE_V1_PADDING 预设值,未使用
        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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    3、http 请求处理

    http 请求处理的流程分为:

    • http 服务初始化:模块初始化,事件初始化,http 会话建立
    • http 请求解析:解析请求行,解析请求头
    • http 请求处理:请求处理的 11 个阶段,调用 phase handler。
    • 解析请求体:由业务模块自行选择处理,丢弃或读取。
    • http 请求响应
    • 结束 http 响应

    这里只介绍与 handler 模块有关的 http 请求处理阶段,其他阶段见文末参考资料。

    3.1、请求处理阶段

    Nginx 的模块化设计使得每一个 http 模块可以仅专注于完成一个独立的、简单的功能,而一个请求的完整处理过程可以由多个 http 模块共同合作完成。为了灵活指定 http 模块的流水顺序,http 框架依据常见的处理流程将处理阶段划分为 11 个阶段,其中每个处理阶段都可以由任意多个 http 模块流水式处理请求,模块必须按照其定义的顺序执行。

    // http 阶段处理 phase handler
    // 7个阶段允许用户处理,其余阶段仅由 http 框架实现
    typedef enum {
        NGX_HTTP_POST_READ_PHASE = 0,   // 1、获取请求内容阶段:接收到完整的http头部处理
        NGX_HTTP_SERVER_REWRITE_PHASE,  // 2、请求地址重写阶段: srv 级别
        NGX_HTTP_FIND_CONFIG_PHASE,     // 配置查找阶段
        NGX_HTTP_REWRITE_PHASE,         // 3、请求地址重写阶段:loc 级别
        NGX_HTTP_POST_REWRITE_PHASE,    // 请求地址重写提交阶段:
        NGX_HTTP_PREACCESS_PHASE,       // 4、访问权限检查准备阶段
        NGX_HTTP_ACCESS_PHASE,          // 5、访问权限检查阶段
        NGX_HTTP_POST_ACCESS_PHASE,     // 访问权限提交阶段
        NGX_HTTP_PRECONTENT_PHASE,     
        NGX_HTTP_CONTENT_PHASE,         // 6、处理 http 请求内容阶段
        NGX_HTTP_LOG_PHASE              // 7、日志模块处理阶段
    } ngx_http_phases;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    除了 4 个阶段不允许挂载任何的 handler 外,其他阶段都可以挂载模块,见代码块注释。一般情况下,自定义的模块大多挂载到 NGX_HTTP_CONTENT_PHASE阶段。挂载的动作一般是在模块上下文调用的 postconfiguration 函数中。

    3.2、获取用户请求

    获取请求

    typedef struct ngx_http_posted_request_s  ngx_http_posted_request_t;
    struct ngx_http_request_s {
        ...
        ngx_uint_t method;        // 响应方法,NGX_HTTP_GET 等
        ngx_str_t uri;            // 用户请求的 uri   
        ...
        ngx_http_headers_in_t             headers_in;   // 获取请求头
        ngx_http_headers_out_t            headers_out;  // 设置响应头
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.3、发送响应

    发送 http 响应头

    ngx_int_t ngx_http_send_header(ngx_http_request_t *r)
    
    • 1

    例:

    // 设置响应头
    r->headers_out.status = 200;
    ngx_str_set(&r->headers_out.content_type, "text/html");
    // 发送响应头
    ngx_http_send_header(r);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    发送 http 响应体

    ngx_int_t ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in);
    
    • 1

    Nginx 是异步服务器,不能在进程的栈里分配内存并将其作为包体发送,当 ngx_http_output_filter 方法返回时,可能由于 tcp 连接上的缓冲区还不可写,导致 ngx_buf_t 缓冲区指向的内存还没有发送,这时控制返回给 Nginx,会导致栈里的内存被释放,造成内存越界错误。

    // 分配内存,封装 buf_t
    ngx_buf_t *b = ngx_pcalloc(r->pool,  sizeof(ngx_buf_t));
    b->pos = html;
    b->last = html + len;
    b->memory = 1;
    b->last_buf = 1;
    
    // 封装 chain_t 
    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;
    
    // 发送响应体
    ngx_http_output_filter(r, &out);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4、例:流量限制模块

    限制过于某 ip 频繁的访问。为了实现这样的模块,首先来自同一个 IP 的多次 TCP 连接有可能会进入不同的 worker 进程,需要用共享内存来存放用户的访问记录。为了高效增删改查访问记录,可以选用 nginx 的红黑树来存放,其关键字就是 IP + URL 的字符串,而值记录了上次成功访问的时间。这样请求到来时,以 IP + URL 组成的字符串为关键字查询红黑树,没有查询到或查到后发现上次访问的时间距现在大于某个阈值,则允许访问,同时将该键值对插入红黑树;反之,若查到了且上次访问的时间距现在小于某个阈值,则拒绝访问。

    考虑到共享内存的大小有限,长期运行时如果不考虑回收不活跃的记录,那么一方面红黑树会越发巨大影响效率,另一方面共享内存会很快耗尽,导致分配不出新的结点。所以,所有的结点将通过一个链表连接起来,其插入顺序按 IP + URL 最后一次访问的时间组织,这样就可以从链表的首部插入新访问的记录,从链表尾部取出最后一行记录,从而检查是否需要淘汰出共享内存。由于最后一行一定是最老的记录,如果它不需要被淘汰,也就不需要继续遍历链表了,因此可以提高执行效率。

    关于 Nginx 内存管理,可以阅读我之前写过的一篇文章 Nginx:内存管理

    4.1、操作共享内存

    4.1.1、红黑树

    删除节点调用 ngx_rbtree_delete 方法,由于参数传入节点指针,因此不需要做任何处理。

    插入方法

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    检索方法

    先检测共享内存是否存在访问记录。查找记录时,先比较 hash 值,相同再比较字符串。如果找到了访问记录,则检查上次访问的时间距当前时间差是否超过阈值,超过了则更新上次访问时间,并把记录重新放入到双向链表头部,同时返回 NGX_DECLINED 允许访问;若没有超过阈值,则返回 NGX_HTTP_FORBIDDEN表示拒绝访问

    /**
     * @brief 检索页面统计量
     * 
     * @param r 	http 请求
     * @param conf 	全局配置结构体
     * @param key 	hash(ip + url)
     * @param data 	字符串 ip + url
     * @param len 	字符串 ip + url 的长度
     * @return ngx_int_t 
     */
    static ngx_int_t ngx_http_pagelimit_lookup(ngx_http_request_t *r, ngx_http_pagelimit_conf_t *conf, ngx_uint_t key, u_char* data, size_t len) 
    {
    	
    	size_t size;
    	ngx_int_t rc;
    	ngx_time_t *tp;
    	ngx_msec_t now;
    	ngx_msec_int_t ms;
    	ngx_rbtree_node_t *node, *sentinel;
    	ngx_http_pagelimit_node_t *lr;
    
    	// 获取当前时间
    	tp = ngx_timeofday();
    	now = (ngx_msec_t) (tp->sec * 1000 + tp->msec);
    
    	node = conf->sh->rbtree.root;
    	sentinel = conf->sh->rbtree.sentinel;
    	
    	// 查询访问该页面的key(hash(ip + url))
    	// ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "this just for test-->\n");
    	while (node != sentinel) {
    		if (key < node->key) {
    			node = node->left;
    			continue;
    		} 
    		else if (key > node->key) {
    			node = node->right;
    			continue;
    		}
    		// key = node->key
    		lr = (ngx_http_pagelimit_node_t *) &node->data;
    		// 精确比较 ip + url 字符串
    		rc = ngx_memn2cmp(data, lr->data, len, (size_t)lr->len);
    		ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "lrdata = %s : data = %s\n", lr->data, data); 
    		ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "rc = %d", rc);
    		
    		if (rc == 0) {
    			ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "Mission start!");
    			// 找到当前时间与上次访问时间的时间差
    			ms = (ngx_msec_int_t) (now - lr->last);
    
    			ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "ms = %d, interval = %d", ms, conf->interval);
    			// 判断是否超过阈值
    			if (ms > conf->interval) {
    
    				// 超过阈值,允许访问,则更新这个节点的上次访问时间
    				lr->last = now;
    
    				// 将该节点移动到链表首部
    				ngx_queue_remove(&lr->queue);
    				ngx_queue_insert_head(&conf->sh->queue, &lr->queue);
    
    				// 返回 NGX_DECLINED 表示当前 handler 允许访问,继续往下执行
    				ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "allow read\n");
    				return NGX_DECLINED;
    			}
    			else {
    				// 向客户端返回 403 拒绝访问
    				ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "forbidden read\n");
    				return NGX_HTTP_FORBIDDEN;
    			}
    		}
    
    		node = (rc < 0) ? node->left : node->right;
    	}
    	
    	// 获取连续内存块的长度
    	size = offsetof(ngx_rbtree_node_t, data) + offsetof(ngx_http_pagelimit_node_t, data) + len;
    
    	// 首先尝试淘汰过期 node,以释放出更多共享内存用于申请
    	ngx_http_pagelimit_expire(r, conf);
    
    	// 申请共享内存,由于已经加锁,不需要调用加锁的方法
    	node = ngx_slab_alloc_locked(conf->shpool, size);
    	if (node == NULL) {
    		// 共享内存不足,返回错误
    		// todo: 处理错误方法
    		return NGX_ERROR;	
    	}
    
    	// 初始化红黑树节点
    	node->key = key;
    	lr = (ngx_http_pagelimit_node_t *) &node->data;
    	
    	// 设置上次访问时间
    	lr->last = now;
    	
    	// 设置共享内存和长度
    	lr->len = (u_char) len;
    	ngx_memcpy(lr->data, data, len);
    
    	// 插入红黑树
    	ngx_rbtree_insert(&conf->sh->rbtree, node);
    
    	// 插入链表首部
    	ngx_queue_insert_head(&conf->sh->queue, &lr->queue);
    	
    	// ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "test over-->\n");
    	// 允许访问
    	return NGX_DECLINED;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    4.1.2、双向链表

    双向链表用于删除过期的访问记录,释放共享内存

    static void ngx_http_pagelimit_expire(ngx_http_request_t *r, ngx_http_pagelimit_conf_t *conf) {
    	ngx_queue_t *q;
    	ngx_time_t *tp;
    	ngx_msec_t now;
    	ngx_msec_int_t ms;
    	ngx_rbtree_node_t *node;
    	ngx_http_pagelimit_node_t *lr;
    
    	// 获取当前时间
    	tp = ngx_timeofday();
    	now = (ngx_msec_t) (tp->sec * 1000 + tp->msec);
    
    	// 循环结束条件:链表为空或遇到了不需要淘汰的结点
    	while (1) {
    		///1、链表为空,结束循环
    		if (ngx_queue_empty(&conf->sh->queue)) {
    			return;
    		}
    		// 从链表尾部开始淘汰
    		q = ngx_queue_last(&conf->sh->queue);
    		lr = ngx_queue_data(q, ngx_http_pagelimit_node_t, queue); // ?
    
    		// 从 lr 找到红黑树节点地址
    		node = (ngx_rbtree_node_t *) ((u_char *)lr - offsetof(ngx_rbtree_node_t, data));
    
    		// 获取当前时间与上次访问时间之差
    		ms = (ngx_msec_int_t) (now - lr->last);
    		if (ms < conf->interval) {
    			// 2、若当前结点没有淘汰掉,则后续结点也不需要淘汰
    			return;
    		}
    
    		// 将淘汰结点移出双向链表
    		ngx_queue_remove(q);
    		// 将淘汰节点移出红黑树
    		ngx_rbtree_delete(&conf->sh->rbtree, node);
    
    		// 释放共享内存
    		ngx_slab_free_locked(conf->shpool, node);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    6、

    4.2、编写模块结构

    4.2.1、模块配置结构

    对于每一条访问记录

    • 用户的 ip + url 变长字符串
    • 描述红黑树节点的结构体
    • 最近访问时间
    // 红黑树节点
    typedef struct {
    	ngx_queue_t queue; 	// 链表结点,方便淘汰过期结点
    	ngx_msec_t last; 	// 1、上一次访问该 url 的时间,毫秒 
    	u_short len;		// 2、客户端 ip + url 组成的字符串长度
    	u_char data[1];		// 3、以字符串保存客户端 ip 地址与 url
    } ngx_http_pagelimit_node_t;
    
    // 共享内存
    typedef struct {
    	ngx_rbtree_t rbtree;		// 红黑树,用于快速检索
    	ngx_rbtree_node_t sentinel; // 红黑树,必须定义的哨兵节点
    
    	ngx_queue_t queue;			// 链表头结点,所有操作记录构成的淘汰链表
    } ngx_http_pagelimit_shm_t;
    
    // 模块配置结构
    typedef struct {
        ssize_t shmsize;				// 共享内存大小	
    	ngx_int_t interval;				// 两次访问所必须间隔的时间,是否开启模块功能
        ngx_slab_pool_t *shpool;		// 指向共享内存池,用于管理共享内存
        ngx_http_pagelimit_shm_t *sh;	// 指向共享内存
    } ngx_http_pagelimit_conf_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    4.2.2、模块配置命令

    模块配置命令

    // 模块配置命令
    static ngx_command_t pagelimit_commands[] = {
    	{
    		ngx_string("test"),					 // 配置项名称
            // 配置项类型: main,两参数,参数1: 访问间隔s,参数2:共享内存大小
    		NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE2,
    		ngx_http_pagelimit_createmem,	       // 配置项参数处理方法
    		0,									// 配置文件中的偏移量
    		0,									// 不采用预设解析方法	 
    		NULL								// 读取配置项后不处理
    	},
    	ngx_null_command
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    解析配置命令

    static char *ngx_http_pagelimit_createmem(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {
    	ngx_str_t *value;
    	ngx_shm_zone_t *shm_zone;
    	
    	// 设置共享内存的名字
    	ngx_str_t name = ngx_string("pagelimit_slab_shm");
    
    	// 获取全局配置
    	ngx_http_pagelimit_conf_t *mconf = (ngx_http_pagelimit_conf_t*)conf;
    
    	// 获取配置命令 "interval" 后的参数数组
    	value = cf->args->elts;
    
    	// 获取两次成功访问的时间间隔,注意时间单位
    	mconf->interval = 1000 *ngx_atoi(value[1].data, value[1].len);
    	if (mconf->interval == NGX_ERROR || mconf->interval == 0) {
    		mconf->interval = -1;
    		return "invalid value";
    	}
    
    	// 获取共享内存大小,预设的 ngx_parse_size 函数
    	mconf->shmsize = ngx_parse_size(&value[2]);
    	if (mconf->shmsize == (ssize_t) NGX_ERROR || mconf->shmsize == 0) {
    		mconf->interval = -1; // 设置-1,关闭模块限速功能
    		return "invalid value"; 
    	}
    
    	// 要求 Nginx 准备分配共享内存
    	shm_zone = ngx_shared_memory_add(cf, &name, mconf->shmsize, &ngx_http_pagelimit_module);
    	if (shm_zone == NULL) {
    		// 分配共享内存失败
    		mconf->interval = -1; // 设置-1,关闭模块限速功能
    		return NGX_CONF_ERROR;
    	}
    
    	// 设置共享内存分配成功后的回调方法
    	shm_zone->init = ngx_http_pagelimit_shm_init;
    	// 设置初始化传递参数,即配置结构体
    	shm_zone->data = mconf;
    
    	ngx_log_error(NGX_LOG_NOTICE, cf->log, 0, "ngx_http_pagelimit_createmem");
    
    	return NGX_CONF_OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    ngx_shared_memory_add 的初始化完毕后的回调函数

    // 回调的初始化共享内存池
    static ngx_int_t ngx_http_pagelimit_shm_init (ngx_shm_zone_t *zone, void *data) {
    	
    	ngx_http_pagelimit_conf_t *conf;
    	ngx_http_pagelimit_conf_t *oconf = data;
    	// size_t len;
    
    	// 指向本次初始化时的配置结构体
    	conf = (ngx_http_pagelimit_conf_t*)zone->data;
    	
    	// 是否是 reload 配置项后导致的初始化共享内存
    	if (oconf) {
    		// 本次初始化的共享内存不是新创建的
    		// 这时,data 成员李就是上次创建的配置项
    		// 将 sh 和 shpool 指针指向旧的共享内存即可
    		conf->sh = oconf->sh;
    		conf->shpool = oconf->shpool;
    		return NGX_OK;
    	}
    	
    	// 初始化模块配置结构体
    	// 管理共享内存的 ngx_slab_pool_t 结构体
    	conf->shpool = (ngx_slab_pool_t*)zone->shm.addr;
    	// 分配共享内存
    	conf->sh = ngx_slab_alloc(conf->shpool, sizeof(ngx_http_pagelimit_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_pagelimit_rbtree_insert_value);
    
    	// 初始化按访问时间顺序的链表
    	ngx_queue_init(&conf->sh->queue);
    	
    	return NGX_OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    4.2.3、模块上下文
    static ngx_http_module_t count_ctx = {
    	NULL,									/* preconfiguration */
    	ngx_http_pagecount_init,				/* postconfiguration */
    	
    	NULL,									/* create main configuration */
    	NULL,									/* init main configuration */
    
    	NULL,									/* create server configuration */
    	NULL,									/* merge server configuration */
    
    	ngx_http_pagecount_create_location_conf,/* create location configuration */
    	NULL,									/* merge location configuration */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    hander 挂载方式:设置 handler 模块在 NGX_HTTP_PREACCESS_PHASE 阶段生效

    static ngx_int_t ngx_http_pagelimit_init(ngx_conf_t *cf) {
    	ngx_http_handler_pt *h;
    	ngx_http_core_main_conf_t *cmcf;
    
    	cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
    
    	// 设置该模块在 NGX_HTTP_PREACCESS_PHASE 阶段生效
    	h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
    	if (h == NULL) {
    		return NGX_ERROR;
    	}
    
    	// 设置请求处理的方法
    	ngx_log_error(NGX_LOG_NOTICE, cf->log, 0, "ngx_http_pagelimit_init");
    	*h = ngx_http_pagelimit_handler;
    
    	return NGX_OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    hander 实现方法:设置请求处理的 handler 方法,配合红黑树操作,实现限流功能。

    // handler 限速方法
    static ngx_int_t ngx_http_pagelimit_handler(ngx_http_request_t *r) {
    	ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "ngx_http_pagelimit_handler");
    	size_t len;
    	uint32_t key;
    	ngx_http_pagelimit_conf_t *conf;
    
    	conf = ngx_http_get_module_main_conf(r, ngx_http_pagelimit_module);
    	
    	if (conf->interval == -1) {
    		return NGX_DECLINED;
    	}
    
    	len = r->connection->addr_text.len + r->uri.len;
    	u_char* data = ngx_pcalloc(r->pool, len);
    	ngx_memcpy(data, r->uri.data, r->uri.len);
    	ngx_memcpy(data + r->uri.len, r->connection->addr_text.data, r->connection->addr_text.len);
    
    	// 使用 crc32 算法将 ip + url 字符串生成 hash 码
    	key = ngx_crc32_short(data, len);
    
    	ngx_log_error(NGX_LOG_EMERG, r->connection->log, 0, "key = %d, data = %s", key, data);
    
    	// 多进程同时操作同一个共享内存需要加锁
    	ngx_shmtx_lock(&conf->shpool->mutex);
    	ngx_http_pagelimit_lookup(r, conf, key, data, len);	
    	ngx_shmtx_unlock(&conf->shpool->mutex);
    
    	return NGX_DECLINED; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    配置结构体生成

    static void  *ngx_http_pagelimit_create_main_conf(ngx_conf_t *cf) {
    	// 创建存储配置项的模块配置结构体
    	ngx_http_pagelimit_conf_t *conf;
    	conf = ngx_palloc(cf->pool, sizeof(ngx_http_pagelimit_conf_t));
    	if (NULL == conf) {
    		return NULL;
    	}
    
    	conf->shmsize = -1;
    	return conf;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    4.2.4、定义模块
    ngx_module_t ngx_http_pagecount_module = {
    	NGX_MODULE_V1,
    	&count_ctx,			/* module context */
    	count_commands,		/* module directives */
    	NGX_HTTP_MODULE,	/* module type */
    	NULL,
    	NULL,
    	NULL,
    	NULL,
    	NULL,
    	NULL,
    	NULL,
    	NGX_MODULE_V1_PADDING
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.3、编译测试

    编写配置文件

    ngx_addon_name=ngx_http_pagelimit_module
    HTTP_MODULES="$HTTP_MODULES ngx_http_pagelimit_module"
    NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_pagelimit_module.c"
    
    • 1
    • 2
    • 3

    进入 nginx 源码目录,执行 configure 脚本,添加模块所在路径

    ./configure --add-module=PATH 
    # 例:
    ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_realip_module --with-http_v2_module --with-openssl=../openssl-1.1.1g --add-module=/root/code/ # 这里是我的模块路径
    
    • 1
    • 2
    • 3

    configure 脚本执行完毕后,Nginx 会生成 objs/Makefile 和 objs/ngx_modules.c 两个文件,这里也可以查看到自定义的模块已添加。当然,也可以直接修改这两个文件添加自定义模块。

    编译,编译过程中显示自定义模块已添加。

    make 
    make install
    
    • 1
    • 2

    进入到 nginx 安装目录,在 ./conf/nginx.conf 的全局作用域添加 test 1000 1m 命令

    启动 nginx

    /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
    /usr/local/nginx/sbin/nginx -s stop
    /usr/local/nginx/sbin/nginx -s reload
    
    • 1
    • 2
    • 3

    5、例:流量统计模块

    流量统计模块实现统计某个 IP 的访问次数,实现方法同上,不再赘述。不同之处在于两者的 handler 挂载阶段不同,流量统计挂载到 NGX_HTTP_CONTENT_PHASE阶段。

    效果图如下:

    在这里插入图片描述

    6、参考

    • 陶辉. 深入理解Nginx:模块开发与架构解析[M]. 北京:机械工业出版社,2016.
    • 聂松松等. Nginx底层设计与源码分析[M]. 北京:机械工业出版社,2021.
    • Nginx 入门指南
  • 相关阅读:
    网站的常见攻击与防护方法
    画中画视频剪辑:批量制作画中画视频,让视频更具吸引力和创意
    【mediasoup】TransportCongestionControlClient 1: 代码走读
    【NOI模拟赛】为NOI加点料(重链剖分,线段树)
    宜家IKEA EDI IFTMBF运输预定请求详解
    软件测试中的43个功能测试点总结
    数字云财务迈入价值重塑新阶段,未来财务已来
    基于最大似然估计与卡尔曼滤波的室内目标跟踪
    Python版按键精灵基础代码
    RK3588 VDD_CPU_BIG0/1 电源PCB设计注意事项
  • 原文地址:https://blog.csdn.net/you_fathe/article/details/127994646