OkHttpDNS实现原理
原文作者 作者在解析 HttpDNS之后,并没有把解析的结果包装为 InetAddress,而是直接调用了 InetAddress.getByName 方法,方法仍然是调用系统原生 dns 查询,所以我写了一个 IPUtils 库,安全的解析返回结果(出现任何异常都会走系统解析,保证安全)
The Application of HTTP DNS on OkHttp.
1. HTTP DNS 的介绍
HTTP DNS通过将域名查询请求放入http中的一种域名解析方式,而不用系统自带的libc库去查询运营商的DNS服务器,有更大的自由度。目前微信,qq邮箱、等业务均使用了HTTP DNS,详见这里
主要优点
- 能够准确地将站点解析到离用户最近的CDN站点,方便进行流量调度
- 解决部分运营商DNS无法解析国外站点的问题
- TCP在一定程度可以防止UDP无校验导致的DNS欺诈(比如墙,运营商广告,404导航站),当然基于HTTP的话本质还是不安全的。
2. HTTP DNS 与 native DNS 的流程对比
使用http与native的区别主要在:
- 需要实现native 自带的 LRU缓存(DNS默认是600s,原生的缓存在JNI内存中,而OkHttp缓存在硬盘中)
- Socket请求拼装环节在java层,而不是在libc库中(当然有的项目非要做成JNI,也没办法)
- 需要维护一个单独的HttpClient进行DNS查询
在进行实现前,我们先看下原生情况的查询方法的strace
2.1. Android下native DNS查询过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//JDK层调用
InetAddress.lookupHostByName()(InetAddress.java);
Libcore.os.android_getaddrinfo(InetAddress.java)
//framework下调用
libcore.io.ForwardingOs.getaddrinfo(ForwardingOs.java)
//framework层
libcore.io.Posix.getaddrinfo(Posix.java)
//JNI层,此方法为java的代理
Posix_android_getaddrinfo(Posic.cpp)
//调用BIONIC的libc标准库
android_getaddrinfofornet(getaddinfo.c)
android_getaddrinfofornetcontext(getaddinfo.c)
...
发送socket包...
2.2. HTTP DNS流程
1
2
3
4
5
6
7
8
9
//拼装HTTP请求(OkHttp)
client.newCall
//JDK层,进行socket请求
Socket.connect(socket.java)
//framework层
libcore.io.IoBridge.connect
//进行tcp连接(JNI/C)
....
3. HTTP DNS的实现
在OkHttp中,提供了DNS的接口(这个接口基本没人知道,甚至网上都没有讨论),方便用户自己实现lookup方法。下文实现了两个OkHttpClient单例,一个负责dns,一个负责业务。
关于OkHttp,可以看以前的文章
经过考察第三方DNS服务,看起来只有腾讯系的DNSPod还是不错的,它直接返回一个ip地址,不需要解析json等乱七八糟的东西,文档在这里。
DNSPod在返回的Header中,没有设置缓存,并主动断开了Socket连接,这样的话每次进行lookup时都会进行一个GET请求,相比以前原生多了40ms。我们都知道DNS默认的TTL时间一般是600s,因此我们可以通过复用OkHttp中的cache,在返回的response中加入Cache-Control
的Header就可以实现再第二次lookup时避免再次访问网络。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public OkHttpClient getHTTPDnsClient() {
if (httpDnsclient == null) {
final File cacheDir = getExternalCacheDir();
httpDnsclient = new OkHttpClient.Builder()
//消费者工作线程池
.dispatcher(getDispatcher())
//Logger拦截器
.addNetworkInterceptor(getLogger())
.addNetworkInterceptor(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
//在返回header中加入缓存消息
//下次将不再发送请求
.header("Cache-Control", "max-age=600").build();
}
})
//5MB的文件缓存
.cache(new Cache(new File(cacheDir, "httpdns"), 5 * 1024 * 1024))
.build();
}
return httpDnsclient;
}
max-age表示可以续600s;如果缓存命中将返回数据,反之抛出IOException。
通过测试,HTTP DNS工作效果如下:
- 在600s内,无论是否联网,都不会进行请求
- 在600s外,没联网时会抛出IOException,反之会进行HTTP请求
接着,我们实现了DNS查询接口,值得注意的一点就是第三方服务器可能会挂,所以一定要注意所有异常并留出后路。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static Dns HTTP_DNS = new Dns(){
@Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
//防御代码
if (hostname == null) throw new UnknownHostException("hostname == null");
//dnspod提供的dns服务
HttpUrl httpUrl = new HttpUrl.Builder().scheme("http")
.host("119.29.29.29")
.addPathSegment("d")
.addQueryParameter("dn", hostname)
.build();
Request dnsRequest = new Request.Builder().url(httpUrl).get().build();
try {
String s = getHTTPDnsClient().newCall(dnsRequest).execute().body().string();
//避免服务器挂了却无法查询DNS
if (!s.matches("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b")) {
return Dns.SYSTEM.lookup(hostname);
}
return Arrays.asList(InetAddress.getAllByName(s));
} catch (IOException e) {
return Dns.SYSTEM.lookup(hostname);
}
}
};
在真正的业务请求时,构建OkHttpClient
时对DNS进行配置就ok了
1
2
3
4
5
6
7
8
9
10
11
12
13
//真正的调用客户端,供Retrofit与Picasso使用
static public synchronized OkHttpClient getClient() {
if (client == null) {
final File cacheDir = GlobalContext.getInstance().getExternalCacheDir();
client = new OkHttpClient.Builder()
.cache(new Cache(new File(cacheDir, "okhttp"), 60 * 1024 * 1024))
//配置DNS查询实现
.dns(HTTP_DNS)
.build();
}
return client;
}
4. 例子
Fork me on [Github](https://github.com/guofeng007/OkHttpDNS),有更多交流可以与我微信沟通,在深圳的开发者可以与我面基。