從起源到發展詳說HTTP從1到3的演變
當我們打開網站時也許不會去留意網站前面的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.12 > Host: www.baidu.com3 > User-Agent: curl/7.52.14 > Accept: /
第一部分:請求行,用來說明請求類型,要訪問的資源以及所使用的HTTP 版本。
第二部分:請求頭部,緊接著請求行(即第一行)之後的部分,用來說明服務器要使用的附加信息
從第二行起為請求頭部,HOST 將指出請求的目的地。User-Agent,服務器端和客戶端腳本都能訪問它,它是瀏覽器類型檢測邏輯的重要基礎。該信息由你的瀏覽器來定義,並且在每個請求中自動發送等等。
第三部分:空行,請求頭部後面的空行是必須的
即使第四部分的請求數據為空,也必須有空行。
第四部分:請求數據也叫主體,可以添加任意的其他數據。
這個例子的請求數據為空。
響應消息
一般情況下,服務器接收並處理客戶端發過來的請求後,會返回一個HTTP 的響應消息。
HTTP 響應也由四個部分組成,分別是:狀態行、消息報頭、空行和響應正文。
例子
1 < HTTP/1.1 200 OK2 < Accept-Ranges: bytes3 < Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform4 < Connection: keep-alive5 < Content-Length: 23816 < Content- Type: text/html7 < Date: Thu, 11 Jun 2020 16:04:33 GMT8 < Etag: “588604c8-94d”9 < Last-Modified: Mon, 23 Jan 2017 13:27:36 GMT10 < Pragma: no-cache11 < Server: bfe/1.0.8.1812 < Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/13 <14 < !DOCTYPE html>15 < !–STATUS OK– ><html> <head><meta HTTP-equiv=content-type content=text/html;charset=utf-8><meta HTTP-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer>… </html>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月,隨著ID 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 的演變過程中,我們可以看到中間又累加了若干層。不知道以後,又會是怎麼樣呢?
大家會發現,筆者在文中不止一次提到了演變這個詞。是的,這是來自達爾文進化論中的理論。在筆者看來,“物競天擇,適者生存”的演變理論和計算機領域的技術變化是很類似的,只不過在這裡,不是天擇,而是人擇。由市場,由用戶來選擇。不知道接下來,作為選擇者的我們,又將怎樣主導技術的走向?