在上一篇博文介绍了nginx中基本hash表的实现,今天主要是来介绍nginx是如何实现支持通配符的hash表。话说在看支持通配符的hash表源码时我惊奇地发现它的设计思路居然和我之前设计的中文字典树基本一致,觉得nginx的设计也不过如此,但是看完hash表源码后我才发现我还是too young too simple,nginx的hash表考虑到的问题我想的要多得多!
二、支持通配符的hash表
nginx为了处理带有通配符的域名字符串的匹配问题,实现了支持通配符的hash表nginx中的通配符有两种形式:一种是通配符在前面的例如“*.test.com”,这样可以省略"*"号,写成“.test.com”;还有一种是通配符在后面的例如"www.test.*",这样域名不能省略“*”号,因为这样的通配符可以匹配“www.test.com”、“www.test.cn”、“www.test.org”等域名。注意:nginx不能同时包含在前和在后的通配符例如“*.test.*”。
而这个支持通配符的hash表ngx_hash_wildcard_t实际上是对基本hash表的一层封装(后面会讲到)。
在查找的字符串时,先是到基本hash表中查找元素,如果没有找到就去前置通配符的hash表中查找,如果还是没找到就去后置通配符的hash表中查找。
(1)支持通配符hash表的数据结构
//带通配符的hash表结构
typedef struct
{
ngx_hash_t hash;// 基本散列表
void *value;//指向真正的value或NULL
} ngx_hash_wildcard_t;
//组合类型哈希表
typedef struct
{
ngx_hash_t hash;//基本hash表
ngx_hash_wildcard_t *wc_head;//前置通配符hash表
ngx_hash_wildcard_t *wc_tail;//后置通配符hash表
} ngx_hash_combined_t;
(2)带通配符hash表的内存布局图
nginx中通过多级hash实现了通配符的hash表,如下图所示:
(3)支持通配符hash表的初始化
nginx在初始化通配符的hash表时,实际上将key字符串以“.”为分隔符拆分成多个key字段。将所有key字符串的第一个key字段去重后存到一个基本hash表中。再将所有有相同的第一个key字段的字符串(可能有好几组)去掉第一个key字段后分别递归处理。
总的流程如下:为临时数组申请空间->遍历数组中元素找出当前字符串的第一个key字段并放入curr_names数组->将具有相同key字段的key字符串的剩余部分放入next_names数组->递归处理next_names数组->回到第二步,如果已经遍历完则将curr_names数组元素放入hash表。
1、为临时数组申请空间
由于是把所有key字符串的第一个key字段提取出来,那么不重复的key字段一定不会超过nelts,同样不重复的去掉key字符串剩余部分字符串个数也不会超过nelts个。
//为临时数组curr_names申请空间,用于存放当前节点的key字段数组
if (ngx_array_init(&curr_names, hinit->temp_pool, nelts, sizeof(ngx_hash_key_t)) != NGX_OK)
{
return NGX_ERROR;
}
//为临时数组next_names申请空间,用于存放除去当前key字段后剩余字符串数组
if (ngx_array_init(&next_names, hinit->temp_pool, nelts, sizeof(ngx_hash_key_t)) != NGX_OK)
{
return NGX_ERROR;
}
for (n = 0; n < nelts; n = i)
{
dot = 0;
//找到当前元素key值中第一次出现“.”的位置
for (len = 0; len < names[n].key.len; len++)
{
if (names[n].key.data[len] == '.')
{
dot = 1;
break;
}
}
//为“.”之前的key字段申请空间并装入该数组
name = ngx_array_push(&curr_names);
if (name == NULL)
{
return NGX_ERROR;
}
name->key.len = len;
name->key.data = names[n].key.data;
name->key_hash = hinit->key(name->key.data, name->key.len);
name->value = names[n].value;
dot_len = len + 1;
if (dot)
{
len++;
}
......
}
3、将具有相同key字段的key字符串的剩余部分放入next_names数组
第一个key字段为"com"的字符串有"com.yexin"、"com.yexin.test."、"com.example.",那么剩余部分字符串即为"yexin"、"yexin.test"、"example",存储next_names数组。
next_names.nelts = 0;
//如果“.”后面还有字符串
if (names[n].key.len != len)
{
//将除去当前key字段的剩余字符串装入next_names数组
next_name = ngx_array_push(&next_names);
if (next_name == NULL)
{
return NGX_ERROR;
}
next_name->key.len = names[n].key.len - len;
next_name->key.data = names[n].key.data + len;
next_name->key_hash = 0;
next_name->value = names[n].value;
}
//搜索后面有没有和当前元素相同key字段的元素
for (i = n + 1; i < nelts; i++)
{
if (ngx_strncmp(names[n].key.data, names[i].key.data, len) != 0)
{
break;
}
if (!dot && names[i].key.len > len && names[i].key.data[len] != '.')
{
break;
}
//如果有则把除去当前key字段的剩余字符串装入该数组
next_name = ngx_array_push(&next_names);
if (next_name == NULL)
{
return NGX_ERROR;
}
next_name->key.len = names[i].key.len - dot_len;
next_name->key.data = names[i].key.data + dot_len;
next_name->key_hash = 0;
next_name->value = names[i].value;
}
4、递归处理next_names数组
将字符串数组"yexin"、"yexin.test"、"example"递归处理。
//如果next_names数组中有元素,递归处理该元素
if (next_names.nelts)
{
h = *hinit;
h.hash = NULL;
if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts, next_names.nelts) != NGX_OK)
{
return NGX_ERROR;
}
......
}
5、将curr_names数组元素放入hash表
在上一步中对next_names数组元素进行递归处理,curr_names中存放的是所有字符串的第一个key字段(去重)的集合,这些key字段会作为key放到hash表中。在ngx_hash_wildcard_t结构体中会出现两种value指针。
//hash表中的元素数据结构
typedef struct
{
void *value;//指向下一个hash表或真正的value数据
u_short len;//key长度
u_char name[1];//key值
} ngx_hash_elt_t;
//hash表结构
typedef struct {
ngx_hash_elt_t **buckets; //指向hash桶指针数组
ngx_uint_t size; //hash桶个数
} ngx_hash_t;
//带通配符的hash表结构
typedef struct
{
ngx_hash_t hash;// 基本hash表
void *value;//指向真正的value或NULL
} ngx_hash_wildcard_t;
基本hash表中ngx_hash_elt_t元素中的value值(这里我简称base_hash_elt_value)指向下一个hash表或者真正value数据,ngx_hash_wildcard_t中的value值(这里我简称wildcard_hash_value)也可能会指向真正的value数据。
base_hash_elt_value和wildcard_hash_value在存放地址的时候会用最低的两位来标志来携带相关信息,代表指向的是hash表还是真正的value数据。由于之前已经考虑过了指针对齐,所以原指针最低两位一定为0。
//00 - value 是 "example.com" 和 "*.example.com"的数据指针(base_hash_elt_value和wildcard_hash_value)
//01 - value 仅仅是 "*.example.com"的数据指针(仅base_hash_elt_value)
//10 - value 是 支持通配符哈希表是 "example.com" 和 "*.example.com" 指针(仅base_hash_elt_value)
//11 - value 仅仅是支持通配符哈希表是 "*.example.com"的指针(仅base_hash_elt_value)
//如果next_names数组中有元素,递归处理该元素
if (next_names.nelts)
{
h = *hinit;
h.hash = NULL;
if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts, next_names.nelts) != NGX_OK)
{
return NGX_ERROR;
}
wdc = (ngx_hash_wildcard_t *) h.hash;
if (names[n].key.len == len)
{
wdc->value = names[n].value;
}
name->value = (void *) ((uintptr_t) wdc | (dot ? 3 : 2));
}
//带有前置通配符的字符串
else if (dot)
{
name->value = (void *) ((uintptr_t) name->value | 1);
}
//将最外层数组封装成一个基本hash表
if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts, curr_names.nelts) != NGX_OK)
{
return NGX_ERROR;
}
(4)支持通配符hash表的前缀通配符查找
查找方式为用“.”将字符串分割成多个key字段,获取最后一个key字段,用该key字段查hash表,如果获取的value值指向下一级hash表,那么就用同样的方法查找倒数第二个key字段对应的value值,直到找出真实的value值。
//支持通配符hash表的前缀通配符查找
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;
n = len;
//从后往前截取出第一个key字段
while (n)
{
if (name[n - 1] == '.')
{
break;
}
n--;
}
key = 0;
//计算该key字段的hash值
for (i = n; i < len; i++)
{
key = ngx_hash(key, name[i]);
}
//在一级hash表中查找该key字段
value = ngx_hash_find(&hwc->hash, key, &name[n], len - n);//这里的value是base_hash_elt_value
if (value)
{
//base_hash_elt_value指向是通配符hash表
//该hash表有两种情况
//1、同时包含 "example.com" 和 "*.example.com" 指针的通配符hash表
//2、仅包含"*.example.com" 指针的通配符hash表
if ((uintptr_t) value & 2)
{
if (n == 0)
{
//搜索的最后一个key字段没有通配符,如"example.com"中的"example"
//但是base_hash_elt_value指向的是只包含前缀通配符元素的hash表,没找到返回NULL
if ((uintptr_t) value & 1)
{
return NULL;
}
//搜索的最后一个key字段有通配符,那么将base_hash_elt_value去掉最低两位即指向真实的value值
hwc = (ngx_hash_wildcard_t *)((uintptr_t) value & (uintptr_t) ~3);
return hwc->value;
}
//如果当前key字段不是最后一个字段,那么截取剩余字符串后递归查找
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;
}
//没找到,即为wildcard_hash_value指向的值
return hwc->value;
}
//base_hash_elt_value实际指向包含前缀通配符元素
if ((uintptr_t) value & 1)
{
//最后一个key字段没有通配符,则查找失败
if (n == 0)
{
return NULL;
}
//最后一个key字段有通配符,返回该值
return (void *)((uintptr_t) value & (uintptr_t) ~3);
}
return value;
}
//如果基本hash表中不存在,那么一定是在通配符hash表的wildcard_hash_value
return hwc->value;
}
//支持通配符hash表的后缀通配符查找
void * ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len)
{
void *value;
ngx_uint_t i, key;
key = 0;
//从前往后截取出第一个key字段
for (i = 0; i < len; i++)
{
if (name[i] == '.')
{
break;
}
//计算该key字段的hash值
key = ngx_hash(key, name[i]);
}
if (i == len) {
return NULL;
}
//在当前hash表中查找该key字段
value = ngx_hash_find(&hwc->hash, key, name, i);
if (value)
{
//如果value指向hash表
if ((uintptr_t) value & 2)
{
i++;
//获取指向下一级hash表的指针
hwc = (ngx_hash_wildcard_t *) ((uintptr_t) value & (uintptr_t) ~3);
//去下一级hash表中查找
value = ngx_hash_find_wc_tail(hwc, &name[i], len - i);
if (value)
{
return value;
}
return hwc->value;
}
//如果value指向真实的value值
return value;
}
//如果当前hash表中不存在,那么一定是在通配符hash表的wildcard_hash_value
return hwc->value;
}
//组合hash表查找
void * ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name, size_t len)
{
void *value;
//先到基本hash表中查找
if (hash->hash.buckets)
{
value = ngx_hash_find(&hash->hash, key, name, len);
if (value)
{
return value;
}
}
if (len == 0)
{
return NULL;
}
//到前缀通配符hash表中查找
if (hash->wc_head && hash->wc_head->hash.buckets)
{
value = ngx_hash_find_wc_head(hash->wc_head, name, len);
if (value)
{
return value;
}
}
//到后缀通配符hash表中查找
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;
}