一、前言談到優化,首先第一步,肯定是把一個大功能,拆分成一個個細小的環節,再單個拎出來找到可以優化的點,App 的網絡優化也是如此。在 App 訪問網絡的時候,DNS 解析是網絡請求的第一步,默認我們使用運營商的 LocalDNS 服務。有數據統計,在這一塊 3G 網絡下,耗時在 200~300ms,4G 網絡下也需要 100ms。解析慢,並不是 LocalDNS 最大的問題,它還存在一些更為嚴重的問題,例如:DNS 劫持、DNS 調度不準確(緩存、轉發、NAT)導致性能退化等等,這些才是網絡優化最應該解決的問題。想要優化 DNS,現在最簡單成熟的方案,就是使用 HTTPDNS。今天就來聊聊,DNS、HTTPDNS,以及在 Android 下,如何使用 OKHttp 來集成 HTTPDNS。二、DNS 和 HTTPDNS2.1 什麼是 DNS在說到 HTTPDNS 之前,先簡單瞭解一下什麼是 DNS?在網絡的世界中,每個有效的域名背後都有為其提供服務的服務器,而我們網絡通信的首要條件,就是知道服務器的 IP 地址。但是記住域名(網址)肯定是比記住 IP 地址簡單。如果有某種方法,可以通過域名,查到其提供服務的服務器 IP 地址,那就非常方便瞭。這裡就需要用到 DNS 服務器以及 DNS 解析。DNS(Domain Name System),它的作用就是根據域名,查出對應的 IP 地址,它是 HTTP 協議的前提。隻有將域名正確的解析成 IP 地址後,後面的 HTTP 流程才可以繼續進行下去。DNS 服務器的要求,一定是高可用、高並發和分佈式的服務器。它被分為多個層次結構。根 DNS 服務器:返回頂級域 DNS 服務器的 IP 地址。頂級域 DNS 服務器:返回權威 DNS 服務器的 IP 地址。權威 DNS 服務器:返回相應主機的 IP 地址。這三類 DNS 服務器,類似一種樹狀的結構,分級存在。當開始 DNS 解析的時候,如果 LocalDNS 沒有緩存,那就會向 LocalDNS 服務器請求(通常就是運營商),如果還是沒有,就會一級一級的,從根域名查對應的頂級域名,再從頂級域名查權威域名服務器,最後通過權威域名服務器,獲取具體域名對應的 IP 地址。DNS 在提供域名和 IP 地址映射的過程中,其實提供瞭很多基於域名的功能,例如服務器的負載均衡,但是它也帶來瞭一些問題。2.2 DNS 的問題DNS 的細節還有很多,本文就不展開細說瞭,其問題總結來說就是幾點。1. 不穩定DNS 劫持或者故障,導致服務不可用。2. 不準確LocalDNS 調度,並不一定是就近原則,某些小運營商沒有 DNS 服務器,直接調用其他運營商的 DNS 服務器,最終直接跨網傳輸。例如:用戶側是移動運營商,調度到瞭電信的 IP,造成訪問慢,甚至訪問受限等問題。3. 不及時運營商可能會修改 DNS 的 TTL(Time-To-Live,DNS 緩存時間),導致 DNS 的修改,延遲生效。還有運營商為瞭保證網內用戶的訪問質量,同時減少跨網結算,運營商會在網內搭建內容緩存服務器,通過把域名強行指向內容緩存服務器的地址,來實現本地本網流量完全留在本地的目的。對此不同運營商甚至實現都不一致,這對我們來說就是個黑匣子。正是因為 DNS 存在種種問題,所以牽出瞭 HTTPDNS。2.3 HTTPDNS 的解決方案DNS 不僅支持 UDP,它還支持 TCP,但是大部分標準的 DNS 都是基於 UDP 與 DNS 服務器的 53 端口進行交互。HTTPDNS 則不同,顧名思義它是利用 HTTP 協議與 NDS 服務器的 80 端口進行交互。不走傳統的 DNS 解析,從而繞過運營商的 LocalDNS 服務器,有效的防止瞭域名劫持,提高域名解析的效率。這就相當於,每傢各自基於 HTTP 協議,自己實現瞭一套域名解析,自己去維護瞭一份域名與 IP 的地址簿,而不是使用同一的地址簿(DNS服務器)。各大雲服務商,阿裡雲和騰訊雲也提供瞭自己的 HTTPDNS 服務,對於我們普通開發者,隻需要付出少量的費用,在手機端嵌入支持 HTTPDNS 的客戶端 SDK,即可使用。三、 OKHttp 接入 HTTPDNS既然瞭解瞭 HTTPDNS 的重要性,接下來看看如何在 OkHttp 中,集成 HTTPDNS。OkHttp 是一個處理網絡請求的開源項目,是 Android 端最火熱的輕量級網絡框架。在 OkHttp 中,默認是使用系統的 DNS 服務 InetAddress 進行域名解析。InetAddress ip2= InetAddress.getByName(“www.cxmydev.com”);
System.out.println(ip2.getHostAddress());
System.out.println(ip2.getHostName());
而想在 OkHttp 中使用 HTTPDNS,有兩種方式。通過攔截器,在發送請求之前,將域名替換為 IP 地址。通過 OkHttp 提供的 .dns() 接口,配置 HTTPDNS。對這兩種方法來說,當然是推薦使用標準 API 來實現瞭。攔截器的方式,也建議有所瞭解,實現很簡單,但是有坑。3.1 攔截器接入方式1. 攔截器接入攔截器是 OkHttp 中,非常強大的一種機制,它可以在請求和響應之間,做一些我們的定制操作。在 OkHttp 中,可以通過實現 Interceptor 接口,來定制一個攔截器。使用時,隻需要在 OkHttpClient.Builder 中,調用 addInterceptor() 方法來註冊此攔截器即可。OkHttp 的攔截器不是本文的重點,我們還是回到攔截器去實現 HTTPDNS 的話題上,攔截器沒什麼好說的,直接上相關代碼。class HTTPDNSInterceptor : Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
val originRequest = chain.request()
val httpUrl = originRequest.url()
val url = httpUrl.toString()
val host = httpUrl.host()
val hostIP = HttpDNS.getIpByHost(host)
val builder = originRequest.newBuilder()
if(hostIP!=null){
builder.url(HttpDNS.getIpUrl(url,host,hostIP))
builder.header(“host”,hostIP)
}
val newRequest = builder.build()
val newResponse = chain.proceed(newRequest)
return newResponse
}
}
在攔截器中,使用 HttpDNS 這個幫助類,通過 getIpByHost() 將 Host 轉為對應的 IP。如果通過抓包工具抓包,你會發現,原本的類似 www.cxmydev.com/api/user 的請求,被替換為:220.181.57.xxx/api/user。2. 攔截器接入的壞處使用攔截器,直接繞過瞭 DNS 的步驟,在請求發送前,將 Host 替換為對應的 IP 地址。這種方案,在流程上很清晰,沒有任何技術性的問題。但是這種方案存在一些問題,例如:HTTPS 下 IP 直連的證書問題、代理的問題、Cookie 的問題等等。其中最嚴重的問題是,此方案(攔截器+HTTPDNS)遇到 https 時,如果存在一臺服務器支持多個域名,可能導致證書無法匹配的問題。在說到這個問題之前,就要先瞭解一下 HTTPS 和 SNI。HTTPS 是為瞭保證安全的,在發送 HTTPS 請求之前,首先要進行 SSL/TLS 握手,握手的大致流程如下:客戶端發起握手請求,攜帶隨機數、支持算法列表等參數。服務端根據請求,選擇合適的算法,下發公鑰證書和隨機數。客戶端對服務端證書,進行校驗,並發送隨機數信息,該信息使用公鑰加密。服務端通過私鑰獲取隨機數信息。雙方根據以上交互的信息,生成 Session Ticket,用作該連接後續數據傳輸的加密密鑰。在這個流程中,客戶端需要驗證服務器下發的證書。首先通過本地保存的根證書解開證書鏈,確認證書可信任,然後客戶端還需要檢查證書的 domain 域和擴展域,看看是否包含本次請求的 HOST。在這一步就出現瞭問題,當使用攔截器時,請求的 URL 中,HOST 會被替換成 HTTPDNS 解析出來的 IP。當服務器存在多域名和證書的情況下,服務器在建立 SSL/TLS 握手時,無法區分到底應該返回那個證書,此時的策略可能返回默認證書或者不返回,這就有可能導致客戶端在證書驗證 domain 時,出現不匹配的情況,最終導致 SSL/TLS 握手失敗。這就引發出來 SNI 方案,SNI(Server Name Indication)是為瞭解決一個服務器使用多個域名和證書的 SSL/TLS 擴展。SNI 的工作原理,在連接到服務器建立 SSL 連接之前,先發送要訪問站點的域名(hostname),服務器根據這個域名返回正確的證書。現在,大部分操作系統和瀏覽器,都已經很好的支持 SNI 擴展。3. 攔截器 + HTTPDNS 的解決方案這個問題,其實也有解決方案,這裡簡單介紹一下。針對 “domain 不匹配” 的問題,可以通過 hook 證書驗證過程中的第二步,將 IP 直接替換成原來的域名,再執行證書驗證。而 HttpURLConnect,提供瞭一個 HostnameVerifier 接口,實現它即可完成替換。public interface HostnameVerifier {
public boolean verify(String hostname, SSLSession session);
}
如果使用 OkHttp,可以參考 OkHostnameVerifier (source://src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java) 的實現,進行替換。本身 OkHttp 就不建議通過攔截器去做 HTTPDNS 的支持,所以這裡就不展開討論瞭,這裡隻提出解決的思路,有興趣可以研究研究源碼。3.2 OKHttp 標準 API 接入OkHttp 其實本身已經暴露瞭一個 Dns 接口,默認的實現是使用系統的 InetAddress 類,發送 UDP 請求進行 DNS 解析。我們隻需要實現 OkHttp 的 Dns 接口,即可獲得 HTTPDNS 的支持。在我們實現的 Dns 接口實現類中,解析 DNS 的方式,換成 HTTPDNS,將解析結果返回。class HttpDns : Dns {
override fun lookup(hostname: String): List<InetAddress> {
val ip = HttpDnsHelper.getIpByHost(hostname)
if (TextUtils.isEmpty(ip)) {
//返回自己解析的地址列表
return InetAddress.getAllByName(ip).toList()
} else {
// 解析失敗,使用系統解析
return Dns.SYSTEM.lookup(hostname)
}
}
}
使用也非常的簡單,在 OkHttp.build() 時,通過 dns() 方法配置。mOkHttpClient = httpBuilder
.dns(HttpDns())
.build();
這樣做的好處在於:還是用域名進行訪問,隻是底層 DNS 解析換成瞭 HTTPDNS,以確保解析的 IP 地址符合預期。HTTPS 下的問題也得到解決,證書依然使用域名進行校驗。OkHttp 既然暴露出 dns 接口,我們就盡量使用它。四、小結時刻現在大傢知道,在做 App 的網絡優化的時候,第一步就是使用 HTTPDNS 優化 DNS 的步驟。所有的優化當然是以最終效果為目的,這裡提兩條大廠公開的數據,對騰訊的產品,在接入 HTTPDNS 後,用戶平均延遲下降超過 10%,訪問失敗率下降超過五分之一。而百度 App 的 Feed 業務,Android 劫持率由 0.25% 降低到 0.05%。此種優化方案,非常依賴 HTTPDNS 服務器,所以建議使用 阿裡雲、騰訊雲 這樣相對穩定的雲服務商。在頭條號私信我。我會送你一些我整理的學習資料,包含:Android反編譯、算法、設計模式、虛擬機、Linux、Kotlin、Python、爬蟲、Web項目源碼。
本文出自快速备案,转载时请注明出处及相应链接。