nginx关于域名解析的源码分析
在nginx中,nginx需要频繁进行域名解析的过程做了自己的优化,使用了自己的一套域名解析过程,并做了缓存处理。我们可以设置DNS解析服务器的地址,即通过resolver指令来设置DNS服务器的地址,由此来启动nginx的域名解析。
创新互联长期为千余家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为平塘企业提供专业的网站设计、做网站,平塘网站改版等技术服务。拥有10多年丰富建站经验和众多成功案例,为您定制开发。本文,我们来看看nginx是如何做的,这里我们只选出重要的代码进行分析,完整代码请参考nginx源代码,本文基于nginx-1.0.6版本进行的分析。
首先,来看看resolver的初始化。
在ngx_http_core_loc_conf_s的声明中,可以看到对reolver:
struct ngx_http_core_loc_conf_s { ngx_resolver_t *resolver; /* resolver */ }
resolver中保存了与域名解析相关的一些数据,它保存了DNS的本地缓存,通过红黑树的方式来组织数据,以达到快速查找。
typedef struct { ngx_event_t *event; // 用于连接dns服务器 ngx_udp_connection_t *udp_connection; // 保存了本地缓存的DNS数据 ngx_rbtree_t name_rbtree; ngx_rbtree_node_t name_sentinel; } ngx_resolver_t;
在nginx初始化的时候,通过ngx_resolver_create来初始化这一结构体,如果有设置resolver,则在ngx_http_core_resolver中有调用:
static char * ngx_http_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf = conf; // 初始化,第二个参数是我们设置的域名解析服务器的IP地址 clcf->resolver = ngx_resolver_create(cf, &u.addrs[0]); if (clcf->resolver == NULL) { return NGX_OK; } return NGX_CONF_OK; }
来看看ngx_resolver_create做了些什么:
ngx_resolver_t * ngx_resolver_create(ngx_conf_t *cf, ngx_addr_t *addr) { ngx_resolver_t *r; ngx_udp_connection_t *uc; r = ngx_calloc(sizeof(ngx_resolver_t), cf->log); if (r == NULL) { return NULL; } // 省略了其它数据的初始化过程 r->event = ngx_calloc(sizeof(ngx_event_t), cf->log); if (r->event == NULL) { return NULL; } // 设置事件的句柄 r->event->handler = ngx_resolver_resend_handler; r->event->data = r; // 设置dns服务器的地址 if (addr) { uc = ngx_calloc(sizeof(ngx_udp_connection_t), cf->log); if (uc == NULL) { return NULL; } r->udp_connection = uc; uc->sockaddr = addr->sockaddr; uc->socklen = addr->socklen; uc->server = addr->name; } return r; }
初始化好了之后,就可以调用了。在nginx中,upstream中使用到了此方法的域名解析。我们结合proxy模块与upstream模块来实例讲解吧,注意在proxy中,只有当proxy_pass中包含有变量时,才会用到nginx自己的DNS解析。而且这里有一个需要特别注意的,如果proxy_pass中包含变量,那么nginx中就需要配置resolver来指定DNS服务器地址了,否则,将直接返回502错误。从下面的代码中我们可以看到。
首先,在ngx_http_proxy_handler函数中,有如下代码:
static ngx_int_t ngx_http_proxy_handler(ngx_http_request_t *r) { // 这里的意思是,如果没有变量,就不进行变量解析 if (plcf->proxy_lengths == NULL) { ctx->vars = plcf->vars; u->schema = plcf->vars.schema; } else { // 只有当proxy_pass里面包含变量时,才解析变量,在ngx_http_proxy_eval中会添加域名解析的需求,请看ngx_http_proxy_eval的实现 if (ngx_http_proxy_eval(r, ctx, plcf) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } } }
而在proxy模块的ngx_http_proxy_eval函数中,可以看到如下代码:
static ngx_int_t ngx_http_proxy_eval(ngx_http_request_t *r, ngx_http_proxy_ctx_t *ctx, ngx_http_proxy_loc_conf_t *plcf) { ngx_str_t proxy; ngx_url_t url; // proxy为要转向的url url.url.data = proxy.data + add; url.default_port = port; url.uri_part = 1; // 注意这里设置的为不用解析域名 url.no_resolve = 1; // 由于有设置不用解析域名,所以在ngx_parse_url中就不会对域名进行解析 if (ngx_parse_url(r->pool, &url) != NGX_OK) { return NGX_ERROR; } // 保存与需要解析域名相关的信息 u->resolved = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_resolved_t)); if (u->resolved == NULL) { return NGX_ERROR; } if (url.addrs && url.addrs[0].sockaddr) { // 如果域名已经是ip地址的格式,就保存起来,这样在upstream里面就不会再进行解析 // 在upsteam模块里面会判断u->resolved->sockaddr是否为空 u->resolved->sockaddr = url.addrs[0].sockaddr; u->resolved->socklen = url.addrs[0].socklen; u->resolved->naddrs = 1; u->resolved->host = url.addrs[0].name; } else { u->resolved->host = url.host; u->resolved->port = (in_port_t) (url.no_port ? port : url.port); u->resolved->no_port = url.no_port; } }
所以,可以看出,只在当proxy_pass到包含变量的url时,才有可能进行域名的解析。因为如果是固定的url,则完全可以在初始化的时候解析域名,而不用在请求的时候进行了。关于这部分代码的实现,可以参考ngx_http_upstream_init_round_robin函数,而且注意,在proxy_pass时,是直接添加upstream来实现的,等有机会介绍upstream代码时再做解释。
接下来在upstream中ngx_http_upstream_init_request在初始化请求时,当u->resolved为不空时,需要解析域名。看代码:
static void ngx_http_upstream_init_request(ngx_http_request_t *r) { ngx_str_t *host; ngx_http_upstream_t *u; u = r->upstream; // 如果已经是ip地址格式了,就不需要再进行解析 if (u->resolved->sockaddr) { if (ngx_http_upstream_create_round_robin_peer(r, u->resolved) != NGX_OK) { ngx_http_upstream_finalize_request(r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); return; } ngx_http_upstream_connect(r, u); return; } // 接下来就要开始查找域名 host = &u->resolved->host; temp.name = *host; // 初始化域名解析器 ctx = ngx_resolve_start(clcf->resolver, &temp); if (ctx == NULL) { ngx_http_upstream_finalize_request(r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); return; } // 返回NGX_NO_RESOLVER表示无法进行域名解析 if (ctx == NGX_NO_RESOLVER) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "no resolver defined to resolve %V", host); ngx_http_upstream_finalize_request(r, u, NGX_HTTP_BAD_GATEWAY); return; } // 设置需要解析的域名的类型与信息 ctx->name = *host; ctx->type = NGX_RESOLVE_A; // 解析完成后的回调函数 ctx->handler = ngx_http_upstream_resolve_handler; ctx->data = r; u->resolved->ctx = ctx; // 开始解析域名 if (ngx_resolve_name(ctx) != NGX_OK) { u->resolved->ctx = NULL; ngx_http_upstream_finalize_request(r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); return; } // 域名还没有解析完成,则直接返回 return; // 其它动作 }
在上面的代码中,我们可以看到,需要解析域名,我们调用ngx_resolve_start,设置好回调函数等上下文信息后,然后再调用ngx_resolve_name,等域名解析完成后会调用ngx_http_upstream_resolve_handler。
那ngx_resolve_start函数的主要工作是初始化当前解析请求的上下文:
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); // 如果要解析的地址已为为ip地址,则会设置temp->quick为1,那么在调用ngx_resolve_name时就不会进行域名解析,在后面代码中可以看到 if (addr != INADDR_NONE) { temp->resolver = r; temp->state = NGX_OK; temp->naddrs = 1; temp->addrs = &temp->addr; temp->addr = addr;
文章标题:nginx关于域名解析的源码分析
文章源于:http://scyanting.com/article/cgppgi.html