閱讀82 返回首頁    go 阿裏雲 go 技術社區[雲棲]


HttpClient/HttpURLConnection + HttpDns最佳實踐

在Android端如果OkHttp作為網絡請求框架,由於其提供了自定義DNS服務接口,可以很優雅地結合HttpDns,相關實現可參考:HttpDns+OkHttp最佳實踐
如果您使用HttpClientHttpURLConnection發起網絡請求,盡管無法直接自定義Dns服務,但是由於HttpClientHttpURLConnection也通過InetAddress進行域名解析,通過修改InetAddress的DNS緩存,同樣可以比通用方案更為優雅地使用HttpDns。

InetAddress在虛擬機層麵提供了域名解析能力,通過調用InetAddress.getByName(String host)即可獲取域名對應的IP。調用InetAddress.getByName(String host)時,InetAddress會首先檢查本地是否保存有對應域名的ip緩存,如果有且未過期則直接返回;如果沒有則調用係統DNS服務(Android的DNS也是采用NetBSD-derived resolver library來實現)獲取相應域名的IP,並在寫入本地緩存後返回該IP。

核心代碼位於java.net.InetAddress.lookupHostByName(String host, int netId)

public class InetAddress implements Serializable {
  ...
      /**
     * Resolves a hostname to its IP addresses using a cache.
     *
     * @param host the hostname to resolve.
     * @param netId the network to perform resolution upon.
     * @return the IP addresses of the host.
     */
    private static InetAddress[] lookupHostByName(String host, int netId)
            throws UnknownHostException {
        BlockGuard.getThreadPolicy().onNetwork();
        // Do we have a result cached?
        Object cachedResult = addressCache.get(host, netId);
        if (cachedResult != null) {
            if (cachedResult instanceof InetAddress[]) {
                // A cached positive result.
                return (InetAddress[]) cachedResult;
            } else {
                // A cached negative result.
                throw new UnknownHostException((String) cachedResult);
            }
        }
        try {
            StructAddrinfo hints = new StructAddrinfo();
            hints.ai_flags = AI_ADDRCONFIG;
            hints.ai_family = AF_UNSPEC;
            // If we don't specify a socket type, every address will appear twice, once
            // for SOCK_STREAM and one for SOCK_DGRAM. Since we do not return the family
            // anyway, just pick one.
            hints.ai_socktype = SOCK_STREAM;
            InetAddress[] addresses = Libcore.os.android_getaddrinfo(host, hints, netId);
            // TODO: should getaddrinfo set the hostname of the InetAddresses it returns?
            for (InetAddress address : addresses) {
                address.hostName = host;
            }
            addressCache.put(host, netId, addresses);
            return addresses;
        } catch (GaiException gaiException) {
          ...
        }
    }
}

其中addressCacheInetAddress的本地緩存:

private static final AddressCache addressCache = new AddressCache();

結合InetAddress的解析策略,我們可以通過如下方法實現自定義DNS服務:

  • 通過HttpDns SDK獲取目標域名的ip
  • 利用反射的方式獲取到InetAddress.addressCache對象
  • 利用反射方式調用addressCache.put()方法,域名和ip的對應關係寫入InetAddress緩存

具體實現可參考以下代碼:

public class CustomDns {

    public static void writeSystemDnsCache(String hostName, String ip) {
        try {
            Class inetAddressClass = InetAddress.class;
            Field field = inetAddressClass.getDeclaredField("addressCache");
            field.setAccessible(true);
            Object object = field.get(inetAddressClass);
            Class cacheClass = object.getClass();
            Method putMethod;
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                //put方法在api21及以上為put(String host, int netId, InetAddress[] address)
                putMethod = cacheClass.getDeclaredMethod("put", String.class, int.class, InetAddress[].class);
            } else {
                //put方法在api20及以下為put(String host, InetAddress[] address)
                putMethod = cacheClass.getDeclaredMethod("put", String.class, InetAddress[].class);
            }
            putMethod.setAccessible(true);
            String[] ipStr = ip.split("\\.");
            byte[] ipBuf = new byte[4];
            for(int i = 0; i < 4; i++) {
                ipBuf[i] = (byte) (Integer.parseInt(ipStr[i]) & 0xff);
            }
            if(Build.VERSION.SDK_INT  >= Build.VERSION_CODES.LOLLIPOP) {
                putMethod.invoke(object, hostName, 0, new InetAddress[] {InetAddress.getByAddress(ipBuf)});
            } else {
                putMethod.invoke(object, hostName, new InetAddress[] {InetAddress.getByAddress(ipBuf)});
            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

和通用方案相比,使用該方法具有下列優勢:

  • 實現簡單
  • 通用性強,該方案在HTTPS,SNI以及設置Cookie等場景均適用。規避了證書校驗,域名檢查等環節
  • 全局生效,InetAddress.addressCache為全局單例,該方案對所有使用InetAddress作為域名解析服務的請求全部生效

  • AddressCache的默認TTL為2S,且默認最多可以保存16條緩存記錄:

    class AddressCache {
    ...
       /**
        * When the cache contains more entries than this, we start dropping the oldest ones.
        * This should be a power of two to avoid wasted space in our custom map.
        */
       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;
       }
    }
    

    Android虛擬機下反射規則與JVM存在差異,無法直接修改final變量的值。所以使用該方法請務必注意IP過期時間及緩存數量。另外針對該問題可嚐試另一種解決方案:重寫AddressCache類,並通過ClassLoader優先加載,覆蓋係統類。

  • AddressCache.put方法在 API 21進行了改動,增加了netId參數,為保證兼容性需要針對不同版本區別處理。具體方案參考上文代碼

  • 該方式可以解決HTTPS,SNI以及設置cookie等場景,但不適用於WebView場景。Android Webview使用ChromiumWebkit作為內核(Android 4.4開始,Webview內核由Chromium替代Webkit)。上述兩者均繞開InetAddress而直接使用係統DNS服務,所以該方案對此場景無效。

最後更新:2017-04-14 13:32:07

  上一篇:go MaxCompute(ODPS)大數據容災方案與實現(及項目落地實例)專有雲
  下一篇:go 阿裏雲APP上線“備案刷臉核驗”功能  網站備案時間大幅縮短