ngx_hash
是nginx中的hash表結構,具備如下特色:nginx
以上特色決定了其高效性與功能侷限性。算法
根據結構體定義與ngx_hash_find
函數能夠看出其內存存放結構數組
typedef struct { void *value; u_short len; u_char name[1]; } ngx_hash_elt_t; typedef struct { //hash表分多個桶,每一個桶內存放hash(key)碰撞的元素 ngx_hash_elt_t **buckets; ngx_uint_t size; } ngx_hash_t; 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; //key % hash->size 選擇桶 elt = hash->buckets[key % hash->size]; if (elt == NULL) { return NULL; } while (elt->value) { if (len != (size_t) elt->len) { goto next; } //比對key for (i = 0; i < len; i++) { if (name[i] != elt->name[i]) { goto next; } } return elt->value; next: //計算下一個ele地址,每一個ele長度不固定。 elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len, sizeof(void *)); continue; } return NULL; }
示意圖以下:函數
整個hash表結構分紅若干個bucket,每一個bucket內存放key值碰撞的元素。ui
每一個元素內保存了完整的key值,注意ngx_hash_elt_t.name
實際存儲的內容包括完成的key,不只是1個字節,len表示其真實長度。因此每一個元素的大小是不一致的,根據key的實際長度決定。指針
初始化使用的是ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
函數。code
ngx_hash_init_t *hinit
結構以下:blog
typedef struct { ngx_hash_t *hash; //出參,初始化好的hash表,後續經過ngx_hash_find()函數使用 ngx_hash_key_pt key; //hash計算函數,經常使用選項有ngx_hash_key和ngx_hash_key_lc ngx_uint_t max_size; //最大桶數量,實際數量在函數中計算。 ngx_uint_t bucket_size; //每一個桶的大小。 char *name; //表名詞 ngx_pool_t *pool; //數據pool ngx_pool_t *temp_pool; //臨時pool,僅在須要通配符的hash表初始化是使用,ngx_hash_init()不須要使用 } ngx_hash_init_t;
ngx_hash_key_t *names
和ngx_uint_t nelts
組成一組key不重複的KV集合。nginx提供了另一組函數ngx_hash_keys_array_init()
和ngx_hash_add_key()
用於創造不重複的KV集合列表。遞歸
typedef struct { ngx_str_t key; ngx_uint_t key_hash; void *value; } ngx_hash_key_t;
ngx_hash_init()
邏輯以下dns
//計算元素大小,元素結構參考ngx_hash_elt_t #define NGX_HASH_ELT_SIZE(name) \ (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *))) 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; } //元素的大小都小於桶大小,保證1個桶能存放至少任意1個元素。 for (n = 0; n < nelts; n++) { 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; } } //test用於計算每一個桶所須要的大小,即hash(key)碰撞的幾個元素大小之和 test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log); if (test == NULL) { return NGX_ERROR; } //計算一個初始的桶數量,算法含義沒理解。 bucket_size = hinit->bucket_size - sizeof(void *); start = nelts / (bucket_size / (2 * sizeof(void *))); start = start ? start : 1; if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) { start = hinit->max_size - 1000; } //逐步調整,找到一個能放下全部元素的桶數量。 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])); //test[key] > bucket_size 表示hash(key)相同的元素總大小 > 桶大小 //則調整桶數量(size++),減小碰撞,減小hash(key)相同的元素總大小 if (test[key] > (u_short) bucket_size) { goto next; } } 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: //從新賦值test[],若是是goto found,和以前的test[]是同樣的。 //test[i]表示第i個桶的大小 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])); } //計算表的大小,且保證每一個桶起始地址能夠是cacheline對齊 len = 0; 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]; } //申請hinit->hash和hinit->hash->buckets基本結構空間 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 { buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *)); if (buckets == NULL) { ngx_free(test); return NGX_ERROR; } } //分配元素空間,且保證元素起始地址是cacheline對齊的 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[]與元素空間關聯 for (i = 0; i < size; i++) { if (test[i] == sizeof(void *)) { continue; } buckets[i] = (ngx_hash_elt_t *) elts; elts += test[i]; } for (i = 0; i < size; i++) { test[i] = 0; } //將names[]的KV列表複製到hash表結構中 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])); } //配置每一個桶內最後一個ele->value = 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; return NGX_OK; }
在使用ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
時要求names[]時一個key內容不重複列表。構造內容不重複的列表若是每次採用循環判斷當列表巨大時,時間開銷較大,nginx提供2個輔助函數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)
經過一個簡易的鏈狀hash進行重複檢查。代碼中部分涉及通配符處理的先略過下文再說。
typedef struct { ngx_uint_t hsize; //簡易hash表的桶數量 ngx_pool_t *pool; ngx_pool_t *temp_pool; ngx_array_t keys; //精確匹配的key列表 ngx_array_t *keys_hash; //使用二維數組構造的簡易hash表,用於檢查key是否重複。 ... } ngx_hash_keys_arrays_t;
ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type) { ... if (ngx_array_init(&ha->keys, ha->temp_pool, asize, sizeof(ngx_hash_key_t)) != NGX_OK) { return NGX_ERROR; } if (ngx_array_init(&ha->dns_wc_head, ha->temp_pool, asize, sizeof(ngx_hash_key_t)) != NGX_OK) { return NGX_ERROR; } ... }
ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key, void *value, ngx_uint_t flags) { ... //計算hash(key) for (i = 0; i < last; i++) { if (!(flags & NGX_HASH_READONLY_KEY)) { key->data[i] = ngx_tolower(key->data[i]); } k = ngx_hash(k, key->data[i]); } k %= ha->hsize; /* check conflicts in exact hash */ //在簡易hash表的桶中查找是否有相同key name = ha->keys_hash[k].elts; if (name) { for (i = 0; i < ha->keys_hash[k].nelts; i++) { if (last != name[i].len) { continue; } if (ngx_strncmp(key->data, name[i].data, last) == 0) { //經過簡易hash表判斷,找到相同key return NGX_BUSY; } } } else { if (ngx_array_init(&ha->keys_hash[k], ha->temp_pool, 4, sizeof(ngx_str_t)) != NGX_OK){ return NGX_ERROR; } } //將key放入簡易hash表中 name = ngx_array_push(&ha->keys_hash[k]); if (name == NULL) { return NGX_ERROR; } *name = *key; //將不重複的key放入結果ha->keys列表中 hk = ngx_array_push(&ha->keys); if (hk == NULL) { return NGX_ERROR; } hk->key = *key; hk->key_hash = ngx_hash_key(key->data, last); hk->value = value; return NGX_OK; ... }
nginx支持3種形式的通配符匹配。
.example.com
能夠匹配example.com
和www.example.com
*.example.com
只能夠匹配www.example.com
不能匹配example.com
www.example.*
能夠匹配www.example.com
內部是使用3張hash表分別保存精確匹配、頭部統配、尾部統配。再查找是也區分精確查找、頭部統配查找、尾部統配查找。
typedef struct { ngx_hash_t hash; ngx_hash_wildcard_t *wc_head; ngx_hash_wildcard_t *wc_tail; } ngx_hash_combined_t; typedef struct { ngx_hash_t hash; void *value; } ngx_hash_wildcard_t;//這個結構的含義見下文。 void * ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name, size_t len) { void *value; //在精確表查找 if (hash->hash.buckets) { value = ngx_hash_find(&hash->hash, key, name, len); if (value) { return value; } } if (len == 0) { return NULL; } //在頭部統配表查找 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; }
關於在前綴表和後綴表種如何查找,須要先了解前綴表和後綴表的結構。
爲了查找方便,特別是爲了實現頭部匹配表的查找,對於3中統配形式會進行必定的變化。
.example.com
形式的通配符會在 精確表中加入example.com
在頭部匹配中加入com.example
。*.example.com
形式的通配符會在頭部匹配中加入com.example.
www.example.*
形式的通配符會在尾部匹配中加入www.example
處理後都就能實現成從左到右分段匹配。處理代碼詳見ngx_hash_add_key()
函數的wildcard:
部分該部分有註釋,比較好讀。
進行初步處理後,就要開始構造分段的hash結構了,相關代碼在ngx_hash_wildcard_init()
。
示例有如下三個處理後的統配符號和對應的value
{ www.aaa.com : X1, img.aaa.com : X2, www.bbb.com. : X3, }
將保存成形如這樣的結構
{ www : { aaa : { com : X1 }, bbb : { com : X2 } }, img : { bbb : { com : X3 } } }
相關代碼以下:
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; ... for (n = 0; n < nelts; n = i) { //按.進行拆分 dot = 0; 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); if (name == NULL) { return NGX_ERROR; } name->key.len = len; name->key.data = names[n].key.data; name->key_hash = hinit->key(name->key.data, name->key.len); name->value = names[n].value; dot_len = len + 1; if (dot) { len++; } //非第一段保存在next_names中 next_names.nelts = 0; 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->key_hash = 0; 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; } //將第一段相同的 後面部分添加到next_name 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; } 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 (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; } //bit[0]表示最後是否有. //bit[1]是否指向中間hash結構,便是否爲根節點 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_find_wc_tail()
與ngx_hash_find_wc_head()
就很是簡單了,經過value指針的bit[1]判斷是否爲根節點,根據bit[0]判斷後續段是否必須。
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; for (i = 0; i < len; i++) { if (name[i] == '.') { break; } key = ngx_hash(key, name[i]); } if (i == len) { return NULL; } value = ngx_hash_find(&hwc->hash, key, name, i); 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.*". */ if ((uintptr_t) value & 2) { i++; 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; } return value; } return hwc->value; }