nginx入门
Nginx的模块有三种角色:
- handlers 处理http请求并构造输出
- filters 处理handler产生的输出
- load-balancers 当有多于一个的后端服务器时,选择一台将http请求发送过去
许多可能你认为是web server的工作,实际上都是由模块来完成的:任何时候,Nginx提供文件或者转发请求到另一个server,都是通过handler来实现的;而当需要Nginx用gzip压缩输出或者在服务端加一些东东的话,filter就派上用场了;Nginx的core模块主要管理网络层和应用层协议,并启动针对特定请求的一系列后续模块。这种分散式的体系结构使得由你自己来实现强大的内部单元成为了可能。
注意:不像Apache的模块那样,Nginx的模块都不是动态链接的。(换句话说,Nginx的模块都是静态编译的) 模块是如何被调用的呢?典型地说,当server启动时,每一个handler都有机会去处理配置文件中的location定义,如果有多个 handler被配置成需要处理某一特定的location时,只有其中一个handler能够“获胜”。
一个handler有三种返回方式:正常、错误、放弃处理转由默认的handler来处理(典型地如处理静态文件的时候)。
如果handler的作用是把请求反向代理到后端服务器,那么就是刚才说的模块的第三种角色load-balancer了。load-balancer主要是负责决定将请求发送给哪个后端服务器。Nginx目前支持两种load-balancer模块:round-robin(轮询,处理请求就像打扑克时发牌那样)和IP hash(众多请求时,保证来自同一ip的请求被分发的同一个后端服务器)。
如果handler返回(就是http响应,即filter的输入)正确无误,那么fileter就被调用了。每个location配置里都可以添加多个filter,所以说(比如)响应可以被压缩和分块。多个filter的执行顺序是编译时就确定了的。filter采用了经典的“接力链表(CHAIN OF RESPONSIBILITY)”模式:一个filter被调用并处理,接下来调用下一个filter,直到最后一个filter被调用完成,Nginx 才真正完成响应流程。
最帅的部分是在 filter链中,每个filter不会等待之前的filter完全完工,它可以处理之前filter正在输出的内容,这有一点像Unix中的管道。 Filter的操作都基于buffers_,buffer通常情况下等于一个页的大小(4k),你也可以在nginx.conf里改变它的大小。这意味着,比如说,模块可以在从后端服务器收到全部的响应之前,就开始压缩这个响应并流化(stream to)给客户端了。
总结一下上面的内容,一个典型的周期应当是这样的:
² 客户端发送HTTP request
² Nginx基于location的配置选择一个合适的handler
² (如果有) load-balancer选择一个后端服务器
² Handler处理请求并顺序将每一个响应buffer发送给第一个filter
² 第一个filter讲输出交给第二个filter
² 第二个给第三个
² 第三个给第四个
² 以此类推
² 最终响应发送给客户端
我之所以说“典型地”是因为Ngingx的模块具有很强的定制性。模块开发者需要花很多精力精确定义模块在何时、如何产生作用。模块调用实际上是通过一系列的回调函数做到的,很多很多。名义上来说,你的函数可以在以下时候被执行:
l server读取配置文件之前
l 读取location和server的每一条配置指令
l 当Nginx初始化main配置段时
l 当Nginx初始化server配置段时(例如:host/port)
l 当Nginx合并server配置和main配置时
l 当Nginx初始化location配置时
l 当Nginx合并location配置和它的父server配置时
l 当Nginx的主进程启动时
l 当一个新的worker进程启动时
l 当一个worker进程退出时
l 当主进程退出时
l handle 一个请求
l Filter响应头
l Filter响应体
l 选择一个后端服务器
l 初始化一个将发往后端服务器的请求
l 重新-初始化一个将发往后端服务器的请求
l 处理来自后端服务器的响应
l 完成与后端服务器的交互
难以置信!有这么多的功能任你处置,而你只需仅仅通过多组有用的钩子(由函数指针组成的结构体)和相应的实现函数。
8.2. 模块类型及配置地方
要知道nginx有哪些模块,一个快速的方法就是编译nginx。编译之后,会在源代码根目录下生成objs目录,该目录中包含有objs/ngx_auto_config.h和objs/ngx_auto_headers.h,以及objs/ngx_modules.c文件,当然,还有Makefile文件等。
其中,生成的objs/ngx_modules.c文件中,重新集中申明(使用extern关键字)了nginx配置的所有模块,这些模块可通过编译前的configure命令进行配置,即设置哪些模块需要编译,哪些不被编译。如下:
extern ngx_module_t ngx_core_module; extern ngx_module_t ngx_errlog_module; extern ngx_module_t ngx_conf_module; extern ngx_module_t ngx_events_module; extern ngx_module_t ngx_event_core_module; extern ngx_module_t ngx_epoll_module; extern ngx_module_t ngx_regex_module; extern ngx_module_t ngx_http_module; extern ngx_module_t ngx_http_core_module; ……. |
很显然,这些模块均是在此处用extern进行申明,以表明其他模块可以访问,而对其本身的定义和初始化ngx_module_t结构在其对应的.c文件中进行。例如,ngx_core_module模块便是在src/core/nginx.c文件中定义并进行静态初始化。实际上,ngx_core_module是一个全局的结构体对象,其他模块类同。
8.3. 模块描述
8.3.1. 数据结构
8.3.1.1. ngx_module_t结构
nginx的模块化架构最基本的数据结构为ngx_module_t,因此,此处,我们先分析这个结构,在./src/core/ngx_conf_file.h文件中定义。如下:
#define NGX_MODULE_V1 0, 0, 0, 0, 0, 0, 1 //该宏用来初始化前7个字段 #define NGX_MODULE_V1_PADDING 0, 0, 0, 0, 0, 0, 0, 0 //该宏用来初始化最后8个字段 struct ngx_module_s{ ngx_uint_t ctx_index; //分类模块计数器 ngx_uint_t index; //模块计数器 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; //该模块的命令集,指向一个ngx_command_t结构数组 ngx_uint_t type; //该模块的种类,为core/event/http/mail中的一种 //以下是一些callback函数 ngx_uint_t (*init_master)(ngx_log_t *log); //初始化master ngx_uint_t (*init_module)(ngx_cycle_t *cycle); //初始化模块 ngx_uint_t (*init_process)(ngx_cycle_t *cycle); //初始化工作进程 ngx_uint_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); //退出master 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; }; |
其中,init_master, init_module, init_process, init_thread, exit_thread, exit_process, exit_master分别在初始化master、初始化模块、初始化工作进程、初始化线程、退出线程、退出工作进程、退出master时被调用。
8.3.1.2. ngx_command_t结构
模块的命令集commands指向一个ngx_command_t结构数组,在src/core/ngx_conf_file.h文件中定义。如下:
struct ngx_command_s { 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; void *post; }; #define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL } //空命令 |
8.3.2. 模块类图
nginx为C语言开发的开源高性能web server,其代码中大量使用了callback方式,例如模块结构ngx_module_t中的init_master等。实际上,我们可以将ngx_module_t看作C++的一个类,其中的数据字段便是其属性,而那些callback便是该类的操作。——这应该就是nginx的模块化思想。画出的ngx_module_t的类图如下:
8.4. 组织模块
8.4.1. 全局数组ngx_modules
nginx拥有几十个模块,那么,这些模块是如保存在一个全局指针数组ngx_modules[]中,数组的每一个元素均为一个全局ngx_module_t对象的指针。如下。请参考objs/ngx_modules.c文件中的定义:
ngx_module_t *ngx_modules[] = { &ngx_core_module, &ngx_errlog_module, &ngx_conf_module, &ngx_events_module, &ngx_event_core_module, …. } |
8.4.2. 组织结构图
这些模块的组织结构图如下所示,因模块较多,图中只画出一部分有代表性的重要模块:
8.5. 模块种类
在对全局数组ngx_modules进行初始化时,即对每一个模块进行了静态初始化。其中对模块的type字段的初始化是通过以下几个宏进行的。
(1) 文件./src/core/ngx_conf_file.h
#define NGX_CORE_MODULE 0x45524F43 #define NGX_CONF_MODULE 0x464E4F43 |
(2) 文件./src/event/ngx_event.h
#define NGX_EVENT_MODULE 0x544E5645 |
(3) 文件./src/http/ngx_http_config.h
#define NGX_HTTP_MODULE 0x50545448 |
即模块种类宏,定义为一个十六进制的数,这个十六进制的数就是其类型对应的ASCII码。因此,nginx共有4种类型的模块,分别为"CORE","CONF","EVNT","HTTP"。
实际上,如果在configure阶段,使用了"--with-mail"参数,mail模块将被编译进来,其对应的宏如下。
#define NGX_MAIL_MODULE 0x4C49414D |
因此,严格来讲,nginx有5中类型的模块,"CORE","CONF","EVNT","HTTP","MAIL"。
8.6. 初始化模块
8.6.1. 静态初始化
即编译期间完成的数据成员初始化。记mname为某个模块的名字,其静态初始化过程如下。
(1) 用宏NGX_MODULE_V1初始化前7个字段
(2) 用全局对象ngx_mname_module_ctx的地址初始化ctx指针
(3) 用全局数组ngx_mname_commands[]初始化commands指针
(4) 用宏NGX_CORE_MODULE等初始化type字段
(5) 初始化init_master等callback
(6) 用宏NGX_MODULE_V1_PADDING初始化最后8个字段
由此可见,在定义该模块(全局结构对象)时,将其ctx_index和index均初始化为0。因此,模块的静态初始化(数据成员初始化)实际上只是对模块上下文、模块命令集和模块类型进行初始化。
8.6.2. 动态初始化
即nginx运行(启动)初期,对模块本身的初始化。
8.6.2.1. index字段的初始化
对各个模块的index字段的初始化是在main函数中进行的,如下。
ngx_max_module = 0; for (i = 0; ngx_modules[i]; i++) { ngx_modules[i]->index = ngx_max_module++; } |
可见,该for-loop执行后,每个模块的index值便是其在ngx_modules[]数组中的下标值,且全局变量ngx_max_module为模块个数。
8.6.2.2. ctx_index字段的初始化
(1) "EVNT"类型的模块
static char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_event_max_module = 0; for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type ! = NGX_EVENT_MODULE) { continue; } ngx_modules[i]->ctx_index = ngx_event_max_module++; } } |
(2) "HTTP"类型的模块
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_max_module = 0; for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type ! = NGX_HTTP_MODULE) { continue; } ngx_modules[m]->ctx_index = ngx_http_max_module++; } } |
(3) "MAIL"类型的模块
static char * ngx_mail_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_mail_max_module = 0; for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type ! = NGX_MAIL_MODULE) { continue; } ngx_modules[m]->ctx_index = ngx_mail_max_module++; } } |
8.6.2.3. 其他初始化
其他的初始化工作,将在nginx启动及其进程启动分析中介绍。
8.7. 小结
本文主要讲述了nginx的模块及其初始化,包括所有模块的组织,及模块的静态初始化和部分动态初始化。
9. 数据结构
9.1. 字符串类型
9.1.1. 简介
Nginx使用自己的、单独的方式去管理字符串类型。
使用的文件:src/core/string.h/c。
9.1.2. 字符串结构
typedef struct { size_t len; //字符串长度 u_char* data; //指向字符串的指针 } ngx_str_t; #define ngx_null_string { 0, NULL } |
字符串类型结构简单,在整个nginx中,到处使用。
9.1.3. 提供的操作
ngx_string |
初始化函数 |
ngx_null_string |
初始化空字符串函数 |
ngx_tolower |
字符转小写函数 |
ngx_toupper |
字符转大写函数 |
ngx_strncmp |
比较指定长度的字符串是否相同 |
ngx_strcmp |
比较字符串是否相同 |
ngx_strstr |
从字符串中找到需要的字符串 |
ngx_strlen |
字符串的长度 |
ngx_strchr |
在字符串中找到匹配的字符,返回 0为匹配 |
ngx_strlchr |
在字符串中找到匹配的字符,返回匹配的指针 |
ngx_memzero |
把一片内存区设置为0 |
ngx_memset |
把一片内存区设置为指定的数 |
ngx_memcpy |
复制内存,没有返回 |
ngx_cpymem |
复制内存,返回复制完了dst的最后一个字符的下一个字符的指针 |
ngx_copy |
同ngx_cpymem |
ngx_memcmp |
比较内存中的数据是否相同 |
ngx_strlow |
把字符串都转换成小写 |
ngx_cpystrn |
复制字符串,并且返回字符串的最后一个字符的下一个字符的指针 |
ngx_pstrdup |
复制字符串到pool,返回字符串的指针 |
ngx_sprintf |
把各种类型的数据格式化输出到buf,最大的长度为65536 |
ngx_snprintf |
把各种类型的数据格式化输出到指定长度的buf |
ngx_strcasecmp |
不分大小写比较两个字符串是否相同 |
ngx_strncasecmp |
指定长短不分大小写比较两个字符串是否相同 |
ngx_strnstr |
在指定大小一个字符串中是否有子字符串 |
ngx_strstrn |
在一个字符串中是否有子指定大小的字符串 |
ngx_strcasestrn |
在一个字符串中是否有子指定大小的字符串,不区分大小写 |
ngx_rstrncmp |
从后往前比较两个字符串是否相同,返回相同的位置 |
ngx_rstrncasecmp |
从后往前比较两个字符串是否相同,返回相同的位置,不区分大小写 |
ngx_memn2cmp |
比较两个指定长度的内存是否相同,也比较长的内存是否包含短的内存 |
ngx_atoi |
指定长度的字符串转换成数字 |
ngx_atosz |
指定长度的字符串转换成ssize_t类型数字 |
ngx_atoof |
指定长度的字符串转换成off_t类型数字 |
ngx_atotm |
指定长度的字符串转换成time_t类型数字 |
ngx_hextoi |
指定长度的字符串转换成十六进制数字 |
ngx_hex_dump |
把数字转换成16进制的字符串 |
ngx_encode_base64 |
base64编码 |
ngx_decode_base64 |
base64解码 |
ngx_utf8_decode |
把 utf8字符解码成双字节的 unicode或是单字节字符,但是该函数会移动*p的值 |
ngx_utf8_length |
得到utf8编码的字符占几个字节 |
ngx_utf8_cpystrn |
赋值utf8字符串,保证完整的复制 |
ngx_escape_uri |
对uri进行编码 |
ngx__uri |
对uri的进行解码 |
ngx_escape_html |
对html进行编码 |
ngx_sort |
排序,主要是用于数组排序 |
ngx_qsort |
快速排序 |
ngx_value |
把宏数字转换成字符串 |
9.2. 内存池ngx_pool_t
9.2.1. 简介
nginx对内存的管理由其自己实现的内存池结构ngx_pool_t来完成,本文重点叙述nginx的内存管理。
nginx内存管理相关文件:
(1) src/os/unix/ngx_alloc.h/.c
内存相关的操作,封装了最基本的内存分配函数。
如free/malloc/memalign/posix_memalign,分别被封装为ngx_free,ngx_alloc/ngx_calloc, ngx_memalign
l ngx_alloc:封装malloc分配内存
l ngx_calloc:封装malloc分配内存,并初始化空间内容为0
l ngx_memalign:返回基于一个指定alignment的大小为size的内存空间,且其地址为alignment的整数倍,alignment为2的幂。
(2) ./src/core/ngx_palloc.h/.c
l 封装创建/销毁内存池,从内存池分配空间等函数
9.2.2. 内存池结构
nginx对内存的管理均统一完成,例如,在特定的生命周期统一建立内存池(如main函数系统启动初期即分配1024B大小的内存池),需要内存时统一分配内存池中的内存,在适当的时候释放内存池的内存(如关闭http链接时调用ngx_destroy_pool进行销毁)。
因此,开发者只需在需要内存时进行申请即可,不用过多考虑内存的释放等问题,大大提高了开发的效率。先看一下内存池结构。
9.2.2.1. ngx_pool_t结构
此处统一一下概念,内存池的数据块:即分配内存在这些数据块中进行,一个内存池可以有多一个内存池数据块。nginx的内存池结构如下:
typedef struct { //内存池的数据块位置信息 u_char *last; //当前内存池分配到此处,即下一次分配从此处开始 u_char *end; //内存池结束位置 ngx_pool_t *next; //内存池里面有很多块内存,这些内存块就是通过该指针连成链表的 ngx_uint_t failed; //内存池分配失败次数 } ngx_pool_data_t; struct ngx_pool_s{ //内存池头部结构 ngx_pool_data_t d; //内存池的数据块 size_t max; //内存池数据块的最大值 ngx_pool_t *current; //指向当前内存池 ngx_chain_t *chain; //该指针挂接一个ngx_chain_t结构 ngx_pool_large_t *large; //大块内存链表,即分配空间超过max的内存 ngx_pool_cleanup_t *cleanup; //释放内存池的callback ngx_log_t *log; //日志信息 }; |
其中,sizeof(ngx_pool_data_t)=16B,sizeof(ngx_pool_t)=40B。
nginx将几乎所有的结构体放在ngx_core.h文件中重新进行了申明,如下:
typedef struct ngx_module_s ngx_module_t; typedef struct ngx_conf_s ngx_conf_t; typedef struct ngx_cycle_s ngx_cycle_t; typedef struct ngx_pool_s ngx_pool_t; typedef struct ngx_chain_s ngx_chain_t; typedef struct ngx_log_s ngx_log_t; typedef struct ngx_array_s ngx_array_t; typedef struct ngx_open_file_s ngx_open_file_t; typedef struct ngx_command_s ngx_command_t; typedef struct ngx_file_s ngx_file_t; typedef struct ngx_event_s ngx_event_t; typedef struct ngx_event_aio_s ngx_event_aio_t; typedef struct ngx_connection_s ngx_connection_t; |
9.2.2.2. 其他相关结构
其他与内存池相干的数据结构,如清除资源的cleanup链表,分配的大块内存链表等,如下。
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1) //在x86体系结构下,该值一般为4096B,即4K #define NGX_DEFAULT_POOL_SIZE (16* 1024) #define NGX_POOL_ALIGNMENT 16 #define NGX_MIN_POOL_SIZE ngx_align((sizeof(ngx_pool_t) + 2 * sizeof(ngx_pool_large_t)), NGX_POOL_ALIGNMENT) typedef void (*ngx_pool_cleanup_pt)(void *data); //cleanup的callback类型 typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t; struct ngx_pool_cleanup_s{ ngx_pool_cleanup_pt handler; void *data; //指向要清除的数据 ngx_pool_cleanup_t *next; //下一个cleanup callback }; typedef struct ngx_pool_large_s ngx_pool_large_t; struct ngx_pool_large_s{ ngx_pool_large_t *next; //指向下一块大块内存 void *alloc; //指向分配的大块内存 }; ... ... typedef struct { ngx_fd_t fd; u_char *name; ngx_log_t *log; } ngx_pool_cleanup_file_t; |
(gdb) p getpagesize()
$18 = 4096
全局变量ngx_pagesize的初始化是在如下函数中完成的。src/os/unix/ngx_posix_init.c
ngx_int_t ngx_os_init(ngx_log_t *log) { ngx_uint_t n; #if (NGX_HAVE_OS_SPECIFIC_INIT) if (ngx_os_specific_init(log) != NGX_OK) { return NGX_ERROR; } #endif ngx_init_setproctitle(log); ngx_pagesize = getpagesize(); ngx_cacheline_size = NGX_CPU_CACHE_LINE; ... } |
这些数据结构之间的关系,请参考后面的图。
9.2.2.3. ngx_pool_t的逻辑结构
这些数据结构逻辑结构图如下。注:本文采用UML的方式画出该图。
9.2.3. 创建内存池
创建内存池有ngx_create_pool()函数完成,代码如下:
ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log) { ngx_pool_t *p; p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log); if (p == NULL) { return NULL; } //last指向ngx_pool_t结构体之后数据取起始位置 p->d.last = (u_char *) p + sizeof(ngx_pool_t); //end指向分配的整个size大小的内存的末尾 p->d.end = (u_char *) p + size; p->d.next = NULL; p->d.failed = 0; size = size - sizeof(ngx_pool_t); p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; //最大不超过4095B p->current = p; p->chain = NULL; p->large = NULL; p->cleanup = NULL; p->log = log; return p; } |
例如,调用ngx_create_pool(1024, 0x80d1c4c)后,创建的内存池物理结构如下图:
9.2.4. 销毁内存池
销毁内存池由如下函数完成:void ngx_destroy_pool(ngx_pool_t *pool)。
该函数将遍历内存池链表,所有释放内存,如果注册了clenup(也是一个链表结构),亦将遍历该cleanup链表结构依次调用clenup的handler清理。同时,还将遍历large链表,释放大块内存。
9.2.5. 重置内存池
重置内存池由下面的函数完成:void ngx_reset_pool(ngx_pool_t *pool)。
该函数将释放所有large内存,并且将d->last指针重新指向ngx_pool_t结构之后数据区的开始位置,同刚创建后的位置相同。
9.2.6. 分配内存
内存分配的函数如下。
void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);
返回值为分配的内存起始地址。选择其中的两个函数进行分析,其他的也很好理解,省略。
9.2.6.1. ngx_palloc()函数分析
ngx_palloc()代码如下,分析请参考笔者所加的注释。
void * ngx_palloc(ngx_pool_t *pool, size_t size) { u_char *m; ngx_pool_t *p; //判断待分配内存与max值 if (size <= pool->max) { //小于max值,则从current节点开始遍历pool链表 p = pool->current; do { m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT); if ((size_t) (p->d.end - m) >= size) { //在该节点指向的内存块中分配size大小的内存 p->d.last = m + size; return m; } p = p->d.next; } while (p); //链表里没有能分配size大小内存的节点,则生成一个新的节点并在其中分配内存 return ngx_palloc_block(pool, size); } //大于max值,则在large链表里分配内存 return ngx_palloc_large(pool, size); } |
例如,在之前说明,创建的内存池中分配200B的内存,调用ngx_palloc(pool, 200)后,该内存池物理结构如下图。
9.2.6.2. ngx_palloc_block()函数分析
ngx_palloc_block函数代码如下,分析请参考笔者所加的注释。
static void * ngx_palloc_block(ngx_pool_t *pool, size_t size) { u_char *m; size_t psize; ngx_pool_t *p, *new, *current; //计算pool的大小 psize = (size_t) (pool->d.end - (u_char *) pool); //分配一块与pool大小相同的内存 m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log); if (m == NULL) { return NULL; } new = (ngx_pool_t *) m; new->d.end = m + psize; //设置end指针 new->d.next = NULL; new->d.failed = 0; //让m指向该块内存ngx_pool_data_t结构体之后数据区起始位置 m += sizeof(ngx_pool_data_t); //按4字节对齐 m = ngx_align_ptr(m, NGX_ALIGNMENT); //在数据区分配size大小的内存并设置last指针 new->d.last = m + size; current = pool->current; for (p = current; p->d.next; p = p->d.next) { if (p->d.failed++ > 4) { //failed的值只在此处被修改 current = p->d.next; //失败4次以上移动current指针 } } p->d.next = new; //将这次分配的内存块new加入该内存池 pool->current = current ? current : new; return m; } |
注意:该函数分配一块内存后,last指针指向的是ngx_pool_data_t结构体(大小16B)之后数据区的起始位置。而创建内存池时时,last指针指向的是ngx_pool_t结构体(大小40B)之后数据区的起始位置。
9.2.7. 释放内存
请参考如下函数:ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)。
需要注意的是该函数只释放large链表中注册的内存,普通内存在ngx_destroy_pool中统一释放。
9.2.8. 注册cleanup
请参考如下函数,该函数实现也很简单,此处不再赘述。
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
9.2.9. 内存池的物理结构
针对本文上节的例子,画出的内存池的物理结构如下图。
从该图也能看出结论,即内存池第一块内存前40字节为ngx_pool_t结构,后续加入的内存块前16个字节为ngx_pool_data_t结构,这两个结构之后便是真正可以分配内存区域。
因此,本文Reference中的内存分配相关中的图是有一点点小问题的,并不是每一个节点的前面都是ngx_pool_t结构。
9.2.10. 一个例子
理解并掌握开源软件的最好方式莫过于自己写一些测试代码,或者改写软件本身,并进行调试来进一步理解开源软件的原理和设计方法。本节给出一个创建内存池并从中分配内存的简单例子。
9.2.10.1. 代码
#include #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } } int main() { ngx_pool_t *pool; printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool); printf("--------------------------------\n"); printf("alloc block 1 from the pool:\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool); printf("--------------------------------\n"); printf("alloc block 2 from the pool:\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool); printf("--------------------------------\n"); printf("alloc block 3 from the pool :\n"); printf("--------------------------------\n"); ngx_palloc(pool, 512); dump_pool(pool); ngx_destroy_pool(pool); return 0; } |
9.2.10.2. 7.4.2、如何编译
这个问题是编写测试代码或者改写软件本身最迫切需要解决的问题,否则,编写的代码无从编译或运行,那也无从进行调试并理解软件了。
如何对自己编写的测试代码进行编译,可参考Linux平台代码覆盖率测试-编译过程自动化及对链接的解释、Linux平台如何编译使用Google test写的单元测试?。我们要做的是学习这种编译工程的方法,针对该例子,笔者编写的makefile文件如下。——这便是本节的主要目的。
CXX = gcc CXXFLAGS += -g -Wall -Wextra NGX_ROOT = /usr/src/nginx-1.0.4 TARGETS = ngx_pool_t_test TARGETS_C_FILE = $(TARGETS).c CLEANUP = rm -f $(TARGETS) *.o all: $(TARGETS) clean: $(CLEANUP) CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \ NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o $(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $^ -o $@ |
9.2.10.3. 7.4.3、运行运行结果
# ./ngx_pool_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922048 .end = 0x8922420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc block 1 from the pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472 -------------------------------- alloc block 2 from the pool: -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x8922450 .failed = 0 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472 pool = 0x8922450 .d .last = 0x8922660 .end = 0x8922850 .next = 0x0 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496 -------------------------------- alloc block 3 from the pool : -------------------------------- pool = 0x8922020 .d .last = 0x8922248 .end = 0x8922420 .next = 0x8922450 .failed = 1 .max = 984 .current = 0x8922020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 472 pool = 0x8922450 .d .last = 0x8922660 .end = 0x8922850 .next = 0x8922880 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496 pool = 0x8922880 .d .last = 0x8922a90 .end = 0x8922c80 .next = 0x0 .failed = 0 .max = 0 .current = 0x0 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 496 |
9.2.11. 小结
本文针对nginx-1.0.4的内存管理进行了较为全面的分析,包括相关内存池数据结构,内存池的创建、销毁,以及从内存池中分配内存等。最后通过一个简单例子向读者展示nginx内存池的创建和分配操作,同时借此向读者展示编译测试代码的方法。
分析完nginx的内存管理,你一定惊叹于nginx作者的聪明才智。这种内存管理的设计方法小巧、快捷,值得借鉴!
9.3. 数组结构ngx_array_t
9.3.1. 简介
本文开始介绍nginx的容器,先从最简单的数组开始。
数组实现文件:文件:./src/core/ngx_array.h/.c。
9.3.2. 数组结构
9.3.2.1. ngx_array_t结构
nginx的数组结构为ngx_array_t,定义如下:
struct ngx_array_s { void *elts; //数组数据区起始位置 ngx_uint_t nelts; //实际存放的元素个数 size_t size; //每个元素大小 ngx_uint_t nalloc; //数组所含空间个数,即实际分配的小空间的个数 ngx_pool_t *pool; //该数组在此内存池中分配 }; typedef struct ngx_array_s ngx_array_t; |
sizeof(ngx_array_t)=20。由其定义可见,nginx的数组也要从内存池中分配。将分配nalloc个大小为size的小空间,实际分配的大小为(nalloc * size)。
9.3.2.2. ngx_array_t的逻辑结构
ngx_array_t结构引用了ngx_pool_t结构,因此本文参考内存池结构ngx_pool_t及内存管理一文画出相关结构的逻辑图,如下:
9.3.3. 数组操作
数组操作共有5个,如下:
//创建数组 ngx_array_t*ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size); //销毁数组 Void ngx_array_destroy(ngx_array_t *a); //向数组中添加元素 void* ngx_array_push(ngx_array_t *a); void* ngx_array_push_n(ngx_array_t *a, ngx_uint_t n); //初始化数组 Static ngx_inline ngx_int_t ngx_array_init(ngx_array_t*array, ngx_pool_t *pool, ngx_uint_t n, size_t size) |
因实现都很简单,本文简单分析前3个函数。
9.3.3.1. 创建数组
创建数组的操作实现如下,首先分配数组头(20B),然后分配数组数据区,两次分配均在传入的内存池(pool指向的内存池)中进行。然后简单初始化数组头并返回数组头的起始位置。
ngx_array_t* ngx_array_create(ngx_pool_t*p, ngx_uint_t n, size_t size) { ngx_array_t *a; a = ngx_palloc(p,sizeof(ngx_array_t)); //从内存池中分配数组头 if (a == NULL) { return NULL; } a->elts = ngx_palloc(p,n * size); //接着分配n*size大小的区域作为数组数据区 if (a->elts == NULL) { return NULL; } a->nelts = 0; //初始化 a->size = size; a->nalloc = n; a->pool = p; return a; //返回数组头的起始位置 } |
创建数组后内存池的物理结构图如下。
9.3.3.2. 销毁数组
销毁数组的操作实现如下,包括销毁数组数据区和数组头。这里的销毁动作实际上就是修改内存池的last指针,并没有调用free等释放内存的操作,显然,这种维护效率是很高的。
void ngx_array_destroy(ngx_array_t*a) { ngx_pool_t *p; p = a->pool; if ((u_char *) a->elts+ a->size * a->nalloc == p->d.last) { //先销毁数组数据区 p->d.last -=a->size * a->nalloc; //设置内存池的last指针 } if ((u_char *) a +sizeof(ngx_array_t) == p->d.last) { //接着销毁数组头 p->d.last = (u_char*) a; //设置内存池的last指针 } } |
9.3.3.3. 添加元素
向数组添加元素的操作有两个,ngx_array_push和ngx_array_push_n,分别添加一个和多个元素。
但实际的添加操作并不在这两个函数中完成,例如ngx_array_push返回可以在该数组数据区中添加这个元素的位置,ngx_array_push_n则返回可以在该数组数据区中添加n个元素的起始位置,而添加操作即在获得添加位置之后进行,如后文的例子。
void * ngx_array_push(ngx_array_t*a) { void *elt, *new; size_t size; ngx_pool_t *p; //数组数据区满 if (a->nelts ==a->nalloc) { //计算数组数据区的大小 size = a->size *a->nalloc; p = a->pool; //若1.内存池的last指针指向数组数据区的末尾 //且2.内存池未使用的区域可以再分配一个size大小的小空间 if ((u_char *)a->elts + size == p->d.last &&p->d.last + a->size <= p->d.end) { //分配一个size大小的小空间(a->size为数组一个元素的大小) p->d.last +=a->size; //实际分配小空间的个数加1 a->nalloc++; } else { //否则,扩展数组数据区为原来的2倍 new =ngx_palloc(p, 2 * size); if (new == NULL) { return NULL; } //将原来数据区的内容拷贝到新的数据区 ngx_memcpy(new,a->elts, size); a->elts = new; //注意:此处转移数据后,并未释放原来的数据区,内存池将统一释放 a->nalloc *= 2; } } //数据区中实际已经存放数据的子区的末尾 elt = (u_char *)a->elts + a->size * a->nelts; //即最后一个数据末尾,该指针就是下一个元素开始的位置 a->nelts++; //返回该末尾指针,即下一个元素应该存放的位置 return elt; } |
由此可见,向数组中添加元素实际上也是在修该内存池的last指针(若数组数据区满)及数组头信息,即使数组满了,需要扩展数据区内容,也只需要内存拷贝完成,并不需要数据的移动操作,这个效率也是相当高的。
下图是向数组中添加10个整型元素后的一个例子。代码可参考下文的例子。当然,数组元素也不仅限于例子的整型数据,也可以是其他类型的数据,如结构体等。
9.3.4. 一个例子
9.3.4.1. 代码
#include #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" #include "ngx_array.h" volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } } void dump_array(ngx_array_t* a) { if (a) { printf("array = 0x%x\n", a); printf(" .elts = 0x%x\n", a->elts); printf(" .nelts = %d\n", a->nelts); printf(" .size = %d\n", a->size); printf(" .nalloc = %d\n", a->nalloc); printf(" .pool = 0x%x\n", a->pool); printf("elements: "); int *ptr = (int*)(a->elts); for (; ptr < (int*)(a->elts + a->nalloc * a->size); ) { printf("0x%x ", *ptr++); } printf("\n"); } } int main() { ngx_pool_t *pool; int i; printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool); printf("--------------------------------\n"); printf("alloc an array from the pool:\n"); printf("--------------------------------\n"); ngx_array_t *a = ngx_array_create(pool, 10, sizeof(int)); dump_pool(pool); for (i = 0; i < 10; i++) { int *ptr = ngx_array_push(a); *ptr = i + 1; } dump_array(a); ngx_array_destroy(a); ngx_destroy_pool(pool); return 0; } |
9.3.4.2. 如何编译
CXX = gcc CXXFLAGS +=-g -Wall -Wextra NGX_ROOT =/usr/src/nginx-1.0.4 TARGETS =ngx_array_t_test TARGETS_C_FILE= $(TARGETS).c CLEANUP = rm-f $(TARGETS) *.o all:$(TARGETS) clean: $(CLEANUP) CORE_INCS =-I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \ NGX_PALLOC =$(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING =$(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC =$(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_ARRAY =$(NGX_ROOT)/objs/src/core/ngx_array.o $(TARGETS):$(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING)$(NGX_ALLOC) $(NGX_ARRAY) $^ -o $@ |
9.3.4.3. 运行结果
# ./ngx_array_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x860b020 .d .last = 0x860b048 .end = 0x860b420 .next = 0x0 .failed = 0 .max = 984 .current = 0x860b020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc an array from the pool: -------------------------------- pool = 0x860b020 .d .last = 0x860b084 .end = 0x860b420 .next = 0x0 .failed = 0 .max = 984 .current = 0x860b020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 924 array = 0x860b048 .elts = 0x860b05c .nelts = 10 .size = 4 .nalloc = 10 .pool = 0x860b020 elements: 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xa |
9.3.5. 小结
本文针对数组结构进行了较为全面的分析,包括数组相关数据结构,数组的创建、销毁,以及向数组中添加元素等。最后通过一个简单例子向读者展示nginx数组的创建、添加元素和销毁操作,同时借此向读者展示编译测试代码的方法。
9.4. 链表结构ngx_list_t
9.4.1. 简介
本文继续介绍nginx的容器——链表。
链表实现文件:文件:src/core/ngx_list.h/.c。
9.4.2. 链表结构
9.4.2.1. ngx_list_t结构
nginx的链表(头)结构为ngx_list_t,链表节点结构为ngx_list_part_t,定义如下:
typedef struct ngx_list_part_s ngx_list_part_t; struct ngx_list_part_s { //链表节点结构 void *elts; //指向该节点实际的数据区(该数据区中可以存放nalloc个大小为size的元素) ngx_uint_t nelts; //实际存放的元素个数 ngx_list_part_t *next; //指向下一个节点 }; typedef struct{ //链表头结构 ngx_list_part_t *last; //指向链表最后一个节点(part) ngx_list_part_t part; //链表头中包含的第一个节点(part) size_t size; //每个元素大小 ngx_uint_t nalloc; //链表所含空间个数,即实际分配的小空间的个数 ngx_pool_t *pool; //该链表节点空间在此内存池中分配 }ngx_list_t; |
其中,sizeof(ngx_list_t)=28,sizeof(ngx_list_part_t)=12。
由此可见,nginx的链表,也要从内存池中分配。对于每一个节点(list part)将分配nalloc个大小为size的小空间,实际分配的大小为(nalloc * size)。
9.4.2.2. ngx_list_t的逻辑结构
ngx_list_t结构引用了ngx_pool_t结构,因此本文参考内存池结构ngx_pool_t及内存管理一文画出相关结构的逻辑图,如下:
9.4.3. 链表操作
链表操作共3个,如下:
//创建链表 ngx_list_t*ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size); //初始化链表 static ngx_inline ngx_int_t ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_tn, size_t size); //添加元素 void*ngx_list_push(ngx_list_t *l) |
9.4.3.1. 创建链表
创建链表的操作实现如下,首先分配链表头(28B),然后分配头节点(即链表头中包含的part)数据区,两次分配均在传入的内存池(pool指向的内存池)中进行。然后简单初始化链表头并返回链表头的起始位置。
ngx_list_t * ngx_list_create(ngx_pool_t*pool, ngx_uint_t n, size_t size) { ngx_list_t *list; list = ngx_palloc(pool,sizeof(ngx_list_t)); //从内存池中分配链表头 if (list == NULL) { return NULL; } list->part.elts =ngx_palloc(pool, n * size); //接着分配n*size大小的区域作为链表数据区 if (list->part.elts == NULL) { return NULL; } list->part.nelts = 0; //初始化 list->part.next = NULL; list->last = &list->part; list->size = size; list->nalloc = n; list->pool = pool; return list; //返回链表头的起始位置 } |
创建链表后内存池的物理结构图如下:
9.4.3.2. 添加元素
添加元素操作实现如下,同nginx数组实现类似,其实际的添加操作并不在该函数中完成。函数ngx_list_push返回可以在该链表数据区中放置元素(元素可以是1个或多个)的位置,而添加操作即在获得添加位置之后进行,如后文的例子。
void * ngx_list_push(ngx_list_t*l) { void *elt; ngx_list_part_t *last; last = l->last; if (last->nelts ==l->nalloc) { //链表数据区满 last =ngx_palloc(l->pool, sizeof(ngx_list_part_t)); //分配节点(list part) if (last == NULL) { return NULL; } last->elts =ngx_palloc(l->pool, l->nalloc * l->size);//分配该节点(part)的数据区 if (last->elts == NULL) { return NULL; } last->nelts = 0; last->next = NULL; l->last->next =last; //将分配的list part插入链表 l->last = last; //并修改list头的last指针 } elt = (char *)last->elts + l->size * last->nelts; //计算下一个数据在链表数据区中的位置 last->nelts++; //实际存放的数据个数加1 return elt; //返回该位置 } |
由此可见,向链表中添加元素实际上就是从内存池中分配链表节点(part)及其该节点的实际数据区,并修改链表节点(part)信息。
注1:与数组的区别,数组数据区满时要扩充数据区空间;而链表每次要分配节点及其数据区。
注2:链表的每个节点(part)的数据区中可以放置1个或多个元素,这里的元素可以是一个整数,也可以是一个结构。
下图是一个有3个节点的链表的逻辑结构图。
图中的线太多,容易眼晕,下面这个图可能好一些。
9.4.4. 一个例子
9.4.4.1. 代码
#include #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" #include "ngx_list.h" volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } } void dump_list_part(ngx_list_t* list, ngx_list_part_t* part) { int *ptr = (int*)(part->elts); int loop = 0; printf(" .part = 0x%x\n", &(list->part)); printf(" .elts = 0x%x ", part->elts); printf("("); for (; loop < list->nalloc - 1; loop++) { printf("0x%x, ", ptr[loop]); } printf("0x%x)\n", ptr[loop]); printf(" .nelts = %d\n", part->nelts); printf(" .next = 0x%x", part->next); if (part->next) printf(" -->\n"); printf(" \n"); } void dump_list(ngx_list_t* list) { if (list == NULL) return; printf("list = 0x%x\n", list); printf(" .last = 0x%x\n", list->last); printf(" .part = 0x%x\n", &(list->part)); printf(" .size = %d\n", list->size); printf(" .nalloc = %d\n", list->nalloc); printf(" .pool = 0x%x\n\n", list->pool); printf("elements:\n"); ngx_list_part_t *part = &(list->part); while (part) { dump_list_part(list, part); part = part->next; } printf("\n"); } int main() { ngx_pool_t *pool; int i; printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool); printf("--------------------------------\n"); printf("alloc an list from the pool:\n"); printf("--------------------------------\n"); ngx_list_t *list = ngx_list_create(pool, 5, sizeof(int)); dump_pool(pool); for (i = 0; i < 15; i++) { int *ptr = ngx_list_push(list); *ptr = i + 1; } printf("--------------------------------\n"); printf("the list information:\n"); printf("--------------------------------\n"); dump_list(list); printf("--------------------------------\n"); printf("the pool at the end:\n"); printf("--------------------------------\n"); dump_pool(pool); ngx_destroy_pool(pool); return 0; } |
9.4.4.2. 如何编译
CXX = gcc CXXFLAGS +=-g -Wall -Wextra NGX_ROOT =/usr/src/nginx-1.0.4 TARGETS =ngx_list_t_test TARGETS_C_FILE= $(TARGETS).c CLEANUP = rm-f $(TARGETS) *.o all:$(TARGETS) clean: $(CLEANUP) CORE_INCS =-I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \ NGX_PALLOC =$(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING =$(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC =$(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_LIST =$(NGX_ROOT)/objs/src/core/ngx_list.o $(TARGETS):$(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING)$(NGX_ALLOC) $(NGX_LIST) $^ -o $@ |
9.4.4.3. 运行结果
# ./ngx_list_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x9208020 .d .last = 0x9208048 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc an list from the pool: -------------------------------- pool = 0x9208020 .d .last = 0x9208078 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 936 -------------------------------- the list information: -------------------------------- list = 0x9208048 .last = 0x9208098 .part = 0x920804c .size = 4 .nalloc = 5 .pool = 0x9208020 elements: .part = 0x920804c .elts = 0x9208064 (0x1, 0x2, 0x3, 0x4, 0x5) .nelts = 5 .next = 0x9208078 --> .part = 0x920804c .elts = 0x9208084 (0x6, 0x7, 0x8, 0x9, 0xa) .nelts = 5 .next = 0x9208098 --> .part = 0x920804c .elts = 0x92080a4 (0xb, 0xc, 0xd, 0xe, 0xf) .nelts = 5 .next = 0x0 -------------------------------- the pool at the end: -------------------------------- pool = 0x9208020 .d .last = 0x92080b8 .end = 0x9208420 .next = 0x0 .failed = 0 .max = 984 .current = 0x9208020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 872 |
9.4.5. 小结
本文针对nginx-1.0.4的容器——链表结构进行了较为全面的分析,包括链表相关数据结构,链表创建和向链表中添加元素等。最后通过一个简单例子向读者展示nginx链表创建和添加元素操作,同时借此向读者展示编译测试代码的方法。
9.5. 队列结构ngx_queue_t
9.5.1. 简介
链表实现文件:文件:src/core/ngx_queue.h/.c。
9.5.2. 队列结构
nginx的队列是由具有头节点的双向循环链表实现的,每一个节点结构为ngx_queue_t,定义如下:
typedef struct ngx_queue_s ngx_queue_t; struct ngx_queue_s { //队列结构 ngx_queue_t *prev; ngx_queue_t *next; }; |
其中,sizeof(ngx_queue_t)=8。从队列结构定义可以看出,nginx的队列结构里并没有其节点的数据内容。
9.5.3. 队列操作
队列有如下操作:
//初始化队列 ngx_queue_init(q) //判断队列是否为空 ngx_queue_empty(h) //在头节点之后插入新节点 ngx_queue_insert_head(h, x) //在尾节点之后插入新节点 ngx_queue_insert_tail(h, x) //删除节点x ngx_queue_remove(x) //分割队列 ngx_queue_split(h, q, n) //链接队列 ngx_queue_add(h, n) //获取队列的中间节点 ngx_queue_t *ngx_queue_middle(ngx_queue_t *queue) //排序队列(稳定的插入排序) void ngx_queue_sort(ngx_queue_t *queue,ngx_int_t (*cmp)(const ngx_queue_t*, const ngx_queue_t*)) |
其中,插入节点、取队列头、取队列尾等操作由宏实现,获取中间节点、排序等操作由函数实现。
9.5.3.1. 初始化队列
#define ngx_queue_init(q) \ (q)->prev = q; \ (q)->next = q |
9.5.3.2. 在头节点之后插入
在头节点之后插入操作由宏ngx_queue_insert_head完成,如下:
#define ngx_queue_insert_head(h, x) \ (x)->next = (h)->next; \ (x)->next->prev = x; \ (x)->prev = h; \ (h)->next = x |
画出该操作的逻辑图,如下:
图中虚线表示被修改/删除的指针,蓝色表示新修改/增加的指针。
9.5.3.3. 在尾节点之后插入
在尾节点之后插入操作由宏ngx_queue_insert_tail完成,如下:
#define ngx_queue_insert_tail(h, x) \ (x)->prev = (h)->prev; \ (x)->prev->next = x; \ (x)->next = h; \ (h)->prev = x |
该操作的逻辑图如下:
9.5.3.4. 删除节点
在尾节点之后插入操作由宏ngx_queue_remove完成,如下:
#if (NGX_DEBUG) #define ngx_queue_remove(x) \ (x)->next->prev = (x)->prev; \ (x)->prev->next = (x)->next; \ (x)->prev = NULL; \ (x)->next = NULL #else #define ngx_queue_remove(x) \ (x)->next->prev = (x)->prev; \ (x)->prev->next = (x)->next #endif |
该操作的逻辑图如下:
9.5.3.5. 分割队列
分割队列操作由宏ngx_queue_split完成,如下:
#define ngx_queue_split(h, q, n) \ (n)->prev = (h)->prev; \ (n)->prev->next = n; \ (n)->next = q; \ (h)->prev = (q)->prev; \ (h)->prev->next = h; \ (q)->prev = n; |
该宏有3个参数,h为队列头(即链表头指针),将该队列从q节点将队列(链表)分割为两个队列(链表),q之后的节点组成的新队列的头节点为n,图形演示如下:
9.5.3.6. 增加链接队列
链接队列由宏ngx_queue_add完成,操作如下:
#define ngx_queue_add(h, n) \ (h)->prev->next = (n)->next; \ (n)->next->prev = (h)->prev; \ (h)->prev = (n)->prev; \ (h)->prev->next = h; |
其中,h、n分别为两个队列的指针,即头节点指针,该操作将n队列链接在h队列之后。演示图形如下:
宏ngx_queue_split和ngx_queue_add只在http模块locations相关操作中使用,在后续的讨论http模块locations相关操作时再详细叙述。
9.5.3.7. 获取中间节点
中间节点,若队列有奇数个(除头节点外)节点,则返回中间的节点;若队列有偶数个节点,则返回后半个队列的第一个节点。操作如下:
ngx_queue_t * ngx_queue_middle(ngx_queue_t *queue) { ngx_queue_t *middle, *next; middle = ngx_queue_head(queue); if (middle == ngx_queue_last(queue)) { return middle; } next = ngx_queue_head(queue); for ( ;; ) { middle = ngx_queue_next(middle); next = ngx_queue_next(next); if (next == ngx_queue_last(queue)) {//偶数个节点,在此返回后半个队列的第一个节点 return middle; } next = ngx_queue_next(next); if (next == ngx_queue_last(queue)) {//奇数个节点,在此返回中间节点 return middle; } } } |
注意:代码中的next指针,其每次均会后移两个位置(节点),而middle指针每次后移一个位置(节点)。演示图形如下:
9.5.3.8. 队列排序
队列排序采用的是稳定的简单插入排序方法,即从第一个节点开始遍历,依次将当前节点(q)插入前面已经排好序的队列(链表)中,下面程序中,前面已经排好序的队列的尾节点为prev。操作如下:
void ngx_queue_sort(ngx_queue_t *queue, ngx_int_t (*cmp)(const ngx_queue_t *, const ngx_queue_t *)) { ngx_queue_t *q, *prev, *next; q = ngx_queue_head(queue); if (q == ngx_queue_last(queue)) { return; } for (q = ngx_queue_next(q); q != ngx_queue_sentinel(queue); q = next) { prev = ngx_queue_prev(q); next = ngx_queue_next(q); ngx_queue_remove(q); do { if (cmp(prev, q) <= 0) { //比较 break; } prev = ngx_queue_prev(prev); //prev指针前移 } while (prev != ngx_queue_sentinel(queue)); ngx_queue_insert_after(prev, q); //将q插入prev节点之后(此处即为简单插入) } } |
该排序操作使用前面介绍的宏来完成其插入动作,只是一些简单的修改指针指向的操作,效率较高。
9.5.3.9. 如何获取队列节点数据
由队列基本结构和以上操作可知,nginx的队列操作只对链表指针进行简单的修改指向操作,并不负责节点数据空间的分配。因此,用户在使用nginx队列时,要自己定义数据结构并分配空间,且在其中包含一个ngx_queue_t的指针或者对象,当需要获取队列节点数据时,使用ngx_queue_data宏,其定义如下:
#define ngx_queue_data(q, type, link) \ (type *) ((u_char *) q – offsetof(type, link)) |
由该宏定义可以看出,一般定义队列节点结构(该结构类型为type)时,需要将真正的数据放在前面,而ngx_queue_t结构放在后面,故该宏使用减法计算整个节点结构的起始地址(需要进行类型转换)。
数据结构如下:
9.5.4. 一个例子
给出一个创建内存池并从中分配队列头节点和其他节点组成队列的简单例子。在该例中,队列的数据是一系列的二维点(x,y分别表示该点的横、纵坐标),将这些点插入队列后进行排序,以此向读者展示nginx队列的使用方法。
9.5.4.1. 代码
#include #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_palloc.h" #include "ngx_queue.h" //2-dimensional point (x, y) queue structure typedef struct{ int x; int y; } my_point_t; typedef struct{ my_point_t point; ngx_queue_t queue; } my_point_queue_t; volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } void dump_pool(ngx_pool_t* pool) { while (pool){ printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } } void dump_queue_from_head(ngx_queue_t *que) { ngx_queue_t *q = ngx_queue_head(que); printf("(0x%x: (0x%x, 0x%x)) <==> \n", que, que->prev, que->next); for (; q != ngx_queue_sentinel(que); q = ngx_queue_next(q)){ my_point_queue_t *point = ngx_queue_data(q, my_point_queue_t, queue); printf("(0x%x: (%-2d, %-2d), 0x%x: (0x%x, 0x%x)) <==> \n", point, point->point.x, point->point.y, &point->queue, point->queue.prev, point->queue.next); } } void dump_queue_from_tail(ngx_queue_t *que) { ngx_queue_t *q = ngx_queue_last(que); printf("(0x%x: (0x%x, 0x%x)) <==> \n", que, que->prev, que->next); for (; q != ngx_queue_sentinel(que); q = ngx_queue_prev(q)) { my_point_queue_t *point = ngx_queue_data(q, my_point_queue_t, queue); printf("(0x%x: (%-2d, %-2d), 0x%x: (0x%x, 0x%x)) <==> \n", point, point->point.x, point->point.y, &point->queue, point->queue.prev, point->queue.next); } } //sort from small to big ngx_int_t my_point_cmp(const ngx_queue_t* lhs, const ngx_queue_t* rhs) { my_point_queue_t *pt1 = ngx_queue_data(lhs, my_point_queue_t, queue); my_point_queue_t *pt2 = ngx_queue_data(rhs, my_point_queue_t, queue); if (pt1->point.x < pt2->point.x) return 0; else if (pt1->point.x > pt2->point.x) return 1; else if (pt1->point.y < pt2->point.y) return 0; else if (pt1->point.y > pt2->point.y) return 1; return 1; } #define Max_Num 6 int main() { ngx_pool_t *pool; ngx_queue_t *myque; my_point_queue_t *point; my_point_t points[Max_Num] = { {10, 1}, {20, 9}, {9, 9}, {90, 80}, {5, 3}, {50, 20} }; int i; printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool); printf("--------------------------------\n"); printf("alloc a queue head and nodes :\n"); printf("--------------------------------\n"); myque = ngx_palloc(pool, sizeof(ngx_queue_t)); //alloc a queue head ngx_queue_init(myque); //init the queue //insert some points into the queue for (i = 0; i < Max_Num; i++) { point = (my_point_queue_t*)ngx_palloc(pool, sizeof(my_point_queue_t)); point->point.x = points[i].x; point->point.y = points[i].y; ngx_queue_init(&point->queue); //insert this point into the points queue ngx_queue_insert_head(myque, &point->queue); } dump_queue_from_tail(myque); printf("\n"); printf("--------------------------------\n"); printf("sort the queue:\n"); printf("--------------------------------\n"); ngx_queue_sort(myque, my_point_cmp); dump_queue_from_head(myque); printf("\n"); printf("--------------------------------\n"); printf("the pool at the end:\n"); printf("--------------------------------\n"); dump_pool(pool); ngx_destroy_pool(pool); return 0; } |
9.5.4.2. 10.4.2、如何编译
CXX = gcc CXXFLAGS += -g -Wall -Wextra NGX_ROOT = /usr/src/nginx-1.0.4 TARGETS = ngx_queue_t_test TARGETS_C_FILE = $(TARGETS).c CLEANUP = rm -f $(TARGETS) *.o all: $(TARGETS) clean: $(CLEANUP) CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \ NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_QUEUE = $(NGX_ROOT)/objs/src/core/ngx_queue.o $(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $(NGX_QUEUE) $^ -o $@ |
9.5.4.3. 10.4.3、运行结果
# ./ngx_queue_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8bcf020 .d .last = 0x8bcf048 .end = 0x8bcf420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8bcf020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- alloc a queue head and nodes : -------------------------------- (0x8bcf048: (0x8bcf058, 0x8bcf0a8)) <==> (0x8bcf050: (10, 1 ), 0x8bcf058: (0x8bcf068, 0x8bcf048)) <==> (0x8bcf060: (20, 9 ), 0x8bcf068: (0x8bcf078, 0x8bcf058)) <==> (0x8bcf070: (9 , 9 ), 0x8bcf078: (0x8bcf088, 0x8bcf068)) <==> (0x8bcf080: (90, 80), 0x8bcf088: (0x8bcf098, 0x8bcf078)) <==> (0x8bcf090: (5 , 3 ), 0x8bcf098: (0x8bcf0a8, 0x8bcf088)) <==> (0x8bcf0a0: (50, 20), 0x8bcf0a8: (0x8bcf048, 0x8bcf098)) <==> -------------------------------- sort the queue: -------------------------------- (0x8bcf048: (0x8bcf088, 0x8bcf098)) <==> (0x8bcf090: (5 , 3 ), 0x8bcf098: (0x8bcf048, 0x8bcf078)) <==> (0x8bcf070: (9 , 9 ), 0x8bcf078: (0x8bcf098, 0x8bcf058)) <==> (0x8bcf050: (10, 1 ), 0x8bcf058: (0x8bcf078, 0x8bcf068)) <==> (0x8bcf060: (20, 9 ), 0x8bcf068: (0x8bcf058, 0x8bcf0a8)) <==> (0x8bcf0a0: (50, 20), 0x8bcf0a8: (0x8bcf068, 0x8bcf088)) <==> (0x8bcf080: (90, 80), 0x8bcf088: (0x8bcf0a8, 0x8bcf048)) <==> -------------------------------- the pool at the end: -------------------------------- pool = 0x8bcf020 .d .last = 0x8bcf0b0 .end = 0x8bcf420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8bcf020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 880 |
该队列的逻辑图如下:
9.5.5. 10.5、小结
本文针对nginx-1.0.4的队列进行了较为全面的分析,包括队列结构和队列操作,队列操作主要包括在头节点之后插入节点、在尾节点之后插入节点、获取中间节点、队列排序等。最后通过一个简单例子向读者展示nginx队列的使用方法,同时借此向读者展示编译测试nginx代码的方法。
9.6. hash结构ngx_hash_t
9.6.1. 简介
链表实现文件:文件:src/core/ngx_hash.h/.c。
9.6.2. hash结构
nginx的hash结构比其list、array、queue等结构稍微复杂一些。
9.6.2.1. ngx_hash_t结构
nginx的hash结构为ngx_hash_t,hash元素结构为ngx_hash_elt_t,定义如下:
typedef struct { //hash元素结构 void *value; //value,即某个key对应的值,即中的value u_short len; //name长度 u_char name[1]; //要hash的数据(在nginx中表现为字符串),即中的key } ngx_hash_elt_t; typedef struct { //hash结构 ngx_hash_elt_t **buckets; //hash桶(有size个桶) ngx_uint_t size; //hash桶个数 } ngx_hash_t; |
其中,sizeof(ngx_hash_t) = 8,sizeof(ngx_hash_elt_t) = 8。实际上,ngx_hash_elt_t结构中的name字段就是ngx_hash_key_t结构中的key。这在ngx_hash_init()函数中可以看到,请参考后续的分析。该结构在模块配置解析时经常使用。
9.6.2.2. ngx_hash_init_t结构
nginx的hash初始化结构是ngx_hash_init_t,用来将其相关数据封装起来作为参数传递给ngx_hash_init()或ngx_hash_wildcard_init()函数。这两个函数主要是在http相关模块中使用,例如ngx_http_server_names()函数(优化http Server Names),ngx_http_merge_types()函数(合并httptype),ngx_http_fastcgi_merge_loc_conf()函数(合并FastCGI Location Configuration)等函数或过程用到的参数、局部对象/变量等。这些内容将在后续的文章中讲述。
ngx_hash_init_t结构如下。sizeof(ngx_hash_init_t)=28。
typedef struct { //hash初始化结构 ngx_hash_t *hash; //指向待初始化的hash结构 ngx_hash_key_pt key; //hash函数指针 ngx_uint_t max_size; //bucket的最大个数 ngx_uint_t bucket_size; //每个bucket的空间 char *name; //该hash结构的名字(仅在错误日志中使用) ngx_pool_t *pool; //该hash结构从pool指向的内存池中分配 ngx_pool_t *temp_pool; //分配临时数据空间的内存池 } ngx_hash_init_t; |
9.6.2.3. ngx_hash_key_t结构
该结构也主要用来保存要hash的数据,即键-值对,在实际使用中,一般将多个键-值对保存在ngx_hash_key_t结构的数组中,作为参数传给ngx_hash_init()或ngx_hash_wildcard_init()函数。其定义如下:
typedef struct { //hash key结构 ngx_str_t key; //key,为nginx的字符串结构 ngx_uint_t key_hash; //由该key计算出的hash值(通过hash函数如ngx_hash_key_lc()) void *value; //该key对应的值,组成一个键-值对 } ngx_hash_key_t; typedef struct { //字符串结构 size_t len; //字符串长度 u_char *data; //字符串内容 } ngx_str_t; |
其中,sizeof(ngx_hash_key_t) = 16。一般在使用中,value指针可能指向静态数据区(例如全局数组、常量字符串)、堆区(例如动态分配的数据区用来保存value值)等。可参考本文后面的例子。
关于ngx_table_elt_t结构和ngx_hash_keys_arrays_t结构,因其对于hash结构本身没有太大作用,主要是为模块配置、referer合法性验证等设计的数据结构,例如http的core模块、map模块、referer模块、SSI filter模块等,此处不再讲述,将在后续的文章中介绍。
9.6.2.4. hash的逻辑结构
ngx_hash_init_t结构引用了ngx_pool_t结构,因此本文参考内存池结构ngx_pool_t及内存管理一文画出相关结构的逻辑图,如下:
9.6.3. hash操作
9.6.3.1. NGX_HASH_ELT_SIZE宏
NGX_HASH_ELT_SIZE宏用来计算上述ngx_hash_elt_t结构大小,定义如下:
#define NGX_HASH_ELT_SIZE(name) //该参数name即为ngx_hash_elt_t结构指针 (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *))) //以4字节对齐 |
在32位平台上,sizeof(void*)=4,(name)->key.len即是ngx_hash_elt_t结构中name数组保存的内容的长度,其中的"+2"是要加上该结构中len字段(u_short类型)的大小。
因此,NGX_HASH_ELT_SIZE(name)=4+ngx_align((name)->key.len + 2, 4),该式后半部分即是(name)->key.len+2以4字节对齐的大小。
9.6.3.2. hash函数
nginx提供的hash函数有以下几种:
#define ngx_hash(key, c) ((ngx_uint_t) key * 31 + c) //hash宏 ngx_uint_t ngx_hash_key(u_char *data, size_t len); ngx_uint_t ngx_hash_key_lc(u_char *data, size_t len); //lc表示lower case,即字符串转换为小写后再计算hash值 ngx_uint_t ngx_hash_strlow(u_char *dst, u_char *src, size_t n); |
hash函数都很简单,以上3个函数都会调用ngx_hash宏,该宏返回一个(长)整数。此处介绍第一个函数,定义如下:
ngx_uint_t ngx_hash_key(u_char *data, size_t len) { ngx_uint_t i, key; key = 0; for (i = 0; i < len; i++) { key = ngx_hash(key, data[i]); } return key; } |
因此,ngx_hash_key函数的计算可表述为下列公式。
Key[0] = data[0] Key[1] = data[0]*31 + data[1] Key[2] = (data[0]*31 + data[1])*31 + data[2] ... Key[len-1] = ((((data[0]*31 + data[1])*31 + data[2])*31) ... data[len-2])*31 + data[len-1] |
key[len-1]即为传入的参数data对应的hash值。
9.6.3.3. hash初始化
hash初始化由ngx_hash_init()函数完成,其names参数是ngx_hash_key_t结构的数组,即键-值对数组,nelts表示该数组元素的个数。因此,在调用该函数进行初始化之前,ngx_hash_key_t结构的数组是准备好的,如何使用,可以采用nginx的ngx_array_t结构,详见本文后面的例子。
该函数初始化的结果就是将names数组保存的键-值对,通过hash的方式将其存入相应的一个或多个hash桶(即代码中的buckets)中,该hash过程用到的hash函数一般为ngx_hash_key_lc等。hash桶里面存放的是ngx_hash_elt_t结构的指针(hash元素指针),该指针指向一个基本连续的数据区。该数据区中存放的是经hash之后的键-值对,即ngx_hash_elt_t结构中的字段。每一个这样的数据区存放的键-值对可以是一个或多个。
此处有几个问题需要说明。
问题1:为什么说是基本连续?
——用NGX_HASH_ELT_SIZE宏计算某个hash元素的总长度时,存在以sizeof(void*)对齐的填补(padding)。因此将names数组中的键-值对中的key拷贝到ngx_hash_elt_t结构的name[1]数组中时,已经为该hash元素分配的空间不会完全被用完,故这个数据区是基本连续的。这一点也可以参考本节后面的结构图或本文后面的例子。
问题2:这些基本连续的数据区从哪里分配的?
——当然是从该函数的第一个参数ngx_hash_init_t的pool字段指向的内存池中分配的。
问题3:与不同的是什么?
——key保存的仅仅是个指针,而key'却是key拷贝到name[1]的结果。而value和value'都是指针。如1.3节说明,value指针可能指向静态数据区(例如全局数组、常量字符串)、堆区(例如动态分配的数据区用来保存value值)等。可参考本文后面的例子。
问题4:如何知道某个键-值对放在哪个hash桶中?
——key = names[n].key_hash % size; 代码中的这个计算是也。计算结果key即是该键要放在那个hash桶的编号(从0到size-1)。
该函数代码如下。一些疑点、难点的解释请参考//后笔者所加的注释,也可参考本节的hash结构图。
//nelts是names数组中(实际)元素的个数 ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts) { u_char *elts; size_t len; u_short *test; ngx_uint_t i, n, key, size, start, bucket_size; ngx_hash_elt_t *elt, **buckets; for (n = 0; n < nelts; n++) { //检查names数组的每一个元素,判断桶的大小是否够分配 if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *)) { //有任何一个元素,桶的大小不够为该元素分配空间,则退出 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should " "increase %s_bucket_size: %i", hinit->name, hinit->name, hinit->bucket_size); return NGX_ERROR; } } //分配2*max_size个字节的空间保存hash数据(该内存分配操作不在nginx的内存池中进行,因为test只是临时的) test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log); if (test == NULL) { return NGX_ERROR; } bucket_size = hinit->bucket_size - sizeof(void *); //一般sizeof(void*)=4 start = nelts / (bucket_size / (2 * sizeof(void *))); // start = start ? start : 1; if (hinit->max_size > 10000 && hinit->max_size / nelts < 100) { start = hinit->max_size - 1000; } for (size = start; size < hinit->max_size; size++) { ngx_memzero(test, size * sizeof(u_short)); //标记1:此块代码是检查bucket大小是否够分配hash数据 for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; } //计算key和names中所有name长度,并保存在test[key]中 key = names[n].key_hash % size; //若size=1,则key一直为0 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); if (test[key] > (u_short) bucket_size) {//若超过了桶的大小,则到下一个桶重新计算 goto next; } } goto found; next: continue; } //若没有找到合适的bucket,退出 ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should increase " "either %s_max_size: %i or %s_bucket_size: %i", hinit->name, hinit->name, hinit->max_size, hinit->name, hinit->bucket_size); ngx_free(test); return NGX_ERROR; found: //找到合适的bucket for (i = 0; i < size; i++) { //将test数组前size个元素初始化为4 test[i] = sizeof(void *); } for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; } //计算key和names中所有name长度,并保存在test[key]中 key = names[n].key_hash % size; //若size=1,则key一直为0 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); } //计算hash数据的总长度 len = 0; for (i = 0; i < size; i++) { if (test[i] == sizeof(void *)) {//若test[i]仍为初始化的值4,即没有变化,则继续 continue; } //对test[i]按ngx_cacheline_size对齐(32位平台,ngx_cacheline_size=32) test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size)); len += test[i]; } if (hinit->hash == NULL) {//在内存池中分配hash头及buckets数组(size个ngx_hash_elt_t*结构) hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t) + size * sizeof(ngx_hash_elt_t *)); if (hinit->hash == NULL) { ngx_free(test); return NGX_ERROR; } //计算buckets的启示位置(在ngx_hash_wildcard_t结构之后) buckets = (ngx_hash_elt_t **) ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t)); } else { //在内存池中分配buckets数组(size个ngx_hash_elt_t*结构) buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *)); if (buckets == NULL) { ngx_free(test); return NGX_ERROR; } } //接着分配elts,大小为len+ngx_cacheline_size,此处为什么+32?——下面要按32字节对齐 elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size); if (elts == NULL) { ngx_free(test); return NGX_ERROR; } //将elts地址按ngx_cacheline_size=32对齐 elts = ngx_align_ptr(elts, ngx_cacheline_size); for (i = 0; i < size; i++) { //将buckets数组与相应elts对应起来 if (test[i] == sizeof(void *)) { continue; } buckets[i] = (ngx_hash_elt_t *) elts; elts += test[i]; } for (i = 0; i < size; i++) { //test数组置0 test[i] = 0; } for (n = 0; n < nelts; n++) { //将传进来的每一个hash数据存入hash表 if (names[n].key.data == NULL) { continue; } //计算key,即将被hash的数据在第几个bucket,并计算其对应的elts位置 key = names[n].key_hash % size; elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]); //对ngx_hash_elt_t结构赋值 elt->value = names[n].value; elt->len = (u_short) names[n].key.len; ngx_strlow(elt->name, names[n].key.data, names[n].key.len); //计算下一个要被hash的数据的长度偏移 test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); } for (i = 0; i < size; i++) { if (buckets[i] == NULL) { continue; } //test[i]相当于所有被hash的数据总长度 elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]); elt->value = NULL; } ngx_free(test); //释放该临时空间 hinit->hash->buckets = buckets; hinit->hash->size = size; return NGX_OK; } |
所谓的hash数据长度即指ngx_hash_elt_t结构被赋值后的长度。nelts个元素存放在names数组中,调用该函数对hash进行初始化之后,这nelts个元素被保存在size个hash桶指向的ngx_hash_elts_t数据区,这些数据区中共保存了nelts个hash元素。即hash桶(buckets)存放的是ngx_hash_elt_t数据区的起始地址,以该起始地址开始的数据区存放的是经hash之后的hash元素,每个hash元素的最后是以name[0]为开始的字符串,该字符串就是names数组中某个元素的key,即键值对中的key,然后该字符串之后会有几个字节的因对齐产生的padding。
一个典型的经初始化后的hash物理结构如下。具体的可参考后文的例子。
9.6.3.4. hash查找
hash查找操作由ngx_hash_find()函数完成,代码如下:
//由key,name,len信息在hash指向的hash table中查找该key对应的value void * ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len) { ngx_uint_t i; ngx_hash_elt_t *elt; elt = hash->buckets[key % hash->size];//由key找到所在的bucket(该bucket中保存其elts地址) if (elt == NULL) { return NULL; } while (elt->value) { if (len != (size_t) elt->len) { //先判断长度 goto next; } for (i = 0; i < len; i++) { if (name[i] != elt->name[i]) { //接着比较name的内容(此处按字符匹配) goto next; } } return elt->value; //匹配成功,直接返回该ngx_hash_elt_t结构的value字段 next: //注意此处从elt->name[0]地址处向后偏移,故偏移只需加该elt的len即可,然后在以4字节对齐 elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len, sizeof(void *)); continue; } return NULL; } |
查找操作相当简单,由key直接计算所在的bucket,该bucket中保存其所在ngx_hash_elt_t数据区的起始地址;然后根据长度判断并用name内容匹配,匹配成功,其ngx_hash_elt_t结构的value字段即是所求。
9.6.4. 一个例子
本节给出一个创建内存池并从中分配hash结构、hash桶、hash元素并将键-值对加入该hash结构的简单例子。
在该例中,将完成这样一个应用,将给定的多个url及其ip组成的二元组作为,初始化时对这些进行hash,然后根据给定的url查找其对应的ip地址,若没有找到,则给出相关提示信息。以此向读者展示nginx的hash使用方法。
9.6.4.1. 代码
#include #include "ngx_config.h" #include "ngx_conf_file.h" #include "nginx.h" #include "ngx_core.h" #include "ngx_string.h" #include "ngx_palloc.h" #include "ngx_array.h" #include "ngx_hash.h" #define Max_Num 7 #define Max_Size 1024 #define Bucket_Size 64 //256, 64 #define NGX_HASH_ELT_SIZE(name) \ (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *))) static ngx_str_t urls[Max_Num] = { ngx_string("www.baidu.com"), //220.181.111.147 ngx_string("www.sina.com.cn"), //58.63.236.35 ngx_string("www.google.com"), //74.125.71.105 ngx_string("www.qq.com"), //60.28.14.190 ngx_string("www.163.com"), //123.103.14.237 ngx_string("www.sohu.com"), //219.234.82.50 ngx_string("www.abo321.org") //117.40.196.26 }; static char* values[Max_Num] = { "220.181.111.147", "58.63.236.35", "74.125.71.105", "60.28.14.190", "123.103.14.237", "219.234.82.50", "117.40.196.26" }; #define Max_Url_Len 15 #define Max_Ip_Len 15 #define Max_Num2 2 static ngx_str_t urls2[Max_Num2] = { ngx_string("www.china.com"), //60.217.58.79 ngx_string("www.csdn.net") //117.79.157.242 }; ngx_hash_t* init_hash(ngx_pool_t *pool, ngx_array_t *array); void dump_pool(ngx_pool_t* pool); void dump_hash_array(ngx_array_t* a); void dump_hash(ngx_hash_t *hash, ngx_array_t *array); ngx_array_t* add_urls_to_array(ngx_pool_t *pool); void find_test(ngx_hash_t *hash, ngx_str_t addr[], int num); volatile ngx_cycle_t *ngx_cycle; void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { } int main() { ngx_pool_t *pool = NULL; ngx_array_t *array = NULL; ngx_hash_t *hash; printf("--------------------------------\n"); printf("create a new pool:\n"); printf("--------------------------------\n"); pool = ngx_create_pool(1024, NULL); dump_pool(pool); printf("--------------------------------\n"); printf("create and add urls to it:\n"); printf("--------------------------------\n"); array = add_urls_to_array(pool); //in fact, here should validate array dump_hash_array(array); printf("--------------------------------\n"); printf("the pool:\n"); printf("--------------------------------\n"); dump_pool(pool); hash = init_hash(pool, array); if (hash == NULL) { printf("Failed to initialize hash!\n"); return -1; } printf("--------------------------------\n"); printf("the hash:\n"); printf("--------------------------------\n"); dump_hash(hash, array); printf("\n"); printf("--------------------------------\n"); printf("the pool:\n"); printf("--------------------------------\n"); dump_pool(pool); //find test printf("--------------------------------\n"); printf("find test:\n"); printf("--------------------------------\n"); find_test(hash, urls, Max_Num); printf("\n"); find_test(hash, urls2, Max_Num2); //release ngx_array_destroy(array); ngx_destroy_pool(pool); return 0; } ngx_hash_t* init_hash(ngx_pool_t *pool, ngx_array_t *array) { ngx_int_t result; ngx_hash_init_t hinit; ngx_cacheline_size = 32; //here this variable for nginx must be defined hinit.hash = NULL; //if hinit.hash is NULL, it will alloc memory for it in ngx_hash_init hinit.key = &ngx_hash_key_lc; //hash function hinit.max_size = Max_Size; hinit.bucket_size = Bucket_Size; hinit.name = "my_hash_sample"; hinit.pool = pool; //the hash table exists in the memory pool hinit.temp_pool = NULL; result = ngx_hash_init(&hinit, (ngx_hash_key_t*)array->elts, array->nelts); if (result != NGX_OK) return NULL; return hinit.hash; } void dump_pool(ngx_pool_t* pool) { while (pool) { printf("pool = 0x%x\n", pool); printf(" .d\n"); printf(" .last = 0x%x\n", pool->d.last); printf(" .end = 0x%x\n", pool->d.end); printf(" .next = 0x%x\n", pool->d.next); printf(" .failed = %d\n", pool->d.failed); printf(" .max = %d\n", pool->max); printf(" .current = 0x%x\n", pool->current); printf(" .chain = 0x%x\n", pool->chain); printf(" .large = 0x%x\n", pool->large); printf(" .cleanup = 0x%x\n", pool->cleanup); printf(" .log = 0x%x\n", pool->log); printf("available pool memory = %d\n\n", pool->d.end - pool->d.last); pool = pool->d.next; } } void dump_hash_array(ngx_array_t* a) { char prefix[] = " "; if (a == NULL) return; printf("array = 0x%x\n", a); printf(" .elts = 0x%x\n", a->elts); printf(" .nelts = %d\n", a->nelts); printf(" .size = %d\n", a->size); printf(" .nalloc = %d\n", a->nalloc); printf(" .pool = 0x%x\n", a->pool); printf(" elements:\n"); ngx_hash_key_t *ptr = (ngx_hash_key_t*)(a->elts); for (; ptr < (ngx_hash_key_t*)(a->elts + a->nalloc * a->size); ptr++) { printf(" 0x%x: {key = (\"%s\"%.*s, %d), key_hash = %-10ld, value = \"%s\"%.*s}\n", ptr, ptr->key.data, Max_Url_Len - ptr->key.len, prefix, ptr->key.len, ptr->key_hash, ptr->value, Max_Ip_Len - strlen(ptr->value), prefix); } printf("\n"); } void dump_hash(ngx_hash_t *hash, ngx_array_t *array) { int loop; char prefix[] = " "; u_short test[Max_Num] = {0}; ngx_uint_t key; ngx_hash_key_t* elts; int nelts; if (hash == NULL) return; printf("hash = 0x%x: **buckets = 0x%x, size = %d\n", hash, hash->buckets, hash->size); for (loop = 0; loop < hash->size; loop++) { ngx_hash_elt_t *elt = hash->buckets[loop]; printf(" 0x%x: buckets[%d] = 0x%x\n", &(hash->buckets[loop]), loop, elt); } printf("\n"); elts = (ngx_hash_key_t*)array->elts; nelts = array->nelts; for (loop = 0; loop < nelts; loop++) { char url[Max_Url_Len + 1] = {0}; key = elts[loop].key_hash % hash->size; ngx_hash_elt_t *elt = (ngx_hash_elt_t *) ((u_char *) hash->buckets[key] + test[key]); ngx_strlow(url, elt->name, elt->len); printf(" buckets %d: 0x%x: {value = \"%s\"%.*s, len = %d, name = \"%s\"%.*s}\n", key, elt, (char*)elt->value, Max_Ip_Len - strlen((char*)elt->value), prefix, elt->len, url, Max_Url_Len - elt->len, prefix); //replace elt->name with url test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&elts[loop])); } } ngx_array_t* add_urls_to_array(ngx_pool_t *pool) { int loop; char prefix[] = " "; ngx_array_t *a = ngx_array_create(pool, Max_Num, sizeof(ngx_hash_key_t)); for (loop = 0; loop < Max_Num; loop++) { ngx_hash_key_t *hashkey = (ngx_hash_key_t*)ngx_array_push(a); hashkey->key = urls[loop]; hashkey->key_hash = ngx_hash_key_lc(urls[loop].data, urls[loop].len); hashkey->value = (void*)values[loop]; } return a; } void find_test(ngx_hash_t *hash, ngx_str_t addr[], int num) { ngx_uint_t key; int loop; char prefix[] = " "; for (loop = 0; loop < num; loop++) { key = ngx_hash_key_lc(addr[loop].data, addr[loop].len); void *value = ngx_hash_find(hash, key, addr[loop].data, addr[loop].len); if (value) { printf("(url = \"%s\"%.*s, key = %-10ld) found, (ip = \"%s\")\n", addr[loop].data, Max_Url_Len - addr[loop].len, prefix, key, (char*)value); } else { printf("(url = \"%s\"%.*s, key = %-10d) not found!\n", addr[loop].data, Max_Url_Len - addr[loop].len, prefix, key); } } } |
9.6.4.2. 如何编译
CXX = gcc CXXFLAGS += -g -Wall -Wextra NGX_ROOT = /usr/src/nginx-1.0.4 TARGETS = ngx_hash_t_test TARGETS_C_FILE = $(TARGETS).c CLEANUP = rm -f $(TARGETS) *.o all: $(TARGETS) clean: $(CLEANUP) CORE_INCS = -I. \ -I$(NGX_ROOT)/src/core \ -I$(NGX_ROOT)/src/event \ -I$(NGX_ROOT)/src/event/modules \ -I$(NGX_ROOT)/src/os/unix \ -I$(NGX_ROOT)/objs \ NGX_PALLOC = $(NGX_ROOT)/objs/src/core/ngx_palloc.o NGX_STRING = $(NGX_ROOT)/objs/src/core/ngx_string.o NGX_ALLOC = $(NGX_ROOT)/objs/src/os/unix/ngx_alloc.o NGX_ARRAY = $(NGX_ROOT)/objs/src/core/ngx_array.o NGX_HASH = $(NGX_ROOT)/objs/src/core/ngx_hash.o $(TARGETS): $(TARGETS_C_FILE) $(CXX) $(CXXFLAGS) $(CORE_INCS) $(NGX_PALLOC) $(NGX_STRING) $(NGX_ALLOC) $(NGX_ARRAY) $(NGX_HASH) $^ -o $@ |
9.6.4.3. 运行结果
bucket_size=64字节:运行结果如下:
# ./ngx_hash_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8870020 .d .last = 0x8870048 .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- create and add urls to it: -------------------------------- array = 0x8870048 .elts = 0x887005c .nelts = 7 .size = 16 .nalloc = 7 .pool = 0x8870020 elements: 0x887005c: {key = ("www.baidu.com" , 13), key_hash = 270263191 , value = "220.181.111.147"} 0x887006c: {key = ("www.sina.com.cn", 15), key_hash = 1528635686, value = "58.63.236.35" } 0x887007c: {key = ("www.google.com" , 14), key_hash = -702889725, value = "74.125.71.105" } 0x887008c: {key = ("www.qq.com" , 10), key_hash = 203430122 , value = "60.28.14.190" } 0x887009c: {key = ("www.163.com" , 11), key_hash = -640386838, value = "123.103.14.237" } 0x88700ac: {key = ("www.sohu.com" , 12), key_hash = 1313636595, value = "219.234.82.50" } 0x88700bc: {key = ("www.abo321.org" , 14), key_hash = 1884209457, value = "117.40.196.26" } -------------------------------- the pool: -------------------------------- pool = 0x8870020 .d .last = 0x88700cc .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 852 -------------------------------- the hash: -------------------------------- hash = 0x88700cc: **buckets = 0x88700d8, size = 3 0x88700d8: buckets[0] = 0x8870100 0x88700dc: buckets[1] = 0x8870140 0x88700e0: buckets[2] = 0x8870180 buckets 1: 0x8870140: {value = "220.181.111.147", len = 13, name = "www.baidu.com" } buckets 2: 0x8870180: {value = "58.63.236.35" , len = 15, name = "www.sina.com.cn"} buckets 1: 0x8870154: {value = "74.125.71.105" , len = 14, name = "www.google.com" } buckets 2: 0x8870198: {value = "60.28.14.190" , len = 10, name = "www.qq.com" } buckets 0: 0x8870100: {value = "123.103.14.237" , len = 11, name = "www.163.com" } buckets 0: 0x8870114: {value = "219.234.82.50" , len = 12, name = "www.sohu.com" } buckets 0: 0x8870128: {value = "117.40.196.26" , len = 14, name = "www.abo321.org" } -------------------------------- the pool: -------------------------------- pool = 0x8870020 .d .last = 0x88701c4 .end = 0x8870420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8870020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 604 -------------------------------- find test: -------------------------------- (url = "www.baidu.com" , key = 270263191 ) found, (ip = "220.181.111.147") (url = "www.sina.com.cn", key = 1528635686) found, (ip = "58.63.236.35") (url = "www.google.com" , key = -702889725) found, (ip = "74.125.71.105") (url = "www.qq.com" , key = 203430122 ) found, (ip = "60.28.14.190") (url = "www.163.com" , key = -640386838) found, (ip = "123.103.14.237") (url = "www.sohu.com" , key = 1313636595) found, (ip = "219.234.82.50") (url = "www.abo321.org" , key = 1884209457) found, (ip = "117.40.196.26") (url = "www.china.com" , key = -1954599725) not found! (url = "www.csdn.net" , key = -1667448544) not found! |
以上结果是bucket_size=64字节的输出。由该结果可以看出,对于给定的7个url,程序将其分到了3个bucket中,详见该结果。该例子的hash物理结构图如下:
bucket_size=256字节:运行结果如下:
# ./ngx_hash_t_test -------------------------------- create a new pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b74048 .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 984 -------------------------------- create and add urls to it: -------------------------------- array = 0x8b74048 .elts = 0x8b7405c .nelts = 7 .size = 16 .nalloc = 7 .pool = 0x8b74020 elements: 0x8b7405c: {key = ("www.baidu.com" , 13), key_hash = 270263191 , value = "220.181.111.147"} 0x8b7406c: {key = ("www.sina.com.cn", 15), key_hash = 1528635686, value = "58.63.236.35" } 0x8b7407c: {key = ("www.google.com" , 14), key_hash = -702889725, value = "74.125.71.105" } 0x8b7408c: {key = ("www.qq.com" , 10), key_hash = 203430122 , value = "60.28.14.190" } 0x8b7409c: {key = ("www.163.com" , 11), key_hash = -640386838, value = "123.103.14.237" } 0x8b740ac: {key = ("www.sohu.com" , 12), key_hash = 1313636595, value = "219.234.82.50" } 0x8b740bc: {key = ("www.abo321.org" , 14), key_hash = 1884209457, value = "117.40.196.26" } -------------------------------- the pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b740cc .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 852 -------------------------------- the hash: -------------------------------- hash = 0x8b740cc: **buckets = 0x8b740d8, size = 1 0x8b740d8: buckets[0] = 0x8b740e0 buckets 0: {value = "220.181.111.147", len = 13, name = "www.baidu.com" } buckets 0: {value = "58.63.236.35" , len = 15, name = "www.sina.com.cn"} buckets 0: {value = "74.125.71.105" , len = 14, name = "www.google.com" } buckets 0: {value = "60.28.14.190" , len = 10, name = "www.qq.com" } buckets 0: {value = "123.103.14.237" , len = 11, name = "www.163.com" } buckets 0: {value = "219.234.82.50" , len = 12, name = "www.sohu.com" } buckets 0: {value = "117.40.196.26" , len = 14, name = "www.abo321.org" } -------------------------------- the pool: -------------------------------- pool = 0x8b74020 .d .last = 0x8b7419c .end = 0x8b74420 .next = 0x0 .failed = 0 .max = 984 .current = 0x8b74020 .chain = 0x0 .large = 0x0 .cleanup = 0x0 .log = 0x0 available pool memory = 644 -------------------------------- find test: -------------------------------- (url = "www.baidu.com" , key = 270263191 ) found, (ip = "220.181.111.147") (url = "www.sina.com.cn", key = 1528635686) found, (ip = "58.63.236.35") (url = "www.google.com" , key = -702889725) found, (ip = "74.125.71.105") (url = "www.qq.com" , key = 203430122 ) found, (ip = "60.28.14.190") (url = "www.163.com" , key = -640386838) found, (ip = "123.103.14.237") (url = "www.sohu.com" , key = 1313636595) found, (ip = "219.234.82.50") (url = "www.abo321.org" , key = 1884209457) found, (ip = "117.40.196.26") (url = "www.china.com" , key = -1954599725) not found! (url = "www.csdn.net" , key = -1667448544) not found! |
以上结果是bucket_size=256字节的输出。由给结果可以看出,对于给定的7个url,程序将其放到了1个bucket中,即ngx_hash_init()函数中的size=1,因这7个url的总长度只有140,因此,只需size=1个bucket,即buckets[0]。
下表是ngx_hash_init()函数在计算过程中的一些数据。物理结构图省略,可参考上图。
url |
计算长度 |
test[0]的值 |
4+ngx_align(13+2,4)=20 |
20 |
|
4+ngx_align(15+2,4)=24 |
44 |
|
4+ngx_align(14+2,4)=20 |
64 |
|
4+ngx_align(10+2,4)=16 |
80 |
|
4+ngx_align(11+2,4)=20 |
100 |
|
4+ngx_align(12+2,4)=20 |
120 |
|
4+ngx_align(14+2,4)=20 |
140 |
9.6.5. 小结
本文针对nginx的hash结构进行了较为全面的分析,包括hash结构、hash元素结构、hash初始化结构等,hash操作主要包括hash初始化、hash查找等。最后通过一个简单例子向读者展示nginx的hash使用方法,并给出详细的运行结果,且画出hash的物理结构图,以此向图这展示hash的设计、原理;同时借此向读者展示编译测试nginx代码的方法。
10. Nginx模块开发
下面本文展示一个简单的Nginx模块开发全过程,我们开发一个叫echo的handler模块,这个模块功能非常简单,它接收“echo”指令,指令可指定一个字符串参数,模块会输出这个字符串作为HTTP响应。例如,做如下配置:
location /echo { echo "hello nginx"; } |
则访问http://hostname/echo时会输出hello nginx。
直观来看,要实现这个功能需要三步:1、读入配置文件中echo指令及其参数;2、进行HTTP包装(添加HTTP头等工作);3、将结果返回给客户端。下面本文将分部介绍整个模块的开发过程。
10.1. 模块配置结构
首先我们需要一个结构用于存储从配置文件中读进来的相关指令参数,即模块配置信息结构。根据Nginx模块开发规则,这个结构的命名规则为ngx_http_[module-name]_[main|srv|loc]_conf_t。其中main、srv和loc分别用于表示同一模块在三层block中的配置信息。
这里我们的echo模块,只需要运行在loc层级下,需要存储一个字符串参数,因此我们可以定义如下的模块配置:
typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t; |
其中字段ed用于存储echo指令指定的需要输出的字符串。注意这里ed的类型,在Nginx模块开发中使用ngx_str_t类型表示字符串,这个类型定义在core/ngx_string中:
typedef struct { size_t len; u_char *data; } ngx_str_t; |
其中两个字段分别表示字符串的长度和数据起始地址。注意在Nginx源代码中对数据类型进行了别称定义,如ngx_int_t为intptr_t的别称,为了保持一致,在开发Nginx模块时也应该使用这些Nginx源码定义的类型而不要使用C原生类型。除了ngx_str_t外,其它三个常用的nginx type分别为:
typedef intptr_t ngx_int_t; typedef uintptr_t ngx_uint_t; typedef intptr_t ngx_flag_t; |
具体定义请参看core/ngx_config.h。关于intptr_t和uintptr_t请参考C99中的stdint.h或http://linux.die.net/man/3/intptr_t。
10.2. 定义指令
一个Nginx模块往往接收一至多个指令,echo模块接收一个指令“echo”。Nginx模块使用一个ngx_command_t数组表示模块所能接收的所有模块,其中每一个元素表示一个条指令。ngx_command_t是ngx_command_s的一个别称(Nginx习惯于使用“_s”后缀命名结构体,然后typedef一个同名“_t”后缀名称作为此结构体的类型名),ngx_command_s定义在core/ngx_config_file.h中:
struct ngx_command_s { 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; void *post; }; |
1.name是词条指令的名称,不能包含空格。
2.type使用掩码标志位的方式,配置指令参数。
相关可用type定义在core/ngx_config_file.h中:
* NGX_HTTP_MAIN_CONF: 指令出现在main配置部分是合法的 * NGX_HTTP_SRV_CONF: 指令在server配置部分出现是合法的 * NGX_HTTP_LOC_CONF: 指令在location配置部分出现是合法的 * NGX_HTTP_UPS_CONF: 指令在upstream配置部分出现是合法的 * NGX_CONF_NOARGS: 指令没有参数 * NGX_CONF_TAKE1: 指令读入1个参数 * NGX_CONF_TAKE2: 指令读入2个参数 * ... * NGX_CONF_TAKE7: 指令读入7个参数 * NGX_CONF_FLAG: 指令读入1个布尔型数据 ("on" or "off") * NGX_CONF_1MORE: 指令至少读入1个参数 * NGX_CONF_2MORE: 指令至少读入2个参数 |
其中NGX_CONF_NOARGS表示此指令不接受参数,NGX_CON F_TAKE1-7表示精确接收1-7个,NGX_CONF_TAKE12表示接受1或2个参数,NGX_CONF_1MORE表示至少一个参数,NGX_CONF_FLAG表示接受“on|off”……
3.set是一个函数指针,这个函数一般是将配置文件中相关指令的参数,转化成需要的格式并存入配置结构体。设定函数会在遇到指令时执行。
a.指向结构体 ngx_conf_t 的指针, 这个结构体里包含需要传递给指令的参数
b.指向结构体 ngx_command_t 的指针
c.指向模块自定义配置结构体的指针
Nginx预定义了一些转换函数,可以方便我们调用,这些函数定义在core/ngx_conf_file.h中,一般以“_slot”结尾,例如:
* ngx_conf_set_flag_slot: 将 "on" or "off" 转换成 1 or 0
* ngx_conf_set_str_slot: 将字符串保存为 ngx_str_t
* ngx_conf_set_num_slot: 解析一个数字并保存为int
* ngx_conf_set_size_slot: 解析一个数据大小(如:"8k", "1m") 并保存为size_t
4-5.conf用于指定Nginx相应配置文件,在内存起始地址。一般可以通过内置常量指定,如NGX_HTTP_MAIN_CONF_OFFSET,NGX_HTTP_SRV_CONF_OFFSET,或者 NGX_HTTP_LOC_CONF_OFFSET。offset指定此条指令的参数的偏移量。
下面是echo模块的指令定义:
static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command }; |
6. post指向模块在读配置的时候需要的一些零碎变量。一般它是NULL。
指令数组的命名规则为ngx_http_[module-name]_commands,注意数组最后一个元素要是ngx_null_command结束。
10.3. 模块上下文
静态的ngx_http_module_t结构体,包含一大坨函数引用,用来创建和合并三段配置 (main,server,location),命名方式一般是:ngx_http__module_ctx,这个结构主要用于定义各个Hook函数。这些函数引用依次是:
函数的入参各不相同,取决于他们具体要做的事情。这里http/ngx_http_config.h是结构体的具体定义:
typedef struct { ngx_int_t (*preconfiguration)(ngx_conf_t *cf); ngx_int_t (*postconfiguration)(ngx_conf_t *cf); void *(*create_main_conf)(ngx_conf_t *cf); char *(*init_main_conf)(ngx_conf_t *cf, void *conf); void *(*create_srv_conf)(ngx_conf_t *cf); char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf); void *(*create_loc_conf)(ngx_conf_t *cf); char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); } ngx_http_module_t; |
* preconfiguration 在读入配置前调用
* postconfiguration 在读入配置后调用
* create_main_conf 在创建main配置时调用(比如,用来分配空间和设置默认值)
* init_main_conf 在初始化main配置时调用(比如,把原来的默认值用nginx.conf读到的值来覆盖)
* init_main_conf 在创建server配置时调用
* merge_srv_conf 合并server和main配置时调用
* create_loc_conf 创建location配置时调用
* merge_loc_conf 合并location和server配置时调用
可以把你不需要的函数设置为NULL,Nginx会忽略掉他们。
绝大多数的 handler只使用最后两个:一个用来为特定location配置来分配内存,(叫做 ngx_http__create_loc_conf);另一个用来设定默认值以及合并继承过来的配置值(叫做 ngx_http__merge_loc_conf)。合并函数同时还会检查配置的有效性,如果有错误,则server的启动将被挂起。
下面是一个使用模块上下文结构体的例子:
static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, NULL, NULL, NULL, NULL, NULL, ngx_http_echo_create_loc_conf, ngx_http_echo_merge_loc_conf }; |
现在开始讲得更深一点。这些配置回调函数看其来很像,所有模块都一样,而且Nginx的API都会用到这个部分。
可以看到一共有8个Hook注入点,分别会在不同时刻被Nginx调用,由于我们的模块仅仅用于location域,这里将不需要的注入点设为NULL即可。其中create_loc_conf用于初始化一个配置结构体,如:为配置结构体分配内存等工作;merge_loc_conf用于将其父block的配置信息合并到此结构体中,也就是实现配置的继承。这两个函数会被Nginx自动调用。注意这里的命名规则:ngx_http_[module-name]_[create|merge]_[main|srv|loc]_conf。
10.4. create_loc_conf
它的入参是(ngx_conf_t),返回值是更新了的模块配置结构体
static void * ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; } |
首先需要指出的是Nginx的内存分配;只要使用了 ngx_palloc(malloc的一个包装函数)或者 ngx_pcalloc (calloc的包装函数),就不用担心内存的释放了。
create_loc_conf新建一个ngx_http_echo_loc_conf_t,分配内存,并初始化其中的数据,然后返回这个结构的指针。
10.5. merge_loc_conf
下面的例子是我的模块echo中的合并函数:
static char * ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; } |
merge_loc_conf将父block域的配置信息合并到create_loc_conf新建的配置结构体中。
这里的需要注意的是Nginx提供了一些好用的合并函数用来合并不同类型的数据(ngx_conf_merge__value),这类函数的入参是:
1. 当前location 的变量值
2. 如果第一个参数没有被设置而采用的值
3. 如果第一第二个参数都没有被设置而采用的值
结果会被保存在第一个参数中。能用的合并函数包括 ngx_conf_merge_size_value, ngx_conf_merge_msec_value 等等。可参见 core/ngx_conf_file.h.
以ngx_conf_merge_str_value为例:
#define ngx_conf_merge_str_value(conf, prev, default) \ if (conf.data == NULL) { \ if (prev.data) { \ conf.len = prev.len; \ conf.data = prev.data; \ } else { \ conf.len = sizeof(default) - 1; \ conf.data = (u_char *) default; \ } \ } |
同时可以看到,core/ngx_conf_file.h还定义了很多merge value的宏用于merge各种数据。它们的行为比较相似:使用prev填充conf,如果prev的数据为空则使用default填充。
同时还需要注意的是:错误的处理。函数需要往log文件写一些东西,同时返回NGX_CONF_ERROR。这个返回值会将server的启动挂起。(因为被标示为NGX_LOG_EMERG级别,所以错误同时还会输出到标准输出。作为参考,core/ngx_log.h列出了所有的日志级别。)
10.6. 模块定义
接下来我们间接地介绍更深一层:结构体ngx_module_t。该结构体变量命名方式为ngx_http__module。它包含模块的内容和指令执行方式,同时也还包含一些回调函数(退出线程,退出进程,等等)。模块定义在有的时候会被用作查找的关键字,来查找与特定模块相关联的数据。模块定义通常像是这样:
ngx_module_t ngx_http__module = { NGX_MODULE_V1, &ngx_http_module_ctx, ngx_http_commands, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING }; |
仅仅替换掉合适的就可以了。模块可以添加一些回调函数来处理线程/进程的创建和销毁,但是绝大多数模块都用NULL。(关于这些回调函数的入参,可以参考 core/ngx_conf_file.h.)
10.7. Handler
下面的工作是编写handler。handler可以说是模块中真正干活的代码。
Handler一般做4件事:
- 读入模块配置(获取location配置)。
- 处理功能业务(生成合适的响应)。
- 产生HTTP header(发送响应头)。
- 产生HTTP body(发送响应体)。
Handler有一个参数,即请求结构体。请求结构体包含很多关于客户请求的有用信息,比如说请求方法,URI,请求头等等。
下面先贴出echo模块的代码,然后通过分析代码的方式介绍如何实现这四步。这一块的代码比较复杂:
static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); } |
handler会接收一个ngx_http_request_t指针类型的参数,这个参数指向一个ngx_http_request_t结构体,此结构体存储了这次HTTP请求的一些信息,这个结构定义在http/ngx_http_request.h中:
struct ngx_http_request_s { uint32_t signature; ngx_connection_t *connection; void **ctx; void **main_conf; void **srv_conf; void **loc_conf; ngx_http_event_handler_pt read_event_handler; ngx_http_event_handler_pt write_event_handler; ngx_http_handler_pt content_handler; ngx_uint_t access_code; ngx_http_variable_value_t *variables; } |
由于ngx_http_request_s定义比较长,这里我只截取了一部分。可以看到里面有诸如uri,args和request_body等HTTP常用信息。这里需要特别注意的几个字段是headers_in、headers_out和chain,它们分别表示request header、response header和输出数据缓冲区链表(缓冲区链表是Nginx I/O中的重要内容,后面会单独介绍)。
第一步是获取模块配置信息,这一块只要简单使用ngx_http_get_module_loc_conf就可以了。现在我们就可以访问之前在合并函数中设置的所有变量了。
第二步是功能逻辑。uri 是请求的路径, e.g. "/echo"。args 请求串参数中问号后面的参数 (e.g. "name=john"). headers_in 包含有很多有用的东西,比如说cookie啊,浏览器信息啊什么的,但是许多模块可能用不到这些东东。如果你感兴趣的话,可以参看http/ngx_http_request.h 。对于生成输出,这些信息应该是够了。完整的ngx_http_request_t结构体定义在http/ngx_http_request.h。
第三步是设置response header。Header内容可以通过填充headers_out实现,我们这里只设置了Content-type和Content-length等基本内容,ngx_http_headers_out_t定义了所有可以设置的HTTP Response Header信息:
typedef struct { ngx_list_t headers; ngx_uint_t status; ngx_str_t status_line; ngx_str_t charset; u_char *content_type_lowcase; ngx_uint_t content_type_hash; ngx_array_t cache_control; off_t content_length_n; time_t date_time; time_t last_modified_time; } ngx_http_headers_out_t; |
这里并不包含所有HTTP头信息,如果需要可以使用agentzh(春来)开发的Nginx模块HttpHeadersMore在指令中指定更多的Header头信息。
设置好头信息后使用ngx_http_send_header就可以将头信息输出,ngx_http_send_header接受一个ngx_http_request_t类型的参数。
第四步也是最重要的一步是输出Response body。这里首先要了解Nginx的I/O机制,Nginx允许handler一次产生一组输出,可以产生多次,Nginx将输出组织成一个单链表结构,链表中的每个节点是一个chain_t,定义在core/ngx_buf.h:
struct ngx_chain_s { ngx_buf_t *buf; ngx_chain_t *next; }; |
其中ngx_chain_t是ngx_chain_s的别名,buf为某个数据缓冲区的指针,next指向下一个链表节点,可以看到这是一个非常简单的链表。ngx_buf_t的定义比较长而且很复杂,这里就不贴出来了,请自行参考core/ngx_buf.h。ngx_but_t中比较重要的是pos和last,分别表示要缓冲区数据在内存中的起始地址和结尾地址,这里我们将配置中字符串传进去,last_buf是一个位域,设为1表示此缓冲区是链表中最后一个元素,为0表示后面还有元素。因为我们只有一组数据,所以缓冲区链表中只有一个节点,如果需要输入多组数据可将各组数据放入不同缓冲区后插入到链表。下图展示了Nginx缓冲链表的结构:
缓冲数据准备好后,用ngx_http_output_filter就可以输出了(会送到filter进行各种过滤处理)。ngx_http_output_filter的第一个参数为ngx_http_request_t结构,第二个为输出链表的起始地址&out。ngx_http_out_put_filter会遍历链表,输出所有数据。
以上就是handler的所有工作,请对照描述理解上面贴出的handler代码。
10.8. Upstream
我已经帮你了解了如何让你的handler来产生响应。有些时候你可以用一小段C代码就可以得到响应,但是通常情况下你需要同另外一台server打交道(比如你正在写一个用来实现某种网络协议的模块)。你当然可以自己实现一套网络编程的东东,但是如果你只收到部分的响应,需要等待余下的响应数据,你会怎么办?你不会想阻塞整个事件处理循环吧?这样会毁掉Nginx的良好性能!幸运的是,Nginx允许你在它处理后端服务器(叫做"upstreams")的机制上加入你的回调函数,因此你的模块将可以和其他的server通信,同时还不会妨碍其他的请求。这一节将介绍模块如何和一个upstream(如 Memcached, FastCGI,或者另一个 HTTP server)通信。
10.8.1. Upstream 回调函数概要
与其他模块的回调处理函数不一样,upstream模块的处理函数几乎不做“实事”。它压根不调用ngx_http_output_filter。它仅仅是告诉回调函数什么时候可以向upstream server写数据了,以及什么时候能从upstream server读数据了。实际上它有6个可用的钩子:
- create_request 生成发送到
- upstream server的请求缓冲(或者一条缓冲链)
- reinit_request 在与后端服务器连接被重置的情况下(在create_request 被第二次调用之前)被调用
- process_header 处理upstream 响应的第一个bit,通常是保存一个指向upstream "payload"的指针
- abort_request 在客户端放弃请求时被调用 finalize_request 在Nginx完成从upstream读取数据后调用
- input_filter 这是一个消息体的filter,用来处理响应消息体(例如把尾部删除)
这些钩子是怎么勾上去的呢?下面是一个例子,简单版本的代理模块处理函数:
static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_http_upstream_t *u; ngx_http_proxy_loc_conf_t *plcf; plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module); u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t)); if (u == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } u->peer.log = r->connection->log; u->peer.log_error = NGX_ERROR_ERR; u->output.tag = (ngx_buf_tag_t) &ngx_http_proxy_module; u->conf = &plcf->upstream; u->create_request = ngx_http_proxy_create_request; u->reinit_request = ngx_http_proxy_reinit_request; u->process_header = ngx_http_proxy_process_status_line; u->abort_request = ngx_http_proxy_abort_request; u->finalize_request = ngx_http_proxy_finalize_request; r->upstream = u; rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init); if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { return rc; } return NGX_DONE; } |
看上去都是些例行事务,不过重要的是那些回调函数。同时还要注意的是ngx_http_read_client_request_body,它又设置了一个回调函数,在Nginx完成从客户端读数据后会被调用。
这些个回调函数都要做些什么工作呢?通常情况下,reinit_request, abort_request, 和 finalize_request用来设置或重置一些内部状态,但这些都是几行代码的事情。真正做苦力的是create_request 和 process_header。
10.8.2. create_request 回调函数
假设我有一个upstream server,它读入一个字符打印出两个字符。那么函数应该如何来写呢? create_request需要申请一个buffer来存放“一个字符”的请求,为buffer申请一个链表,并且把链表挂到upstream结构体上。看起来就像这样:
static ngx_int_t ngx_http_character_server_create_request(ngx_http_request_t *r) { ngx_buf_t *b; ngx_chain_t *cl; b = ngx_create_temp_buf(r->pool, sizeof("a") - 1); if (b == NULL) return NGX_ERROR; cl = ngx_alloc_chain_link(r->pool); if (cl == NULL) return NGX_ERROR; cl->buf = b; r->upstream->request_bufs = cl; b->pos = "a"; b->last = b->pos + sizeof("a") - 1; return NGX_OK; } |
当然实际应用中你很可能还会用到请求里面的URI。r->uri作为一个 ngx_str_t类型也是有效的,GET的参数在r->args中,最后别忘了你还能访问请求头和 cookie信息。
10.8.3. process_header 回调函数
process_header把响应指针移到客户端可以接收到的部分。同时它还会从upstream 读入头信息,并且相应的设置发往客户端的响应头。
这里有个小例子,读进两个字符的响应。我们假设第一个字符代表“状态”字符。如果它是问号,我们将返回一个404错误并丢弃剩下的那个字符。如果它是空格,我们将以 200 OK的响应把另一个字符返回给客户端。那么我们如何来实现这个process_header 函数呢?
static ngx_int_t ngx_http_character_server_process_header(ngx_http_request_t *r) { ngx_http_upstream_t *u; u = r->upstream; switch(u->buffer.pos[0]) { case '?': r->header_only; u->headers_in.status_n = 404; break; case ' ': u->buffer.pos++; u->headers_in.status_n = 200; break; } return NGX_OK; } |
就是这样。操作头部,改变指针,搞定!注意headers_in实际上就是我们之前提到过的头部结构体( http/ngx_http_request.h),但是它位于来自upstream的头中。一个真正的代理模块,会在头信息的处理上做很多文章,不光是错误处理,做什么完全取决于你的想法。
10.8.4. 状态保持
好了,还记得我说过abort_request, reinit_request和finalize_request 可以用来重置内部状态吗?这是因为许多upstream模块都有其内部状态。模块需要定义一个自定义上下文结构 ,来标记目前为止从upstream读到了什么。这跟之前说的“模块上下文”不是一个概念。“模块上下文”是预定义类型,而自定义上下文结构可以包含任何你需要的数据和字段(这可是你自己定义的结构体)。这个结构体在create_request函数中被实例化,大概像这样:
ngx_http_character_server_ctx_t *p; p = ngx_pcalloc(r->pool, sizeof(ngx_http_character_server_ctx_t)); if (p == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } ngx_http_set_ctx(r, p, ngx_http_character_server_module); |
最后一行实际上将自定义上下文结构体注册到了特定的请求和模块名上,以便在稍后取用。当你需要这个结构体时(可能所有的回调函数中都需要它),只需要:
ngx_http_proxy_ctx_t *p; p = ngx_http_get_module_ctx(r, ngx_http_proxy_module); |
指针 p 可以得到当前的状态. 设置、重置、增加、减少、往里填数据……你可以随心所欲的操作它。当upstream服务器返回一块一块的响应时,读取这些响应的过程中使用持久状态机是个很nx的办法,它不用阻塞主事件循环。
10.9. Handler的装载
Handler的装载通过往模块启用了的指令的回调函数中添加代码来完成。比如,我的echo中ngx_command_t是这样的:
{ ngx_string("echo "), NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, ngx_http_echo, 0, 0, NULL } |
回调函数是里面的第三个元素,在这个例子中就是那个ngx_http_echo。回调函数的参数是由指令结构体(ngx_conf_t, 包含用户配置的参数),相应的ngx_command_t结构体以及一个指向模块自定义配置结构体的指针组成的。我的echo模块中,这些函数是这样子的:
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; } |
这个函数除了调用ngx_conf_set_str_slot转化echo指令的参数外,还将修改了核心模块配置(也就是这个location的配置),将其handler替换为我们编写的handler:ngx_http_echo_handler。这样就屏蔽了此location的默认handler,使用ngx_http_echo_handler产生HTTP响应。
这里可以分为两步:首先,得到当前location的“core”结构体,再分配给它一个 handler。很简单,不是吗? 我已经把我知道的关于hanler模块的东西全招了,现在可以来说说输出过滤链上的filter模块了。
11. Filters
Filter操作handler生成的响应。头部filter操作HTTP头,body filter操作响应的内容。
11.1. 剖析Header Filter
头部Filter由三个步骤组成:
1.决定何时操作响应
2.操作响应
3.调用下一个filter
举个例子,比如有一个简化版本的“不改变”头部filter:如果客户请求头中的If- Modified-Since和响应头中的Last-Modified相符,它把响应状态设置成304。注意这个头部filter只读入一个参数:ngx_http_request_t结构体,而我们可以通过它操作到客户请求头和一会将被发送的响应消息头。
static ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r) { time_t if_modified_since; if_modified_since = ngx_http_parse_time(r->headers_in.if_modified_since->value.data, r->headers_in.if_modified_since->value.len); if (if_modified_since != NGX_ERROR && if_modified_since == r->headers_out.last_modified_time) { r->headers_out.status = NGX_HTTP_NOT_MODIFIED; r->headers_out.content_type.len = 0; ngx_http_clear_content_length(r); ngx_http_clear_accept_ranges(r); } return ngx_http_next_header_filter(r); } |
结构headers_out和我们在hander那一节中看到的是一样的(参考http/ngx_http_request.h),也可以随意处置。
11.2. 剖析Body Filter
因为body filter一次只能操作一个buffer(链表),这使得编写body filter需要一定的技巧。模块需要知道什么时候可以覆盖输入buffer,用新申请的buffer_替换已有的,或者在现有的某个buffer前或后插入一个新buffer。有时候模块会收到许多buffer使得它不得不操作一个不完整的链表,这使得事情变得更加复杂了。而更加不幸的是,Nginx没有为我们提供上层的API来操作buffer链表,所以body filter是比较难懂(当然也比较难写)。但是,有些操作你还是可以看出来的。
一个body filter原型大概是这个样子(例子代码从Nginx源代码的“chunked” filter中取得): static ngx_int_t ngx_http_chunked_body_filter(ngx_http_request_t *r, ngx_chain_t *in);
第一个参数是请求结构体,第二个参数则是指向当前部分链表(可能包含0,1,或更多的buffer)头的指针。
再来举个例子好了。假设我们想要做的是在每个请求之后插入文本""。首先,我们需要判断给我们的buffer链表中是否已经包含响应的最终buffer。就像之前我说的,这里没有简便好用的API,所以我们只能自己来写个循环:
ngx_chain_t *chain_link; int chain_contains_last_buffer = 0; for ( chain_link = in; chain_link != NULL; chain_link = chain_link->next ) { if (chain_link->buf->last_buf) chain_contains_last_buffer = 1; } |
如果我们没有最后的缓冲区,就返回:
if (!chain_contains_last_buffer) return ngx_http_next_body_filter(r, in); |
很好,现在最后一个缓冲区已经存在链表中了。接下来我们分配一个新缓冲区:
ngx_buf_t *b; b = ngx_calloc_buf(r->pool); if (b == NULL) { return NGX_ERROR; } |
把数据放进去:
b->pos = (u_char *) ""; b->last = b->pos + sizeof("") - 1; |
把这个缓冲区挂在新的链表上:
ngx_chain_t added_link; added_link.buf = b; added_link.next = NULL; |
最后,把这个新链表挂在先前链表的末尾:
chain_link->next = added_link; |
并根据变化重置变量"last_buf"的值:
chain_link->buf->last_buf = 0; added_link->buf->last_buf = 1; |
再将修改过的链表传递给下一个输出过滤函数:
return ngx_http_next_body_filter(r, in); |
现有的函数做了比我们更多的工作,比如mod_perl($response->body =~ s/$/ /),但是缓冲区链确实是一个强大的构想,它可以让程序员渐进地处理数据,这使得客户端可以尽可能早地得到响应。但是依我来看,缓冲区链表实在需要一个更为干净的接口,这样程序员也可以避免操作不一致状态的链表。但是目前为止,所有的操作风险都得自己控制。
11.3. Filter的装载
Filter在在回调函数post-configuration中被装载。header filter和body filter都是在这里被装载的。
我们以chunked filter模块为例来具体看看:
static ngx_http_module_t ngx_http_chunked_filter_module_ctx = { NULL, ngx_http_chunked_filter_init, ... }; |
ngx_http_chunked_filter_init中的具体实现如下:
static ngx_int_t ngx_http_chunked_filter_init(ngx_conf_t *cf) { ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_chunked_header_filter; ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_chunked_body_filter; return NGX_OK; } |
发生了什么呢?好吧,如果你还记得,过滤模块组成了一条”接力链表“。当handler生成一个响应后,调用2个函数:ngx_http_output_filter它调用全局函数ngx_http_top_body_filter;以及ngx_http_send_header 它调用全局函数ngx_top_header_filter。
ngx_http_top_body_filter 和 ngx_http_top_header_filter是body和header各自的头部filter链的“链表头”。链表上的每一个“连接”都保存着链表中下一个连接的函数引用(分别是 ngx_http_next_body_filter 和 ngx_http_next_header_filter)。当一个filter完成工作之后,它只需要调用下一个filter,直到一个特殊的被定义成“写”的filter被调用,这个“写”filter的作用是包装最终的HTTP响应。你在这个filter_init函数中看到的就是,模块把自己添加到filter链表中;它先把旧的“头部”filter当做是自己的“下一个”,然后再声明“它自己”是“头部”filter。(因此,最后一个被添加的filter会第一个被执行。)
每个filter要么返回一个错误码,要么用`return ngx_http_next_body_filter();`来作为返回语句
因此,如果filter顺利链执行到了链尾(那个特别定义的的”写“filter),将返回一个"OK"响应,但如果执行过程中遇到了错误,链将被砍断,同时Nginx将给出一个错误的信息。这是一个单向的,错误快速返回的,只使用函数引用实现的链表!
12. Load-balancers
Load-balancer用来决定哪一个后端将会收到请求;具体的实现是round-robin方式或者把请求进行hash。本节将介绍load-balancer模块的装载及其调用。我们将用upstream_hash_module(full source)作例子。upstream_hash将对nginx.conf里配置的变量进行 hash,来选择后端服务器。
一个load-balancer分为六个部分:
1.启用配置指令 (e.g, hash;) 将会调用注册函数
2.注册函数将定义一些合法的server 参数 (e.g., weight=) 并注册一个 upstream初始化函数
3. upstream初始化函数将在配置经过验证后被调用,并且:
* 解析 server 名称为特定的IP地址
* 为每个sokcet连接分配空间
* 设置对端初始化函数的回调入口
4.对端初始化函数将在每次请求时被调用一次,它主要负责设置一些负载均衡函数将会使用的数据结构。
5.负载均衡函数决定把请求分发到哪里;每个请求将至少调用一次这个函数(如果后端服务器失败了,那就是多次了)。
6.对端释放函数 可以在与对应的后端服务器结束通信之后更新统计信息 (成功或失败)
12.1. 启用指令
指令声明,既确定了他们在哪里生效又确定了一旦流程遇到指令将要调用什么函数。load-balancer的指令需要置NGX_HTTP_UPS_CONF标志位,一遍让Nginx知道这个指令只会在upstream块中有效。同时它需要提供一个指向注册函数的指针。下面列出的是upstream_hash模块的指令声明:
{ ngx_string("hash"), NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS, ngx_http_upstream_hash, 0, 0, NULL } |
12.2. 注册函数
上面的回调函数ngx_http_upstream_hash就是所谓的注册函数。之所以这样叫(我起得名字)是因为它注册了把upstream初始化函数和周边的upstream配置注册到了一块。另外,注册函数还定义了特定upstream块中的server指令的一些选项(如weight=, fail_timeout=),下面是upstream_hash模块的注册函数:
ngx_int_t ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_upstream_srv_conf_t *uscf; ngx_http_script_compile_t sc; ngx_str_t *value; ngx_array_t *vars_lengths, *vars_values; value = cf->args->elts; ngx_memzero(&sc, sizeof(ngx_http_script_compile_t)); vars_lengths = NULL; vars_values = NULL; sc.cf = cf; sc.source = &value[1]; sc.lengths = &vars_lengths; sc.values = &vars_values; sc.complete_lengths = 1; sc.complete_values = 1; if (ngx_http_script_compile(&sc) != NGX_OK) { return NGX_CONF_ERROR; } uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); uscf->peer.init_upstream = ngx_http_upstream_init_hash; uscf->flags = NGX_HTTP_UPSTREAM_CREATE; uscf->values = vars_values->elts; uscf->lengths = vars_lengths->elts; if (uscf->hash_function == NULL) { uscf->hash_function = ngx_hash_key; } return NGX_CONF_OK; } |
除了依葫芦画瓢的用来计算$variable的代码,剩下的都很简单,就是分配一个回调函数,设置一些标志位。哪些标志位是有效的呢?
*NGX_HTTP_UPSTREAM_CREATE: 让upstream块中有 server 指令。我实在想不出那种情形会用不到它。
*NGX_HTTP_UPSTREAM_WEIGHT: 让server指令获取选项 weight=
* NGX_HTTP_UPSTREAM_MAX_FAILS: 允许选项max_fails=
* NGX_HTTP_UPSTREAM_FAIL_TIMEOUT: 允许选项fail_timeout=
* NGX_HTTP_UPSTREAM_DOWN: 允许选项 down
* NGX_HTTP_UPSTREAM_BACKUP: 允许选项backup
每一个模块都可以访问这些配置值。
一切都取决于模块自己的决定。也就是说,max_fails不会被自动强制执行;所有的失败逻辑都是由模块作者决定的。过会我们再说这个。目前,我们还没有完成对回调函数的追踪呢。接下来,我们来看upstream初始化函数 (上面的函数中的回调函数init_upstream )。
12.3. upstream 初始化函数
upstream 初始化函数的目的是,解析主机名,为socket分配空间,分配(另一个)回调函数。下面是upstream_hash:
ngx_int_t ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { ngx_uint_t i, j, n; ngx_http_upstream_server_t *server; ngx_http_upstream_hash_peers_t *peers; us->peer.init = ngx_http_upstream_init_upstream_hash_peer; if (!us->servers) { return NGX_ERROR; } server = us->servers->elts; for (n = 0, i = 0; i < us->servers->nelts; i++) { n += server[i].naddrs; } peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t) + sizeof(ngx_peer_addr_t) * (n - 1)); if (peers == NULL) { return NGX_ERROR; } peers->number = n; for (n = 0, i = 0; i < us->servers->nelts; i++) { for (j = 0; j < server[i].naddrs; j++, n++) { peers->peer[n].sockaddr = server[i].addrs[j].sockaddr; peers->peer[n].socklen = server[i].addrs[j].socklen; peers->peer[n].name = server[i].addrs[j].name; } } us->peer.data = peers; return NGX_OK; } |
这个函数包含的东西ms比我们期望的多些。大部分的工作ms都该被抽象出来,但事实却不是,我们只能忍受这一点。倒是有一种简化的策略:调用另一个模块的upstream初始化函数,把这些脏活累活(对端的分配等等)都让它干了,然后再覆盖其us->peer.init这个回调函数。例子可以参见http/modules/ngx_http_upstream_ip_hash_module.c。
在我们这个观点中的关键点是设置对端初始化函数的指向,在我们这个例子里是ngx_http_upstream_init_upstream_hash_peer。
12.4. 对端初始化函数
对端初始化函数每个请求调用一次。它会构造一个数据结构,模块会用这个数据结构来选择合适的后端服务器;这个数据结构保存着和后端交互的重试次数,通过它可以很容易的跟踪链接失败次数或者是计算好的哈希值。这个结构体习惯性地被命名为ngx_http_upstream__peer_data_t。
另外,对端初始化函数还会构建两个回调函数:
*get: load-balancing 函数
*free: 对端释放函数 (通常只是在连接完成后更新一些统计信息)
似乎还不止这些,它同时还初始化了一个叫做tries的变量。只要tries是正数,Nginx将继续重试当前的load-banlancer。当tries变为0时,Nginx将放弃重试。一切都取决于get和 free 如何设置合适的tries。
下面是upstream_hash中对端初始化函数的例子:
static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) { ngx_http_upstream_hash_peer_data_t *uhpd; ngx_str_t val; if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) { return NGX_ERROR; } uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t) + sizeof(uintptr_t) * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number / (8 * sizeof(uintptr_t))); if (uhpd == NULL) { return NGX_ERROR; } r->upstream->peer.data = uhpd; uhpd->peers = us->peer.data; r->upstream->peer.free = ngx_http_upstream_free_hash_peer; r->upstream->peer.get = ngx_http_upstream_get_hash_peer; r->upstream->peer.tries = us->retries + 1; uhpd->hash = us->hash_function(val.data, val.len); return NGX_OK; } |
12.5. 负载均衡函数
主要部分现在才开始。模块就是在这里选择upstream服务器的。负载均衡函数的原型看上去是这样的: static ngx_int_t ngx_http_upstream_get__peer(ngx_peer_connection_t *pc, void *data);
data是我们存放所关注的客户端连接中有用信息的结构体。pc则是要存放我们将要去连接的server的相关信息。负载均衡函数做的事情就是填写pc->sockaddr, pc->socklen, 和 pc->name。如果你懂一点网络编程的话,这些东西应该都比较熟悉了;但实际上他们跟我们手头上的任务来比并不算很重要。我们不关心他们代表什么;我们只想知道从哪里找到合适的值来填写他们。
这个函数必须找到一个可用server的列表,挑一个分配给pc。我们来看看upstream_hash是怎么做的吧: 之前upstream_hash模块已经通过调用ngx_http_upstream_init_hash,把server列表存放在了ngx_http_upstream_hash_peer_data_t这一结构中。这个结构就是现在的data:
ngx_http_upstream_hash_peer_data_t *uhpd = data;
对端列表现在在uhpd->peers->peer中了。我们通过对哈希值与 server总数取模来从这个数组中取得最终的对端服务器:
ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number];
终于大功告成了:
pc->sockaddr = peers->sockaddr;
pc->socklen = peers->socklen;
pc->name =& peers->name;
return NGX_OK;
就是这样!如果load-balancer模块返回 NGX_OK,则意味着”来吧,上这个 server吧!“。如果返回的是NGX_BUSY,说明所有的后端服务器目前都不可用,此时Nginx应该重试。
12.6. 对端释放函数
对端释放函数在upstream连接就绪之后开始运行,它的目的是跟踪失败。函数原型如下:
Void ngx_http_upstream_free__peer(ngx_peer_connection_t *pc, void *data,
ngx_uint_t state);
头两个参数和我们在load-balancer函数中看到的一样。第三个参数是一个state变量,它表明了当前连接是成功还是失败。它可能是NGX_PEER_FAILED (连接失败) 和 NGX_PEER_NEXT (连接失败或者连接成功但程序返回了错误)按位或的结果。如果它是0则代表连接成功。
这些失败如何处理则由模块的开发者自己定。如果根本不再用,那结果则应存放到data中,这是一个指向每个请求自定义的结构体。
但是对端释放函数的关键作用是可以设置pc->tries为 0来阻止Nginx在load-balancer模块中重试。最简单的对端释放函数应该是这样的:
pc->tries = 0;
这样就保证了如果发往后端服务器的请求遇到了错误,客户端将得到一个502 Bad Proxy的错误。
这儿还有一个更为复杂的例子,是从upstream_hash模块中拿来的。如果后端连接失败,它会在位向量 (叫做 tried,一个 uintptr_t类型的数组)中标示失败,然后继续选择一个新的后端服务器直至成功。
#define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t)) #define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t)) static void ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data, ngx_uint_t state) { ngx_http_upstream_hash_peer_data_t *uhpd = data; ngx_uint_t current; if (state & NGX_PEER_FAILED && --pc->tries) { current = uhpd->hash % uhpd->peers->number; uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current); do { uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t)); current = uhpd->hash % uhpd->peers->number; } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries); } } |
因为load-balancer函数只会看新的uhpd->hash的值,所以这样是行之有效的。
许多应用程序不提供重试功能,或者在更高层的逻辑中进行了控制。但其实你也看到了,只需这么几行代码这个功能就可以实现了。
13. 组合Nginx Module
上面完成了Nginx模块各种组件的开发下面就是将这些组合起来了。一个Nginx模块被定义为一个ngx_module_t结构,这个结构的字段很多,不过开头和结尾若干字段一般可以通过Nginx内置的宏去填充,下面是我们echo模块的模块主体定义:
ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, ngx_http_echo_commands, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING }; |
开头和结尾分别用NGX_MODULE_V1和NGX_MODULE_V1_PADDING 填充了若干字段,就不去深究了。这里主要需要填入的信息从上到下以依次为context、指令数组、模块类型以及若干特定事件的回调处理函数(不需要可以置为NULL),其中内容还是比较好理解的,注意我们的echo是一个HTTP模块,所以这里类型是NGX_HTTP_MODULE,其它可用类型还有NGX_EVENT_MODULE(事件处理模块)和NGX_MAIL_MODULE(邮件模块)。
这样,整个echo模块就写好了,下面给出echo模块的完整代码:
#include #include #include typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t; static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command }; static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, NULL, NULL, NULL, NULL, NULL, ngx_http_echo_create_loc_conf, ngx_http_echo_merge_loc_conf }; ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, ngx_http_echo_commands, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING }; static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); } static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; } static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; } static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; } |
14. Nginx模块的安装
Nginx不支持动态链接模块,所以安装模块需要将模块代码与Nginx源代码进行重新编译。安装模块的步骤如下:
1.编写模块config文件,这个文件需要放在和模块源代码文件放在同一目录下。文件内容如下:
ngx_addon_name=模块完整名称 HTTP_MODULES="$HTTP_MODULES 模块完整名称" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/源代码文件名" |
2.进入Nginx源代码,使用下面命令编译安装
./configure --prefix=安装目录 --add-module=模块源代码文件目录 make make install |
这样就完成安装了,例如,我的源代码文件放在/home/yefeng/ngxdev/ngx_http_echo下,我的config文件为:
ngx_addon_name=ngx_http_echo_module HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module" NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c" |
编译安装命令为:
./configure --prefix=/usr/local/nginx --add-module=/home/yefeng/ngxdev/ngx_http_echo make sudo make install |
这样echo模块就被安装在我的Nginx上了,下面测试一下,修改配置文件,增加以下一项配置:
location /echo { echo "This is my first nginx module!!!"; } |
然后用curl测试一下: curl -i http://localhost/echo
结果如下:
可以看到模块已经正常工作了,也可以在浏览器中打开网址,就可以看到结果:
15. 更深入的学习
本文只是简要介绍了Nginx模块的开发过程,由于篇幅的原因,不能面面俱到。因为目前Nginx的学习资料很少,如果读者希望更深入学习Nginx的原理及模块开发,那么阅读源代码是最好的办法。在Nginx源代码的core/下放有Nginx的核心代码,对理解Nginx的内部工作机制非常有帮助,http/目录下有Nginx HTTP相关的实现,http/module下放有大量内置http模块,可供读者学习模块的开发,另外在http://wiki.nginx.org/3rdPartyModules上有大量优秀的第三方模块,也是非常好的学习资料。
from:http://blog.sina.com.cn/s/blog_69f80ee60102vhe0.html