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);
posted @ 2018-04-25 09:37  季末的天堂  阅读(628)  评论(0编辑  收藏  举报