本来想在这一节开始就分析一个nginx源码了,觉得好像还有高级数据结构没说,当然也是看《深入理解nginx》这本书的拉,想学的可以去买一本看看,内容还真不错。
6.1 双向链表
也看了一些双链表的实现,感觉基本双链表实现都差不多,不知道是不是都借鉴了linux内核中的实现,不管了,这里也简单介绍一下。
6.1.1 数据结构
struct ngx_queue_s {
ngx_queue_t *prev;
ngx_queue_t *next;
};
都是直接指定两个指针,一个指向prev一个指向next,不过感觉这种链表的设计确实比较优美。
6.1.2 双链表的使用方法
#define ngx_queue_init(q) \
(q)->prev = q; \
(q)->next = q
#define ngx_queue_empty(h) \
(h == (h)->prev)
#define ngx_queue_insert_head(h, x) \
(x)->next = (h)->next; \
(x)->next->prev = x; \
(x)->prev = h; \
(h)->next = x
#define ngx_queue_insert_after ngx_queue_insert_head
#define ngx_queue_insert_tail(h, x) \
(x)->prev = (h)->prev; \
(x)->prev->next = x; \
(x)->next = h; \
(h)->prev = x
#define ngx_queue_head(h) \
(h)->next
#define ngx_queue_last(h) \
(h)->prev
#define ngx_queue_sentinel(h) \
(h)
#define ngx_queue_next(q) \
(q)->next
#define ngx_queue_prev(q) \
(q)->prev
#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
#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;
#define ngx_queue_add(h, n) \
(h)->prev->next = (n)->next; \
(n)->next->prev = (h)->prev; \
(h)->prev = (n)->prev; \
(h)->prev->next = h;
#define ngx_queue_data(q, type, link) \
(type *) ((u_char *) q - offsetof(type, link))
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 *));
源码都在这里了,还有所有方法都在了。
6.1.3 简单使用双链表
typedef struct {
u_char *str;
ngx_queue_t qele;
int num;
} TestNode;
//初始化
TestNode node;
ngx_queue_init(&node); //看上面的源码,初始化只是赋值而已
//插入方法,有三种,分别都是指针操作,可以自行分析
ngx_queue_insert_head
ngx_queue_insert_after
ngx_queue_insert_tail
有点偷懒,其他的就不写了,这样再分析两个函数:
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;
}
}
}
这个实现真有意思,就是几个指针不断的移动,判断哪个是中间元素,这个应该是要排序之后的了
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); //否则取出前前一个元素
} while (prev != ngx_queue_sentinel(queue));
ngx_queue_insert_after(prev, q); //然后把q插入到合适位置
}
}
这个排序只要是循环遍历链表,然后把小的移动到前面,确实不适合大的数据。
6.2 动态数组
所谓的动态数组,就是可以动态分配的数组,我们都知道数组的缺点就是大小固定,优点就是寻找快,如果可以动态分配数组那就是相当于解决了一个问题。
6.2.1 数组结构
typedef struct {
void *elts; //指向数组的首地址
ngx_uint_t nelts; //nelts是数组中已经使用的元素个数
size_t size; //每个数组元素占用的内存大小
ngx_uint_t nalloc; //当前数组中能够容纳元素个数
ngx_pool_t *pool; //内存池
} ngx_array_t;
感觉也差不多,动态数组的实现方式也是这样实现的,可能是看的比较多吧,下面我们看看提供的方法:
6.2.2 提供方法
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)
{
/*
* set "array->nelts" before "array->elts", otherwise MSVC thinks
* that "array->nelts" may be used without having been initialized
*/
array->nelts = 0; //很简单,就是赋值
array->size = size;
array->nalloc = n;
array->pool = pool;
array->elts = ngx_palloc(pool, n * size); //想内存池中申请
if (array->elts == NULL) {
return NGX_ERROR;
}
return NGX_OK;
}
怎么感觉动态数组没有删除这个功能,可能是nginx不需要吧,感觉这种数据结构的设计还是要跟实际应用搭边。
6.2.3 解析数组
动态数组代码量不大,我们这里分析一下:
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;
}
if (ngx_array_init(a, p, n, size) != NGX_OK) {
return NULL;
}
return a;
}
创建函数就不分析了,太简单了。
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;
}
if ((u_char *) a + sizeof(ngx_array_t) == p->d.last) {
p->d.last = (u_char *) a;
}
}
这个不简单了,这个关于内存池的内容了,这里先略过,以后会有一个专门的一章解析内存池。
6.2.4 动态数组扩容
扩容的代码就不贴出来,动态扩容分为两种情况:
- 如果当前内存池中的剩余空间大于或者等于本次需要新增的空间,那么本次扩容将只扩充新增的空间。例如:对于ngx_array_push来说就是1字节,对于ngx_array_push_n来说就是n字节
- 如果当前当前内存池中剩余空间小于本次需要新增的空间,那么对ngx_array_push来说扩容一倍,对于ngx_array_push_n来说,如果参数小于原先动态数组的容量,将会扩容一倍,如果参数n大于原先动态数组的容量,这是会分配2Xn的空间。
6.3 单向链表
单向链表在之前已经讲过了,这里就简单的复制一下,做做回忆。
6.3.1 数据结构
typedef struct ngx_list_part_s ngx_list_part_t;
struct ngx_list_part_s {
void *elts; //指向数组的起始地址
ngx_uint_t nelts; //数组中已经使用了多少个元素
ngx_list_part_t *next; //下一个链表元素ngx_list_part_t的地址
};
typedef struct {
ngx_list_part_t *last; //指向链表的最后一个数组元素
ngx_list_part_t part; //链表的首个数组元素
size_t size; //size限制每一个数组元素的占用的空间大小,也就是用户要存储的一个数据所占用的字节数必须小于或等于size
ngx_uint_t nalloc; //表示每个ngx_list_part_s数组的容量,即最多可存储多少个数据
ngx_pool_t *pool; //链表中管理内存分配的内存池对象
} ngx_list_t;
6.3.2 内存分布图
6.3.3 提供接口
/*
创建链表
pool : nginx内存池
n : 每个链表数组可容纳n个元素
size : 每个元素的大小
*/
ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);
/*
初始化链表
list:分配好内存的ngx_list_t链表
pool:nginx内存池
n:每个链表数组可容纳n个元素
size : 每个元素的大小
*/
static ngx_inline ngx_int_t ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size);
/*
添加元素
list:创建好的ngx_list_t链表
*/
void *ngx_list_push(ngx_list_t *list);
6.4 红黑树
红黑树是nginx核心的数据结构,在需要快速检索,查找的场合下使用红黑树是比较好的。
关于红黑树的原理介绍,可以看我之前的章节,数据结构篇,里面详细介绍了红黑树,这一篇只介绍nginx的红黑树实现。
6.4.1 数据结构
红黑树根结点:
typedef void (*ngx_rbtree_insert_pt) (ngx_rbtree_node_t *root,
ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
struct ngx_rbtree_s {
ngx_rbtree_node_t *root; //指向树的根结点
ngx_rbtree_node_t *sentinel; //执行NIL哨兵结点
ngx_rbtree_insert_pt insert; //表示红黑树添加元素的函数指针
};
红黑树的数据结构分为根结点和普通结点,上面是根结点的结构。
struct ngx_rbtree_node_s {
ngx_rbtree_key_t key; //无符号整形的关键字
ngx_rbtree_node_t *left; //左子结点
ngx_rbtree_node_t *right; //右子结点
ngx_rbtree_node_t *parent; //父结点
u_char color; //结点的颜色,0表示黑色,1表示红色
u_char data; //仅1个字节的结点数据,由于表示的空间太小,所以一般很少使用
};
红黑树中比较重要的成员是:ngx_rbtree_insert_pt,这个函数指针,这个函数指针是给红黑树添加元素时的回调函数,可能用户需要自己定义的添加函数,就可以实现这个函数,当然官方来提供了几个默认的插入函数:
void ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node);
void ngx_rbtree_insert_value(ngx_rbtree_node_t *root, ngx_rbtree_node_t *node,
ngx_rbtree_node_t *sentinel);
void ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *root,
ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
这几个都是插入函数,还有其他函数,今天偷懒一下,就不贴出来了。
在nginx中比较常用的函数就是查找最小值:
static ngx_inline ngx_rbtree_node_t *
ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
while (node->left != sentinel) {
node = node->left;
}
return node;
}
6.4.2 简单使用
红黑树也是把红黑树的结构封装成一个结构体,如果要使用红黑树结构,需要重新封装一个新的结构体,
typedef struct {
ngx_rbtree_node_t node;
ngx_uint_t num;
} TestRBTreeNode;
其他的就先不写。
6.5 基数树
基数树也是一种二叉查找树,它跟红黑树有两点的区别:
还是来看看基本的数据结构:
typedef struct {
ngx_radix_node_t *root; //指向根结点
ngx_pool_t *pool; //内存池,它负责给基数树的结点分配内存
ngx_radix_node_t *free; //管理已经分配但暂时未使用的结点
char *start; //已分配内存中还没使用内存的首地址
size_t size; //已分配内存中还未使用的内存大小
} ngx_radix_tree_t;
这个感觉比较好的地方就是释放的结点,不释放内存,用一个链表来连接,然后等使用的时候再分配。
下面的一个结点的数据结构:
struct ngx_radix_node_s {
ngx_radix_node_t *right; //右子树
ngx_radix_node_t *left; //左子树
ngx_radix_node_t *parent; //父节点
uintptr_t value; //真正的数据
};
计数树就是依靠value这个指针,指向用户的数据,这样就可以把用户的数据挂接在基数树上,这也是我们以前用的方法。
6.5.2 操作函数
ngx_radix_tree_t *ngx_radix_tree_create(ngx_pool_t *pool,
ngx_int_t preallocate);
ngx_int_t ngx_radix32tree_insert(ngx_radix_tree_t *tree,
uint32_t key, uint32_t mask, uintptr_t value);
ngx_int_t ngx_radix32tree_delete(ngx_radix_tree_t *tree,
uint32_t key, uint32_t mask);
uintptr_t ngx_radix32tree_find(ngx_radix_tree_t *tree, uint32_t key);
这个操作函数是比较简单额,也就这么几个。
6.5.2 原理
其实原理也是比较简单的,我们来看创建函数,创建函数有个变量preallocate,这个叫预分配,不过我们使用可以直接赋值-1,我们来看看效果:
ngx_radix_tree_t *
ngx_radix_tree_create(ngx_pool_t *pool, ngx_int_t preallocate)
{
uint32_t key, mask, inc;
ngx_radix_tree_t *tree;
tree = ngx_palloc(pool, sizeof(ngx_radix_tree_t));
if (tree == NULL) {
return NULL;
}
tree->pool = pool;
tree->free = NULL;
tree->start = NULL;
tree->size = 0;
tree->root = ngx_radix_alloc(tree);
if (tree->root == NULL) {
return NULL;
}
tree->root->right = NULL;
tree->root->left = NULL;
tree->root->parent = NULL;
tree->root->value = NGX_RADIX_NO_VALUE;
if (preallocate == 0) {
return tree;
}
/*
* Preallocation of first nodes : 0, 1, 00, 01, 10, 11, 000, 001, etc.
* increases TLB hits even if for first lookup iterations.
* On 32-bit platforms the 7 preallocated bits takes continuous 4K,
* 8 - 8K, 9 - 16K, etc. On 64-bit platforms the 6 preallocated bits
* takes continuous 4K, 7 - 8K, 8 - 16K, etc. There is no sense to
* to preallocate more than one page, because further preallocation
* distributes the only bit per page. Instead, a random insertion
* may distribute several bits per page.
*
* Thus, by default we preallocate maximum
* 6 bits on amd64 (64-bit platform and 4K pages)
* 7 bits on i386 (32-bit platform and 4K pages)
* 7 bits on sparc64 in 64-bit mode (8K pages)
* 8 bits on sparc64 in 32-bit mode (8K pages)
*/
if (preallocate == -1) {
switch (ngx_pagesize / sizeof(ngx_radix_node_t)) {
/* amd64 */
case 128:
preallocate = 6;
break;
/* i386, sparc64 */
case 256:
preallocate = 7;
break;
/* sparc64 in 32-bit mode */
default:
preallocate = 8;
}
}
mask = 0;
inc = 0x80000000;
while (preallocate--) {
key = 0;
mask >>= 1;
mask |= 0x80000000;
do {
if (ngx_radix32tree_insert(tree, key, mask, NGX_RADIX_NO_VALUE)
!= NGX_OK)
{
return NULL;
}
key += inc;
} while (key);
inc >>= 1;
}
return tree;
}
-1是取默认的值,预分配其他结点,把叶子结点做为用户数据插入,有点像B+树的感觉。
插入的时候需要掩码,像我们上面创建的树,叶子结点在第三次,所以掩码是0xe0000000,前3位有效,就是通过前3位的值来判断插入的位置,如果我们要将0x20000000插入这个基数树,插入的位置就是第二个,前3位是001,用户的有效数据全部插入到第三层的叶子结点上。
原理都理解了用法就不写了,哈哈。
6.6 散列表
散列表也叫哈希表是典型的以空间换时间的数据结构,在一些合理的假设下,对任意元素的检索,插入速度的期望时间为O(1),这种高效的方式非常适合频繁读取、插入、删除元素,以及对速度敏感的场合。
6.6.1 基本散列表
我们先看一下哈希表中存储的元素的结构,
typedef struct {
void *value; //指向用户自定义元素数据的指针
u_short len; //元素关键字的长度
u_char name[1]; //元素关键字的首地址
} ngx_hash_elt_t;
name[1]成员只用于指出关键字的首地址,而关键字的长度是可变长度。这个只是哈希表中的一个元素的结构,我们来看看散列表的结构体:
typedef struct {
ngx_hash_elt_t **buckets; //指向散列表的首地址,也是第1个槽的地址
ngx_uint_t size; //散列表中槽的总数
} ngx_hash_t;
散列表结构体用了一个二维指针指向了第一个元素的槽的地址,一个元素的内存大小应该也是在初始化中分配的,不过现在我们先不分析初始化。
6.6.2 哈希碰撞
我们都知道,使用哈希表一个很大的问题就是会出现碰撞,解决哈希碰撞的问题大概有两种方法:
- 分离链表法,就是把哈希表中散列到同一个槽中的所有元素都放在散列表外的一个链表中,这样查询元素时,在找到这个槽后,还要遍历链表才能找到正确的元素。
- 开放寻址法,即所有元素都存放在散列表中,当查找一个元素时,要检查规则内的所有的表项,直到找到所需的元素,或者最终发现元素不在表中。
nginx的散列表使用的是开放寻址法。
开发寻址法有许多种实现方式,nginx使用的是连续非空槽存储碰撞元素的方法。例如:当插入一个元素时,可以按照散列方法找到指定槽,如果该槽非空且存在的元素与待插入元素并非一个元素,则依次检查其后连续的槽,直到找到一个空槽来放置这个元素为止。
6.6.3 nginx散列方法
nginx设计了typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);散列方法的指针,如果用户需要自定义散列方法,就可以实现这个函数,并指向这个函数指针,当然nginx也实现了自己的散列方法:
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);
这两个函数的实现都比较简单:
#define ngx_hash(key, c) ((ngx_uint_t) key * 31 + c)
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_uint_t
ngx_hash_key_lc(u_char *data, size_t len)
{
ngx_uint_t i, key;
key = 0;
for (i = 0; i < len; i++) {
key = ngx_hash(key, ngx_tolower(data[i]));
}
return key;
}
6.6.4 支持通配符的散列表
nginx自己实现了一个可以支持简单通配符的散列表,只支持前置通配符或者后置通配符,接下来我们看看:
typedef struct {
ngx_hash_t hash; //基本散列表
void *value; //可以使用这个value指针指向用户数据
} ngx_hash_wildcard_t;
这个结构只是对ngx_hash_t进行了简单的封装,同时nginx也对这个结构提供了两种方法,不过我们先不看方法,nginx其实还对这个结构做了封装,这个结构才是真正具体使用的:
typedef struct {
ngx_hash_t hash; //用于精确匹配的基本散列表
ngx_hash_wildcard_t *wc_head; //用于查询前置通配符的散列表
ngx_hash_wildcard_t *wc_tail; //用于查询后置通配符的散列表
} ngx_hash_combined_t;
看到这个数据结构是否想到nginx匹配server_name主机通配符支持的规则
- 选择所有字符串完全匹配的server_name
- 选择通配符在前面的server_name
- 选择通配符在后面的server_name
这样的规则再对应到这个数据结构,是不是很清楚明了。
是不是很好奇前置通配符散列表和后置通配符的散列表是啥,其实很简单的,比如关键字"www.test.*"这样带通配符的情况,直接在后置通配符散列表中存储,存储元素的关键字为www.test。这样如果要匹配www.test.cn这个字符串,就会把www.test.cn转为www.test字符串再做查询。
6.6.5 查询字符
我们先看看查询元素时,nginx所提供的方法:
void *ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);
void *ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);
void *ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key,
u_char *name, size_t len);
我们先看看ngx_hash_find_combined查询:
void *
ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name,
size_t len)
{
void *value;
if (hash->hash.buckets) {
value = ngx_hash_find(&hash->hash, key, name, len);
if (value) {
return value;
}
}
if (len == 0) {
return NULL;
}
if (hash->wc_head && hash->wc_head->hash.buckets) {
value = ngx_hash_find_wc_head(hash->wc_head, name, len);
if (value) {
return value;
}
}
if (hash->wc_tail && hash->wc_tail->hash.buckets) {
value = ngx_hash_find_wc_tail(hash->wc_tail, name, len);
if (value) {
return value;
}
}
return NULL;
}
是不是很简单,先从完全匹配的字符串查起,然后查询前置通配符,最后后置通配符。
在看看前置通配符查询:
void *
ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len)
{
void *value;
ngx_uint_t i, n, key;
#if 0
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "wch:\"%*s\"", len, name);
#endif
n = len;
while (n) {
if (name[n - 1] == '.') {
break;
}
n--;
}
key = 0;
for (i = n; i < len; i++) {
key = ngx_hash(key, name[i]);
}
#if 0
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "key:\"%ui\"", key);
#endif
value = ngx_hash_find(&hwc->hash, key, &name[n], len - n);
#if 0
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "value:\"%p\"", value);
#endif
if (value) {
/*
* the 2 low bits of value have the special meaning:
* 00 - value is data pointer for both "example.com"
* and "*.example.com";
* 01 - value is data pointer for "*.example.com" only;
* 10 - value is pointer to wildcard hash allowing
* both "example.com" and "*.example.com";
* 11 - value is pointer to wildcard hash allowing
* "*.example.com" only.
*/
if ((uintptr_t) value & 2) {
if (n == 0) {
/* "example.com" */
if ((uintptr_t) value & 1) {
return NULL;
}
hwc = (ngx_hash_wildcard_t *)
((uintptr_t) value & (uintptr_t) ~3);
return hwc->value;
}
hwc = (ngx_hash_wildcard_t *) ((uintptr_t) value & (uintptr_t) ~3);
value = ngx_hash_find_wc_head(hwc, name, n - 1);
if (value) {
return value;
}
return hwc->value;
}
if ((uintptr_t) value & 1) {
if (n == 0) {
/* "example.com" */
return NULL;
}
return (void *) ((uintptr_t) value & (uintptr_t) ~3);
}
return value;
}
return hwc->value;
}
去掉前缀,然后以剩下的字符去hash表中查找,找到会返回一个value就是散列表元素中的value,这个value拿来做状态了,然后需要判断一下状态,在返回,所以上面看到有封装了一个value存储用户数据。
6.6.6 初始化
讲了这么多,终于到了初始化阶段,我们先来看看初始化的函数:
ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);
ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);
我们通过函数都可以看到初始化函数都需要一个结构体,我们来看看这个结构体:
typedef struct {
ngx_hash_t *hash; //指向普通的完全匹配散列表
ngx_hash_key_pt key; //用于初始化预添加元素的散列方法
ngx_uint_t max_size; //散列表中槽的最大数目
ngx_uint_t bucket_size; //散列表中一个槽的空间大小,它限制了每个散列表元素关键字的最大长度
char *name; //散列表的名称
ngx_pool_t *pool; //内存池
ngx_pool_t *temp_pool; //临时内存池
} ngx_hash_init_t;
分配多少个槽,并不完全由max_size来决定,初始化函数中有说明。
接下来看看另外一个参数:
typedef struct {
ngx_str_t key; //元素关键字
ngx_uint_t key_hash; //由散列函数计算出来的关键吗
void *value; //指向实际的用户数据
} ngx_hash_key_t;
其实我们的ngx_hash_key_t这个参数,其实是一个数组,这个数组就是通过ngx_hash_array_t构造的,我们先看看这个:
typedef struct {
ngx_uint_t hsize; //下面的keys_hash、dns_wc_head_hash、dns_wc_tail_hash都是简易散列表,而hsize指明了散列表的槽个数
ngx_pool_t *pool; //内存池,该pool没有任何意义
ngx_pool_t *temp_pool; //临时内存池,下面的动态数据都是由temp_pool内存池分配的
ngx_array_t keys; //用动态数组以ngx_hash_key_t结构体保存这不含有通配符关键字的元素
ngx_array_t *keys_hash; //一个简易的散列表,数组的形式保存这不带通配符的元素
ngx_array_t dns_wc_head; //用动态数据保存这前置通配符的元素
ngx_array_t *dns_wc_head_hash; //
ngx_array_t dns_wc_tail; //用动态元素保存这后置通配符的元素
ngx_array_t *dns_wc_tail_hash;
} ngx_hash_keys_arrays_t;
感觉不是很理解这玩意,我们先来看看这个数组的初始化函数,
ngx_int_t
ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type)
{
ngx_uint_t asize;
if (type == NGX_HASH_SMALL) {
asize = 4;
ha->hsize = 107;
} else {
asize = NGX_HASH_LARGE_ASIZE;
ha->hsize = NGX_HASH_LARGE_HSIZE;
}
//asize每个数组元素的大小
if (ngx_array_init(&ha->keys, ha->temp_pool, asize, sizeof(ngx_hash_key_t)) //要先分配临时内存池
!= NGX_OK)
{
return NGX_ERROR;
}
if (ngx_array_init(&ha->dns_wc_head, ha->temp_pool, asize,
sizeof(ngx_hash_key_t))
!= NGX_OK)
{
return NGX_ERROR;
}
if (ngx_array_init(&ha->dns_wc_tail, ha->temp_pool, asize,
sizeof(ngx_hash_key_t))
!= NGX_OK)
{
return NGX_ERROR;
}
ha->keys_hash = ngx_pcalloc(ha->temp_pool, sizeof(ngx_array_t) * ha->hsize); //分配了存储几个元素
if (ha->keys_hash == NULL) {
return NGX_ERROR;
}
ha->dns_wc_head_hash = ngx_pcalloc(ha->temp_pool,
sizeof(ngx_array_t) * ha->hsize);
if (ha->dns_wc_head_hash == NULL) {
return NGX_ERROR;
}
ha->dns_wc_tail_hash = ngx_pcalloc(ha->temp_pool,
sizeof(ngx_array_t) * ha->hsize);
if (ha->dns_wc_tail_hash == NULL) {
return NGX_ERROR;
}
return NGX_OK;
}
看了一下,感觉啥都没做,就是分配了内存,接下来再看看添加key的函数:
ngx_int_t
ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key, void *value,
ngx_uint_t flags)
{
size_t len;
u_char *p;
ngx_str_t *name;
ngx_uint_t i, k, n, skip, last;
ngx_array_t *keys, *hwc;
ngx_hash_key_t *hk;
last = key->len;
if (flags & NGX_HASH_WILDCARD_KEY) {
/*
* supported wildcards:
* "*.example.com", ".example.com", and "www.example.*"
*/
n = 0;
for (i = 0; i < key->len; i++) {
if (key->data[i] == '*') {
if (++n > 1) {
return NGX_DECLINED;
}
}
if (key->data[i] == '.' && key->data[i + 1] == '.') {
return NGX_DECLINED;
}
if (key->data[i] == '\0') {
return NGX_DECLINED;
}
}
if (key->len > 1 && key->data[0] == '.') {
skip = 1;
goto wildcard;
}
//这个是跳转到通配符的方式
if (key->len > 2) {
if (key->data[0] == '*' && key->data[1] == '.') {
skip = 2;
goto wildcard;
}
if (key->data[i - 2] == '.' && key->data[i - 1] == '*') {
skip = 0;
last -= 2;
goto wildcard;
}
}
if (n) {
return NGX_DECLINED;
}
}
/* exact hash */
//这个是插入没有通配符的方法
k = 0;
for (i = 0; i < last; i++) {
if (!(flags & NGX_HASH_READONLY_KEY)) {
key->data[i] = ngx_tolower(key->data[i]);
}
k = ngx_hash(k, key->data[i]);
}
k %= ha->hsize;
/* check conflicts in exact hash */
name = ha->keys_hash[k].elts;
if (name) { //如果已经存在,就进行匹配
for (i = 0; i < ha->keys_hash[k].nelts; i++) {
if (last != name[i].len) {
continue;
}
if (ngx_strncmp(key->data, name[i].data, last) == 0) {
return NGX_BUSY;
}
}
} else { //如果没有的话,就申请内存key_hash的内存
if (ngx_array_init(&ha->keys_hash[k], ha->temp_pool, 4,
sizeof(ngx_str_t))
!= NGX_OK)
{
return NGX_ERROR;
}
}
name = ngx_array_push(&ha->keys_hash[k]); //保存到keys_hash中,这是按k存储的
if (name == NULL) {
return NGX_ERROR;
}
*name = *key;
hk = ngx_array_push(&ha->keys); //也保存在keys中,按顺序存储
if (hk == NULL) {
return NGX_ERROR;
}
hk->key = *key;
hk->key_hash = ngx_hash_key(key->data, last);
hk->value = value;
return NGX_OK;
wildcard:
....
return NGX_OK;
}
简单分析了一下添加函数,才明白了3个简易的散列表keys_hash,dns_wc_head,dns_wc_tail_hash的作用,我们向keys,dns_wc_head、dns_wc_tail这些数组添加元素的时候,如果要避免出现相同关键字,就需要遍历整个数组,这个效率很低,如果我们用这些简易的哈希,就可以直接查询是否有冲突,原来为了实现这么多东西,是为了以空间换时间。
接下来终于可以看我们的初始化函数了,太难了
这里就不贴代码了,看着确实很难,指针到处飞,内存到处申请。不过总体来说就是根据keys,dns_wc_head、dns_wc_tail这三个数组建立散列表,ok,就这样。
有一点需要注意的是,初始化前置后缀通配符时,需要自动手动把hash表的指针连接到ngx_hash_combined_t结构中的wc_head或者wc_tail中,这样在查找的时候才能查到。
太难了。