本文分析基于Nginx-1.2.6,与旧版本或将来版本可能有些许出入,但应该差别不大,可做参考
上篇对nginx中的hash分析还没有结束,因为带有wildcard或wc字样的结构体和函数还没有说明。但我也不知道该从何说起。(其实这篇我理解的可能有错,因为涉及到http module中的代码,还没读透,所以还不吝赐教)
在这里有这个指令的详解,这里也有个例子。这个指令有一个动作是比较HTTP报文头中的HOST与server_name的参数,选择最匹配的那个server配置,匹配的先后顺序为:
比如有配置如下:
#下面这两个配置等价,因为.example.com等价于 example.com *.example.com
server {
server_name example.com *.example.com www.example.*;
}
server {
server_name .example.com www.example.*;
}
当读取完配置加载到内存后,应该维持一个什么样的数据结构,才能在每个HTTP请求到来时按上述顺序快速寻找到最佳匹配?
容易想到的是hash表,其实nginx中正是利用了三个hash表实现这项功能,一个是普通hash表对应不带通配符的server_name配置,另两个对应带通配符的配置。
系统处理过程大致为:
解析出请求的HOST
然后调用ngx_http_find_virtual_server(r,host,len),其中r是封装了HTTP请求的一个结构体,找到最佳匹配的配置cscf(类型为ngx_http_core_srv_conf_t),并在r结构体中记录下来,对应代码如下所示。
继续往下处理
第二步代码:
<!-- lang: cpp -->
cscf = ngx_hash_find_combined(&r->virtual_names->names,
ngx_hash_key(host, len), host, len);
.....
r->srv_conf = cscf->ctx->srv_conf;
r->loc_conf = cscf->ctx->loc_conf;
在寻找最佳匹配时调用了ngx_hash.c中定义的一个函数,要重点关注这个调用:
<!-- lang: cpp -->
cscf = ngx_hash_find_combined(&r->virtual_names->names,
ngx_hash_key(host, len), host, len);
/*r->virtual_names是这个类型,与server_name指令的匹配顺序可以对着看*/
typedef struct {
ngx_hash_combined_t names;
ngx_uint_t nregex;
ngx_http_server_name_t *regex;
} ngx_http_virtual_names_t;
typedef struct {
ngx_hash_t hash;
ngx_hash_wildcard_t *wc_head;
ngx_hash_wildcard_t *wc_tail;
} ngx_hash_combined_t;
现在回到正题,ngx_hash_combined_t中有三个hash表,调用ngx_hash_find_combined(hash,key,name,len)时,就是在按先后顺序在三个hash表中查找对应的项。
<!-- lang: cpp -->
void *
ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name,
size_t len)
{
void *value;
/*在普通hash表中查找是否有精准匹配*/
if (hash->hash.buckets) {
value = ngx_hash_find(&hash->hash, key, name, len);
if (value) {
return value;
}
}
if (len == 0) {
return NULL;
}
/*在wc_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;
}
}
/*在wc_tail哈希表中查找是否能与某个带后置通配符的项匹配*/
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_init中进行的,但我们只知道这个函数的第二个参数是ngx_hash_key_t数组,其实这个数组中存放的并不是带通配符的域名,而是经过转换之后的。因为对sub.example.com选择匹配时可能会要判断是否有*.example.com或sub.example.*之类的server_name配置,那怎么才能把sub.example.com快速匹配到*.example.com而不是*.example.org呢?
而且当给出的key值既有不带通配符的记做A,又有带前置通配符的记为B,又有带后置通配符的记为C,我们希望从中筛选出A存放到exact hash中,B存放到_head wildcard hash中,C存放到tail wildcard hash中,ngx_hash_keys_array_init和ngx_hash_add_key是做这个的,源码里面有注释。在说明这两个函数之前,先看下涉及到的数据结构:
<!-- lang: cpp -->
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;
这个数据结构的说明在这里,下面这段是从他那里复制过来的
hsize: 将要构建的hash表的桶的个数。对于使用这个结构中包含的信息构建的三种类型的hash表都会使用此参数。
pool: 构建这些hash表使用的pool。
temp_pool:在构建这个类型以及最终的三个hash表过程中可能用到临时pool。该temp_pool可以在构建完成以后,被销毁掉。这里只是存放临时的一些内存消耗。
keys: 存放所有非通配符key的数组。
keys_hash: 这是个二维数组,第一个维度代表的是bucket的编号,那么keys_hash[i]中存放的是所有的key算出来的hash值对hsize取模以后的值为i的key。假设有3个key,分别是key1,key2和key3假设hash值算出来以后对hsize取模的值都是i,那么这三个key的值就顺序存放在keys_hash[i][0],keys_hash[i][5], keys_hash[i][6]。该值在调用的过程中用来保存和检测是否有冲突的key值,也就是是否有重复。
dns_wc_head: 存放前向通配符key被处理完成以后的值。比如:“*.abc.com”被处理完成以后,变成“com.abc.”被存放在此数组中。
dns_wc_tail: 存放后向通配符key被处理完成以后的值。比如:“mail.xxx.*”被处理完成以后,变成“mail.xxx.”被存放在此数组中。
dns_wc_head_hash: 该值在调用的过程中用来保存和检测是否有冲突的前向通配符的key值,也就是是否有重复。
dns_wc_tail_hash: 该值在调用的过程中用来保存和检测是否有冲突的后向通配符的key值,也就是是否有重复。
注:keys,dns_wc_head,dns_wc_tail,三个数组中存放的元素时ngx_hash_key_t类型的,而keys_hash,dns_wc_head_hash,dns_wc_tail_hash,三个二维数组中存放的元素是ngx_str_t类型的。
ngx_hash_keys_array_init就是为上述结构分配空间。
ngx_hash_add_key是将带或不带通配符的key转换后存放在上述结构中的,其过程是
调用ngx_hash_wildcard_init初始化后,生成的哈希表结构(是个决策树吧)如下图所示:
注释:
<!-- lang: cpp -->
/*
* the 2 low bits of value have the special meaning:
* 00 - value is data pointer for both "example.com"
* and "*.example.com";
* 01 - value is data pointer for "*.example.com" only;
* 10 - value is pointer to wildcard hash allowing
* both "example.com" and "*.example.com";
* 11 - value is pointer to wildcard hash allowing
* "*.example.com" only.
*/
在ngx_http_server_names中有使用。
<!-- lang: cpp -->
if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK) {
goto failed;
}
......
rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server,
NGX_HASH_WILDCARD_KEY);
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;
}
}
if (ha.dns_wc_head.nelts) {
/*这里有排序*/
ngx_qsort(ha.dns_wc_head.elts, (size_t) ha.dns_wc_head.nelts,
sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);
hash.hash = NULL;
hash.temp_pool = ha.temp_pool;
if (ngx_hash_wildcard_init(&hash, ha.dns_wc_head.elts,
ha.dns_wc_head.nelts)
!= NGX_OK)
{
goto failed;
}
addr->wc_head = (ngx_hash_wildcard_t *) hash.hash;
}
if (ha.dns_wc_tail.nelts) {
/*这里有排序*/
ngx_qsort(ha.dns_wc_tail.elts, (size_t) ha.dns_wc_tail.nelts,
sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);
hash.hash = NULL;
hash.temp_pool = ha.temp_pool;
if (ngx_hash_wildcard_init(&hash, ha.dns_wc_tail.elts,
ha.dns_wc_tail.nelts)
!= NGX_OK)
{
goto failed;
}
addr->wc_tail = (ngx_hash_wildcard_t *) hash.hash;
}
有了上面wildcard hash的树状的结构图,会容易地读懂代码。
ngx_hash_find_wc_head:
而当查找domain.com时,先查找com,其value低两位是11,继续在级联hash中查找domain,发现其value是10,且待查找key已从后往前遍历到头,则返回级联的wildcard hash中的value值和~3的与。说明domain.com 匹配 .domain.com。
ngx_hash_find_wc_tail:后置通配符与前置通配符的处理类似,而且更简单,因为后置通配符没有类似”*.example.com"与”.example.com"的区别。
这篇写的杂乱。。哎。。。