Nginx怎么做域名解析,?怎么在你自己開發(fā)的模塊里面使用Nginx提供的方法解析域名,?它內(nèi)部實現(xiàn)是什么樣的?
本文以Nginx 1.5.1為例,,從nginx_mail_smtp模塊如何進行域名解析出發(fā),,分析Nginx進行域名解析的過程。為了簡化流程,,突出重點,,在示例代碼中省掉了一些異常部分的處理,比如內(nèi)存分配失敗等,。DNS查詢分為兩種:根據(jù)域名查詢地址和根據(jù)地址查詢域名,,在代碼結(jié)構(gòu)上這兩種方式非常相似,這里只介紹根據(jù)域名查詢地址這一種方式,。本文將從以下幾個方面進行介紹:
- 域名查詢的函數(shù)接口介紹
- 域名解析流程分析
- 查詢場景分析及實現(xiàn)介紹
一,、域名查詢的函數(shù)接口介紹
在使用同步IO的情況下,調(diào)用gethostbyname()或者gethostbyname_r()就可以根據(jù)域名查詢到對應(yīng)的IP地址, 但因為可能會通過網(wǎng)絡(luò)進行遠程查詢,,所以需要的時間比較長,。
為了不阻塞當前線程,Nginx采用了異步的方式進行域名查詢,。整個查詢過程主要分為三個步驟,,這點在各種異步處理時都是一樣的:
- 準備函數(shù)調(diào)用需要的信息,并設(shè)置回調(diào)方法
- 調(diào)用函數(shù)
- 處理結(jié)束后回調(diào)方法被調(diào)用
另外,,為了盡量減少查詢花費的時間,,Nginx還對查詢結(jié)果做了本地緩存。為了初始化DNS Server地址和本地緩存等信息,,需要在真正查詢前需要先進行一些全局的初始化操作,。
下面先從調(diào)用者的角度對每個步驟做詳細的分析:
-
初始化域名查詢所需要的的全局信息
需要初始化的全局信息包括:
- DNS 服務(wù)器的地址,如果指定了多個服務(wù)器,,nginx會采用Round Robin的方式輪流查詢每個服務(wù)器
- 對查詢結(jié)果的緩存,,采用Red Black Tree的數(shù)據(jù)結(jié)構(gòu),以要查詢名字的Hash作為Key, 節(jié)點信息存放在 struct ngx_resolver_node_t中,。
因為resolver是全局的,,與任何一個connection都無關(guān),所有需要放在一個隨時都可以取到的地方,,如 ngx_mail_core_srv_conf_t結(jié)構(gòu)體上,,在使用時從當前session找到ngx_mail_core_srv_conf_t,然后找到resolver,。
DNS 服務(wù)器的信息需要在配置文件中明確指出,,比如
1 2 3 4 5 6 | #nginx.conf
resolver 8.8.8.8
#nginx 默認會根據(jù)DNS請求結(jié)果里的TTL值來進行緩存,
#當然也可以通過一個可選的參數(shù)valid來設(shè)置過期時間,如:
#resolver 127.0.0.1 [::1]:5353 valid=30s;
|
下面根據(jù)配置中的resolver參數(shù),,初始化全局的ngx_resolver_t,,其中保存了前面提及的DNS服務(wù)器地址和查詢結(jié)果等信息:
01 02 03 04 05 06 07 08 09 10 11 | static char *
ngx_mail_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_mail_core_srv_conf_t *cscf = conf;
ngx_str_t *value;
value = cf->args->elts;
cscf->resolver = ngx_resolver_create(cf, &value[1],
cf->args->nelts - 1);
return NGX_CONF_OK;
}
|
-
準備本次查詢的信息
和本次查詢相關(guān)的信息放在ngx_resolver_ctx_t結(jié)構(gòu)體中,包括要查詢的名稱,,查詢完的回調(diào)方法,以及超時時間等,。如果本次要查詢的地址已經(jīng)是IPv4用點分隔的地址了,,比如74.125.128.100, nginx會在ngx_resolve_start中進行判斷,并設(shè)置好標志位,,在調(diào)用ngx_resolve_name時不會發(fā)送真正的DNS查詢請求,。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | static void
ngx_mail_smtp_resolve_name(ngx_event_t *rev)
{
ngx_connection_t *c;
ngx_mail_session_t *s;
ngx_resolver_ctx_t *ctx;
ngx_mail_core_srv_conf_t *cscf;
c = rev->data;
s = c->data;
cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
ctx = ngx_resolve_start(cscf->resolver, NULL);
if (ctx == NULL) {
ngx_mail_close_connection(c);
return ;
}
ctx->name = s->host;
ctx->type = NGX_RESOLVE_A;
ctx->handler = ngx_mail_smtp_resolve_name_handler;
ctx->data = s;
ctx->timeout = cscf->resolver_timeout;
//根據(jù)名字進行IP地址查詢
if (ngx_resolve_name(ctx) != NGX_OK) {
ngx_mail_close_connection(c);
}
}
|
-
根據(jù)名字進行IP地址查詢
前面方法的最后通過ngx_resolve_name方法進行IP地址查詢。查詢時,,Nginx會先檢查本地緩存,,如果在緩存中,就更新緩存過期時間,,并回調(diào)設(shè)置的handler, 如前面設(shè)置的:ngx_mail_smtp_resolve_name_handler,,然后整個查詢過程結(jié)束。如果沒有在緩存中就發(fā)送查詢請求給dns server,,同時方法返回,。
-
查詢完成后回調(diào)在ngx_resolver_ctx_t中指定的方法
真正的DNS查詢完成后,不管成功,,失敗或是超時,,nginx會回調(diào)相應(yīng)查詢的handler, 如前面設(shè)置的:ngx_mail_smtp_resolve_name_handler。在handler中都需要調(diào)用ngx_resolve_addr_done來標識查詢結(jié)束,。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | static void
ngx_mail_smtp_resolve_name_handler(ngx_resolver_ctx_t *ctx)
{
in_addr_t addr;
ngx_uint_t i;
ngx_connection_t *c;
struct sockaddr_in * sin ;
ngx_mail_session_t *s;
s = ctx->data;
c = s->connection;
if (ctx->state) {
ngx_log_error(NGX_LOG_ERR, c-> log , 0,
"" %V " could not be resolved (%i: %s)" ,
&ctx->name, ctx->state,
ngx_resolver_strerror(ctx->state));
} else {
/* AF_INET only */
sin = ( struct sockaddr_in *) c->sockaddr;
for (i = 0; i < ctx->naddrs; i++) {
addr = ctx->addrs[i];
ngx_log_debug4(NGX_LOG_DEBUG_MAIL, c-> log , 0,
"name was resolved to %ud.%ud.%ud.%ud" ,
(ntohl(addr) >> 24) & 0xff,
(ntohl(addr) >> 16) & 0xff,
(ntohl(addr) >> 8) & 0xff,
ntohl(addr) & 0xff);
if (addr == sin ->sin_addr.s_addr) {
goto found;
}
}
s->host = smtp_unavailable;
}
found:
//不管成功失敗都要執(zhí)行
ngx_resolve_name_done(ctx);
}
|
二,、域名解析流程分析
通過Nginx進行域名查詢的流程圖如下,顏色越深花費的時間越長,。調(diào)用過程分為三種:
- 首先判斷是不是IPv4地址,,如果是就直接調(diào)用Handler
- 再次檢查是不是在緩存中,如果有,,就調(diào)用Handler
- 最后發(fā)送遠程DNS請求,,收到回復(fù)后調(diào)用Handler
三、查詢場景分析及實現(xiàn)介紹
-
查詢的地址是IP v4地址
比如74.125.128.100, nginx會在ngx_resolve_start中通過ngx_inet_addr方法進行判斷,,如果是IPv4的地址,,就設(shè)置好標志位 ngx_resolver_ctx_t->quick,在接下來的ngx_resolve_name中會對這個標志位進行判斷,,如果為1,,就直接調(diào)用ngx_resolver_ctx_t->handler
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | ngx_resolver_ctx_t *
ngx_resolve_start(ngx_resolver_t *r, ngx_resolver_ctx_t *temp)
{
in_addr_t addr;
ngx_resolver_ctx_t *ctx;
if (temp) {
addr = ngx_inet_addr(temp->name.data, temp->name.len);
if (addr != INADDR_NONE) {
temp->resolver = r;
temp->state = NGX_OK;
temp->naddrs = 1;
temp->addrs = &temp->addr;
temp->addr = addr;
temp->quick = 1;
return temp;
}
}
...
}
|
-
超時沒有得到查詢結(jié)果
調(diào)用ngx_resolve_name時設(shè)置的回調(diào)方法被調(diào)用,同時ngx_resolver_ctx_t->state被設(shè)置為NGX_RESOLVE_TIMEDOUT。相應(yīng)的代碼為:
1 2 3 4 5 6 7 8 | static void
ngx_resolver_timeout_handler(ngx_event_t *ev)
{
ngx_resolver_ctx_t *ctx;
ctx = ev->data;
ctx->state = NGX_RESOLVE_TIMEDOUT;
ctx->handler(ctx);
}
|
-
正常查詢一個不在緩存中的域名
如果要查詢的域名不在緩存中,,首先把域名按hash值放在緩存中,,然后準備查詢需要的數(shù)據(jù),發(fā)送DNS查詢的UDP請求給DNS服務(wù)器,,
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | static ngx_int_t
ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx)
{
ngx_resolver_node_t *rn;
rn = ngx_resolver_alloc(r, sizeof (ngx_resolver_node_t));
ngx_rbtree_insert(&r->name_rbtree, &rn->node);
ngx_resolver_create_name_query(rn, ctx);
ngx_resolver_send_query(r, rn);
rn->cnlen = 0;
rn->naddrs = 0;
rn->valid = 0;
rn->waiting = ctx;
ctx->state = NGX_AGAIN;
}
//收到DNS查詢結(jié)果后的回調(diào)方法
static void
ngx_resolver_read_response(ngx_event_t *rev)
{
ssize_t n;
ngx_connection_t *c;
u_char buf[NGX_RESOLVER_UDP_SIZE];
c = rev->data;
do {
n = ngx_udp_recv(c, buf, NGX_RESOLVER_UDP_SIZE);
if (n < 0) {
return ;
}
ngx_resolver_process_response(c->data, buf, n);
} while (rev->ready);
}
static void
ngx_resolver_process_a(ngx_resolver_t *r, u_char *buf, size_t last,
ngx_uint_t ident, ngx_uint_t code, ngx_uint_t nan, ngx_uint_t ans)
{
hash = ngx_crc32_short(name.data, name.len);
rn = ngx_resolver_lookup_name(r, &name, hash);
//copy addresses to cached node
rn->u.addrs = addrs;
//回調(diào)所有等待本域名解析的請求
next = rn->waiting;
rn->waiting = NULL;
while (next) {
ctx = next;
ctx->state = NGX_OK;
ctx->naddrs = naddrs;
ctx->addrs = (naddrs == 1) ? &ctx->addr : addrs;
ctx->addr = addr;
next = ctx->next;
ctx->handler(ctx);
}
}
|
-
對同一域名查詢多次查詢
如果多次查詢時,,之前的查詢結(jié)果還在緩存中并且沒有失效,就直接從緩存中取到查詢結(jié)果,,并調(diào)用設(shè)置的回調(diào)方法,。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | static ngx_int_t
ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx)
{
uint32_t hash;
in_addr_t addr, *addrs;
ngx_uint_t naddrs;
ngx_resolver_ctx_t *next;
ngx_resolver_node_t *rn;
hash = ngx_crc32_short(ctx->name.data, ctx->name.len);
rn = ngx_resolver_lookup_name(r, &ctx->name, hash);
if (rn) {
if (rn->valid >= ngx_time()) {
naddrs = rn->naddrs;
if (naddrs) {
ctx->next = rn->waiting;
rn->waiting = NULL;
do {
ctx->state = NGX_OK;
ctx->naddrs = naddrs;
ctx->addrs = (naddrs == 1) ? &ctx->addr : addrs;
ctx->addr = addr;
next = ctx->next;
ctx->handler(ctx);
ctx = next;
} while (ctx);
return NGX_OK;
}
}
}
}
|
-
得到查詢結(jié)果時同時超時了
如果在得到查詢結(jié)果的同時,設(shè)置的超時時間也到期了,,那該怎么辦呢,? Nginx會先處理各種網(wǎng)絡(luò)讀寫事件,再處理超時事件,,在處理網(wǎng)絡(luò)事件時,,會相應(yīng)地把設(shè)置的定時器刪除,所以在執(zhí)行超時事件時就不會再執(zhí)行了,。
01 02 03 04 05 06 07 08 09 10 11 12 | void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
//處理各種網(wǎng)絡(luò)事件
( void ) ngx_process_events(cycle, timer, flags);
//處理各種timer事件,,其中包含了查詢超時
ngx_event_expire_timers();
}
|
-
得到查詢結(jié)果時客戶端已經(jīng)關(guān)閉連接
如果不做任何處理,那么在收到dns查詢結(jié)果后,,會回調(diào)查詢時設(shè)置的回調(diào)方法,,但因為連接已經(jīng)被關(guān)閉,相應(yīng)的內(nèi)存已經(jīng)被釋放,,所以會有非法內(nèi)存訪問的問題,。怎么避免呢?在處理連接關(guān)閉事件時,,同時需要調(diào)用ngx_resolve_name_done(ctx)方法,調(diào)用時需要把state設(shè)為NGX_AGAIN或者NGX_RESOLVE_TIMEDOUT,,這樣就會刪除查詢所設(shè)置的回調(diào)信息:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | void ngx_close_xxx_session(ngx_xxx_session_t *s)
{
if (s->resolver_ctx != NULL) {
s->resolver_ctx->state = NGX_RESOLVE_TIMEDOUT;
ngx_resolve_name_done(s->resolver_ctx);
s->resolver_ctx = NULL;
}
}
void ngx_resolve_name_done(ngx_resolver_ctx_t *ctx)
{
uint32_t hash;
ngx_resolver_t *r;
ngx_resolver_ctx_t *w, **p;
ngx_resolver_node_t *rn;
r = ctx->resolver;
if (ctx->state == NGX_AGAIN || ctx->state == NGX_RESOLVE_TIMEDOUT) {
hash = ngx_crc32_short(ctx->name.data, ctx->name.len);
rn = ngx_resolver_lookup_name(r, &ctx->name, hash);
if (rn) {
p = &rn->waiting;
w = rn->waiting;
while (w) {
if (w == ctx) {
*p = w->next;
goto done;
}
p = &w->next;
w = w->next;
}
}
}
done:
ngx_resolver_free_locked(r, ctx);
}
|
-
本地緩存的地址沒有再次被查詢
每次在查詢結(jié)束的時候(調(diào)用ngx_resolve_addr_done),都會檢查有沒有緩存過期,,如果有,,就會進行釋放。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | static void
ngx_resolver_expire(ngx_resolver_t *r, ngx_rbtree_t *tree,
ngx_queue_t *queue)
{
time_t now;
ngx_uint_t i;
ngx_queue_t *q;
ngx_resolver_node_t *rn;
now = ngx_time();
for (i = 0; i < 2; i++) {
if (ngx_queue_empty(queue)) {
return ;
}
q = ngx_queue_last(queue);
rn = ngx_queue_data(q, ngx_resolver_node_t, queue);
if (now <= rn->expire) {
return ;
}
ngx_log_debug2(NGX_LOG_DEBUG_CORE, r-> log , 0,
"resolver expire " %*s "" , ( size_t ) rn->nlen, rn->name);
ngx_queue_remove(q);
ngx_rbtree_delete(tree, &rn->node);
ngx_resolver_free_node(r, rn);
}
}
|
-
域名對應(yīng)這多個IP地址
如果對應(yīng)的有多個ip,那么在每次查詢時,,會隨機的重新排列順序,,然后返回。對于調(diào)用者來說,,只要去第一個地址,,就可以達到取隨機地址的目的了。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | static ngx_int_t
ngx_resolve_name_locked(ngx_resolver_t *r, ngx_resolver_ctx_t *ctx)
{
if (naddrs) {
if (naddrs != 1) {
addr = 0;
addrs = ngx_resolver_rotate(r, rn->u.addrs, naddrs);
if (addrs == NULL) {
return NGX_ERROR;
}
} else {
addr = rn->u.addr;
addrs = NULL;
}
}
}
static in_addr_t *
ngx_resolver_rotate(ngx_resolver_t *r, in_addr_t *src, ngx_uint_t n)
{
void *dst, *p;
ngx_uint_t j;
dst = ngx_resolver_alloc(r, n * sizeof (in_addr_t));
j = ngx_random() % n;
if (j == 0) {
ngx_memcpy(dst, src, n * sizeof (in_addr_t));
return dst;
}
p = ngx_cpymem(dst, &src[j], (n - j) * sizeof (in_addr_t));
ngx_memcpy(p, src, j * sizeof (in_addr_t));
return dst;
}
|
-
指定了多個dns server地址會怎么查詢
如果在配置文件里指定了多個dns server地址會發(fā)生什么呢,?比如
1 2 | #nginx.conf
resolver 8.8.8.8 8.8.4.4
|
那么nginx 會采用Round Robin 的方式輪流查詢各個dns server,。在方法ngx_resolver_send_query中通過在每次調(diào)用時改變last_connection來輪流使用不同的dns server進行查詢
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | static ngx_int_t
ngx_resolver_send_query(ngx_resolver_t *r, ngx_resolver_node_t *rn)
{
ssize_t n;
ngx_udp_connection_t *uc;
uc = r->udp_connections.elts;
uc = &uc[r->last_connection++];
if (r->last_connection == r->udp_connections.nelts) {
r->last_connection = 0;
}
...
}
|
一邊用一邊學一邊寫,會理解的更透徹,祝大家也玩的高興,。
|