一、前言
談到優化,首先第一步,肯定是把一個大功能,拆分成一個個細小的環節,再單個拎出來找到可以優化的點,App 的網路優化也是如此。
在 App 訪問網路的時候,DNS 解析是網路請求的第一步,預設我們使用運營商的 LocalDNS 服務。有資料統計,在這一塊 3G 網路下,耗時在 200~300ms,4G 網路下也需要 100ms。
解析慢,並不是 LocalDNS 最大的問題,它還存在一些更為嚴重的問題,例如:DNS 劫持、DNS 排程不準確(快取、轉發、NAT)導致效能退化等等,這些才是網路優化最應該解決的問題。
想要優化 DNS,現在最簡單成熟的方案,就是使用 HTTPDNS。
今天就來聊聊,DNS、HTTPDNS,以及在 Android 下,如何使用 OKHttp 來整合 HTTPDNS。
二、DNS 和 HTTPDNS
2.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 協議與 DNS 伺服器進行互動。不走傳統的 DNS 解析,從而繞過運營商的 LocalDNS 伺服器,有效的防止了域名劫持,提高域名解析的效率。
這就相當於,每家各自基於 HTTP 協議,自己實現了一套域名解析,自己去維護了一份域名與 IP 的地址簿,而不是使用同一的地址簿(DNS伺服器)。
據說微信有自己部署的 NETDNS,而各大雲服務商,阿里雲和騰訊雲也提供了自己的 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。
如果通過抓包工具抓包,你會發現,原本的類似 http://www.cxmydev.com/api/user
的請求,被替換為:http://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 伺服器,所以建議使用 阿里雲、騰訊雲 這樣相對穩定的雲服務商。
references:
【2】SIN:https://blog.csdn.net/makenothing/article/details/53292335
公眾號後臺回覆成長『成長』,將會得到我準備的學習資料,也能回覆『加群』,一起學習進步;你還能回覆『提問』,向我發起提問。
推薦閱讀:
關於字元編碼,你需要知道的都在這裡 | 圖解:HTTP 範圍請求 | Java 異常處理 | 安卓防止使用者關閉動畫導致動畫失效 | Git 找回遺失的程式碼 | 阿里的 Alpha 助力 App 啟動速度優化