当我谈 HTTP 时,我谈些什么?
當我們打開網站時也許不會去留意網站前面的HTTP是怎么來的。但是它毫無疑問在網絡中有著舉足輕重的地位。本文從起源到發展,詳說HTTP從1到3的演變。
說在前面
本文不致力于講完 HTTP 的全部內容,事實上短短的篇幅也不可能講完。本文也無意于深挖 HTTP 中的某一點,這是像 《HTTP 權威指南》或者是 RFC 協議做的事。
本文目標是幫助讀者理清 HTTP 的演化過程,說說 HTTP 變化的那些事。
HTTP 的起源
HTTP 最初是 Tim BernersLee 1989 年在歐洲核子研究組織(CERN)所發起的。Tim BernersLee 提出了一種能讓遠隔兩地的研究者們共享知識的設想。這個設想的基本理念是:借助多文檔之間相互關聯形成的超文本(HyperText),連成可相互參閱的 WWW(World Wide Web,萬維網)。用于傳輸的超文本傳輸協議(HyperText Transfer Protocol),即 HTTP 由此誕生。
WWW 這一名稱,是 Web 瀏覽器當年用來瀏覽超文本的客戶端應用程序時的名稱。現在則用來表示這一系列的集合,也可簡稱為 Web。
HTTP 本身是一個簡單的請求-響應協議,它通常運行在 TCP 之上。從整個網絡模型來看,HTTP 是應用層的一個協議。在 OSI 七層模型中,HTTP 位于最上層。它并不涉及數據包的傳輸,只是規定了客戶端和服務器之間的通信格式。定了客戶端可能發送給服務器什么樣的消息以及得到什么樣的響應。請求和響應消息的頭以 ASCII 碼形式給出。
HTTP 采用 BS 架構,也就是瀏覽器到服務器的架構,客戶端通過瀏覽器發送 HTTP 請求給服務器,服務器經過解析響應客戶端的請求。就是這個簡單實用的模型,使得 HTTP 這個基于 TCP/IP 的協議迅速推廣。
HTTP/0.9 到 HTTP/1.1
HTTP 的演化并不是一蹴而就的。當年 HTTP 的出現主要是為了解決文本傳輸的難題。由于協議本身非常簡單,于是在此基礎上設想了很多應用方法并投入了實際使用。現在 HTTP 已經超出了 Web 這個框架的局限,被運用到了各種場景里。
HTTP/0.9
HTTP 協議最早的一個版本是 1990 年發布的 HTTP/0.9。
前面說到,HTTP 于 1989 年問世。那時的 HTTP 并沒有作為正式的標準被建立。這時的 HTTP 其實含有 HTTP/1.0 之前版本的意思,因此被稱為 HTTP/0.9。這個版本只有一個命令:GET。通過 GET 可以獲取服務器的資源,比如請求服務器根目錄下的 index.html 文件。這個版本的協議規定,服務器只能回應 HTML 格式的字符串,不能回應其它格式,也就是說圖像、視頻等多媒體資源,在 HTTP/0.9 這個版本上是無法進行傳輸的。
HTTP/1.0
HTTP 正式作為標準被公布是在 1996 年的 5 月,版本被命名為 HTTP/1.0,并記載于 RFC1945 [https://www.ietf.org/rfc/rfc1945.txt]。雖說是初期標準,但該協議標準至今仍被廣泛使用在服務器端。
HTTP/1.0 版本發布,增加了 POST 命令和 HEAD 命令,豐富了瀏覽器與服務器的互動手段。這個版本的 HTTP 協議可以發送任何格式的內容,包括傳輸文字、圖像、視頻、文件等,這為互聯網的大發展奠定了基礎。
HTTP/1.0 除了增加了請求方法以及對發送文件的支持之外,還增加了格式的改變。除了數據部分,每次通信都必須包括頭信息(HTTP header),用來描述一些元數據。另外還增加了狀態碼、多字符集支持、多部分發送(multi-part type)、權限(authorization)、緩存(cache)、內容編碼(content encoding)等等。
HTTP/1.1
HTTP/1.0 版也并不是完美的,它的主要缺點是,每一次建立 TCP 連接只能發送一個請求。發送數據完畢,連接就關閉,如果還要請求其他資源,就必須再新建一個連接。如果多次請求,勢必就會對服務器產生較大的資源性能損耗。
1997 年 1 月公布的 HTTP/1.1 是目前主流的 HTTP 協議版本。當初的標準是 RFC2068,之后發布的修訂版 RFC2616 就是當前的最新版本。
其中最著名的是 1999 年 6 月公布的 RFC 2616 [https://tools.ietf.org/html/rfc2616],定義了 HTTP 協議中現今廣泛使用的一個版本——HTTP/1.1。
這個版本最大的變化就是將持久化連接加入了 HTTP 標準,即 TCP 連接默認不關閉,可以被多個請求復用。此外,HTTP/1.1 版還新增了許多方法,例如:PUT、PATCH、HEAD、OPTIONS、DELETE。得到進一步完善的HTTP/1.1 版本,一直沿用至今。
HTTP 協議簡單介紹
請求
客戶端發送一個 HTTP 請求到服務器,請求消息包括以下格式:
請求行(request line)、請求頭部(header)、空行和請求數據四個部分組成。
Get 請求例子
1 > GET / HTTP/1.1
2 > Host: www.baidu.com
3 > User-Agent: curl/7.52.1
4 > Accept: /
第一部分:請求行,用來說明請求類型,要訪問的資源以及所使用的 HTTP 版本。
第二部分:請求頭部,緊接著請求行(即第一行)之后的部分,用來說明服務器要使用的附加信息
從第二行起為請求頭部,HOST 將指出請求的目的地。User-Agent,服務器端和客戶端腳本都能訪問它,它是瀏覽器類型檢測邏輯的重要基礎。該信息由你的瀏覽器來定義,并且在每個請求中自動發送等等。
第三部分:空行,請求頭部后面的空行是必須的
即使第四部分的請求數據為空,也必須有空行。
第四部分:請求數據也叫主體,可以添加任意的其他數據。
這個例子的請求數據為空。
響應消息
一般情況下,服務器接收并處理客戶端發過來的請求后,會返回一個 HTTP 的響應消息。
HTTP 響應也由四個部分組成,分別是:狀態行、消息報頭、空行和響應正文。
例子
1 < HTTP/1.1 200 OK
2 < Accept-Ranges: bytes
3 < Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
4 < Connection: keep-alive
5 < Content-Length: 2381
6 < Content-Type: text/html
7 < Date: Thu, 11 Jun 2020 16:04:33 GMT
8 < Etag: “588604c8-94d”
9 < Last-Modified: Mon, 23 Jan 2017 13:27:36 GMT
10 < Pragma: no-cache
11 < Server: bfe/1.0.8.18
12 < Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
13 <
14 < !DOCTYPE html>
15 < !–STATUS OK–> <meta HTTP-equiv=content-type content=text/html;charset=utf-8><meta HTTP-equiv=X-UA-Compatible content=IE=Edge>…
16
第一部分:狀態行,由 HTTP 協議版本號、狀態碼、狀態消息三部分組成。
第一行為狀態行,(HTTP/1.1)表明 HTTP 版本為 1.1 版本,狀態碼為 200,狀態消息為(ok)
第二部分:消息報頭,用來說明客戶端要使用的一些附加信息
第二行和第三行為消息報頭。
Date:生成響應的日期和時間;Content-Type:指定了 MIME 類型的 HTML(text/html),編碼類型是 UTF-8
第三部分:空行,消息報頭后面的空行是必須的
第四部分:響應正文,服務器返回給客戶端的文本信息。
空行后面的 HTML 部分為響應正文。
狀態碼
狀態代碼有三位數字組成,第一個數字定義了響應的類別,共分五種類別:
-
1xx:指示信息–表示請求已接收,繼續處理
-
2xx:成功–表示請求已被成功接收、理解、接受
-
3xx:重定向–要完成請求必須進行更進一步的操作
-
4xx:客戶端錯誤–請求有語法錯誤或請求無法實現
-
5xx:服務器端錯誤–服務器未能實現合法的請求
安全性與 HTTPS
HTTP 的誕生是為了解決信息傳遞和共享的問題,并沒有考慮到互聯網高速發展后面臨的安全問題。
一般來說 HTTP 從 TCP 三次握手后,便開始了數據傳輸。由于 HTTP 本身以明文形式來傳輸數據,并不具備任何數據加密、身份校驗的機制。同時下層協議并不對數據安全性、保密性提供保證。所以在網絡傳輸的過程中,任意節點的第三方都可以隨意劫持流量、篡改數據或竊取信息。
HTTP 無法確保數據的保密性、完整性和真實性,已經不能適應現代互聯網應用的安全需求。
隨著 Web 的日益壯大,HTTP 的使用呈巨額增長趨勢,對信息安全的需求也愈來愈迫切,SSL(Secure SocketsLayer ,安全套接層)應運而生。
當對于安全需求,首先想到的就是對信息進行加密。SSL ,安全套接層,顧名思義是在 TCP 上提供的安全套接字層。其位于應用層和傳輸層之間,應用層數據不再直接傳遞給傳輸層而是傳遞給 SSL 層,SSL 層對從應用層收到的數據進行加密,利用數據加密、身份驗證和消息完整性驗證機制,為網絡上數據的傳輸提供安全性保證。HTTPS 便是指 Hyper Text Transfer Protocol over SecureSocket Layer。
談到具體實施上,業內通常采用的一般有對稱加密和非對稱加密。采用何種方式進行加密?如何判斷服務器未被篡改?如何傳遞加密密鑰?帶著這樣的問題,我們來看看 HTTPS 的工作流程。
1、客戶端發起 HTTPS 請求
這個沒什么好說的,就是用戶在瀏覽器里輸入一個 HTTPS 網址,然后連接到 server 的 443 端口。
2、服務端的配置
采用 HTTPS 協議的服務器必須要有一套數字證書,可以自己制作,也可以向組織申請,區別就是自己頒發的證書需要客戶端驗證通過,才可以繼續訪問,而使用受信任的公司申請的證書則不會彈出提示頁面(Let‘s Encrypt 就是個不錯的選擇,免費的 SSL 證書)。
這套證書其實就是一對公鑰和私鑰,如果對公鑰和私鑰不太理解,可以想象成一把鑰匙和一個鎖頭,只是全世界只有你一個人有這把鑰匙,你可以把鎖頭給別人,別人可以用這個鎖把重要的東西鎖起來,然后發給你,因為只有你一個人有這把鑰匙,所以只有你才能看到被這把鎖鎖起來的東西。
3、傳送證書
這個證書其實就是公鑰,只是包含了很多信息,如證書的頒發機構,過期時間等等。
4、客戶端解析證書
這部分工作是有客戶端的 TLS 來完成的,首先會驗證公鑰是否有效,比如頒發機構,過期時間等等,如果發現異常,則會彈出一個警告框,提示證書存在問題。
如果證書沒有問題,那么就生成一個隨機值,然后用證書對該隨機值進行加密,就好像上面說的,把隨機值用鎖頭鎖起來,這樣除非有鑰匙,不然看不到被鎖住的內容。
5、傳送加密信息
這部分傳送的是用證書加密后的隨機值,目的就是讓服務端得到這個隨機值,以后客戶端和服務端的通信就可以通過這個隨機值來進行加密解密了。
6、服務段解密信息
服務端用私鑰解密后,得到了客戶端傳過來的隨機值(私鑰),然后把內容通過該值進行對稱加密,所謂對稱加密就是,將信息和私鑰通過某種算法混合在一起,這樣除非知道私鑰,不然無法獲取內容,而正好客戶端和服務端都知道這個私鑰,所以只要加密算法夠彪悍,私鑰夠復雜,數據就夠安全。
7、傳輸加密后的信息
這部分信息是服務段用私鑰加密后的信息,可以在客戶端被還原。
8、客戶端解密信息
客戶端用之前生成的私鑰解密服務段傳過來的信息,于是獲取了解密后的內容,整個過程第三方即使監聽到了數據,也束手無策。
簡單說完了 HTTPS 的工作流程。讓我們再將注意力放在 SSL 的演化上。
1994年,Netscape 創建了 SSL 協議的原始規范并逐步發布協議改進版本。1995 年發布 SSL 2.0。1996年,Netscape 和 Paul Kocher 共同設計發布 SSL 3.0 協議,獲得互聯網廣泛認可和支持。因特網工程任務組(IETF)接手負責該協議,并將其重命名為 TLS(傳輸層安全)協議。
我們看到,SSL 2.0 規范是在 1995 年左右發布的,而 SSL 3.0 是在 1996 年 11 月發布的。有趣的是,SSL 3.0 是在 RFC 6101 [https://tools.ietf.org/html/rfc6101] 中描述的,該 RFC 于 2011 年 8 月發布。它位于歷史類別中,該類別通常是被考慮和被丟棄的文檔想法,或者是在決定記錄它們時已經具有歷史意義的協議(根據 IETF [https://www.ietf.org/about/groups/iesg/statements/] 說明)。在這種情況下,有一個描述 SSL 3.0 的 IETF 文檔是很有必要的,因為在其可以被用作規范參考。
再來看看,SSL 是如何激發 TLS 的發展的。后者在 1996 年 11 月以 draft-ietf-tls-protocol-00 [https://tools.ietf.org/html/draft-ietf-tls-protocol-00] 宣告開始。它經歷了六個草案版本,并于 1999 年初作為 RFC 2246 [https://tools.ietf.org/html/rfc2246] - TLS 1.0 正式發布。
在 1995 和 1999 年間,SSL 和 TLS 協議用于保護互聯網上的 HTTP 通信。這作為事實上的標準運行良好。直到 1998 年 1 月,隨著 I-D draft-ietf-tls-HTTPs-00 [https://tools.ietf.org/html/draft-ietf-tls-HTTPs-00] 的發布,HTTPS 的正式標準化過程才開始。該工作于 2000 年 5 月以 RFC 2616 - HTTP 上的 TLS 的發布結束。
TLS 在 2000 到 2007 年間繼續發展,標準化為 TLS 1.1 和 1.2。直至七年后,TLS 的下一個版本開始進行,該版本在 2014 年四月被采納為 draft-ietf-tls-tls13-00 [https://tools.ietf.org/html/draft-ietf-tls-tls13-00],并在 28 份草稿后,于 2018 年八月出了完成版本 RFC 8446 [https://tools.ietf.org/html/rfc8446] - TLS 1.3。
改進與 HTTP2
回到 HTTP 本身。在很長一段時間里,HTTP/1.1 已經足夠好了(確實是,現在仍應用最為廣泛),但是,Web 不斷變化的需求使得我們需要一個更好更合適的協議。
HTTP/1.1 自從 1997 年發布以來,我們已經使用 HTTP/1.x 相當長一段時間了。但隨著互聯網近十年爆炸式的發展,從當初網頁內容以文本為主,到現在以富媒體(如圖片、聲音、視頻)為主,而且對頁面內容實時性高要求的應用越來越多(比如聊天、視頻直播),所以當時協議規定的某些特性,已經逐漸無法滿足現代網絡的需求了。
如果你有仔細觀察,那些最流行的網站首頁所需要下載資源的話,會發現一個非常明顯的趨勢。近年來加載網站首頁需要下載的數據量在逐漸增加,并已經超過了 2100K。但在這里我們更關心的是:平均每個頁面為了完成顯示與渲染所需要下載的資源數也已經超過了 100 個。
基于此,在 2010 年到 2015 年,谷歌通過實踐一個實驗性的 SPDY 協議,證明了一個在客戶端和服務器端交換數據的另類方式。其收集了瀏覽器和服務器端的開發者的焦點問題,明確了響應數量的增加和解決復雜的數據傳輸。在啟動 SPDY 這個項目時預設的目標是:
-
頁面加載時間 (PLT) 減少 50%。
-
無需網站作者修改任何內容。
-
將部署復雜性降至最低,無需變更網絡基礎設施。
-
與開源社區合作開發這個新協議。
-
收集真實性能數據,驗證這個實驗性協議是否有效。為了達到降低目標,減少頁面加載時間的目標,SPDY 引入了一個新的二進制分幀數據層,以實現多向請求和響應、優先次序、最小化及消除不必要的網絡延遲,目的是更有效地利用底層 TCP 連接。
**HTTP/1.1 有兩個主要的缺點:安全不足和性能不高,**由于背負著 HTTP/1.x 龐大的歷史包袱,所以協議的修改,兼容性是首要考慮的目標,否則就會破壞互聯網上無數現有的資產。
而如上圖所示,SPDY 位于 HTTP 之下,TCP 和 SSL 之上,這樣可以輕松兼容老版本的 HTTP 協議同時可以使用已有的 SSL 功能。
SPDY 協議在 Chrome 瀏覽器上證明可行以后,就被當作 HTTP/2 的基礎,主要特性都在 HTTP/2 之中得到繼承。
于是時間來到 2015 年,HTTP/2.0 問世。
HTTP/2 相比 HTTP/1.1 的修改并不會破壞現有程序的工作,但是新的程序可以借由新特性得到更好的速度。
HTTP/2 保留了 HTTP/1.1 的大部分語義,例如請求方法、狀態碼、乃至 URI 和絕大多數 HTTP 頭部字段一致。而 HTTP/2 采用了新的方法來編碼、傳輸客戶端和服務器間的數據。
來看看 HTTP/2 的具體特點:
-
二進制分幀層:在應用層與傳輸層之間增加一個二進制分幀層,以此達到在不改動 HTTP 的語義,HTTP 方法、狀態碼、URI 及首部字段的情況下,突破 HTTP/1.1 的性能限制,改進傳輸性能,實現低延遲和高吞吐量。在二進制分幀層上,HTTP/2.0 會將所有傳輸的信息分割為更小的消息和幀,并對它們采用二進制格式的編碼,其中 HTTP1.x 的首部信息會被封裝到 Headers 幀,而我們的 request body 則封裝到 Data 幀里面。
-
多路復用:對于 HTTP/1.x,即使開啟了長連接,請求的發送也是串行發送的,在帶寬足夠的情況下,對帶寬的利用率不夠,HTTP/2.0 采用了多路復用的方式,可以并行發送多個請求,提高對帶寬的利用率。
-
數據流優先級:由于請求可以并發發送了,那么如果出現了瀏覽器在等待關鍵的 CSS 或者 JS 文件完成對頁面的渲染時,服務器卻在專注的發送圖片資源的情況怎么辦呢?HTTP/2.0 對數據流可以設置優先值,這個優先值決定了客戶端和服務端處理不同的流采用不同的優先級策略。
-
服務端推送:在 HTTP/2.0 中,服務器可以向客戶發送請求之外的內容,比如正在請求一個頁面時,服務器會把頁面相關的 logo,CSS 等文件直接推送到客戶端,而不會等到請求來的時候再發送,因為服務器認為客戶端會用到這些東西。這相當于在一個 HTML 文檔內集合了所有的資源。
-
頭部壓縮:使用首部表來跟蹤和存儲之前發送的鍵值對,對于相同的內容,不會再每次請求和響應時發送。
-
HTTP/2.0 支持明文 HTTP 傳輸,而 SPDY 強制使用 HTTPS。
-
HTTP/2.0 消息頭的壓縮算法采用 HPACK,而非 SPDY 采用的 DEFLATE。
QUIC 和 HTTP3
雖然 HTTP/2 提高了網頁的性能,但是并不代表它已經是完美的了,HTTP/3 就是為了解決 HTTP/2 所存在的一些問題而被推出來的。隨著時間的演進,越來越多的流量都往手機端移動,手機的網絡環境會遇到的問題像是封包丟失機率較高、較長的 Round Trip Time (RTT)和連接遷移等問題,都讓主要是為了有線網路設計的HTTP/TCP協議遇到貧頸。
我們可以看兩個典型的問題。
第一握手帶來的消耗。HTTP/2 使用 TCP 協議來傳輸的,而如果使用 HTTPS 的話,還需要使用 TLS 協議進行安全傳輸,而使用 TLS 也需要一個握手過程,這樣就需要有兩個握手延遲過程:
-
在建立 TCP 連接的時候,需要和服務器進行三次握手來確認連接成功,也就是說需要在消耗完 1.5 個 RTT 之后才能進行數據傳輸。
-
進行 TLS 連接,TLS 有兩個版本——TLS 1.2 和 TLS 1.3,每個版本建立連接所花的時間不同,大致是需要1~2個 RTT。
總之,在傳輸數據之前,我們需要花掉 3~4 個 RTT。
第二,TCP 的隊頭阻塞并沒有得到徹底解決。我們知道,為了實現多路復用,在 HTTP/2 中多個請求是跑在一個 TCP 管道中的。但當出現了丟包時,HTTP/2 的表現反倒不如 HTTP/1.X 了。因為 TCP 為了保證可靠傳輸,有個特別的丟包重傳機制,丟失的包必須要等待重新傳輸確認,HTTP/2 出現丟包時,整個 TCP 都要開始等待重傳,那么就會阻塞該 TCP 連接中的所有請求。而對于 HTTP/1.1 來說,可以開啟多個 TCP 連接,出現這種情況反到只會影響其中一個連接,剩余的 TCP 連接還可以正常傳輸數據。
至此,我們很容易就會想到,為什么不直接去修改 TCP 協議?其實這已經是一件不可能完成的任務了。因為 TCP 存在的時間實在太長,已經充斥在各種設備中,并且這個協議是由操作系統實現的,更新起來非常麻煩,不具備顯示操作性。
HTTP/3 乘著 QUIC 來了。
HTTP3 是基于 QUIC 的協議,如上圖。先說 QUIC,QUIC 協議是 Google 提出的一套開源協議,它基于 UDP 來實現,直接競爭對手是 TCP 協議。QUIC 協議的性能非常好,甚至在某些場景下可以實現 0-RTT 的加密通信。
在 Google 關于 QUIC [https://docs.google.com/document/d/1gY9-YNDNAB1eip-RTPbqphgySwSNSDHLq9D5Bty4FSU/edit] 的文件中提到,與 HTTP/2 相比,QUIC 主要具有下列優勢:
-
Reduce connection establishment latency (減少連接建立時間)
-
Improved congestion control (改進擁塞控制)
-
Multiplexing without head-of-line blocking (沒有隊頭阻塞的多路復用)
-
Forward error correction (修復之前的錯誤)
-
Connection migration(支持網絡遷移)
多路復用,避免隊頭阻塞
這句話說起來很容易,但理解起來并不那么顯然,要想理解 QUIC 協議到底做了什么以及這么做的必要性,我想還是從最基礎的 HTTP/1.0 聊起比較合適。
Pipiline
根據谷歌的調查, 現在請求一個網頁,平均涉及到 80 個資源,30 多個域名。考慮最原始的情況,每請求一個資源都需要建立一次 TCP 請求,顯然不可接受。HTTP 協議規定了一個字段 Connection,不過默認的值是 close,也就是不開啟。
早在 1999 年提出的 HTTP 1.1 [https://www.ietf.org/rfc/rfc2616.txt] 協議 中就把 Connection 的默認值改成了Keep-Alive,這樣同一個域名下的多個 HTTP 請求就可以復用同一個 TCP 連接。這種做法被稱為 HTTP Pipeline,優點是顯著的減少了建立連接的次數,也就是大幅度減少了 RTT。
以上面的數據為例,如果 80 個資源都要走一次 HTTP 1.0,那么需要建立 80 個 TCP 連接,握手 80 次,也就是 80 個 RTT。如果采用了 HTTP 1.1 的 Pipeline,只需要建立 30 個 TCP 連接,也就是 30 個 RTT,提高了 62.5% 的效率。
Pipeline 解決了 TCP 連接浪費的問題,但它自己還存在一些不足之處,也就是所有管道模型都難以避免的隊頭阻塞問題。
隊頭阻塞
我們再舉個簡單而且直觀的例子,假設加載一個 HTML 一共要請求 10 個資源,那么請求的總時間是每一個資源請求時間的總和。最直觀的體驗就是,網速越快請求時間越短。然而如果某一個資源的請求被阻塞了(比如 SQL 語句執行非常慢)。但對于客戶端來說所有后續的請求都會因此而被阻塞。
隊頭阻塞(Head of line blocking,下文簡稱 HOC)說的是當有多個串行請求執行時,如果第一個請求不執行完,后續的請求也無法執行。比如上圖中,如果第四個資源的傳輸花了很久,后面的資源都得等著,平白浪費了很多時間,帶寬資源沒有得到充分利用。
因此,HTTP 協議允許客戶端發起多個并行請求,比如在筆者的機器上最多支持六個并發請求。并發請求主要是用于解決 HOC 問題,當有三個并發請求時,情況會變成這樣:
可見雖然第四個資源的請求被阻塞了,但是其他的資源請求并不一定會被阻塞,這樣總的來說網絡的平均利用率得到了提升。
支持并發請求是解決 HOC 問題的一種方案,這句話沒有錯。但是我們要理解到:“并發請求并非是直接解決了 HOC 的問題,而是盡可能減少 HOC 造成的影響“,以上圖為例,HOC 的問題依然存在,只是不會太浪費帶寬而已。
有讀者可能會好奇,為什么不多搞幾個并發的 HTTP 請求呢?剛剛說過筆者的電腦最多支持 6 個并發請求,谷歌曾經做過實驗,把 6 改成 10,然后嘗試訪問了三千多個網頁,發現平均訪問時間竟然還增加了 5% 左右。這是因為一次請求涉及的域名有限,再多的并發 HTTP 請求并不能顯著提高帶寬利用率,反而會消耗性能。
SPDY 的做法
有沒有辦法解決隊頭阻塞呢?
答案是肯定的。SPDY 協議的做法很值得借鑒,它采用了多路復用(Multiplexing)技術,允許多個 HTTP 請求共享同一個 TCP 連接。我們假設每個資源被分為多個包傳遞,在 HTTP 1.1 中只有前面一個資源的所有數據包傳輸完畢后,后面資源的包才能開始傳遞(HOC 問題),而 SPDY 并不這么要求,大家可以一起傳輸。
這么做的代價是數據會略微有一些冗余,每一個資源的數據包都要帶上標記,用來指明自己屬于哪個資源,這樣客戶端最后才能把他們正確的拼接起來。不同的標記可以理解為圖中不同的顏色,每一個小方格可以理解為資源的某一個包。
TCP 窗口
是不是覺得 SPDY 的多路復用已經夠厲害了,解決了隊頭阻塞問題?很遺憾的是,并沒有,而且我可以很肯定的說,只要你還在用 TCP 鏈接,HOC 就是逃不掉的噩夢,不信我們來看看 TCP 的實現細節。
我們知道 TCP 協議會保證數據的可達性,如果發生了丟包或者錯包,數據就會被重傳。于是問題來了,如果一個包丟了,那么后面的包就得停下來等這個包重新傳輸,也就是發生了隊頭阻塞。當然 TCP 協議的設計者們也不傻,他們發明了滑動窗口的概念:
這樣的好處是在第一個數據包(1-1000) 發出后,不必等到 ACK 返回就可以立刻發送第二個數據包。可以看出圖中的 TCP 窗口大小是 4,所以第四個包發送后就會開始等待,直到第一個包的 ACK 返回。這樣窗口可以向后滑動一位,第五個包被發送。
如果第一、二、三個的包都丟失了也沒有關系,當發送方收到第四個包時,它可以確信一定是前三個 ACK 丟了而不是數據包丟了,否則不會收到 4001 的 ACK,所以發送方可以大膽的把窗口向后滑動四位。
滑動窗口的概念大幅度提高了 TCP 傳輸數據時抗干擾的能力,一般丟失一兩個 ACK 根本沒關系。但如果是發送的包丟失,或者出錯,窗口就無法向前滑動,出現了隊頭阻塞的現象。
QUIC 是如何做的
QUIC 協議基于 UDP 實現,我們知道 UDP 協議只負責發送數據,并不保證數據可達性。這一方面為 QUIC 的多路復用提供了基礎,另一方面也要求 QUIC 協議自己保證數據可達性。
SPDY 為各個數據包做好標記,指明他們屬于哪個 HTTP 請求,至于這些包能不能到達客戶端,SPDY 并不關心,因為數據可達性由 TCP 協議保證。既然客戶端一定能收到包,那就只要排序、拼接就行了。QUIC 協議采用了多路復用的思想,但同時還得自己保證數據的可達性。
TCP 協議的丟包重傳并不是一個好想法,因為一旦有了前后順序,隊頭阻塞問題將不可避免。而無序的數據發送給接受者以后,如何保證不丟包,不錯包呢?這看起來是個不可能完成的任務,不過如果把要求降低成:最多丟一個包,或者錯一個包。事情就簡單多了,操作系統中有一種存儲方式叫 RAID 5,采用的是異或運算加上數據冗余的方式來保證前向糾錯(FEC: Forward Error Correcting)。QUIC 協議也是采用這樣的思想,這里不再贅述。
利用冗余數據的思想,QUIC 協議基本上避免了重發數據的情況。當然 QUIC 協議還是支持重傳的,比如某些非常重要的數據或者丟失兩個包的情況。
少 RTT,請求更快速
前面說到,一次 HTTPS 請求,它的基本流程是三次 TCP 握手外加四次 SSL/TLS 握手。也就是需要三個 RTT。但是 QUIC 在某些場景下,甚至能夠做到 0RTT。
首先介紹下什么是 0RTT。所謂的 0RTT 就是通信雙方發起通信連接時,第一個數據包便可以攜帶有效的業務數據。而我們知道,這個使用傳統的TCP是完全不可能的,除非你使能了 TCP 快速打開特性,而這個很難,因為幾乎沒人愿意為了這個收益去對操作系統的網絡協議棧大動手腳。未使能 TCP 快速打開特性的TCP傳輸第一筆數據前,至少要等1個RTT。
我們這里再說說 HTTP2。對于 HTTP2 來說,本來需要一個額外的 RTT 來進行協商,判斷客戶端與服務器是不是都支持 HTTP2,不過好在它可以和 SSL 握手的請求合并。這也導致了一個現象,就是大多數主流瀏覽器僅支持 HTTPS2 而不單獨支持 HTTP2。因為 HTTP2 需要一個額外的 RTT,HTTPS2 需要兩個額外的 RTT,僅僅是增加一個 RTT 就能獲得數據安全性,還是很劃算的。
TCP 快速打開
何謂 TCP 快速打開,即客戶端可以在發送第一個 SYN 握手包時攜帶數據,但是 TCP 協議的實現者不允許將把這個數據包上傳給應用層。這主要是為了防止 TCP 泛洪攻擊 [https://tools.ietf.org/html/rfc4987]。
因為如果 SYN 握手的包能被傳輸到應用層,那么現有的防護措施都無法防御泛洪攻擊,而且服務端也會因為這些攻擊而耗盡內存和 CPU。
當然 TCP 快速打開并不是完全不可行的。人們設計了 TFO (TCP Fast Open),這是對 TCP 的拓展,不僅可以在發送 SYN 時攜帶數據,還可以保證安全性。
TFO 設計了一個 Cookie,它在第一次握手時由 server 生成,Cookie 主要是用來標識客戶端的身份,以及保存上次會話的配置信息。因此在后續重新建立 TCP 連接時,客戶端會攜帶 SYN + Cookie + 請求數據,然后不等 ACK 返回就直接開始發送數據。
服務端收到 SYN 后會驗證 Cookie 是否有效,如果無效則會退回到三次握手的步驟,如下圖所示:
同時,為了安全起見,服務端為每個端口記錄了一個值 PendingFastOpenRequests,用來表示有多少請求利用了 TFO,如果超過預設上限就不再接受。
關于 TFO 的優化,可以總結出三點內容:
-
TFO 設計的 Cookie 思想和 SSL 恢復握手時的 Session Ticket 很像,都是由服務端生成一段 Cookie 交給客戶端保存,從而避免后續的握手,有利于快速恢復。
-
第一次請求絕對不會觸發 TFO,因為服務器會在接收到 SYN 請求后把 Cookie 和 ACK 一起返回。后續客戶端如果要重新連接,才有可能使用這個 Cookie 進行 TFO
-
TFO 并不考慮在 TCP 層過濾重復請求,以前也有類似的提案想要做過濾,但因為無法保證安全性而被拒絕。所以 TFO 僅僅是避免了泛洪攻擊(類似于 backlog),但客戶端接收到的,和 SYN 包一起發來的數據,依然有可能重復。不過也只有可能是 SYN 數據重復,所以 TFO 并不處理這種情況,要求服務端程序自行解決。這也就是說,不僅僅要操作系統的支持,更要求應用程序(比如 MySQL)也支持 TFO。
TFO 使得 TCP 協議有可能變成 0-RTT,核心思想和 Session Ticket 的概念類似: 將當前會話的上下文緩存在客戶端。如果以后需要恢復對話,只需要將緩存發給服務器校驗,而不必花費一個 RTT 去等待。
結合 TFO 和 Session Ticket 技術,一個本來需要花費 3 個 RTT 才能完成的請求可以被優化到一個 RTT。如果使用 QUIC 協議,我們甚至可以更進一步,將 Session Ticket 也放到 TFO 中一起發送,這樣就實現了 0-RTT 的對話恢復。
QUIC 是怎么做的
讓我們看看 QUIC 是怎么做的。
首先聲明一點,如果一對使用 QUIC 進行加密通信的雙方此前從來沒有通信過,那么 0-RTT 是不可能的,即便是 QUIC 也是不可能的。
QUIC 握手的過程需要一次數據交互,0-RTT 時延即可完成握手過程中的密鑰協商,比 TLS 相比效率提高了 5 倍,且具有更高的安全性。在握手過程中使用 Diffie-Hellman 算法協商初始密鑰,初始密鑰依賴于服務器存儲的一組配置參數,該參數會周期性的更新。初始密鑰協商成功后,服務器會提供一個臨時隨機數,雙方根據這個數再生成會話密鑰。
具體握手過程如下:
(1) 客戶端判斷本地是否已有服務器的全部配置參數,如果有則直接跳轉到(5),否則繼續
(2) 客戶端向服務器發送 inchoate client hello(CHLO) 消息,請求服務器傳輸配置參數
(3) 服務器收到 CHLO,回復 rejection(REJ) 消息,其中包含服務器的部分配置參數
(4) 客戶端收到 REJ,提取并存儲服務器配置參數,跳回到(1)
(5) 客戶端向服務器發送 full client hello 消息,開始正式握手,消息中包括客戶端選擇的公開數。此時客戶端根據獲取的服務器配置參數和自己選擇的公開數,可以計算出初始密鑰。
(6) 服務器收到 full client hello,如果不同意連接就回復 REJ,同(3);如果同意連接,根據客戶端的公開數計算出初始密鑰,回復 server hello(SHLO)消息,SHLO 用初始密鑰加密,并且其中包含服務器選擇的一個臨時公開數。
(7) 客戶端收到服務器的回復,如果是 REJ 則情況同(4);如果是 SHLO,則嘗試用初始密鑰解密,提取出臨時公開數
(8) 客戶端和服務器根據臨時公開數和初始密鑰,各自基于 SHA-256 算法推導出會話密鑰
(9) 雙方更換為使用會話密鑰通信,初始密鑰此時已無用,QUIC 握手過程完畢。之后會話密鑰更新的流程與以上過程類似,只是數據包中的某些字段略有不同。
寫在最后
想起有一個名言:計算機領域沒有什么問題是加一層解決不了的,如果有,就再加一層。網絡模型本來就是層層累加,到了 Web 得以快速生動的展現給人們以豐富的內容。從 HTTP 的演變過程中,我們可以看到中間又累加了若干層。不知道以后,又會是怎么樣呢?
大家會發現,筆者在文中不止一次提到了演變這個詞。是的,這是來自達爾文進化論中的理論。在筆者看來,“物競天擇,適者生存”的演變理論和計算機領域的技術變化是很類似的,只不過在這里,不是天擇,而是人擇。由市場,由用戶來選擇。不知道接下來,作為選擇者的我們,又將怎樣主導技術的走向?
總結
以上是生活随笔為你收集整理的当我谈 HTTP 时,我谈些什么?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三分钟了解 Python3 的异步 We
- 下一篇: TCP 和 UDP,哪个更胜一筹