nginx源码分析2———基础数据结构五(ngx_hash_wildcard_t)

相关介绍

所谓支持通配符的散列表,就是把基本的散列表(ngx_hash_t)中的元素的关键字,用去除通配符以后的字符作为关键字加入,原理其实没有那么神秘。当然得先熟悉ngx_hash_t,可以看上一节,对ngx_hash_t做深入的了解。
ngx_hash_wildcard_t类型的hash表包含通配符在前的key或者是通配符在后的key。例如,对于关键字为”www.test.*”这样带通配符的情况,直接建立一个专用的后置通配符散列表,存储元素的关键字为”www.test”

源码分析

通配符hash表的结构

这事通配符hash表的结构,可以看到里面包含一个基本hash表。

typedef struct {
    ngx_hash_t        hash;
    void             *value;
} ngx_hash_wildcard_t;

下面ngx_hash_key_t是我们上一节说过,存储key,value,hashcode的结构体,用于初始化hash表

typedef struct {
    ngx_str_t         key;
    ngx_uint_t        key_hash;
    void             *value;
} ngx_hash_key_t;
通配符hash表的初始化
ngx_int_t
ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,ngx_uint_t nelts)
{
    size_t                len, dot_len;
    ngx_uint_t            i, n, dot;
    ngx_array_t           curr_names, next_names;
    ngx_hash_key_t       *name, *next_name;
    ngx_hash_init_t       h;
    ngx_hash_wildcard_t  *wdc;

    /*在temp_pool上初始化数组curr_names,用于存放ngx_hash_key_t,容量为参数nelts */
    if (ngx_array_init(&curr_names, hinit->temp_pool, nelts, sizeof(ngx_hash_key_t)) != NGX_OK)
    {
        return NGX_ERROR;
    }
    //同上,初始化数组(如果不能理解,请看数组那一节)
    if (ngx_array_init(&next_names, hinit->temp_pool, nelts,sizeof(ngx_hash_key_t)) != NGX_OK)
    {
        return NGX_ERROR;
    }    
    //遍历注意最后不是n++
    for (n = 0; n < nelts; n = i) {
//====================push name'.' 前面进入cur===============
        dot = 0;
        //遍历每一个k的每一个字符
        for (len = 0; len < names[n].key.len; len++){
            //如果这个key包含一个dot就break该层for循环
            if (names[n].key.data[len] == '.') {
                dot = 1;
                break;
            }
        }
        //push进数组一个元素,返回值是该元素的首地址
        name = ngx_array_push(&curr_names);
        if (name == NULL) {
            return NGX_ERROR;
        }
        /*初始化该元素(注意该元素是ngx_hash_key_t),是用于初始化哈希表的结构*/
        //初始化key
        name->key.len = len;
        name->key.data = names[n].key.data;
        //初始化hashcode
        name->key_hash = hinit->key(name->key.data, name->key.len);
        //初始化value
        name->value = names[n].value;
        /*
        如果包含'.' 那么len为'.'所在的pos
        如果不包含'.' 那么len为字符的长度。
        例如test与test.t
        他们的dotlen是一样的   
        */
        dot_len = len + 1;

        if (dot) {
        /*
        如果包含'.' 那么len包含'.'及'.'以前的长度
        如果不包含'.' 那么len为字符的长度。

        例如test与test.t分别计算的是
        test与test.的长度
        分别是4和5  */
            len++;
        }
        //相当于将数组reset(给你一个眼神,自己体会)
        next_names.nelts = 0;
//====================push name'.' 后面进入next==============
        //理解后的意思是key中含有点,而且不是在最后
        if (names[n].key.len != len) {
            //在next_name压入元素
            next_name = ngx_array_push(&next_names);
            if (next_name == NULL) {
                return NGX_ERROR;
            }
            //对该元素进行赋值
            next_name->key.len = names[n].key.len - len;
            /*将该字符的起始地址移动'.'后
            例如test.com相当与取com放到该数组
            */
            next_name->key.data = names[n].key.data + len;
            //设定hashcode为0
            next_name->key_hash = 0;
            //value赋值
            next_name->value = names[n].value;
        }
//=================push name[n]'.' 后面进入next==============
        //遍历names[n]后的所有参数names"数组"的元素
        for (i = n + 1; i < nelts; i++) {
            /* e.g:  
              tetetet与tetetetksj将相等
              test.com与test.cn将相等
              test与test.将相等(len = 4)
              test.与test将不相等(len = 5)
            */
            if (ngx_strncmp(names[n].key.data, 
            names[i].key.data, len) != 0) {
                break;
            }

            /*如果names[n].key没有包含'.',而且后面的key长度比这个key大,而且后面的key的第len+1个字符不是'.'*/
            // e.g:  tetetet与tetetetksj将成立
            // e.g:  tetetet与tetetet.ksj将不成立
            //包含点一定不成立
            if (!dot && names[i].key.len > len &&                  
            names[i].key.data[len] != '.')
            {
                break;
            }

            next_name = ngx_array_push(&next_names);
            if (next_name == NULL) {
                return NGX_ERROR;
            }
            /*
                如果主key为test.com
                test.com --> com
                test.cn --> cn
            */
            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;
        }
//===================push end============================
        if (next_names.nelts) {
            h = *hinit;
            h.hash = NULL;
            //继续创建通配符hash表
            /*
                如果主key为test.com
                test.com --> com
                test.cn --> cn
                新的key将是com cn等等字符串。
            */
            if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts, next_names.nelts)
                != NGX_OK)
            {
                return NGX_ERROR;
            }
            //该hash表的首部是ngx_hash_wildcard_t
            wdc = (ngx_hash_wildcard_t *) h.hash;
            /*
                该字符串是刚刚好'.'前面的部分,如果是,就将该value存在通配符的value上
            */
            if (names[n].key.len == len) {
                wdc->value = names[n].value;
            }
            /*设置name后有没有点的标志(也就是name[n])最后一个是不是有点的,帮我们分清
                1,test
                  test.c
                2,test.
                  test.c
                的区别
            */
            name->value = (void *) ((uintptr_t) wdc | (dot ? 3 : 2));
        } else if (dot) {
            //同上(由于内存对齐了,低位都是0)
            name->value = (void *) ((uintptr_t) name->value | 1);
        }
    }
    //初始化最初的hash表 使用cur数组
    if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts, curr_names.nelts)
        != NGX_OK)
    {
        return NGX_ERROR;
    }
    return NGX_OK;
}

这个是很难理解的,有一点要注意的是所有的key都不包含字符’.’号,点好都是设置标志的。就是names数组中元素的value值低两位bit必须为0,当然也肯定为0,因为内存对齐后,低两位为0,作者为了节约内存,使用了这点内存。如果要完全理解这段代码,应该边画边想,那样会更清晰。
本人不会绘图,现在就传入下面数据绘出一个基本的草图,不喜勿喷

1.   key = test;value = v1
2.   key = test.;value = v2
3.   key = test.a;value = v3
4.   key = test.ab;value = v4
5.   key = test.abc;value = v5
6.   key = abc; value = v6
下面头部的hash可能还对应value结构(ngx_hash_wildcard_t)

数据结构草图

通配符hash表的查找

有了上面的数据结构,让我们从ngx_hash_find_wc_head入手去查看一下通配符hash表如何查找。我习惯用key代表里面的name,习惯性的说key-value结构。
对于name的初始化

*.test.com   ------->  com.test.
*.te.com     ------->  com.te.
*.st.com     ------->  com.st.
.smjd.dff    ------->  dff.smjd
.tst.dff     ------->  dff.tst
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;
    //字符串尾部查找含有'.'的字符
    while (n) {
        if (name[n - 1] == '.') {
            break;
        }
        n--;
    }
    //计算hash值
    key = 0;
    for (i = n; i < len; i++) {
        key = ngx_hash(key, name[i]);
    }

    //在当前的hash中寻找hash,寻找范围为'.'后的
    /*
        如果name为test.com,那么这一层hash将寻找com
    */
    value = ngx_hash_find(&hwc->hash, key, &name[n], len - n);

    //如果寻找存在
    if (value) {
        //如果value里面存放的是ngx_hash_wildcard_t
        if ((uintptr_t) value & 2) {
            //如果这个name是在前面的,相当于test.com的test
            if (n == 0) {
                //如果查找到的这个key是包含'.'的
                if ((uintptr_t) value & 1) {
                //返回NULL,交个上一层,让上一层返回自己的value
                    return NULL;
                }
                //如果这个key不包含'.',那么这就是我们要的key
                //对内存标志进行恢复,并返回
                hwc = (ngx_hash_wildcard_t *)((uintptr_t) value & (uintptr_t) ~3);
                return hwc->value;
            }
            //如果该key还有前面的部分比如test.gen的test
            //获取下一层通配符结构
            hwc = (ngx_hash_wildcard_t *)((uintptr_t) value & (uintptr_t) ~3);
            //继续在下一层的结构查找test。
            value = ngx_hash_find_wc_head(hwc, name, n - 1);
            //如果下一层返回不为NULL,就当成该key的value
            if (value) {
                return value;
            }
            /*否侧取这一层的value(如果这一层也为NULL,显而会继续往上层朔)*/
            return hwc->value;
        }

        /*如果该key对应的value是最下层,也就是实际存放的就是value,且key后面一定是有'.'字符*/
        if ((uintptr_t) value & 1) {
            //如果这个name(key)是在前面的,相当于test.com的test
            if (n == 0) {
               //返回并上朔
                return NULL;
            }
            //否则返回该value,已经到最下层,查找完毕
            return (void *) ((uintptr_t) value & (uintptr_t) ~3);
        }
        /*如果该key对应的value是最下层,也就是实际存放的就是value,且key后面一定是没有有'.'字符,直接return*/        
        return value;
    }
    //如果寻找不存在,返回这一层key对应的value
    return hwc->value;
}

这个逻辑比较绕,要多加思考,就能融会贯通。当然查找还有ngx_hash_find_wc_tail与其对应。实现大致一样。该函数查询包含通配符在末尾的key的hash表的,我们mail.xxx.*为例,请特别注意通配符在末尾的不像位于开始的通配符可以被省略掉。这样的通配符,可以匹配mail.xxx.com、mail.xxx.com.cn、mail.xxx.net之类的域名。
对于name中的key的初始化

test.com.*   ------->  test.com
te.com.*     ------->  te.com
st.com.*     ------->  st.com
smjd.dff.*   ------->  smjd.dff
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;
    //从前面开始查找test.*取test
    for (i = 0; i < len; i++) {
        if (name[i] == '.') {
            break;
        }
        key = ngx_hash(key, name[i]);
    }
    //如果没有字符'.',也就是只剩最后的'*'字符,直接返回给上一层
    if (i == len) {
        return NULL;
    }
    //查找test
    value = ngx_hash_find(&hwc->hash, key, name, i);

    //如果test存在
    if (value) {
          /*
         * the 2 low bits of value have the special meaning:
         *     00 - value is data pointer;
         *     11 - value is pointer to wildcard hash allowing "example.*".
         */ 
        //如果对应的value是ngx_hash_wildcard_t
        if ((uintptr_t) value & 2) {
            i++;
            //由于最后一个'*'没有省略,所以key后都是有'.'的
            hwc = (ngx_hash_wildcard_t *) ((uintptr_t) value & (uintptr_t) ~3);
            //递归查找下一层
            value = ngx_hash_find_wc_tail(hwc, &name[i], len - i);
            //如果下一层查找成功
            if (value) {
                return value;
            }
            //如果下一层查找没有
            return hwc->value;
        }
        /*如果是具体的value,已经没有下一层hash表了,存储的具体字符串,最后一个字符不是'.',所以后两位一定是00*/
        return value;
    }
    //如果test不存在,则返回该层的value
    return hwc->value;
}

最后我们来看一下ngx_hash_combined_t 的查找,结构如下

typedef struct {
    ngx_hash_t            hash;
    ngx_hash_wildcard_t  *wc_head;
    ngx_hash_wildcard_t  *wc_tail;
} ngx_hash_combined_t;
void *
ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name,
    size_t len)
{
    void  *value;
    //在hash里面查找,如果有,就立即返回value
    if (hash->hash.buckets) {
        value = ngx_hash_find(&hash->hash, key, name, len);
        if (value) {
            return value;
        }
    }

    if (len == 0) {
        return NULL;
    }

    //head通配符查找。
    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;
}

总结

ngx_hash_wildcard_t可以说是对hash表的灵活应用,值得我们多看几遍。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值