HttpClient/HttpURLConnection + HttpDns最佳實(shí)踐

在Android端如果OkHttp作為網(wǎng)絡(luò)請求框架泻拦,由于其提供了自定義DNS服務(wù)接口,可以很優(yōu)雅地結(jié)合HttpDns忽媒,相關(guān)實(shí)現(xiàn)可參考:HttpDns+OkHttp最佳實(shí)踐争拐。
如果您使用HttpClientHttpURLConnection發(fā)起網(wǎng)絡(luò)請求,盡管無法直接自定義Dns服務(wù)晦雨,但是由于HttpClientHttpURLConnection也通過InetAddress進(jìn)行域名解析架曹,通過修改InetAddress的DNS緩存隘冲,同樣可以比通用方案更為優(yōu)雅地使用HttpDns。

InetAddress在虛擬機(jī)層面提供了域名解析能力绑雄,通過調(diào)用InetAddress.getByName(String host)即可獲取域名對應(yīng)的IP展辞。調(diào)用InetAddress.getByName(String host)時,InetAddress會首先檢查本地是否保存有對應(yīng)域名的ip緩存万牺,如果有且未過期則直接返回罗珍;如果沒有則調(diào)用系統(tǒng)DNS服務(wù)(Android的DNS也是采用NetBSD-derived resolver library來實(shí)現(xiàn))獲取相應(yīng)域名的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();

結(jié)合InetAddress的解析策略覆旱,我們可以通過如下方法實(shí)現(xiàn)自定義DNS服務(wù):

  • 通過HttpDns SDK獲取目標(biāo)域名的ip
  • 利用反射的方式獲取到InetAddress.addressCache對象
  • 利用反射方式調(diào)用addressCache.put()方法,域名和ip的對應(yīng)關(guān)系寫入InetAddress緩存

具體實(shí)現(xiàn)可參考以下代碼:

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();
        }
    }
}

和通用方案相比核无,使用該方法具有下列優(yōu)勢:

  • 實(shí)現(xiàn)簡單
  • 通用性強(qiáng)扣唱,該方案在HTTPS,SNI以及設(shè)置Cookie等場景均適用。規(guī)避了證書校驗(yàn)厕宗,域名檢查等環(huán)節(jié)
  • 全局生效画舌,InetAddress.addressCache為全局單例,該方案對所有使用InetAddress作為域名解析服務(wù)的請求全部生效

<font color="red">另外使用該方案請務(wù)必注意以下幾點(diǎn):</font>

  • AddressCache的默認(rèn)TTL為2S已慢,且默認(rèn)最多可以保存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虛擬機(jī)下反射規(guī)則與JVM存在差異曲聂,無法直接修改final變量的值。所以使用該方法請務(wù)必注意IP過期時間及緩存數(shù)量佑惠。另外針對該問題可嘗試另一種解決方案:重寫AddressCache類朋腋,并通過ClassLoader優(yōu)先加載,覆蓋系統(tǒng)類膜楷。

  • AddressCache.put方法在 API 21進(jìn)行了改動旭咽,增加了netId參數(shù),為保證兼容性需要針對不同版本區(qū)別處理赌厅。具體方案參考上文代碼

  • 該方式可以解決HTTPS穷绵,SNI以及設(shè)置cookie等場景,但不適用于WebView場景特愿。Android Webview使用ChromiumWebkit作為內(nèi)核(Android 4.4開始仲墨,Webview內(nèi)核由Chromium替代Webkit)。上述兩者均繞開InetAddress而直接使用系統(tǒng)DNS服務(wù)揍障,所以該方案對此場景無效目养。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市毒嫡,隨后出現(xiàn)的幾起案子癌蚁,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,599評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件努释,死亡現(xiàn)場離奇詭異碘梢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)伐蒂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評論 3 385
  • 文/潘曉璐 我一進(jìn)店門痘系,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人饿自,你說我怎么就攤上這事×淦海” “怎么了昭雌?”我有些...
    開封第一講書人閱讀 158,084評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長健田。 經(jīng)常有香客問我烛卧,道長,這世上最難降的妖魔是什么妓局? 我笑而不...
    開封第一講書人閱讀 56,708評論 1 284
  • 正文 為了忘掉前任总放,我火速辦了婚禮,結(jié)果婚禮上好爬,老公的妹妹穿的比我還像新娘局雄。我一直安慰自己,他們只是感情好存炮,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評論 6 386
  • 文/花漫 我一把揭開白布炬搭。 她就那樣靜靜地躺著,像睡著了一般穆桂。 火紅的嫁衣襯著肌膚如雪宫盔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,021評論 1 291
  • 那天享完,我揣著相機(jī)與錄音灼芭,去河邊找鬼。 笑死般又,一個胖子當(dāng)著我的面吹牛彼绷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播倒源,決...
    沈念sama閱讀 39,120評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼苛预,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了笋熬?” 一聲冷哼從身側(cè)響起热某,我...
    開封第一講書人閱讀 37,866評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后昔馋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體筹吐,經(jīng)...
    沈念sama閱讀 44,308評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評論 2 327
  • 正文 我和宋清朗相戀三年秘遏,在試婚紗的時候發(fā)現(xiàn)自己被綠了丘薛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,768評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡邦危,死狀恐怖洋侨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情倦蚪,我是刑警寧澤希坚,帶...
    沈念sama閱讀 34,461評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站陵且,受9級特大地震影響裁僧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜慕购,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評論 3 317
  • 文/蒙蒙 一聊疲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沪悲,春花似錦获洲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,850評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至握截,卻和暖如春飞崖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谨胞。 一陣腳步聲響...
    開封第一講書人閱讀 32,082評論 1 267
  • 我被黑心中介騙來泰國打工固歪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胯努。 一個月前我還...
    沈念sama閱讀 46,571評論 2 362
  • 正文 我出身青樓牢裳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叶沛。 傳聞我的和親對象是個殘疾皇子蒲讯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評論 2 350

推薦閱讀更多精彩內(nèi)容