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作为域名解析服务的请求全部生效

<font color="red">另外使用该方案请务必注意以下几点:</font>

  • 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服务,所以该方案对此场景无效。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容