Android DNS解析的过程

4 篇文章 1 订阅
3 篇文章 0 订阅

Android DNS解析的过程

DNS解析概念

DNS的全称是domain name system,即域名系统。DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的去访问互联网而不用去记住能够被机器直接读取的IP地址。通过域名最终得到该域名对应的IP地址的过程则是域名解析的过程。

DNS解析过程

  1. 系统会检查浏览器缓存中有没有这个域名对应的解析过的IP地址,如果缓存中有,这个解析过程就将结束。
    Android在Java层和native都有缓存,java层缓存16个,时间为2秒
    native的缓存取决于TTL值

  2. 如果用户的浏览器缓存中没有,浏览器会查找操作系统缓存中即为本地的Host文件。 Android的Host文件路径(/system/etc/hosts)

  3. 如果本地Host文件中没有那么操作系统会把这个域名发送给LocalDNS(本地域名服务器)

       这个LocalDNS通常都提供给你本地互联网接入的一个DNS解析服务。这个专门的域名解析服务器性能都会很好,它们一般都会缓存域名解析结果,当然缓存时间是受域名的失效时间控制的,一般缓存空间不是影响域名失效的主要因素。大约90%的域名解析都到这里就已经完成了,所以LDNS主要承担了域名的解析工作。

  4. 如果LDNS仍然没有命中,就直接到Root Server(根域名服务器)请求解析(LDNS 去Root Server 请求)

       根域名服务器是最高层次的域名服务器,所有的根域名服务器都知道所有的顶级域名服务器的IP地址。根域名服务器有13个域名,由一群服务器组成。

根域名服务器

  1. 根域名服务器返回给本地域名服务器一个所查询的域的顶级域名服务(gTLD Server)地址。

       gTLD 是国际顶级域名服务器, 如.com、.cn、.org等

  2. 本地域名服务器(Local DNS Server)再向上一步返回的gTLD服务器发送请求。

  3. 接受请求的gTLD服务器查找并返回此域名对应的权威域名服务器(又叫权限域名服务器)的地址

  4. 权威域名服务器会查询存储的域名和IP的映射关系表,正常情况下都根据域名得到目标IP记录,连同一个TTL值返回给DNS Server域名服务器。

  5. 返回该域名对应的IP和TTL值,Local DNS Server会缓存这个域名和IP的对应关系,缓存的时间由TTL值控制。

  6. 把解析的结果返回给用户,用户根据TTL值缓存在本地系统缓存中,域名解析过程结束。

DNS请求流程图

DNS请求的过程

Android的DNS解析的过程(Android 9.0的源码)

java层

网络请求都会经过此方法,查询IP地址(发送一个https请求,在此方法打断点)。

  1. 从缓存中拿
  2. 获取不到去请求
  3. 请求后的结果放在缓存中
private static InetAddress[] lookupHostByName(String host, int netId)
          throws UnknownHostException {
      BlockGuard.getThreadPolicy().onNetwork();
      1.
      Object cachedResult = addressCache.get(host, netId);
     
      try {
          StructAddrinfo hints = new StructAddrinfo();
          hints.ai_flags = AI_ADDRCONFIG;
          hints.ai_family = AF_UNSPEC;
          hints.ai_socktype = SOCK_STREAM;
          2.
          InetAddress[] addresses = Libcore.os.android_getaddrinfo(host, hints, netId);
          for (InetAddress address : addresses) {
              address.holder().hostName = host;
              address.holder().originalHostName = host;
          }
          3.
          addressCache.put(host, netId, addresses);
          return addresses;
      } 
     
}
缓存结果

addressCache.put(host, netId, addresses);将请求的结果放到缓存中

class AddressCache {
     
    private static final int MAX_ENTRIES = 16;

    // The TTL for the Java-level cache is short, just 2s.
    private static final long TTL_NANOS = 2 * 1000000000L;

    private final BasicLruCache<AddressCacheKey, AddressCacheEntry> cache
            = new BasicLruCache<AddressCacheKey, AddressCacheEntry>(MAX_ENTRIES);

    static class AddressCacheEntry {
        final Object value;
        final long expiryNanos;
        AddressCacheEntry(Object value) {
            this.value = value;
            this.expiryNanos = System.nanoTime() + TTL_NANOS;
        }
    }

    public void put(String hostname, int netId, InetAddress[] addresses) {
        cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(addresses));
    }
    
}
  1. java层的TTL值是一个固定的2s,那网络DNS请求包中的TTL值存在那里了?
  2. 是不是可以修改Java层的值TTL_NANOS 和 MAX_ENTRIES ,缓存ip的值,达到加快网络速度
发起DNS请求

Libcore.os.android_getaddrinfo()

public final class Libcore {
    private Libcore() { }
    public static Os rawOs = new Linux();
    public static Os os = new BlockGuardOs(rawOs);
}

BlockGuardOs.java 文件
public InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException {
       return os.android_getaddrinfo(node, hints, netId);
}
public final class Linux implements Os {
    Linux() { }
  、、、
    public native InetAddress[] android_getaddrinfo(String node, StructAddrinfo hints, int netId) throws GaiException;
  、、、
    }

所以最终调用了native的方法

Java层流程图

java层流程图

JNI 层

关注 /libcore/luni/src/main/native/libcore_io_Linux.cpp 文件

Native层

Native层(客户端进程)

Libcore.os.android_getaddrinfo() 最终调用到getaddrinfo.c文件中的android_getaddrinfo_proxy方法(详细流程和调用链,关注流程图)

static int
android_getaddrinfo_proxy(
    const char *hostname, const char *servname,
    const struct addrinfo *hints, struct addrinfo **res, unsigned netid)
{
	FILE* proxy = android_open_proxy();
	// Send the request.
	if (fprintf(proxy, "getaddrinfo %s %s %d %d %d %d %u",
		    hostname == NULL ? "^" : hostname,
		    servname == NULL ? "^" : servname,
		    hints == NULL ? -1 : hints->ai_flags,
		    hints == NULL ? -1 : hints->ai_family,
		    hints == NULL ? -1 : hints->ai_socktype,
		    hints == NULL ? -1 : hints->ai_protocol,
		    netid) < 0) {
		goto exit;
	}
}
1.android_open_proxy()
__LIBC_HIDDEN__ FILE* android_open_proxy() {
    、、、
  	int s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);  
  	const int one = 1;
  	setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
    
  	struct sockaddr_un proxy_addr;
  	memset(&proxy_addr, 0, sizeof(proxy_addr));
  	proxy_addr.sun_family = AF_UNIX;
  	strlcpy(proxy_addr.sun_path, "/dev/socket/dnsproxyd", sizeof(proxy_addr.sun_path));
  
  	if (TEMP_FAILURE_RETRY(connect(s, (const struct sockaddr*) &proxy_addr, sizeof(proxy_addr))) != 0) {
  		close(s);
  		return NULL;
  	}
  	return fdopen(s, "r+");
}
  1. 创建socket套接字并初始化
  2. 将netd进程的socket拷贝到 proxy_addr.sun_path(strlcpy() 字符串的拷贝(比Strcpy多了一个‘\0’))
  3. 建立连接 这里就涉及到socket网络编程了,就不去探索了,感兴趣的可以关注connet,accept 等函数

. socket基础知识

2.fprintf(proxy, “getaddrinfo”)

Linux下一切皆文件,将命令“getaddrinfo” 写入文件中,这样就与net进程进行通信;

Native层流程图

Native层流程图

netd进程

先储备一点点netd进程的知识

init进程解析init.rc文件,创建zygote进程,netd进程,serviceManager服务,surfaceFlinger等一系类服务和进程(通过adb shell ps 命令可以查看进程名称和id);

DNS的解析是通过Netd代理的方式进行的。Netd是Network Daemon的缩写,Netd在Android中负责物理端口的网络操作相关的实现,如Bandwidth,NAT,PPP,soft-ap等。Netd为Framework隔离了底层网络接口的差异,提供了统一的调用接口,简化了整个网络逻辑的使用。简单来说就是Android将监听/dev/socket/dnsproxyd,如果系统需要DNS解析服务,那么就需要打开dnsproxyd,然后安装一定的格式写入命令,然后监听等待目标回答(CDNS 对netd进程比较好的解释)。

这里省略netd进程创建和初始化的过程,假设我们现在都了解这些知识,并且已经走到该函数中。
这里是netd进程,已经跨进程;

void DnsProxyListener::GetAddrInfoHandler::run() {
    、、、
    if (queryLimiter.start(uid)) {
        rv = android_getaddrinfofornetcontext(mHost, mService, mHints, &mNetContext, &result);
        queryLimiter.finish(uid);
    } 
    、、、
}
static int explore_fqdn(const struct addrinfo *pai, const char *hostname,
    const char *servname, struct addrinfo **res,
    const struct android_net_context *netcontext)
{
	static const ns_dtab dtab[] = {
		NS_FILES_CB(_files_getaddrinfo, NULL)
		{ NSSRC_DNS, _dns_getaddrinfo, NULL },	/* force -DHESIOD */
		NS_NIS_CB(_yp_getaddrinfo, NULL)
		{ 0, 0, 0 }
	};
  、、、
  nsdispatch(&result, dtab, NSDB_HOSTS, "getaddrinfo",
			default_dns_files, hostname, pai, netcontext)
	
	}
}
static void _sethtent(FILE **hostf)
{
	if (!*hostf)
		*hostf = fopen(_PATH_HOSTS, "re");
	else
		rewind(*hostf);
}
#define	_PATH_HOSTS	"/system/etc/hosts"
  1. 从文件/system/etc/hosts获取

hosts文件

  1. 从dns服务器获取

netd进程到底是怎么向DNS服务发起请求的,在res_send.c文件send_dg()真正发起网络请求的地方,有没有一种熟悉的感觉,创建套接字,建立连接,发送请求,等待结果。(详细流程和调用链,关注流程图)

static int
send_dg(res_state statp,
	const u_char *buf, int buflen, u_char *ans, int anssiz,
	int *terrno, int ns, int *v_circuit, int *gotsomewhere,
	time_t *at, int *rcode, int* delay)
{

	if (EXT(statp).nssocks[ns] == -1) {
		EXT(statp).nssocks[ns] = socket(nsap->sa_family, SOCK_DGRAM | SOCK_CLOEXEC, 0);

		if (random_bind(EXT(statp).nssocks[ns], nsap->sa_family) < 0) {
			Aerror(statp, stderr, "bind(dg)", errno, nsap,
			    nsaplen);
			res_nclose(statp);
			return (0);
		}
		if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
			Aerror(statp, stderr, "connect(dg)", errno, nsap,
			    nsaplen);
			res_nclose(statp);
			return (0);
		}
	if (sendto(s, (const char*)buf, buflen, 0, nsap, nsaplen) != buflen)
	{
		Aerror(statp, stderr, "sendto", errno, nsap, nsaplen);
		res_nclose(statp);
		return (0);
	}
	/*
	 * Wait for reply.
	 */
	seconds = get_timeout(statp, ns);
	now = evNowTime();
	timeout = evConsTime((long)seconds, 0L);
	finish = evAddTime(now, timeout);
	n = retrying_select(s, &dsmask, NULL, &finish);
  }
}
  1. java层的TTL值是一个固定的2s,那网络DNS请求包中的TTL值存在那里了?
    当发送DNS请求,请求回来以后,将请求结果保存
void _resolv_cache_add(const void* answer)//这里省略部分参数
{
    //从响应报文中获取本次查询结果中指定的查询结果的有效期
    ttl = answer_getTTL(answer, answerlen);
    if (ttl > 0) {
    	//ttl大于0,表示该地址可以保留一段时间,那么创建一个新的cache项,
        //然后设定其有效期,并将其加入到cache中
        e = entry_alloc(key, answer, answerlen);
        if (e != NULL) {
            e->expires = ttl + _time_now();
            _cache_add_p(cache, lookup, e);
        }
    }
}

在res_nsend()真正向DNS服务器发起DNS查询请求之前,会首先向自己的cache查询,如果cache可以命中,那么直接返回,否则才继续向DNS服务器查询。该查询过程是通过_resolv_cache_lookup()完成的。

ResolvCacheStatus _resolv_cache_lookup(unsigned netid,const void* query,int querylen,void*  answer,intanswersize,int   *answerlen )
{
  、、、
    lookup = _cache_lookup_p(cache, key);
    e      = *lookup;
    now = _time_now();
    //查询结果无效,返回没有查询到结果,向DNS服务器发起查询请求
    if (now >= e->expires) {
        XLOG( " NOT IN CACHE (STALE ENTRY %p DISCARDED)", *lookup );
        XLOG_QUERY(e->query, e->querylen);
        _cache_remove_p(cache, lookup);
        goto Exit;
    }
    //ok,到这里说明cache中的结果没问题
    memcpy( answer, e->answer, e->answerlen );
    //返回查询成功
    XLOG( "FOUND IN CACHE entry=%p", e );
    result = RESOLV_CACHE_FOUND;
    、、、
    return result;
}

Android系统中通过这种方式来管理DNS的好处是,所有解析后得到的 DNS 记录都将缓存在 Netd 进程中,从而使这些信息成为一个公共的资源,最大程度做到信息共享。

Netd进程流程图

Netd进程流程图

流程图

概括流程图

最后

Android DNS请求解析,缓存机制不仅仅是这么简单,这里只是大概的流程,路漫漫其修远兮!!

  • 22
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: OkHttp是一个用于Android和Java应用程序的HTTP客户端库。当使用OkHttp进行网络请求时,它会自动解析主机名并将其转换为IP地址。这个过程被称为DNS解析。默认情况下,OkHttp使用系统默认的DNS解析器来解析主机名。 但是,你也可以使用自定义的DNS解析器来替换系统默认的DNS解析器。这可以通过OkHttp的Dns接口实现。你只需要实现Dns接口并将其传递给OkHttpClient即可。 例如,你可以创建一个自定义的DNS解析器,它将所有主机名解析为特定的IP地址: ``` public class CustomDns implements Dns { private static final InetAddress[] CUSTOM_IPS = new InetAddress[] { InetAddress.getByName("192.168.0.1"), InetAddress.getByName("192.168.0.2") }; @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException { return Arrays.asList(CUSTOM_IPS); } } ``` 然后,你可以将这个自定义的DNS解析器传递给OkHttpClient: ``` OkHttpClient client = new OkHttpClient.Builder() .dns(new CustomDns()) .build(); ``` 这样,在进行网络请求时,OkHttp将使用你自定义的DNS解析器来解析主机名。 ### 回答2: OkHttp DNS 是 OkHttp 框架中的一部分,用于处理网络请求时解析域名的功能。 在进行网络请求时,我们通常会使用域名来表示目标主机,如 "www.example.com"。然而,计算机网络通信实际上是通过 IP 地址进行的。因此,我们需要将域名解析为对应的 IP 地址,以便与服务器进行通信。 OkHttp DNS 提供了一种机制,可以自定义和配置域名解析的策略。它允许开发人员替换默认的 DNS 解析器,从而实现更高效、灵活的域名解析。 OkHttp 内置了一个默认的 DNS 解析器,它使用 Java 标准库中的 InetAddress 类来解析 IP 地址。然而,对于某些特定的场景和需求,我们可能需要使用其他的 DNS 解析器。 通过实现自定义的 Dns 接口,我们可以将其传递给 OkHttp,从而替换默认的 DNS 解析器。我们可以自己实现域名解析的逻辑,例如在本地缓存 IP 地址、使用其他第三方 DNS 解析库等。这样可以提高解析效率、减少网络请求的延迟。 在使用 OkHttp DNS 时,我们可以通过 OkHttpClient.Builder 类的 dns() 方法来设置自定义的 DNS 解析器。例如: Dns customDns = new CustomDns(); OkHttpClient client = new OkHttpClient.Builder() .dns(customDns) .build(); 通过上述代码,我们将自定义的 DNS 解析器 customDns 设置为 OkHttpClient 的属性。在后续的网络请求中,OkHttp 将使用该解析器来解析域名。 总之,OkHttp DNS 提供了灵活、可定制的域名解析策略,可以满足不同场景下的需求,提升网络请求的性能。 ### 回答3: OkHttp是一个开源的轻量级的HTTP客户端库,主要用于Android平台。它提供了一组接口来发送和接收HTTP请求和响应数据,并且支持各种常见的HTTP协议和功能。 OkHttp DNS是OkHttp库提供的一个功能,用于解析主机名(域名)并将其转换为IP地址。DNS(Domain Name System)是互联网中用于将域名转换为IP地址的系统,它允许我们使用域名来访问Web服务器,而不是直接使用IP地址。 OkHttp默认使用系统的DNS解析服务来解析主机名。但是,OkHttp也提供了自定义DNS功能,使我们可以根据需要修改或扩展DNS解析过程。 通过使用OkHttp DNS,我们可以实现以下功能: 1. 自定义DNS解析:我们可以使用自定义的DNS解析服务来解析主机名。这对于需要使用特定的DNS服务器或实现一些自定义逻辑的应用程序非常有用。 2. 域名解析优化:OkHttp DNS还可以实现域名解析的优化。例如,我们可以缓存最近解析的域名,以便下次访问时可以更快地获取IP地址。这可以提高应用程序的性能和响应速度。 3. DNS解析故障处理:如果默认的DNS解析服务发生故障或无法解析某个域名,我们可以使用OkHttp DNS来处理这些错误情况。例如,我们可以在DNS解析失败时使用备用的DNS服务器。 总之,OkHttp DNS是一个非常有用的功能,它可以让我们更灵活地处理主机名的解析,并提供更好的性能和可靠性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值