nginx哈希表数组的构造

        前面二章已经分析了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);这是一种典型的空间换时间方法。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值