zoukankan      html  css  js  c++  java
  • Nginx数据结构之散列表

    1. 散列表(即哈希表概念)

    散列表是根据元素的关键码值而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,
    以加快查找速度。这个映射函数 f 叫做散列方法,存放记录的数组叫做散列表。

    若结构中存在关键字和 K 相等的记录,则必定在 f(K) 的存储位置上。由此,不需要比较便可直接取得所查记录。我们称这
    个对应关系 f 为散列方法,按这个思想建立的表则为散列表。

    对于不同的关键字,可能得到同一散列地址,即关键码 key1 ≠ key2,而 f(key1) = f(key2),这种现象称为碰撞。对该散列
    方法来说,具有相同函数值的关键字称作同义词。综上所述,根据散列方法 H(key) 和处理碰撞的方法将一组关键字映像到一
    个有限的连续的地址集(区间)上,并以关键字在地址集中的 "象" 作为记录在表中的存储位置,这种表便称为散列表,这一
    映像过程称为散列造表或散列,所得的存储位置称为散列地址。

    1.1 如何解决碰撞问题

    通常有两个简单的解决方法:分离链接法和开放寻址法。

    分离链接法,就是把散列到同一个槽中的所有元素都放在散列表外的一个链表中,这样查询元素时,在找到这个槽后,还得遍
    历链表才能找到正确的元素,以此来解决碰撞问题。

    开放寻址法,即所有元素都存放在散列表中,当查找一个元素时,要检查规则内的所有表项(例如,连续的非空槽或者整个空
    间内符合散列方法的所有槽),直到找到所需的元素,或者最终发现元素不在表中。开放寻址法中没有链表,也没有元素存放
    在散列表外。

    Nginx 的散列表使用的是开放寻址法。
    开放寻址法有许多种实现方法,Nginx 使用的是连续非空槽存储碰撞元素的方法。例如,当插入一个元素时,可以按照散列方
    法找到指定槽,如果该槽非空且其存储的元素与待插入元素并非同一个元素,则依次检查其后连续的槽,直到找到一个空槽来
    放置这个元素为止。查询元素时也是使用类似的方法,即从散列方法指定的位置起检查连续的非空槽中的元素。

    2. Nginx 散列表的实现

    2.1 ngx_hash_elt_t 结构体

    对于散列表中的元素,Nginx 使用 ngx_hash_elt_t 结构体来存储。

    typedef struct {
        /* 指向用户自定义元素数据的指针,如果当前 ngx_hash_elt_t 槽为空,则 value 的值为 0 */
        void             *value;
        /* 元素关键字的长度 */
        u_short           len;
        /* 元素关键字的首地址 */
        u_char            name[1];
    } ngx_hash_elt_t;
    

    每一个散列表槽都由 1 个 ngx_hash_elt_t 结构体表示,当然,这个槽的大小与 ngx_hash_elt_t 结构体的大小(即
    sizeof(ngx_hash_elt_t))是不相等的,这是因为 name 成员只用于指出关键字的首地址,而关键字的长度是可变的。一个槽
    占用多大的空间是在初始化散列表时决定的。

    2.2 ngx_hash_t 结构体

    基本的散列表由 ngx_hash_t 结构体表示。

    typedef struct {
        /* 指向散列表的首地址,也是第 1 个槽的地址 */
        ngx_hash_elt_t  **buckets;
        /* 散列表中槽的总数 */
        ngx_uint_t        size;
    } ngx_hash_t;
    

    因此,在分配 buckets 成员时就决定了每个槽的长度(限制了每个元素关键字的最大长度),以及整个散列表所占用的空
    间。

    基本散列表的结构示意图


    如上图,散列表的每个槽的首地址都是 ngx_hash_elt_t 结构体,value 成员指向用户有意义的结构体,而 len 是当前这
    个槽中 name(也就是元素的关键字)的有效长度。ngx_hash_t 散列表的 buckets 指向了散列表的起始地址,而 size 指出
    散列表中槽的总数。

    2.3 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;
        /* 内存池,用于分配散列表(最多3个,包括1个普通散列表、1个前置通配符散列表、1个后置通配符散列表)
         * 中的所有槽 */
        ngx_pool_t       *pool;
        /* 临时内存池,仅存在于初始化散列表之前。它主要用于分配一些临时的动态数组,
         * 带通配符的元素在初始化时需要用到这些数组 */
        ngx_pool_t       *temp_pool;
    } ngx_hash_init_t;
    

    该结构体用于初始化一个散列表。

    2.4 ngx_hash_key_t 结构体

    typedef struct {
        /* 元素关键字 */
        ngx_str_t         key;
        /* 由散列方法算出来的关键码 */
        ngx_uint_t        key_hash;
        /* 指向实际的用户数据 */
        void             *value;
    }ngx_hash_key_t;
    

    2.3 ngx_hash_init():初始化一个基本散列表

    /* 计算该实际元素 name 所需的内存空间(有对齐处理),而 sizeof(void *) 就是结束哨兵的所需内存空间 */
    #define NGX_HASH_ELT_SIZE(name)                                               
        (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
    
    /*
     * @hinit:该指针指向的结构体中包含一些用于建立散列表的基本信息
     * @names:元素关键字数组,该数组中每个元素以ngx_hash_key_t作为结构体,存储着预添加到散列表中的元素
     * @nelts: 元素关键字数组中元素个数
     */
    ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
    {
        u_char          *elts;
        size_t           len;
        u_short         *test;
        ngx_uint_t       i, n, key, size, start, bucket_size;
        ngx_hash_elt_t  *elt, **buckets;
    
        if (hinit->max_size == 0) 
        {
            ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                          "could not build %s, you should "
                          "increase %s_max_size: %i",
                          hinit->name, hinit->name, hinit->max_size);
            return NGX_ERROR;
        }
    
        for (n = 0; n < nelts; n++) 
        {
            /* 这个判断是确保一个 bucket 至少能存放一个实际元素以及结束哨兵,如果有任意一个实际元素
             * (比如其 name 字段特别长)无法存放到 bucket 内则报错返回 */
            if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
            {
                ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                              "could not build %s, you should "
                              "increase %s_bucket_size: %i",
                              hinit->name, hinit->name, hinit->bucket_size);
                return NGX_ERROR;
            }
        }
    
        /* 接下来的测试针对当前传入的所有实际元素,测试分配多少个 Hash 节点(也就是多少个 bucket)会比较好,
         * 即能省内存又能少冲突,否则的话,直接把 Hash 节点数目设置为最大值 hinit->max_size 即可。 */
         
        test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
        if (test == NULL) 
        {
            return NGX_ERROR;
        }
    
        /* 计算一个 bucket 除去结束哨兵所占空间后的实际可用空间大小 */
        bucket_size = hinit->bucket_size - sizeof(void *);
    
        /* 计算所需 bucket 的最小个数,注意到存储一个实际元素所需的内存空间的最小值也就是
         * (2*sizeof(void *)) (即宏 NGX_HASH_ELT_SIZE 的对齐处理),所以一个 bucket 可以存储
         * 的最大实际元素个数就为 bucket_size / (2 * sizeof(void *)),然后总实际元素个数 nelts
         * 除以这个值就是最少所需要的 bucket 个数 */
        start = nelts / (bucket_size / (2 * sizeof(void *)));
        start = start ? start : 1;
    
        /* 如果这个 if 条件成立,意味着实际元素个数非常多,那么有必要直接把 start 起始值调高,否则在后面的  
         * 循环里要执行过多的无用测试 */
        if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) 
        {
            start = hinit->max_size - 1000;
        }
    
        /* 下面的 for 循环就是获取 Hash 结构最终节点数目的逻辑。就是逐步增加 Hash 节点数目(那么对应的
         *  bucket 数目同步增加),然后把所有的实际元素往这些 bucket 里添放,这有可能发生冲突,但只要
         * 冲突的次数可以容忍,即任意一个 bucket 都还没满,那么就继续填,如果发生有任何一个 bucket 
         * 满溢了(test[key] 记录了 key 这个 hash 节点所对应的 bucket 内存储实际元素后的总大小,如果它大
         * 于一个 bucket 可用的最大空间 bucket_size,自然就是满溢了),那么就必须增加 Hash 节点、增加 
         * bucket。如果所有实际元素都填完后没有发生满溢,那么当前的 size 值就是最终的节点数目值 */
        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 0
                ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                              "%ui: %ui %ui "%V"",
                              size, key, test[key], &names[n].key);
    #endif
    
                /* 判断是否满溢,若满溢,则必须增加 Hash 节点、增加 bucket */
                if (test[key] > (u_short) bucket_size) 
                {
                    goto next;
                }
            }
    
            /* 这里表示已将所有元素都添放到 bucket 中,则此时的 size 即为所需的节点数目值 */
            goto found;
    
        next:
    
            continue;
        }
    
        size = hinit->max_size;
    
        ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
                      "could not build optimal %s, you should increase "
                      "either %s_max_size: %i or %s_bucket_size: %i; "
                      "ignoring %s_bucket_size",
                      hinit->name, hinit->name, hinit->max_size,
                      hinit->name, hinit->bucket_size, hinit->name);
    
    found:
    
        /* 找到需创建的 Hash 节点数目值,接下来就是实际的 Hash 结构创建工作。
         * 注意:所有 buckets 所占的内存空间是连接在一起的,并且是按需分配(即某个 bucket 需多少内存
         * 存储实际元素就分配多少内存,除了额外的对齐处理)*/
    
        /* 初始化test数组中每个元素的值为 sizeof(void *),即ngx_hash_elt_t的成员value的所占内存大小 */
        for (i = 0; i < size; i++) 
        {
            test[i] = sizeof(void *);
        }
    
        /* 遍历所有的实际元素,计算出每个元素在对应槽上所占内存大小,并赋给该元素在test数组上的
         * 相应位置,即散列表中对应的槽 */
        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]));
        }
    
        len = 0;
    
        /* 对test数组中的每个元素(也即每个实际元素在散列表中对应槽所占内存的实际大小)
         * 进行对齐处理 */
        for (i = 0; i < size; i++) 
        {
            if (test[i] == sizeof(void *))
            {
                continue;
            }
    
            test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
    
            /* len 统计所有实际元素所占的内存总大小 */
            len += test[i];
        }
    
        if (hinit->hash == NULL) 
        {
            hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
                                                 + size * sizeof(ngx_hash_elt_t *));
            if (hinit->hash == NULL) 
            {
                ngx_free(test);
                return NGX_ERROR;
            }
    
            buckets = (ngx_hash_elt_t **)
                          ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
    
        } 
        else 
        {
            /* 为槽分配内存空间,每个槽都是一个指向 ngx_hash_elt_t 结构体的指针 */
            buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
            if (buckets == NULL)
            {
                ngx_free(test);
                return NGX_ERROR;
            }
        }
    
        /* 分配一块连续的内存空间,用于存储槽的实际数据 */
        elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
        if (elts == NULL)
        {
            ngx_free(test);
            return NGX_ERROR;
        }
    
        /* 进行内存对齐 */
        elts = ngx_align_ptr(elts, ngx_cacheline_size);
    
        /* 使buckets[i]指向 elts 这块内存的相应位置 */
        for (i = 0; i < size; i++) 
        {
            if (test[i] == sizeof(void *)) 
            {
                continue;
            }
    
            buckets[i] = (ngx_hash_elt_t *) elts;
            elts += test[i];
        }
    
        /* 复位teset数组的值 */
        for (i = 0; i < size; i++) 
        {
            test[i] = 0;
        }
    
        for (n = 0; n < nelts; n++) 
        {
            if (names[n].key.data == NULL) 
            {
                continue;
            }
    
            /* 计算该实际元素在散列表的映射位置 */
            key = names[n].key_hash % size;
            /* 根据key找到该实际元素应存放在槽中的具体位置的起始地址 */
            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]的值,以便当有多个实际元素映射到同一个槽中时便于解决冲突问题,
             * 从这可以看出Nginx解决碰撞问题使用的方法是开放寻址法中的用连续非空槽来解决 */
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
        }
    
        /* 遍历所有的槽,为每个槽的末尾都存放一个为 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;
        }
    
        ngx_free(test);
    
        hinit->hash->buckets = buckets;
        hinit->hash->size    = size;
    
    #if 0
    
        for (i = 0; i < size; i++) {
            ngx_str_t   val;
            ngx_uint_t  key;
    
            elt = buckets[i];
    
            if (elt == NULL) {
                ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                              "%ui: NULL", i);
                continue;
            }
    
            while (elt->value) {
                val.len = elt->len;
                val.data = &elt->name[0];
    
                key = hinit->key(val.data, val.len);
    
                ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                              "%ui: %p "%V" %ui", i, elt, &val, key);
    
                elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
                                                       sizeof(void *));
            }
        }
    
    #endif
    
        return NGX_OK;
    }
    
    hash 数据结构的使用

    2.4 ngx_hash_find()

    /*
     * 参数含义:
     * - hash:是散列表结构体的指针
     * - key:是根据散列方法算出来的散列关键字
     * - name和len:表示实际关键字的地址与长度
     *
     * 执行意义:
     * 返回散列表中关键字与name、len指定关键字完全相同的槽中,ngx_hash_elt_t结构体中value
     * 成员所指向的用户数据.
     */
    void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len)
    {
        ngx_uint_t       i;
        ngx_hash_elt_t  *elt;
    
       
    #if 1
        ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "hf:"%*s"", len, name);
    #endif
    
        /* 对key取模得到对应的hash节点 */
        elt = hash->buckets[key % hash->size];
    
        if (elt == NULL) 
        {
            return NULL;
        }
    
        /* 然后在该hash节点所对应的bucket里逐个(该bucket的实现类似数组,结束有
         * 哨兵保证)对比元素名称来找到唯一的那个实际元素,最后返回其value值
         * (比如,如果在addr->hash结构里找到对应的实际元素,返回的value就是
         * 其ngx_http_core_srv_conf_t配置) */
        while (elt->value) 
        {
            if (len != (size_t) elt->len) 
            {
                goto next;
            }
    
            for (i = 0; i < len; i++) 
            {
                if (name[i] != elt->name[i]) 
                {
                    goto next;
                }
            }
    
            return elt->value;
    
        next:
    
            elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
                                                   sizeof(void *));
            continue;
        }
    
        return NULL;
    }
    

    2.5 Nginx提供的两种散列方法

    /* 散列方法1:使用BKDR算法将任意长度的字符串映射为整型 */
    ngx_uint_t ngx_hash_key(u_char *data, size_t len)
    {
        ngx_uint_t  i, key;
    
        key = 0;
    
        for (i = 0; i < len; i++)
        {
            key = ngx_hash(key, data[i]);
        }
    
        return key;
    }
    
    
    /* 散列方法2:将字符串全小写后,再使用BKDR算法将任意长度的字符串映射为整型 */
    ngx_uint_t ngx_hash_key_lc(u_char *data, size_t len)
    {
        ngx_uint_t  i, key;
    
        key = 0;
    
        for (i = 0; i < len; i++)
        {
            key = ngx_hash(key, ngx_tolower(data[i]));
        }
    
        return key;
    }
    

    2.6 基本散列表的使用实例

    Nginx 对虚拟主机的管理使用到了 Hash 数据结构,比如假设配置文件nginx.conf中有如下配置:

    server {
        listen       192.168.1.1:80;
        server_name  www.web_test2.com blog.web_test2.com;
    ...
    server {
        listen       192.168.1.1:80;
        server_name  www.web_test1.com bbs.web_test1.com;
    ...
    

    当Nginx使用该配置文件启动后,如果来了一个客户端请求到192.168.1.1的80端口,那么Nginx需要做
    一个查找,看当前请求该使用哪个Server配置。为了提高查找效率,在启动时,Nginx就将根据这些
    server_name建立一个Hash数据结构。

    在ngx_http.c的ngx_http_server_names方法中:

    .
        hash.key = ngx_hash_key_lc;
        hash.max_size = cmcf->server_names_hash_max_size;
        hash.bucket_size = cmcf->server_names_hash_bucket_size;
        hash.name = "server_names_hash";
        hash.pool = cf->pool;
    
        if (ha.keys.nelts) 
        {
            hash.hash = &addr->hash;
            hash.temp_pool = NULL;
    
            if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) 
            {
                goto failed;
            }
        }
        ...
    
    调用ngx_hash_init前Hash数据结构初始状态

    调用ngx_hash_init后Hash数据结构状态


    图中,字段buckets指向的就是Hash节点所对应的存储空间,由于buckets是一个二级指针,那么*buckets本身是一个数组,每
    一个数组元素用来存储映射到此的Hash节点。由于可能有多个实际元素映射到同一个Hash节点(即发生冲突),所以对实际元
    素再次进行数组形式的组织存储在一个bucket内,这个数组的结束以哨兵元素NULL作为标记,而前面的每一个ngx_hash_elt_t
    结构对应一个实际元素的存储。

    3. Nginx 通配符散列表的实现

    3.1 原理

    支持通配符的散列表,就是把基本散列表中元素的关键字,用去除通配符以后的字符作为关键字加入。
    例如,对于关键字为 "www.test." 这样带通配符的情况,直接建立一个专用的后置通配符散列表,
    存储元素的关键字为 "www.test"。这样,如果要检索 "www.test.cn" 是否匹配 "www.test.
    ",可用
    Nginx 提供的专用方法 ngx_hash_find_wc_tail 检索,ngx_hash_find_wc_tail 方法会把要查询的
    www.test.cn 转化为 www.test 字符串再开始查询。

    同理,对于关键字为 "*.test.com" 这样带前置通配符的情况,也直接建立一个专用的前置通配符散
    列表,存储元素的关键字为 "com.test."。如果我们要检索 smtp.test.com 是否匹配 *.test.com,
    可用 Nginx 提供的专用方法 ngx_hash_find_wc_head 检索,ngx_hash_find_wc_head 方法会把要查
    询的 smtp.test.com 转化为 com.test. 字符串再开始查询。

    3.2 相应结构体

    3.2.1 ngx_hash_wildcard_t 结构体

    typedef struct {
        /* 基本散列表 */
        ngx_hash_t        hash;
        /* 当使用这个ngx_hash_wildcard_t通配符散列表作为某个容器的元素时,可以使用这个value  
         * 指针指向用户数据 */
        void             *value;
    }ngx_hash_wildcard_t;
    

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

    注:前置通配符散列表中元素的关键字,在把 * 通配符去掉后,会按照 "." 符号分隔,并以倒序的
    方式作为关键字来存储元素。相应地,在查询元素时也是做相同处理。

    3.2.3 ngx_hash_keys_arrays_t 结构体

    typedef struct {
        /* 下面的keys_hash、dns_wc_head_hash、dns_wc_tail_hash都是简易散列表,而hsize指明了  
         * 散列表中槽的个数,其简易散列方法也需要对hsize求余 */
        ngx_uint_t        hsize;
    
        /* 内存池,用于分配永久性内存 */
        ngx_pool_t       *pool;
        /* 临时内存池,下面的动态数组需要的内存都由temp_pool内存池分配 */
        ngx_pool_t       *temp_pool;
    
        /* 用动态数组以ngx_hash_key_t结构体保存着不含有通配符关键字的元素 */
        ngx_array_t       keys;
        /* 一个极其简易的散列表,它以数组的形式保存着hsize个元素,每个元素都是ngx_array_t  
         * 动态数组。在用户添加的元素过程中,会根据关键码将用户的ngx_str_t类型的关键字添加
         * 到ngx_array_t动态数组中。这里所有的用户元素的关键字都不可以带通配符,表示精确
         * 匹配 */
        ngx_array_t      *keys_hash;
    
        /* 用动态数组以ngx_hash_key_t结构体保存着含有前置通配符关键字的元素生成的中间关键字 */
        ngx_array_t       dns_wc_head;
        /* 一个极其简易的散列表,它以数组的形式保存着hsize个元素,每个元素都是ngx_array_t  
         * 动态数组。在用户添加的元素过程中,会根据关键码将用户的ngx_str_t类型的关键字添加
         * 到ngx_array_t动态数组中。这里所有的用户元素的关键字都带前置通配符 */
        ngx_array_t      *dns_wc_head_hash;
    
        /* 用动态数组以ngx_hash_key_t结构体保存着含有后置通配符关键字的元素生成的中间关键字 */
        ngx_array_t       dns_wc_tail;
        /* 一个极其简易的散列表,它以数组的形式保存着hsize个元素,每个元素都是ngx_array_t  
         * 动态数组。在用户添加的元素过程中,会根据关键码将用户的ngx_str_t类型的关键字添加
         * 到ngx_array_t动态数组中。这里所有的用户元素的关键字都带后置通配符 */
        ngx_array_t      *dns_wc_tail_hash;
    } ngx_hash_keys_arrays_t;
    

    3.3 通配符散列表相关函数

    3.3.1 ngx_hash_wildcard_init(): 初始化通配符散列表

    /*
     * 参数含义:
     * - hinit:是散列表初始化结构体的指针
     * - names:是数组的首地址,这个数组中每个元素以ngx_hash_key_t作为结构体,
     *          它存储着预添加到散列表中的元素(这些元素的关键字要么是含有前
     *          置通配符,要么含有后置通配符)
     * - nelts:是names数组的元素数目
     *
     * 执行意义:
     * 初始化通配符散列表(前置或者后置)。
     */
    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中分配一个元素个数为nelts,大小为sizeof(ngx_hash_key_t)
         * 的数组curr_name */
        if (ngx_array_init(&curr_names, hinit->temp_pool, nelts,
                           sizeof(ngx_hash_key_t))
            != NGX_OK)
        {
            return NGX_ERROR;
        }
    
        /* 从临时内存池temp_pool中分配一个元素个数为nelts,大小为sizeof(ngx_hash_key_t)
         * 的数组next_name */
        if (ngx_array_init(&next_names, hinit->temp_pool, nelts,
                           sizeof(ngx_hash_key_t))
            != NGX_OK)
        {
            return NGX_ERROR;
        }
    
        /* 遍历names数组中保存的所有通配符字符串 */
        for (n = 0; n < nelts; n = i) 
        {
    
    #if 0
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "wc0: "%V"", &names[n].key);
    #endif
    
            dot = 0;
    
            /* 遍历该通配符字符串的每个字符,直到找到 '.' 为止 */
            for (len = 0; len < names[n].key.len; len++) 
            {
                if (names[n].key.data[len] == '.') 
                {
                    /* 找到则置位该标识位 */
                    dot = 1;
                    break;
                }
            }
    
            /* 从curr_names数组中取出一个类型为ngx_hash_key_t的指针 */
            name = ngx_array_push(&curr_names);
            if (name == NULL) 
            {
                return NGX_ERROR;
            }
    
            /* 若dot为1,则len为'.'距该通配符字符串起始位置的偏移值,
             * 否则为该通配符字符串的长度 */
            name->key.len  = len;
            /* 将通配符字符串赋值给name->key.data */
            name->key.data = names[n].key.data;
            /* 以该通配符字符串作为关键字通过key散列方法算出该通配符字符串在散列表中的
             * 映射位置 */
            name->key_hash = hinit->key(name->key.data, name->key.len);
            /* 指向用户有意义的数据结构 */
            name->value    = names[n].value;
    
    #if 0
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "wc1: "%V" %ui", &name->key, dot);
    #endif
    
            dot_len = len + 1;
    
            /* 若前面的遍历中已找到'.',则len加1 */
            if (dot) 
            {
                len++;
            }
    
            next_names.nelts = 0;
    
            /* 当通配符字串的长度与len不等时,即表明dot为1 */
            if (names[n].key.len != len) 
            {
                /* 从next_names数组中取出一个类型为ngx_hash_key_t的指针 */
                next_name = ngx_array_push(&next_names);
                if (next_name == NULL)
                {
                    return NGX_ERROR;
                }
                
                /* 将该通配符第一个'.'字符之后的字符串放在next_name中 */
                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;
    
    #if 0
                ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                              "wc2: "%V"", &next_name->key);
    #endif
            }
    
            /* 这里n为names数组中余下尚未处理的通配符字符串中的第一个在names数组中的下标值,
             * 该for循环是用于提高效率,其实现就是比较当前通配符字符串与names数组中的下一个
             * 通配符字符,若发现'.'字符之前的字符串都完全相同,则直接将该通配符字符串'.'
             * 之后的字符串添加到next_names数组中 */
            for (i = n + 1; i < nelts; i++) 
            {
                /* 对该通配符字符串与names数组中的下一个通配符字符串进行比较,若不等,则
                 * 直接跳出该for循环,否则继续往下处理 */
                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_names数组中取出一个类型为ngx_hash_key_t的指针 */
                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;
    
    #if 0
                ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                              "wc3: "%V"", &next_name->key);
    #endif
            }
    
            /* 若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);
            }
        }
    
        if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts,
                          curr_names.nelts)
            != NGX_OK)
        {
            return NGX_ERROR;
        }
    
        return NGX_OK;
    }
    
    ngx_hash_combined_t通配符散列表的结构示意图

    3.4 带通配符散列表的使用实例

    散列表元素ngx_hash_elt_t中value指针指向的数据结构为下面定义的TestWildcardHashNode结构体,代码如下:

    typedef struct {
        /* 用于散列表中的关键字 */
        ngx_str_t servername;
        /* 这个成员仅是为了方便区别而已 */
        ngx_int_t se;
    }TestWildcardHashNode;
    

    每个散列表元素的关键字是servername字符串。下面先定义ngx_hash_init_t和ngx_hash_keys_arrays_t变量,为初始化散列
    表做准备,代码如下:

    /* 定义用于初始化散列表的结构体 */
    ngx_hash_init_t hash;
    /* ngx_hash_keys_arrays_t用于预先向散列表中添加元素,这里的元素支持带通配符 */
    ngx_hash_keys_arrays_t ha;
    /* 支持通配符的散列表 */
    ngx_hash_combined_t combinedHash;
    
    ngx_memzero(&ha, sizeof(ngx_hash_keys_arrays_t));
    

    combinedHash 是我们定义的用于指向散列表的变量,它包括指向 3 个散列表的指针,下面依次给这 3 个散列表指针赋值。

    /* 临时内存池只是用于初始化通配符散列表,在初始化完成后就可以销毁掉 */
    ha.temp_pool = ngx_create_pool(16384, cf->log);
    if (ha.temp_pool == NULL)
    {
        return NGX_ERROR;
    }
    
    /* 假设该例子是在ngx_http_xxx_postconf函数中的,所以就用了ngx_conf_t类型的cf下的内存池
     * 作为散列表的内存池 */
    ha.pool = cf->pool;
    
    /* 调用ngx_hash_keys_array_init方法来初始化ha,为下一步向ha中加入散列表元素做好准备 */
    if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK)
    {
        return NGX_ERROR;
    }
    

    如下代码,建立的 testHashNode[3] 这 3 个 TestWildcardHashNode 类型的结构体,分别表示可以用前置通配符匹配的散
    列表元素、可以用后置通配符匹配的散列表元素、需要完全匹配的散列表元素。

    TestWildcardHahsNode testHashNode[3];
    testHashNode[0].servername.len = ngx_strlen("*.text.com");
    testHashNode[0].servername.data = ngx_pcalloc(cf->pool, ngx_strlen("*.test.com"));
    ngx_memcpy(testHashNode[0].servername.data, "*.test.com", ngx_strlen("*.test.com"));
    
    testHashNode[1].servername.len = ngx_strlen("www.test.*");
    testHashNode[1].servername.data = ngx_pcalloc(cf->pool, ngx_strlen("www.test.*"));
    ngx_memcpy(testHashNode[1].servername.data, "www.test.*", ngx_strlen("www.test.*"));
    
    testHashNode[2].servername.len = ngx_strlen("www.text.com");
    testHashNode[2].servername.data = ngx_pcalloc(cf->pool, ngx_strlen("www.test.com"));
    ngx_memcpy(testHashNode[2].servername.data, "www.test.com", ngx_strlen("www.test.com"));
    
    for (i = 0; i < 3; i++)
    {
        testHashNode[i].seq = i;
        /* 这里flag必须设置为NGX_HASH_WILDCARD_KEY,才会处理带通配符的关键字 */
        ngx_hash_add_key(&ha, &testHashNode[i].servername, 
                                        &testHashNode[i], NGX_HASH_WILDCARD_KEY);
    }
    

    在调用ngx_hash_init_t的初始化函数前,先设置好ngx_hash_init_t中的成员,如槽的大小、散列方法等:

    hash.key         = ngx_hash_key_lc;
    hash.max_size    = 100;
    hash.bucket_size = 48;
    hash.name        = "test_server_name_hash";
    hash.pool        = cf->pool;
    

    ha的keys动态数组中存放的是需要完全匹配的关键字,如果keys数组不为空,那么开始初始化第 1 个散列表:

    if (ha.keys.nelts)
    {
        /* 需要显式地把ngx_hash_init_t中的hash指针指向combinedHash中的完全匹配散列表 */
        hash.hash = &combinedHash.hash;
        /* 初始化完全匹配散列表时不会使用到临时内存池 */
        hash.temp_pool = NULL;
        
        /* 将keys动态数组直接传给ngx_hash_init方法即可,ngx_hash_init_t中的
         * hash指针就是初始化成功的散列表 */
        if (ngx_hash_init(&hash, ha.keys.nelts, ha.keys.nelts) != NGX_OK)
        {
            return NGX_ERROR;
        }
    }
    

    下面继续初始化前置通配符散列表:

    if (ha.dns_wc_head.nelts)
    {
        hash.hash = NULL;
        /* ngx_hash_wildcard_init方法需要用到临时内存池 */
        hash.temp_pool = ha.temp_pool;
        if (ngx_hash_wildcard_init(&hash, ha.dns_wc_head.elts, ha.dns_wc_head.nelts) != NGX_OK)
        {
            return NGX_ERROR;
        }
        
        /* ngx_hash_init_t中的hash指针是ngx_hash_wildcard_init初始化成功的散列表,
         * 需要将它赋到combinedHash.wc_head前置通配符散列表指针中 */
        combinedHash.wc_head = (ngx_hash_wildcard_t *)hash.hash;
    }
    

    接着继续初始化后置通配符散列表:

    if (ha.dns_wc_tail.nelts)
    {
        hash.hash = NULL;
        hash.temp_pool = hs.temp_pool;
        if (ngx_hash_wildcard_init(&hash, ha.dns_wc_tail.elts, ha.dns_wc_tail.nelts) != NGX_OK)
        {
            return NGX_ERROR;
        }
        
        /* ngx_hash_init_t中的hash指针是ngx_hash_wildcard_init初始化成功的散列表,需要将它赋到
         * combinedHash.wc_tail后置通配符散列表指针中 */
        combinedHash.wc_tail = (ngx_hash_wildcard_t *) hash.hash;
    }
    

    此时,临时内存池已经没有存在意义了,即ngx_hash_keys_arrays_t中的这些数组、简易散列表都可以销毁了。这里只需要
    简单地把temp_pool内存池销毁即可:

    ngx_destroy_pool(ha.temp_pool);
    

    下面检查一下散列表是否工作正常。首先,查询关键字www.test.org,实际上,它应该匹配后置通配符散列表中的元素
    www.text.*:

    /* 首先定义待查询的关键字符串findServer */
    ngx_str_t findServer;
    findServer.len = ngx_strlen("www.test.org");
    /* 为什么必须要在内存池中分配空间以保存关键字呢?因为我们使用的散列方法是 ngx_hash_key_l,它会试着把
     * 关键字全小写 */
    findServer.data = ngx_pcalloc(cf->pool, ngx_strlen("www.test.org"));
    ngx_memcpy(findServer.data, "www.test.org", ngx_strlen("www.test.org"));
    
    /* ngx_hash_find_combined方法会查找出www.test.*对应的散列表元素,返回其指向的用户数据
     * ngx_hash_find_combined, 也就是testHashNode[1] */
    TestWildcardHashNode *findHashNode = ngx_hash_find_combined(&combinedHash, 
            ngx_hash_key_lc(findServer.data, findServer.len), findServer.data, findServer.len);
    

    如果没有查询到的话,那么findHashNode值为NULL。

    接着查询www.test.com,实际上,testHashNode[0]、testHashNode[1]、testHashNode[2]这 3 个节点都是匹配的,因为
    .test.com、www.test.、www.test.com都是匹配的。但按照完全匹配最优先的规则,ngx_hash_find_combined方法会返回
    testHashNode[2]的地址,也就是www.test.com对应的元素。

    findServer.len = ngx_strlen("www.test.com");
    findServer.data = ngx_pcalloc(cf->pool, ngx_strlen("www.test.com"));
    ngx_memcpy(findServer.data, "www.test.com", ngx_strlen("www.test.com");
    
    findHashNode = ngx_hash_find_combined(&combinedHash, 
                        ngx_hash_key_lc(findServer.data, findServer.len),
                        findServer.data, findServer.len);
    
  • 相关阅读:
    URAL 1998 The old Padawan 二分
    URAL 1997 Those are not the droids you're looking for 二分图最大匹配
    URAL 1995 Illegal spices 贪心构造
    URAL 1993 This cheeseburger you don't need 模拟题
    URAL 1992 CVS
    URAL 1991 The battle near the swamp 水题
    Codeforces Beta Round #92 (Div. 1 Only) A. Prime Permutation 暴力
    Codeforces Beta Round #7 D. Palindrome Degree hash
    Codeforces Beta Round #7 C. Line Exgcd
    Codeforces Beta Round #7 B. Memory Manager 模拟题
  • 原文地址:https://www.cnblogs.com/jimodetiantang/p/8921960.html
Copyright © 2011-2022 走看看