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