Android DNS更新與DNS-Prefetch

1、什麼是DNS

DNS(Domain Name System,域名系統),dns用於將域名解析解析爲ip地址。css

例如:給你www.baidu.com的主機名,你給 
我查出對應的ip地址:163.177.151.109。一些主機名還會有別名,如www.baidu.com就 
有別名www.a.shifen.com,甚至不止一個別名,或一個別名有2個ip地址。在linux機子 
上,運行nslookup(name service lookup)就是進行域名解析。以下面:java

~$ 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工做方式分爲遞歸查詢和迭代查詢,具體可參考下圖linux

 

DNS還能夠用於負載均衡、域名污染、防火牆,這些不在這裏討論。算法

 

2、DNS緩存

所謂DNS緩存有兩種,好比主從同步緩存和本地緩存,這裏對於手機來講,重點是本地DNS緩存。Android基於Linux系統,對於Android App來講,這個緩存又多了java層。緩存

2.1 使用場景安全

固然,咱們須要明白在Android App中那些場景須要進行,這纔是最重要的,有時候其實並無必要去更新緩存。總結一下,這裏的場景無非以下幾種:網絡

 

場景一:存在多個運營商或者多個地區的分佈式業務系統併發

好比互聯網分佈式業務系統,採起的是分區域、分運營商的方式不是業務系統。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

 

3、Android DNS緩存更新

3.一、修正緩存過時時間

在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 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 null; } } /** * 清除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.三、DNS防篡改與安全

Android 4.3以前的DNS可能存在被污染的可能,如修改resolv.conf文件,在Android 4.3+以後,統一使用Netd方式,安全性上有所提升。所以,對Android 4.3以前的系統,建議使用HttpDNS等方案,此外採起HTTPS的通訊方式,必定程度上幾乎能夠絕對避免此類問題的發生。

此外,咱們在ip與域名對應數量不大的app中,能夠在App中提早創建不一樣機房的域名映射也是一種放置篡改的方案。

 

3.四、Android底層DNS更新

Android基於linux,底層經過Libcore.so更新DNS,目前沒有方式來更新Linux層面的DNS緩存。那麼,咱們的DNS-Prefetch功能是否有必要呢?這個問題咱們須要明確,雖然咱們不必定能更新底層DNS,可是,能夠促進底層DNS更新,相似System.gc()的做用。

相關文章
相關標籤/搜索