一、什么是DNS
DNS(Domain Name System,域名系统),dns用于将域名解析解析为ip地址。
例如:给你www.baidu.com的主机名,你给
我查出对应的ip地址:163.177.151.109。一些主机名还会有别名,如www.baidu.com就
有别名www.a.shifen.com,甚至不止一个别名,或一个别名有2个ip地址。在linux机子
上,运行nslookup(name service lookup)就是进行域名解析。如下面:
~$ nslookup www.baidu.com
Server: 127.0.0.1
Address: 127.0.0.1#53
Non-authoritative answer:
www.baidu.com canonical name = www.a.shifen.com.
Name: www.a.shifen.com
Address: 163.177.151.109
Name: www.a.shifen.com
Address: 163.177.151.110
DNS工作方式分为递归查询和迭代查询,具体可参考下图
DNS还可以用于负载均衡、域名污染、防火墙,这些不在这里讨论。
二、DNS缓存
所谓DNS缓存有两种,比如主从同步缓存和本地缓存,这里对于手机来说,重点是本地DNS缓存。Android基于Linux系统,对于Android App来说,这个缓存又多了java层。
2.1 使用场景
当然,我们需要明白在Android App中那些场景需要进行,这才是最重要的,有时候其实并没有必要去更新缓存。总结一下,这里的场景无非如下几种:
场景一:存在多个运营商或者多个地区的分布式业务系统
比如互联网分布式业务系统,采取的是分区域、分运营商的方式不是业务系统。
场景二:存在多个域名的业务系统,需要提前解析并且缓存ip
<link rel="dns-prefetch" href="//g.alicdn.com" />
<link rel="dns-prefetch" href="//img.alicdn.com" />
<link rel="dns-prefetch" href="//tui.taobao.com" />
这是taobao网的dns-prefetch link,这一步是为了加速其他页面的dns
场景三:ip地址唯一,但是存在多个子域名高并发请求
综上所述:我们可以理解为,当且仅当域名和ip地址的关系是“一对多”、“多对多”和“多对一”的情况下,可适当更新DNS缓存。
2.2系统版本情况说明
Android 4.3之前的TTL(Time To Live)分为正负两种有效期,正有效期为10分钟,最大缓存为120个,采用TTL算法回收。
// 默认有效DNS缓存时间(TTL). 600 seconds (10 minutes).
private static final long DEFAULT_POSITIVE_TTL_NANOS = 600 * 1000000000L;
// 默认无效缓存时间(TTL). 10 seconds.
private static final long DEFAULT_NEGATIVE_TTL_NANOS = 10 * 1000000000L;
Android 4.3+的系统,缓存修正为2秒,最大缓存为16个,采用LRU算法和TTL算法进行回收。
private static final long TTL_NANOS = 2 * 1000000000L;
注意:以上代码参见java.net.AddressCache.java
三、Android DNS缓存更新
3.1、修正缓存过期时间
在Android4.3之前,TTL可以用个System.setProperties进行设置,就可以将TTL修正为何Android 4.3+一致的生存时间
Security.setProperty("networkaddress.cache.ttl", String.valueOf(2 * 1000000000L));
Security.setProperty("networkaddress.cache.negative.ttl", String.valueOf(2 * 1000000000L))
3.2 实现DNS-Prefetch
步骤3.1只是让缓存过期时间缩短了,一定程度上处理了Android 4.3之前系统的不足。但是,对于存在域名和ip“一对多”,“多对多”和“多对一”的分布式系统,如果出现网络切换,那么下次获“可能”取依旧比较耗时。因此,预获取dns是非常必要的。那么如何实现DNS-Prefetch呢
首先,我们需要统一规范接口
public interface Dns {
Dns SYSTEM = new Dns() {
@Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
if (hostname == null) throw new UnknownHostException("hostname == null");
return Arrays.asList(InetAddress.getAllByName(hostname));
}
};
List<InetAddress> lookup(String hostname) throws UnknownHostException;
}
实现接口
public class DnsManager implements Dns {
private static DnsManager singleInstance;
private final TreeSet<String> HOST_SET = new TreeSet<String>();
public static DnsManager getDefault(){
if(singleInstance==null) {
synchronized (DnsManager.class)
{
if (singleInstance == null) {
singleInstance = new DnsManager();
}
}
}
return singleInstance;
}
@Override
public synchronized List<InetAddress> lookup(String hostname) throws UnknownHostException {
try {
if(TextUtils.isEmpty(hostname) || TextUtils.isEmpty(hostname.trim())){
throw new UnknownHostException("hostname == null");
}
List<InetAddress> list = Dns.SYSTEM.lookup(hostname);
HOST_SET.add(hostname);
return list;
}catch (Exception e){
e.printStackTrace();
return Arrays.asList(null);
}
}
public synchronized String quickLookup(String hostname) throws UnknownHostException {
try {
if(TextUtils.isEmpty(hostname) || TextUtils.isEmpty(hostname.trim())){
throw new UnknownHostException("hostname == null");
}
final Uri uri = Uri.parse(hostname);
InetAddress inetAddress = InetAddress.getByName(uri.getHost());
if(inetAddress==null) {
Throw.exception("unkown host",UnknownHostException.class);
}
String dnsIp = inetAddress.getHostAddress();
HOST_SET.add(hostname);
return dnsIp;
} catch (Exception e) {
e.printStackTrace();
return Lists.newArrayList();
}
}
/**
* 清除dns缓存
*/
public synchronized void clearDnsCache(){
try {
ReflectUtils.invokeMethodByName(InetAddress.class, "clearDnsCache");
}catch (Exception e){
e.printStackTrace();
return;
}
}
/**
* 获取主机集合
* @return
*/
public synchronized TreeSet<String> getHostSet() {
return HOST_SET;
}
/**
* 预加载DNS
* @param hosts
*/
public synchronized void prefetchDns(List<String> hosts) {
if(hosts==null && hosts.size()==0) return;
for (String hostname:hosts ) {
prefetchDns(hostname);
}
}
/**
* 预加载DNS
* @param hostname
*/
public synchronized void prefetchDns(String hostname) {
try{
InetAddress.getAllByName(hostname);
}catch (Exception e){
e.printStackTrace();
return;
}
}
}
使用时机
通常网络切换后,并且下次联网成功时,我们prefetch时最好的时间,这里我们需要通过Broadcast+IntentService
对于广播部分,我们需要监听如下两个Action(这里推荐使用动态广播)
IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
广播实现代码
public class NetStateChangeReceiver extends BroadcastReceiver{
private static final String TAG = NetStateChangeReceiver.class.getSimpleName();
private AtomicReference<String> pendingNetworkState = null;
private AtomicReference<String> pendingSSID = null;
public NetStateChangeReceiver() {
pendingNetworkState = new AtomicReference<String>();
pendingSSID = new AtomicReference<>();
}
@Override
public void onReceive(Context context, Intent intent) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
NetworkType networkType = NetworkUtils.getNetworkType(context);
notifyObservers(networkType);
}
if(shouldStartDnsUpdateService(context,intent)) {
Intent cloneFilter = intent.cloneFilter();
cloneFilter.setClass(context, DnsUpdateIntentService.class);
context.startService(cloneFilter);
}
}
//网络可用并且网络切换的情况下启动IntentService更新
public boolean shouldStartDnsUpdateService(Context context,Intent intent){
if(NetworkUtils.isAvailable(context)){
NetworkType type = NetworkUtils.getNetworkType(context);
if(type==null) return false ;
String newState = type.toString();
String lastState = pendingNetworkState.get();
if(!TextUtils.isEmpty(lastState) && !lastState.equals(newState))
{
pendingNetworkState.set(newState);
return true;
}else{
pendingNetworkState.set(newState);
if(NetworkUtils.isWifiConnected(context)){
WifiInfo wifiInfo= intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
if(wifiInfo!=null)
{
String nextSSID = wifiInfo.getSSID();
String lastSSID = pendingSSID.get();
if(nextSSID!=null && nextSSID.equals(lastSSID))
{
return true;
}
}
}
}
}else{
pendingNetworkState.set(NetworkType.NETWORK_NO.toString());
}
return false;
}
}
DnsUpdateIntentService代码如下
public class DnsUpdateIntentService extends IntentService {
public DnsUpdateIntentService() {
super(DnsUpdateIntentService.class.getName());
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
runTask();
}
private void runTask() {
GFLog.d(DnsUpdateIntentService.class.getSimpleName()," startDns : 开始更新DNS ");
updateDnsCache();
GFLog.d(DnsUpdateIntentService.class.getSimpleName()," endDns : DNS更新完成 ");
}
private void updateDnsCache() {
try{
DnsManager dm = DnsManager.getDefault();
dm.clearDnsCache();
TreeSet<String> hostSet = dm.getHostSet();
List<String> hosts = new ArrayList<>();
hosts.addAll(hostSet);
dm.prefetchDns(hosts);
}catch (Exception e){
e.printStackTrace();
return;
}
}
}
注意:DnsUpdateIntentService不可以注册为多进程,否则缓存无法更新
3.3、DNS防篡改与安全
Android 4.3之前的DNS可能存在被污染的可能,如修改resolv.conf文件,在Android 4.3+之后,统一使用Netd方式,安全性上有所提高。因此,对Android 4.3之前的系统,建议使用HttpDNS等方案,此外采取HTTPS的通信方式,一定程度上几乎可以绝对避免此类问题的发生。
此外,我们在ip与域名对应数量不大的app中,可以在App中提前建立不同机房的域名映射也是一种放置篡改的方案。
3.4、Android底层DNS更新
Android基于linux,底层通过Libcore.so更新DNS,目前没有方式来更新Linux层面的DNS缓存。那么,我们的DNS-Prefetch功能是否有必要呢?这个问题我们需要明确,虽然我们不一定能更新底层DNS,但是,可以促进底层DNS更新,类似System.gc()的作用。