精华内容
下载资源
问答
  • 哈希表是一种存储键值对(key-value)的数据结构,根据查找的时间复杂度为O(1)。我们经常见到的哈希表都是采用数组+链表实现(即所谓的拉链)。 与普通哈希表不同,nginx哈希表以下特点: 创建nginx哈希表...

    1.基本概念

    哈希表是一种存储键值对(key-value)的数据结构,根据键查找值的时间复杂度为O(1)。我们经常见到的哈希表都是采用数组+链表实现(即所谓的拉链法)。

    与普通哈希表不同,nginx哈希表有以下特点:

    • 创建nginx哈希表时,所有的key-value键值对已经确定,哈希表创建完成后不允许再添加或者删除键值对
    • nginx哈希表并没有采用拉链法,而是开放地址法;
    • nginx哈希表支持前缀与后缀通配符,比如说“.example.com”、“www.example.”,其实现了类似于最大前缀匹配与最大后缀匹配的功能。

    第一点简化了nginx哈希表的实现;第二点开放地址法可能很多人都不太熟悉;而最大前缀匹配和最大后缀匹配功能难度较大。

    最大前缀匹配与最大后缀匹配最容易实现的方式就是遍历了,此时时间复杂度为O(N)。

    而nginx在location匹配时,同样存在最大前缀匹配功能,比如“location /uri {}”,想想这是怎么实现的?location配置被组织成为了一棵三叉树,如图1所示。节点node可能会有三个子节点,left、right与tree;left节点的uri小于node节点的uri,且left节点的uri不是node节点uri的子字符串;right节点的uri大于node节点的uri,且node节点的uri不是right节点uri的子字符串;node节点的uri是tree节点的子字符串。此时location最大前缀匹配时间复杂度为O(logN)。

    image

    图1:location树示意图

    为什么location最大前缀匹配不能使用哈希表实现呢?复杂度不是可以达到O(1)吗?

    需要注意的是nginx哈希表支持前缀与后缀通配符并不是无任何限制的,只支持这三种格式:"*.example.com"、".example.com"和"www.example.*"。想想这种格式与location配置有什么不同呢?location只是普通的字符串匹配,而这里匹配的通常是类似于host这种使用点号分割的,点号将匹配字符串分割成为了很有限的子字符串,可以为这些子字符串建立哈希表,这时候的哈希表也成为了多级哈希表。示意图如图2所示:

    image

    图2:多级通配符哈希表示意图

    这里再思考一个问题:"*.example.com"和 “.example.com"都用于前缀匹配,有什么区别吗?这里的*其实表示的是至少一个字符,即输入字符串"example.com"时,只能匹配到”.example.com";而输入字符串"www.example.com"时,是能同时匹配到"*.example.com"和 ".example.com"的。读者可以做以下实验,配置多个server,验证host最终会匹配到哪个server_name。

    2. nginx哈希表基本数据结构

    1)结构体ngx_hash_elt_t表示哈希表的元素,即key-value键值对:

    typedef struct {
        void             *value;
        u_short           len;
        u_char            name[1];
    } ngx_hash_elt_t;
    

    value指向值对象;key通常为字符串,len为key长度,name柔性数组存储key内容。

    2)普通哈希表(没有前缀匹配符与后缀匹配功能)使用结构体ngx_hash_t表示:

    typedef struct {
        ngx_hash_elt_t  **buckets;
        ngx_uint_t        size;
    } ngx_hash_t;
    

    buckets桶数组,数组中每个元素的类型为ngx_hash_elt_t *,指向当前桶的第一个哈希元素;size为桶数目,hash值对size取模计算桶索引。

    所有的哈希元素连续存储在一个字节数组中,当hash冲突时桶,bucket可能需要存储多个哈希元素,这时候如何处理?观察ngx_hash_elt_t结构,元素长度是很容易计算的,所以多个哈希元素连续存储即可,且每个桶的最后都有一个8字节的NULL。

    普通哈希表结构如图3所示(元素类型即为ngx_hash_elt_t):

    image

    图3:普通哈希表结构

    3)nginx多级通配符哈希表与图2非常类似(包含一个头部value与普通哈希表),使用结构ngx_hash_wildcard_t表示,其定义如下:

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

    多级通配符哈希表通常使用下面代码分配内存空间:

    hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t) + size * sizeof(ngx_hash_elt_t *));
    
    buckets = (ngx_hash_elt_t **)((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
    
    hinit->hash->buckets = buckets;
    

    其中size为桶bucket数目;上面代码创建的通配符哈希表结构如图4所示:

    image

    图4 通配符哈希表示意图

    假设需要创建一个后缀通配符哈希表,包含key-value为:

    apache.* => value1
    nginx.* => value2
    nginx.doc.* => value3
    nginx.api.* => value4
    nginx.api.test.* =>value5
    

    此时最终创建的多级通配符哈希表如图5所示,后面会详细介绍创建过程。

    image
    图5 多级通配符哈希表

    4)第一节提到,创建nginx哈希表时,所有的key-value键值对已经确定;因此可以预先计算合适的桶bucket数目,以及每个bucket负责存储哪些key-value等,从而为每个bucket分配足够的内存空间。

    但是,如何确保这些key-value键值对的key没有重复呢?针对这些已有的key-value需要有一个去重过程,去重后的元素即为最终哈希表的所有元素;去重过程会创建一个临时的数据结构ngx_hash_keys_arrays_t,该结构用于保存去重后的所有key-value,待创建完成哈希表后这个结构会被释放。

    typedef struct {
        ngx_uint_t        hsize;
    
        ngx_pool_t       *pool;
        ngx_pool_t       *temp_pool;
    
        ngx_array_t       keys;
        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;
    

    pool与temp_pool表示内存池,去重过程需要一些临时变量会在temp_pool分配空间,去重过程结束后会释放temp_pool内存池的所有空间;keys、dns_wc_head和dns_wc_tail是三个数组,分别存储普通字符串key、前缀通配符key和后缀通配符key(这些key都是去重后的);注意这三个字段keys_hash、dns_wc_head_hash、dns_wc_tail_hash,这是三个哈希表,采用的是拉链法,毕竟采用哈希表实现去重的复杂度可以达到O(1)。

    ngx_hash_keys_arrays_t结构如图4所示:

    image

    图6:ngx_hash_keys_arrays_t结构示意图

    3.nginx哈希表源码分析

    nginx哈希表相关API可以分为3类:

    1)哈希键数组相关

    ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type);
    ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key, void *value, ngx_uint_t flags);
    

    函数ngx_hash_keys_array_init初始化哈希键数组;函数ngx_hash_add_key向哈希键数组中添加键并实现去重功能(包括普通键,前缀与后缀键),其输入第一个参数为哈希键数组。

    2)哈希表创建

    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);
    

    函数ngx_hash_init创建一个普通哈希表;函数ngx_hash_wildcard_init创建的是多级通配符哈希表。两个函数的输入参数names为哈希键ngx_hash_key_t数组,nelts为哈希键数目。第一个输入参数hinit的类型为ngx_hash_init_t,其存储创建哈希表所需的一些变量,定义如下:

    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;
    

    其中hash指向最终创建的哈希表;key是一个函数指针,用于计算键哈希值;max_sizewei最大桶数目;bucket_size为每个桶最大字节大小;name为哈希表的名称;pool和temp_pool为内存池,只是temp_pool表示的是临时内存池,创建完哈希表会被回收。

    3)查找哈希表

    void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);
    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);
    

    这三个函数分别实现了普通哈希表查找、前缀通配符哈希表查找和后缀通配符哈希表查找功能。

    3.1 哈希键数组初始化

    函数ngx_hash_keys_array_init只是初始化结构体ngx_hash_keys_arrays_t,比较简单,这里不做详述。

    函数ngx_hash_add_key向哈希键数组中添加键并去重,其主要包括以下6个过程:

    1)判断键类型(普通键/前缀通配符/后缀通配符)

    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;
        }
    }
    

    变量skip表示跳过的字符串数目;变量last表示字符串结束索引;通过skip可以区分出键字符串的类型。

    键字符串 分类类型
    www.example.com
    *.example.com skip=2
    .example.com skip=1
    www.example.* skip=0

    2)普通键去重

    判断该键是否已经存在keys_hash哈希表中,如果不存在则添加该键到keys_hash哈希表与keys键数组中;如果存在则返回NGX_BUSY;

    3)skip=1第一次去重

    当键字符串格式为".example.com"时,skip=1,第一节就提到其可以匹配输入字符串"example.com"。那假如普通键中存在着"example.com"呢?岂不是冲突了?所以对于skip=1这种类型,同样需要校验是否已经存在keys_hash哈希表中,如果存在则返回NGX_BUSY;

    4)前缀通配符转换

    前缀通配符格式"*.example.com"和 “.example.com”,无法直接通过哈希表实现前缀匹配功能,键格式需要做一步转换过程,如下表:

    原始字符串 转换结果
    *.example.com com.example.\0
    .example.com com.example\0

    转换后的字符串通过最后一个字符是否为点号可以区分两种前缀通配符格式。转换代码如下:

    for (i = last - 1; i; i--) {
        if (key->data[i] == '.') {
            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;
    }
    

    5)后缀通配符转换

    后缀通配符"www.example.*“会转换为"www.example\0”;(后缀通配符只有一种格式,因此点号不需要保留)

    6)通配符键去重

    校验前缀通配符键是否已经存在dns_wc_head_hash哈希表,如果存在则返回NGX_BUSY;如果不存在则将转换后的键字符串添加到dns_wc_head_hash哈希表和dns_wc_head键数组;

    校验后缀通配符键是否已经存在dns_wc_tail_hash哈希表,如果存在则返回NGX_BUSY;如果不存在则将转换后的键字符串添加到dns_wc_tail_hash哈希表和dns_wc_tail键数组;

    3.2 创建普通哈希表

    nginx哈希表一大特点就是在创建哈希表时所有的key-value都已确定,且创建后不会再添加或者删除哈希元素。因此创建哈希表时可以预先计算好合适的桶桶数目,以及每个桶存储哪些元素,以及每个桶的字节大小等。因此创建普通哈希表可以分为以下7个过程:

    1)首先需要校验桶最大字节大小是否可以至少存储一个哈希元素

    for (n = 0; n < nelts; n++) {
        if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *)){
         
        }
    }
    

    哈希元素字节长度比较容易计算,需要按照8字节对齐,在GDB打印桶中元素时需要也别注意。

    #define NGX_HASH_ELT_SIZE(name)                                               \
        (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
    

    2)选取合适的桶数目。什么样的桶数目才算是满足条件的呢?通数目已知,可以计算出每个哈希元素的桶索引,统计每个桶存储的哈希元素长度之和,只要没有超过桶最大字节大即可。

    计算合适的桶数目需要遍历,首先会粗略计算出一个最小桶数目start,最大桶数目max_size,在此范围内遍历即可。

    每个哈希元素最小为16字节,因此计算最小桶数目为(每个桶最后都有8字节的NULL表示桶结束):

    bucket_size = hinit->bucket_size - sizeof(void *);
    
    start = nelts / (bucket_size / (2 * sizeof(void *)));
    start = start ? start : 1;
    

    其中nelts为哈希元素数目。

    这里还需要统计每个桶存储的哈希元素长度之和,因此分配test数组(用于计算桶元素总长度):

    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
    

    遍历过程如下:

    
    for (size = start; size <= hinit->max_size; size++) {
    
        ngx_memzero(test, size * sizeof(u_short));
    
        for (n = 0; n < nelts; n++) {
            if (names[n].key.data == NULL) {
                continue;
            }
    
            key = names[n].key_hash % size;
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    
            if (test[key] > (u_short) bucket_size) {
                goto next;
            }
        }
    
        goto found;
    
        next:
            continue;
    }
    
    

    这块代码还是比较简单的。需要注意的是这里for循环使用的是goto next,不能直接continue吗?当然肯定是不行的。

    3)计算每个桶存储的所有哈希元素总长度,并分配空间

    for (i = 0; i < size; i++) {
            test[i] = sizeof(void *);
        }
    
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }
    
        key = names[n].key_hash % size;
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }
    

    size即为上面计算的桶数目。这里需要注意的是每个哈希桶最后肯定都有一个8字节的NULL表示桶结束。

    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }
    
        test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
    
        len += test[i];
    }
    
    elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
    

    遍历test数组,计算所有桶总长度,并分配内存空间。注意这里在分配空间时,按照ngx_cacheline_size大小字节对齐了;CPU在加载内存中数据到高速缓存时,是一个加载一个数据块,数据块大小称之为cacheline_size,通常为64字节;分配内存时按照cacheline_size字节对齐加载到高速缓存效率较高。

    4)前面已经计算出每个桶所有元素总长度,因此这里可以将桶指针指向每个桶第一个元素首地址;

    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }
    
        buckets[i] = (ngx_hash_elt_t *) elts;
        elts += test[i];
    
    }
    

    5)存储每个哈希元素到响应桶位置;

    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }
    
        key = names[n].key_hash % size;
        elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);
    
        elt->value = names[n].value;
        elt->len = (u_short) names[n].key.len;
    
        ngx_strlow(elt->name, names[n].key.data, names[n].key.len);
    
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }
    

    6)每个桶最后添加8字节NULL结束标志;

    for (i = 0; i < size; i++) {
            if (buckets[i] == NULL) {
                continue;
            }
    
            elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);
    
            elt->value = NULL;
        }
    

    7)一些收尾工作

    ngx_free(test);
    
        hinit->hash->buckets = buckets;
        hinit->hash->size = size;
    

    3.3 创建前缀通配符哈希表

    前缀通配符哈希表重在分级,即按照点号将键字符串切割为多个子字符串,为第一个子字符串创建第一级哈希表,第二个子字符串创建第二级哈希表……;多级哈希表之间还之前了类似于链式引用。

    前缀通配符哈希表的创建函数为ngx_hash_wildcard_init,遍历所有哈希键,将其按照点号分割,并创建多级哈希表。函数通过递归调用实现多级哈希表的创建以及引用功能。

    将每个哈希键按照点号分割后,每一级的子字符串可能还需要做聚合操作。比如"com.api."、“com.doc.”、“com.doc.nginx.”、“com.aoc.apache.”,分割后第一级子字符串4个都是com,第二级子字符串1个为api,三个为doc,第三级子字符串分别为nginx何apache;每一级子字符串相等时需要做聚合操作。

    另外需要注意一点的是,在调用函数ngx_hash_wildcard_init创建通配符哈希表之前,所有的哈希键都会进行排序,比如:

    ngx_qsort(ha.dns_wc_head.elts, (size_t) ha.dns_wc_head.nelts,
                      sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);
    

    函数ngx_http_cmp_dns_wildcards为键比较函数,键按照字母序比较。

    为了更好的理解ngx_hash_wildcard_init函数实现,请参照下图7多级通配符哈希结构图表。

    字符串分割,聚合功能代码实现如下:

    
    for (n = 0; n < nelts; n = i) {
        for (len = 0; len < names[n].key.len; len++) {
        	if (names[n].key.data[len] == '.') {
            	dot = 1;
            	break;
        	}
    	}
    	
    	//当前级子字符串存储在变量curr_names;
    	//用于创建当前级哈希表
    	name = ngx_array_push(&curr_names);
    	name->key.len = len;
    	name->key.data = names[n].key.data;
    	name->value = names[n].value;
    
        //后续子字符串存储在变量next_names;
        //用于递归调用函数ngx_hash_wildcard_init创建多级哈希表
    	if (names[n].key.len != len) {
            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->value = names[n].value;
    	}
    
        //注意在查找后续键字符串中当前级子字符串与当前键当前级子字符串相等的键
    	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;
            }
    
            next_name = ngx_array_push(&next_names);
            
    
            next_name->key.len = names[i].key.len - dot_len;
            next_name->key.data = names[i].key.data + dot_len;
            next_name->value = names[i].value;
        }
    }
    
    

    变量curr_names存储的是当前级的子字符串,用于创建当前级哈希表;next_names存储的是下一级子字符串,用于递归调用函数ngx_hash_wildcard_init创建下一级哈希表。

    注意在查找后续键字符串中当前级子字符串与当前键当前级子字符串相等的键时,一旦发现子字符串不相等直接break结束循环,不需要遍历所有键字符串吗?肯定是不需要的,因为在调用函数ngx_hash_wildcard_init创建通配符哈希表之前,所有的哈希键都进行了排序。

    上面代码进行了键字符串的分割,子字符串的聚合,接下来会递归调用函数ngx_hash_wildcard_init创建下一级哈希表,并实现哈希表链级引用

    
    for (n = 0; n < nelts; n = i) {
    	if (next_names.nelts) {
    
        h = *hinit;
        h.hash = NULL;
        //注意调用ngx_hash_wildcard_init函数时,都会初始化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;
    
        //当前键已经处理完毕时,下一级哈希表头部value字段需要赋值
        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);
    	}
    }
    
    

    这里需要重点分析两点:

    1)调用ngx_hash_wildcard_init函数创建通配符哈希表时,为什么h.hash一定是NULL呢?原因在于函数ngx_hash_init创建哈希表时,会有这么一步操作:

    if (hinit->hash == NULL) {
        hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)+ size * sizeof(ngx_hash_elt_t *));
            
        buckets = (ngx_hash_elt_t **) ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
    
        
    }else {
            buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
    }
    

    hinit->hash等于NULL时创建的是通配符哈希表结构;否则创建的是通用哈希表结构。

    2)创建完成下一级哈希表时,多级哈希表需要形成链式结构串起来。因此name->value字段存储的要么是下一级哈希表的首地址,要么直接是哈希元素值对象。

    注意这里判断变量dot是否为true,以及是否有下一级哈希表,会对value按位或上0、1、2或者3。令flag为value低位2比特,其标记value字段存储的是什么数据。

    当flag为0或者1时,表示value指向的是哈希元素值对象;当flag等于2或者3时,表示value指向的是下一级哈希表。

    那为什么需要判断遍历dot呢?还记得之前讲述的"*.example.com"和 ".example.com"前缀匹配的区别吗?这两者键字符串分别会被转换为"com.example.“和"com.example”,最后一个字符点号用于区分这两种类型,这里需要通过flag标记,否则查找时将无法区分了。

    想想为什么可以value按位或上0、1、2和3呢?不会修改value的值吗?因为value存储的是地址,且这块内存通常都会按照8字节或者其他字节对齐;即value的低2比特恒为0。

    下一级哈希表创建完毕后,会调用函数ngx_hash_init当前级哈希表,该函数已经讲过,这里不再赘述。

    假设需要创建一个前缀通配符哈希表,包含key-value为:

    *.apache => value1
    *.nginx => value2
    *.doc.nginx => value3
    *.api.nginx => value4
    *.test.api.nginx =>value5
    

    最终创建的多级通配符哈希表如图7所示:
    image
    图7 多级通配符哈希表结构

    3.4 查找前缀通配符哈希表

    前缀通配符查找时,同样需要将输入字符串按照点号倒序分割,如example.com会分割为com和example两个子字符串;先使用com子字符串在第一级哈希表中查找,判断查找到的value类型是哈希元素值对象还是下一级哈希表地址,以及是"*.example.com"还是 ".example.com"前缀匹配,以此决定继续查找下一级哈希表,还是返回NUll,还是返回value。

    下面代码实现了输入字符串倒序分割的功能

    while (n) {
            if (name[n - 1] == '.') {
                break;
            }
    
            n--;
        }
    
        key = 0;
    
        for (i = n; i < len; i++) {
            key = ngx_hash(key, name[i]);
        }
    

    分割并计算该子字符串hash值后,会按照普通哈希表查找方式查找:

    value = ngx_hash_find(&hwc->hash, key, &name[n], len - n);
    

    函数ngx_hash_find实现比较简单,这里不做详述;难点应该在于value标志位的判定。

    //value标志位为2或者3,说明指向的是下一级哈希表
    if ((uintptr_t) value & 2) {
    
        //当前输入字符串已经遍历结束
        if (n == 0) {
    
            //value标志位为3;说明前缀匹配格式应该为"*.example.com";
            //此时不满足条件,返回NULL
            if ((uintptr_t) value & 1) {
                return NULL;
            }
    
            //value标志位为2;说明前缀匹配格式应该为".example.com";
            //返回头部value
            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;
    }
    
    //value标志位为1,指向的是哈希元素值对象;
    //且前缀匹配格式应该为"*.example.com"
    if ((uintptr_t) value & 1) {
    
        if (n == 0) {
    
            /* "example.com" */
    
            return NULL;
        }
    
        return (void *) ((uintptr_t) value & (uintptr_t) ~3);
    }
    
    //value标志位为0,指向的是哈希元素值对象;
    //且前缀匹配格式应该为".example.com";
    return value;
    

    参照图7多级通配符哈希表结构图,标志位的判断以及value的取值应该比较容易理解了。

    总结

    nginx哈希表基于开放地址法,且支持前缀后缀通配符哈希表,本文主要对普通哈希表与前缀通配符符哈希表做了详细介绍,后缀通配符哈希表同理可得。

    展开全文
  • CURRENT_USER\Control Panel\desktop,看看右边有没有 “CursorBlinkRate”,如果没有请在右边空白处单击鼠标右键,选择“新建”的“字符串”,输入名字为“CursorBlinkRate”,再双击它,修改为-1,就OK了。...
  • 编写程序构造一个有序表La,从键盘接收一个关键字key,用二分查找在La 中查找key,若找到则提示查找成功并输出key所在的位置,否则提示没有找到信息。 2.编写程序实现Hash表的建立、删除、插入以及查找操作。 ...
  • 选中要修改的分区,按F11进入修改状态(如图2),将光标移动到要修改的参数,键入你要设定的。修改完毕后选“确定”退出。 提示:用此可调整分区的起始和终止柱面号、磁头号、扇区号,从而调整分区大小,...
  • 2. 10个数存放在一个数组中,输入一个数,要求用折半查找找出该数是数组中第几个元素的。如果该数不在数组中,则输出“无此数”。以10个数用赋初值的方法在程序中给出。要找的数用scanf函数输入。 3. 找出一个...
  • 你必须知道的495个C语言问题

    千次下载 热门讨论 2015-05-08 11:09:25
    5.15 有没有什么简单点儿的办法理解所有这些与空指针有关的东西呢? 5.16 考虑到有关空指针的所有这些困惑,要求它们的内部表示都必须为0不是更简单吗? 5.17 说真的,真有机器用非零空指针吗,或者不同类型用...
  • 手机 pdf 阅读器

    2009-02-12 23:00:29
    暂时删除了播放功能及网络相关的功能,由于以上两项功能一直没有能稳定下来,故暂时删除 增强了ZIP功能,支持带文件夹结构的ZIP/JAR文件 (对于大部分JAR电子书都,可以从文件管理器中找到非.class结尾的文件,并且...
  • 5.15 有没有什么简单点儿的办法理解所有这些与空指针有关的东西呢? 60 5.16 考虑到有关空指针的所有这些困惑,要求它们的内部表示都必须为0不是更简单吗? 60 5.17 说真的,真有机器用非零空指针吗,或者不同...
  • 《你必须知道的495个C语言问题》

    热门讨论 2010-03-20 16:41:18
    5.15 有没有什么简单点儿的办法理解所有这些与空指针有关的东西呢? 60 5.16 考虑到有关空指针的所有这些困惑,要求它们的内部表示都必须为0不是更简单吗? 60 5.17 说真的,真有机器用非零空指针吗,或者不同...
  • (1)编写一个程序实现如下功能:一个整型数组10个元素,删除所有为n的元素。要求: ① 主函数完成n的输入,数组元素输入以及删除后数组元素的输出。 ② 删除功能用子函数完成。 (2)编写一个程序实现如下功能...
  • 在满二叉树中,每一层上的结点数都达到最大,即在满二叉树的第k层上2k-1个结点,且深度为m的满二叉树2m-1个结点。 完全二叉树是指这样的二叉树:除最后一层外,每一层上的结点数均达到最大;在最后一层上只...
  • MySQL命令大全

    2018-01-15 11:19:17
    (按回车出现Database changed 时说明操作成功!) 4:查看现在的数据库中存在什么表 mysql> SHOW TABLES; 5:创建一个数据库表 mysql> Create TABLE MYTABLE (name VARCHAR(20), sex CHAR(1)); 6:显示表的结构...
  • 糖果的软件

    2014-08-03 20:08:35
    ),而操作者又没有相应的权限,那么删除文件时就可能 出现“访问被拒绝”的提示。 通常情况下, 管理员具有取得任何文件所有权的隐含能力, 文件所有者也 具有修改文件权限的隐含能力。不过,这些默认的权限是...
  • 实例193 做成一个时间限制的测试版 实例194 判断经历多少个工作日 实例195 实现系统分段报时 实例196 利用timeGetTime函数更精准地计算时间差 实例197 使用DateAdd函数向日期型数据加 第7章 数据处理技术 ...
  • 实例193 做成一个时间限制的测试版 实例194 判断经历多少个工作日 实例195 实现系统分段报时 实例196 利用timeGetTime函数更精准地计算时间差 实例197 使用DateAdd函数向日期型数据加 第7章 数据处理技术 ...
  • 4.1.7 有没有有顺序的Map实现类,如果有,他们是怎么保证有序的。 4.1.8 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。 4.1.9 继承和聚合的区别在哪。 4.2.0 IO模型有...
  • 5-8-9 测试键盘缓冲区是否字符 5-8-10 传回控制状态 5-9 屏幕输出控制 5-9-1 显示字符 5-9-2 显示字符串 5-9-3 设定光标位置 5-9-4 向上滚动屏幕 5-10 打印机输出控制 5-10-1 输出字符至打印机 5-10-2 打印一个...
  • o 3.11 为什么 sizeof 返回的大于结构的期望, 是不是尾部填充? o 3.12 如何确定域在结构中的字节偏移? o 3.13 怎样在运行时用名字访问结构中的域? o 3.14 程序运行正确, 但退出时却 ``core dump''了,...
  • DISKGEN命令详解

    2008-12-19 16:23:26
    笔者曾在某网站论坛上发现一张有关DISKMAN疑问的帖子,询问有没有办法将分区参数表格中全部为零的“第2项、第3项”删除掉, 这当然是不可能的,发帖者显然对硬盘分区知识缺乏了解。想真正弄懂分区参数表格中各项的...
  • //在单链表中删除第 i 个元素,成功删除返回 True,并用 e 返回该元素,失败返回 False BOOL ListDelete(LinkList &L, int i, ElemType &e); //在单链表中查找关键字为 e 的元素,成功返回 True,并用 i 返回该元素...
  • MYSQL常用命令大全

    2011-05-30 13:31:24
    命令:insert into <表名> [( <字段名1>[,..<字段名n > ])] values ( 1 )[, ( n )] 例如,往表 MyClass中插入二条记录, 这二条记录表示:编号为1的名为Tom的成绩为96.45, 编号为2 的名为Joan 的成绩为82.99,...
  • 12、 已知一个顺序表中的元素按元素非递减有序排列,编写一个函数删除表中多余的相同的元素。 13、 分别写出求二叉树结点总数及叶子总数的算法。 分治术 14、 金币15枚,已知其中一枚是假的,而且它的重量...
  • 如果你使用“解压到”关联菜单命令解压一个压缩文件、根文件夹仅包含一个文件夹并且没有文件, 则该选项将从解压路径中删除基于文件夹的多余压缩文件名称. 4. 在“解压路径和选项”对话框中的修改: a) “新建...
  • • sample06.htm 使用缺省的构造函数创建对象 • sample07.htm 使用带方法的构造函数创建对象 • sample08.htm 使用带方法的构造函数创建对象 • sample09.htm 遍历对象属性 •...
  • 后面是IDE设备的类型和硬件参数,TYPE用来说明硬盘设备的类型,我们可以选择AUTO、USER、NONE的工作模式,AUTO是由系统自己检测硬盘类型,在系统中存储了1-45类硬盘参数,在使用该设置时不必再设置其它参数;...

空空如也

空空如也

1 2 3 4 5 6
收藏数 120
精华内容 48
关键字:

值法有没有删除键