Android OkHttp實現HttpDns的最佳實踐(非攔截器)
之前寫過一篇文章 Android 使用OkHttp支援HttpDNS,該文章中使用的是OkHttp的攔截器來實現HttpDNS。在請求發出去之前,將URL中的域名替換成ip,再往Header中新增Host。這種方式有以下優點。
- 上層方便控制哪些請求使用了HttpDNS,可以做相應的容災處理,比如ip請求失敗時使用域名進行重試。
同樣的也有很多缺點。
- Https場景下ip直連出現的證書校驗問題
- 代理場景下的HttpDNS問題
- ip訪問的時候Cookie的問題
於是,不得不尋找一種更加的解決方案,OkHttp其實暴露了一個Dns介面,預設的實現是使用系統的方法傳送udp請求進行dns解析。於是,我們就可以實現一個Dns介面,解析的方式使用httpdns,將解析結果返回,介面實現之後將系統預設的Dns介面替換成我們的Dns介面。
首先,新建HttpDns類,實現Dns介面。內部維持一個系統預設的Dns物件。
public class HttpDns implements Dns {
private static final Dns SYSTEM = Dns.SYSTEM;
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
Log.e("HttpDns", "lookup:" + hostname);
return SYSTEM.lookup(hostname);
}
}
我們只需要在lookup方法中呼叫HttpDns的SDK去獲取IP,如果獲取到的ip非空,並且ttl沒有過期,則使用HttpDns。完整的方法的程式碼如下
public class HttpDns implements Dns {
private static final Dns SYSTEM = Dns.SYSTEM;
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
Log.e("HttpDns" , "lookup:" + hostname);
String ip = DNSHelper.getIpByHost(hostname);
if (ip != null && !ip.equals("")) {
List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
Log.e("HttpDns", "inetAddresses:" + inetAddresses);
return inetAddresses;
}
return SYSTEM.lookup(hostname);
}
}
DNSHelper類中做的就是將域名轉換為ip,具體轉換過程涉及到快取,這裡不展開,可以參考新浪的HTTPDNS庫https://github.com/CNSRE/HTTPDNSLib
之後初始化OkHttp的時候將Dns替換為HttpDns物件。
OkHttpClient client = new OkHttpClient.Builder()
.dns(new HttpDns())
.build();
這樣,使用client物件發出去的請求都是走httpdns解析dns的,除非沒有命中httpdns快取。
這樣做有什麼好處呢?這樣做相當於還是用域名進行訪問,只不過底層的dns解析換成了http協議。也就是和之前系統的dns解析沒有差別,但是得保證httpdns返回的ip是正確的。
- https下不會存在任何問題,證書校驗依然使用域名進行校驗
- cookie的問題也自然不存在。
同樣的,解決了一部分問題後,也有一部分的風險。風險在哪呢?
- 過於底層,容災不好做,除非強制關閉Httpdns。
- 伺服器返回的ip如果不正確,這次請求就掛了,甚至下次也可能掛了。
- OkHttp預設對解析結果有一定時間的快取,萬一ttl過期了,okhttp可能依然會去使用,這時候也是有風險的。
對比兩種方案,各有各的優點,一個方便做容災,但同時也暴露出了很多問題,一個不方便做容災,但是之前暴露出來的問題都不存在。所以,這時候就得根據實際情況衡量選擇哪一種方案了。
還有就是WebView的HttpDns,目前看來Android的Webview簡直就是一個Bug的存在,沒有什麼好的解決方法。在IOS中,Webview的請求是一個正常的HttpRequest物件,不會像Android中存在這種問題,Andorid中比較好的解決方法就是在native層的網路庫裡開一個代理伺服器,將Webview的所有請求轉發至這個代理伺服器,由這個代理伺服器將請求通過httpdns轉換成ip請求,將請求結果返回給webview。
或者通過WebView的資源攔截介面攔截資源請求,不過這種方式只能處理資源,處理正常的http/https請求會存在問題,在Android5.0以下存在cookie種不進去的問題。
總之WebView就是蛋疼的存在,沒有什麼好的辦法,除非有自己的Webview的容器~