nginx关于域名解析的源码分析

原文链接:http://www.xuebuyuan.com/1017477.html

在nginx中,nginx需要频繁进行域名解析的过程做了自己的优化,使用了自己的一套域名解析过程,并做了缓存处理。我们可以设置DNS解析服务器的地址,即通过resolver指令来设置DNS服务器的地址,由此来启动nginx的域名解析。 
本文,我们来看看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;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值