前面二章已经分析了nginx普通哈希表,通配符哈希表的初始化流程以及查找流程。在分析前两章时,要创建一个哈希表,都假设要插入到哈希表中的数据已经准备好了。本节将分析nginx如何对要插入到哈希表中的数据进行转换。例如: 前置通配符<*.baidu.com.cn>转换为cn.com.baidu; 后置通配符<www.baidu.com.*>转换为www.baidu.com。下面是一张使用哈希表的整体流程。可以参考ngx_http_server_names函数的实现。
图:哈希表使用流程
在图中,使用ngx_hash_init初始化普通哈希表,以及ngx_hash_wildcard_init初始化通配符哈希表在前面两章已经分析过了,这里重点分析中间部分转换过程。
一、哈希数组结构
//哈希数组结构
typedef struct
{
ngx_uint_t hsize; //下面三个哈希表槽个数,
//也就是keys_hash,dns_wc_head_hash,dns_wc_tail_hash指针数组元素个数
ngx_pool_t *pool; //无意义
ngx_pool_t *temp_pool; //用于下面动态数据空间的申请
ngx_array_t keys; //动态数组以ngx_hash_key_t结构体保存着不含通配符关键字key-value
//例如存放www.baidu.com关键字以及对应的value
ngx_array_t *keys_hash; //存放完全匹配的关键字,例如:存放www.baidu.com关键字,
ngx_array_t dns_wc_head; //动态数组以ngx_hash_key_t结构体保存着前置通配符关键字key-value
//例如存放:com.domain关键字以及对于的value
ngx_array_t *dns_wc_head_hash;//存放前置通配符关键字,例如:*.domain.com, 则存放domain.com
ngx_array_t dns_wc_tail; //动态数组以ngx_hash_key_t结构体保存着后置通配符关键字key-value
//例如存放:www.baidu关键字以及对于的value
ngx_array_t *dns_wc_tail_hash;//存放后置通配符关键字,例如:存放www.baidu
} ngx_hash_keys_arrays_t;
二、哈希数组初始化
ngx_hash_keys_array_init函数为构造普通哈希表、前置通配符哈希表、后置通配符哈希表做准备。不管是ngx_hash_init还是ngx_hash_wildcard_init函数,都有一个names数组参数。 这个函数就是为了开辟这些names数组空间。有了这个names数组空间,后续才能调用初始化哈希表函数,用这个数组构造一个哈希表。
//功能:开辟哈希表数组空间。
// 该函数会同时开辟前置哈希表数组、后置通配符哈希表数组、普通哈希表数组空间
//type值为NGX_HASH_SMALL时,哈希表槽元素较少;值为NGX_HASH_LARGE表示哈希槽元素多
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;
}
//初始化正常key-value数组
if (ngx_array_init(&ha->keys, ha->temp_pool, asize, sizeof(ngx_hash_key_t))
!= NGX_OK)
{
return NGX_ERROR;
}
//初始化前置key-value数组
if (ngx_array_init(&ha->dns_wc_head, ha->temp_pool, asize,
sizeof(ngx_hash_key_t))
!= NGX_OK)
{
return NGX_ERROR;
}
//初始化后置key-value数组
if (ngx_array_init(&ha->dns_wc_tail, ha->temp_pool, asize,
sizeof(ngx_hash_key_t))
!= NGX_OK)
{
return NGX_ERROR;
}
//初始化正常key数组:"www.example.com",则将存放www.example.com关键字key
ha->keys_hash = ngx_pcalloc(ha->temp_pool, sizeof(ngx_array_t) * ha->hsize);
if (ha->keys_hash == NULL)
{
return NGX_ERROR;
}
//初始化前置key数组:"*.example.com",则将存放example.com关键字key
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;
}
//初始化后置key数组:"www.example.*",则将存放www.example关键字key
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;
}
三、哈希数组的转换
ngx_hash_add_key函数对输入的key-value值进行转换。
如果为普通的关键字,例如:www.example.com,则不需要转换,直接把这个关键字保存到keys_hash数组中,而关键字对应的值则保存在keys数组;
如果为前置通配符,例如: *.example.com,则需要转换为com.example进行存储。.example.com存储在dns_wc_head_hash, 而转换后的com.example存储在dns_wc_head。
如果为后置通配符,例如: www.example.*,则需要转换为www.example进行存储。 www.example存储在dns_wc_tail_hash,而www.example对应的值则存储在dns_wc_tail
//功能: 将key,value插入到ha中相应的哈希表中保存,
//例如:"*.example.com"或者".example.com" 将保存到ha->dns_wc_head前置哈希表
//"www.example.*" 将保存到ha->dns_wc_tail后置哈希表
//"www.example.com"将保存到ha->keys正常哈希表
//flags值为NGX_HASH_WILDCARD_KEY表示可以保存通配符
//注意: 如果存在相同的key则保存失败,因为不允许存在相同的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;
}
}
//.example.com前置通配符情况
if (key->len > 1 && key->data[0] == '.')
{
skip = 1; //值为1表示将跳过第一个字符. 甚于的字符当做key
goto wildcard;
}
if (key->len > 2)
{
//*.example.com前置通配符情况
if (key->data[0] == '*' && key->data[1] == '.')
{
skip = 2; //值为2表示跳过开头的*.两个字符,甚于的字符当做key
goto wildcard;
}
//www.example.*后置通配符这种情况
if (key->data[i - 2] == '.' && key->data[i - 1] == '*')
{
skip = 0; //值为0表示不跳过任何字符,从第0个字符开始取key
last -= 2; //key的长度为总长度减去*.这三个字符
goto wildcard;
}
}
if (n)
{
return NGX_DECLINED;
}
}
/* exact hash */
/******************执行到这里表示精确查找*****************/
k = 0;
//计算出key的哈希值
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;
//数组不为空,则如果在数组中查找到相同的key,则说明key值相同,不应该插入相同的key
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
{
//数组不存在,则开辟一个数据元素空间
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]);
if (name == NULL)
{
return NGX_ERROR;
}
//保存key值
*name = *key;
//获取一个数组元素
hk = ngx_array_push(&ha->keys);
if (hk == NULL)
{
return NGX_ERROR;
}
//保存key对应的value值
hk->key = *key;
hk->key_hash = ngx_hash_key(key->data, last);
hk->value = value;
return NGX_OK;
wildcard:
/* wildcard hash */
//将原字符串src全部转为小写后,存放到目的串dst中,并将dst字符串根据哈希函数计算出哈希关键字
k = ngx_hash_strlow(&key->data[skip], &key->data[skip], last - skip);
k %= ha->hsize;
//.example.com前置通配符情况
if (skip == 1)
{
/* check conflicts in exact hash for ".example.com" */
name = ha->keys_hash[k].elts;
//判断key是否在数组中存在,如果存在则不应该有相同的key,直接返回
if (name)
{
len = last - skip;
for (i = 0; i < ha->keys_hash[k].nelts; i++)
{
if (len != name[i].len)
{
continue;
}
if (ngx_strncmp(&key->data[1], name[i].data, len) == 0)
{
return NGX_BUSY;
}
}
}
else
{
//开辟数组元素空间
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]);
if (name == NULL)
{
return NGX_ERROR;
}
//开辟空间,保存key
name->len = last - 1;
name->data = ngx_pnalloc(ha->temp_pool, name->len);
if (name->data == NULL)
{
return NGX_ERROR;
}
ngx_memcpy(name->data, &key->data[1], name->len);
}
//*.example.com这种前置通配符情况,或者.example.com情况
if (skip)
{
/*
* convert "*.example.com" to "com.example.\0"
* and ".example.com" to "com.example\0"
*/
p = ngx_pnalloc(ha->temp_pool, last);
if (p == NULL)
{
return NGX_ERROR;
}
len = 0;
n = 0;
//返回key中每个以.分割的内容。
//例如:*.example.com, 则执行这个循环后,将把com.example.\0保存到p中
for (i = last - 1; i; i--)
{
if (key->data[i] == '.')
{
//保存到p中
ngx_memcpy(&p[n], &key->data[i + 1], len);
n += len;
p[n++] = '.';
len = 0;
continue;
}
len++;
}
if (len)
{
ngx_memcpy(&p[n], &key->data[1], len);
n += len;
}
p[n] = '\0';
//表示这个key与value后面将要保存到前置哈希表中
hwc = &ha->dns_wc_head;
keys = &ha->dns_wc_head_hash[k];
}
else
{
/* convert "www.example.*" to "www.example\0" */
//www.example.*后置通配符情况
last++;//添加了一个\0,所有长度要加1
//开辟空间,存放key
p = ngx_pnalloc(ha->temp_pool, last);
if (p == NULL)
{
return NGX_ERROR;
}
ngx_cpystrn(p, key->data, last);
hwc = &ha->dns_wc_tail;
keys = &ha->dns_wc_tail_hash[k];
}
//保存key对应的值到哈希表中
hk = ngx_array_push(hwc);
if (hk == NULL)
{
return NGX_ERROR;
}
hk->key.len = last - 1;
hk->key.data = p;
hk->key_hash = 0;
hk->value = value;
/* check conflicts in wildcard hash */
name = keys->elts;
if (name)
{
len = last - skip;
//判断是否有相同的key,如果有,则直接退出。不应该有相同的key存在
for (i = 0; i < keys->nelts; i++)
{
//先比较长度
if (len != name[i].len)
{
continue;
}
if (ngx_strncmp(key->data + skip, name[i].data, len) == 0)
{
return NGX_BUSY;
}
}
}
else
{
//开辟数组元素空间
if (ngx_array_init(keys, ha->temp_pool, 4, sizeof(ngx_str_t)) != NGX_OK)
{
return NGX_ERROR;
}
}
name = ngx_array_push(keys);
if (name == NULL)
{
return NGX_ERROR;
}
name->len = last - skip;
name->data = ngx_pnalloc(ha->temp_pool, name->len);
if (name->data == NULL)
{
return NGX_ERROR;
}
ngx_memcpy(name->data, key->data + skip, name->len);
return NGX_OK;
}
例如: 存在普通关键字www.baidu.com, www.sign.com;
前置通配符: *.baidu.com, *.sina.com
后置通配符: www.baidu.*, www.sina.*
则哈希数组内存布局如下图:
图:哈希数组转换内存布局
有个疑问?不管是普通哈希表,还是前置通配符、后置通配符,都有两个数组。一个存储key-value的数组,另一个指针数组存放关键字。例如: 为什么有了dns_wc_tail这个数组保存着key-value值,还需要而外的dns_wc_tail_hash指针数组呢? 这是因为nginx为了提高查找性能,而外的构造了dns_wc_tail_hash这个由指针数组构成的哈希表。如果在这个哈希表中查找到相同元素,则说明存在两个一样的key值,就不需要再保存到dns_wc_tail数组中了,时间复杂度为O(1)。而如果只使用dns_wc_tail数组,则要查找一个元素是否存在,则需要遍历整个数组,时间复杂度为O(n);这是一种典型的空间换时间方法。