1. 程式人生 > 實用技巧 >深入理解web協議(二):DNS、WebSocket

深入理解web協議(二):DNS、WebSocket

本文首發於 vivo網際網路技術 微信公眾號
連結:https://mp.weixin.qq.com/s/AkbAN4UZLDf841g1ZLFPBQ
作者:Wu Yue

本文系統性的講述了 DNS 協議與 WebSocket 協議的重要細節。

一、DNS

1、Linux dig命令

我們首先通過 Linux 下的dig命令來了解一下 DNS 是怎麼做域名解析的。我們首先輸入命令:

dig www.baidu.com

看下標註的紅框,從左到右依次代表:

  • 域名的名稱 也就是伺服器名稱
  • 網路型別,DNS協議在設計的時候考慮到了其他網路型別,但是目前位置這個值還是寫死的IN 你就理解成是網際網路就可以了。這個值一般不變
  • 標識域名對應何種型別的地址,A 就代表ip的地址。

這裡可能有人會問了,這個域名的後面為啥還有個“.”?我們輸入的明明是www.baidu.com不是www.baidu.com.啊 。

這裡要提一下:

末尾的.代表的就是根域名,每個域名都有根域名,所以通常我們會省略它

根域名的下一級叫頂級域名,比如我們熟知的.com與.net。

再下一級就是次級域名了,比如例子中的.baidu。這個次級域名只要你有錢是可以隨便註冊的。

最後這個www,這個代表三級域名。一般是使用者在自己的域裡面為伺服器分配的名稱。使用者可以隨便分他。

所以可以看出來這裡的域名是分級別的。能弄明白這點就能搞清楚為什麼DNS的查詢過程是分級查詢

了。

我們可以利用dig+trace命令來完整的還原一次分級查詢的過程:

你看通過命令的方式就能一目瞭然的理解DNS查詢的過程了。這遠比你在網上搜一些什麼DNS是遞迴查詢啊之類的要來的直觀。這裡有眼尖的小夥伴可能會問,這個CNAME是用來幹嘛的?大家只要理解CNAME主要用來做CDN加速的即可。詳細的可以去維基百科查詢,那裡說的很清楚,本文受限於篇幅就不在這個知識點上展開了。

2、WireShark學習理解DNS報文

這裡注意因為 Wireshark的捕獲過濾器無法設定DNS協議,又因為DNS是基於 UDP協議的,所以這裡捕獲過濾器我們就設定為 UDP就好。

然後就可以在一堆 UDP報文中找到我們想看的DNS報文了,我們在瀏覽器中輸入

www.airbnb.com:

這裡要注意左邊有兩個箭頭,向右的箭頭代表“請求”,向左的箭頭就代表該“請求”的回覆了。

這些DNS報文經過 Wireshark的解析以後,格式已經幫我們分析好了,所以看起來很清晰。也很簡單。這裡我們不再詳細分析DNS的二進位制報文格式,有興趣的可以自行查詢相關資料。在我們上述展示的DNS報文抓包截圖的時候,細心的同學已經發現了,我們DNS報文的查詢地址是172.22.3.102。一般而言,大部分公司內部網路都會提供一個統一的DNS伺服器,這個地址就是內部的DNS伺服器地址了,有圖為證:

我們當然也可以使用其他DNS查詢,比如使用著名的谷歌DNS

3、傳統DNS服務查詢的缺點

經過上述的分析看起來DNS的查詢過程好像比較簡單,但實際上DNS帶來的效能或者安全問題很多很多。我們首先來還原一下完整的DNS查詢過程(假設我們想訪問csdn的網站):

  1. 瀏覽器輸入一個域名地址,如果作業系統的DNS快取中有這個域名的Ip地址 那麼直接返回,沒有的話 就去第二步。
  2. 作業系統會向 本系統設定的tcp/ip 引數中的DNS伺服器地址 發出DNS查詢報文。注意這個伺服器我們通常叫他本地DNS伺服器。也就是上述我們截圖中的172.22.3.102
  3. 如果本地DNS伺服器的快取中有這個域名對應的ip地址,那就直接返回,如果沒有,繼續下一步。
  4. 首先看DNS伺服器的架構圖:
  5. 也就是說當我們的本地DNS伺服器快取中沒有該域名的ip地址的時候,本地DNS伺服器就會直接向 根DNS(全世界只有13臺)伺服器去查詢,然後根DNS伺服器就會分析這個域名,告訴我們的本地DNS伺服器 你應該去.net 這個DNS伺服器去查詢。然後.net這個DNS伺服器又告訴本地DNS伺服器 你應該去csdn.net 這個DNS伺服器 去查詢DNS地址。然後最終csdn的 DNS伺服器就將正確的ip地址返回給我們的本地DNS,本地DNS再將這個值返回給我們的瀏覽器(這個過程其實你用前面的dig+trace命令可以更直觀的體會到)。

通過上述的一次完整的DNS互動過程,我們可以至少得出三個結論:

  1. DNS伺服器是可以做負載均衡的。當然前提條件是你這個域名得自己建一個DNS伺服器。一般大廠都會自建。
  2. DNS的查詢是一個遞迴的過程,弱網的情況,這個時間會變的很漫長。且DNS使用的是 UDP傳輸協議,弱網有直接查詢失敗的可能
  3. DNS的查詢過程不可控,比如說本地DNS伺服器完全可以返回一個錯誤的ip地址。比如你訪問了一個京東的連結,然後返回給你的ip地址是拼多多的。

這還只是表面上看出來的傳統DNS查詢的缺點,實際上現在我們每天大部分的流量都來自於行動網路。行動網路中,傳統DNS服務暴露出來的問題更多:

  1. 前文我們說過本地DNS伺服器會快取域名的ip,但這個快取時效我們控制不了,全靠運營商的操守。有可能發生我們ip地址已經變化,但是本地DNS伺服器返回的還是老ip的情況。
  2. 有些運營商為了節省運營商和運營商之間的流量計算成本,會偷偷的將一些靜態頁面快取。當用戶訪問這些頁面的時候 往往訪問的是這些靜態頁面的快取伺服器的地址。此時不管我們的頁面更新了多少內容,對於使用者來說都是老的頁面。
  3. 運營商在某些場景,例如人口集中的地鐵站,演唱會,足球場附近等等,一旦發現自己的使用者太多,本地DNS伺服器壓力巨大的時候,就會手動設定將本地DNS伺服器向根域名伺服器
  4. 查詢 然後遞迴查詢 DNS的過程 修改成:直接向另外一個運營商(假設這個運營商名字為B)的DNS伺服器地址進行查詢,B的DNS伺服器就會返回一個B的地址,此時運營商A的使用者訪問的ip地址就是運營商B的ip了,這種跨運營商訪問的場景速度會非常慢。
  5. 某些寬頻提供方的NAT服務非常不穩定,大家都知道我們在家上網的時候 本機地址其實就是一個內網地址,我們之所以能訪問外部的網路是因為這些寬頻提供方提供了一層閘道器來負責NAT,這個NAT會將我們的內網地址轉換成一個外網的地址,NAT之後的ip,某些權威DNS伺服器就無法判斷這個ip到底屬於哪個運營商。也會帶來跨運營商訪問的問題。
  6. 除了自己的DNS伺服器,其他公共DNS伺服器的快取時效都不可控,這對雙機房部署,異地多活,多域名等策略都會有影響。
  7. 弱網環境下,因為DNS使用的傳輸協議是不可靠的 UDP,又因為DNS查詢的過程是一個遞迴的過程,所以DNS查詢在弱網環境下是有概率失敗的

4、HTTPDNS

基於上述缺點,越來越多的大廠使用了HTTPDNS的這種技術(據騰訊的公開資料顯示,15年騰訊每天的localDNS失敗次數就達到了80w次,接入HTTPDNS以後,使用者平均訪問延遲下降超過10%,訪問失敗率下降了超過五分之一,使用者訪問體驗的效果提升非常顯著):

這種技術的原理其實挺簡單的,無非就是讓我們的手機App 發起一個HTTP請求(這個請求地址多數使用ip直連,如果使用域名那麼依然針對此請求依然有傳統DNS的問題),這個請求可以攜帶使用者所在的運營商,地理位置,精確到省市,然後伺服器根據這些資訊 返回一個最佳的ip地址給App,然後App將這個域名-ip的對映關係設定到我們的okhttp中。這樣手機中的大部分請求就會直接使用我們HTTP伺服器返回的ip地址而不是運營商的地址了。

注意這裡我說的是大部分請求而不是全部請求的原因是,對於Android系統來說,webview的DNS查詢過程程式碼全部在c層,且版本和版本之間有一定差異,這部分的hook過程極為艱難,截止到這篇文章編寫的時候,筆者依舊沒有查詢到公開的能夠hook webview DNS的原始碼,而iOS這點做的顯然就比Android好一些,對於iOS來說webview的HTTP就是一個正常的HTTPrequest 與原生的程式碼並沒有任何區別。對於Android客戶端來說,接入HTTPDNS也不是一件特別容易的事。即使現在擁有了okhttp。

方案一:

通過okhttp的攔截器,在發出請求之前將我們的url中的域名直接替換成ip,再手動往header中新增host頭部資訊。缺點:如果url是https的,ip直連會出現證書校驗的問題。另外因為請求的時候 我們直接用的ip 但是 服務端返回的set cookie頭部資訊卻帶上的域名,這裡也要額外處理。優點:因為是攔截器的實現機制,所以很容易做開關進行降級處理。

方案二:

通過okhttp的DNS直接接管。

public class HttpDNS implements DNS {
    private static final DNS SYSTEM = DNS.SYSTEM;
  
    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        //假設這個DNShelper可以返回我們httpDNS查詢的結果
        String ip = DNSHelper.getIpByHost(hostname);
        if (ip != null && !ip.equals("")) {
            List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
            return inetAddresses;
        }
        return SYSTEM.lookup(hostname);
    }
}
 //然後讓okhttp使用我們的DNS實現就好
 OkHttpClient client = new OkHttpClient.Builder()
                .DNS(new HttpDNS())
                .build();

這種方案就不存在攔截器哪種缺點,因為本質上這種方案和系統的DNS查詢方案並無二致,無非系統的是 UDP去localDNS找,我們的是用HTTP去 HTTP伺服器上找。這種方案可以解決方案一的所有缺點,但是有一個問題就是一旦這個HTTPDNS返回的結果有問題,那麼很難降級。且okhttp的DNS查詢也是有一層快取的,一旦我們的HTTP DNS伺服器返回的地址有誤,那麼在一定時間範圍內後續針對這個域名的訪問都會有問題。

前面我們說過Android自身webview的機制導致HTTPDNS很難在webview中起到作用,但是仍舊有一些方法可以儘量規避掉webview中loacalDNS速度慢的缺點。例如我們可以在html中設定預載入靜態資源的DNS請求,而不用等到真正請求這些資源的時候才會查詢DNS。

<!--域名預解析-->
<meta http-equiv="x-DNS-prefetch-control" content="on" >
<link rel="DNS-prefetch" href="//vivo.com.cn" >

考慮到實際上webview和App自身程式碼使用的DNS快取都是作業系統中的同一塊儲存區域,我們也可以統計出我們常用web頁面中頻繁請求的url的域名,在App一啟動的時機,就提前訪問這些域名,這樣等到熱點web頁面在載入的時候,如果作業系統DNS快取已經有了對應的ip,則可以省略一次DNS的查詢。

5、DNS真的是基於UDP協議的嗎?

其實DNS協議真的不是完全基於UDP協議的,DNS的協議裡面其實有主DNS伺服器和輔DNS伺服器的概念,輔DNS伺服器在啟動的時候會主動去主DNS伺服器上拉取最新的該區域DNS資訊。這個拉取的過程採用的就是TCP協議,而不是UDP協議。也就是協議文件中說的zone transfer。

這裡有人可能會想到,為什麼不用UDP協議來完成這個過程,因為UDP協議最大隻能傳送512個byte的資料,而輔DNS要拉取該區域的DNS資訊很容易就超過這個最大報文數量的限制,所以這裡採用的就是TCP協議來完成拉取資料的操作。

二、WebSocket

1、有HTTP輪詢為什麼還需要 WebSocket 技術?

很多人不明白為什麼一定要用 WebSocket,明明我輪詢HTTP請求一樣可以完成需求。這句話本身並不錯,可以用 WebSocket 的地方確實全部都可以用輪詢HTTP請求來替代。但是其背後的效率卻天差地別。

我們可以把 WebSocket 看成是 HTTP 協議為了支援長連線所打的一個大補丁,它和 HTTP 有一些共性,是為了解決 HTTP 本身無法解決的某些問題而做出的一個改良設計。在以前 HTTP 協議中所謂的 keep-alive 長連線是指在一次 TCP 連線中完成多個 HTTP 請求,但是對每個請求仍然要單獨發 header;所謂的輪詢是指從客戶端不斷主動的向伺服器發 HTTP 請求查詢是否有新資料。這種模式有三個缺點:

  • 除了真正的資料部分外,伺服器和客戶端還要大量交換 HTTP header,資訊交換效率很低。
  • 因為HTTP是無狀態的,每次請求服務端都要通過客戶端傳遞來的引數來查詢這個請求到底是誰的,例如每次都要查詢一下這個userId下面有多少存款,買過幾部手機等等,對伺服器的寶貴的計算資源是一種浪費。
  • 輪詢的時間間隔不好設定,設定高了,使用者的介面響應不及時,設定的太低,又怕流量消耗大,伺服器扛不住。

當然輪詢也有優點就是實現成本極低,幾乎不需要客戶端和服務端有額外的開發成本。WebSocket在首次使用的時候還是需要做一些基礎設施改造的(例如nginx相應的配置)。WebSocket的實現成本:雖然說現代伺服器程式設計中預設都提供了WebSocket的實現,但是我們知道考慮到擴充套件性等因素,我們通常都不會直接和源伺服器打交道,而是和代理伺服器打交道。對WebSocket來說同樣如此,所以這裡對於首次實現WebSocket的團隊是有一定技術成本。

上圖是一個簡單的伺服器架構圖,客戶端發出去的請求經過一臺專門做負載均衡的代理伺服器以後將這些請求逐一轉發到對應的源伺服器上。而對於WebSocket來說 情況則變的稍微有點複雜:

相比純HTTP來說,WebSocket通常會增加一層專門的訊息分發系統提高訊息的處理效率。通常是Kafka或者是RabbitMQ。

2、Wireshark解析WebSocket報文

首先 來看一下WebSocket的幀格式。我們首先設定一下 Wireshark的捕獲器:

設定一下我們想要捕獲的域名和埠號。注意WebSocket是可以複用HTTP埠號的。http://demos.kaazing.com這個網址是一個專門用來體驗WebSocket技術的網址。我們以這網站為例。

可以看出來這裡我們操作步驟一共是 connect,然後發訊息,伺服器返回我們傳送的訊息,最後我們主動斷開連線。

WebSocket是一個基於幀的協議,所以這裡我們著重分析一下WebSocket的幀格式,每個幀頭部的 第4-第7個 bit位,這4個bit 代表的就是Opcode,比較重要的就是幾個值:

  • 2:代表這是二進位制幀,
  • 1:代表這是一個文字幀,
  • 8:代表關閉幀。

3、WebSocket連線的建立過程

這裡有人就要問了,既然WebSocket是能保證長連線(tcp)的,那麼這條長連線是由誰發起的?看下圖:

這個抓包結果顯示的說明了WebSocket下面使用的tcp連線是交給HTTP1.1來發起的。來詳細看一下,這裡我用箭頭標註的都是必須要設定的HTTP頭部資訊,否則是無法完成WebSocket連線的建立的。

此外我們還需要注意Sec-WebSocket-Accept,和Sec-WebSocket-Key 這2個頭部資訊。

客戶端生成一個隨機數以後用base64加密以後放到Sec-WebSocket-Key頭部資訊中,然後伺服器接受到這個資訊,用這個值與rfc中規定的一個魔法字串:“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼起來,然後再使用sha-1加密 再經過base64 以後計算出來的值 放到Sec-WebSocket-Accept頭部中返回給客戶端。

這麼做的原因是帶來一些基礎的保障,前面我們說過WebSocket連線的建立是依託HTTP訊息的,為了防止這個WebSocket連線的建立是呼叫者無心誤觸發或者其他異常情況,所以這裡有一次額外的資料校驗的過程。

4、WebSocket連線的斷開過程

看完連線,我們再看一下斷開連線,與WebSocket的連線不同,WebSocket的斷開連線是有明確步驟的,需要先斷開WebSocket的連線,然後才是tcp的斷開連線。

可以看出來,斷開連線的步驟是客戶端先發了一個斷開連線的幀,然後服務端再給客戶端發一個確認斷開WebSocket連線的幀。最後就是tcp的四次揮手了,保證了tcp連線的徹底斷開。

另外HTTP1.1中保持長連線的方法其實就是一個定時器,定時器大概時間為60s,如果60s都沒有HTTP訊息,那麼這個tcp連線就斷掉了。WebSocket雖然也是利用了HTTP1.1的訊息來保證tcp的連線,但是保證這條tcp連線不被斷開的方法卻又不是定時器了,與mqtt xmpp等協議類似,WebSocket保持長連線的方法也是利用了心跳包。

在RFC協議中,規定了opcode 為0x9 0xA的幀為心跳幀,但是往往 這個關於心跳包的協議卻很少有人遵守,很多時候人們選擇間隔一段時間傳送一個任意幀(當然這個任意幀的內容需要客戶端和服務端提前約定好)來保證心跳包的建立。比如前文中我們拿來做例子的http://demos.kaazing.com網站,他的心跳包就沒有遵守協議 而是:

圖中可以看出來這個心跳包大概是30s傳送一次,而且並沒有使用rfc中約定好的0x9或者0xA的所謂ping pong的心跳幀,而是就用了最簡單的文字幀來表示。

上圖所示,左邊的就是WebSocket 服務端發起的心跳包,opcode的值還是text文字幀的意思,只不過文字的內容很特殊。右邊就是WebSocket客戶端回覆的心跳包。

5、WebSocket的代理快取汙染

這裡要注意的是 Wireshark抓包的時候,最右邊有一個masked的標識,這通常代表這一個WebSocket的幀是由客戶端傳送給服務端的,這是一個掩碼的標識。在WebSocket協議中只要是客戶端發起的訊息,都必須經過這個隨機的masking-key的掩碼計算之後才能傳輸。這是為了解決代理快取汙染的問題。

注意這裡問題的核心是實現不當的代理伺服器,所謂實現不當的代理伺服器就是指沒有完整實現好WebSocket協議的代理伺服器。而不是真正意義上惡意的代理伺服器,惡意的代理伺服器,用mask幀的技術是無法避免的。

所謂mask掩碼技術就是指瀏覽器在傳送WebSocket幀的時候必須生成一個隨機的mask-key,在幀的二進位制中將傳輸的內容與這個mask-key做異或操作。得出來的值才可以在網路中傳輸。

當我們的伺服器接收到這個WebSocket幀以後就可以用這個mask-key來反異或,從而就可以得出真正的內容了,這是最低成本實現檢測WebSocket幀是否遭到篡改的方案。例如:我們用WebSocket 傳輸一個 文字幀,內容為字串vivo,vivo的ascii碼的16進製為:76 69 76 6f。而這個訊息,這次瀏覽器生成的mask-key 為 23 68 c0 a3。

我們將這2個值進行異或操作:

可以得到值為55 01 b6 cc。然後看一下抓包的幀內容裡面是不是這個值:

深入理解 web 協議(一)- http 包體傳輸

更多內容敬請關注vivo 網際網路技術微信公眾號

注:轉載文章請先與微訊號:Labs2020聯絡