圖解 HTTP 連接管理
Hey guys ,這里是程序員cxuan,歡迎你收看我最新一期的文章。
熟悉我的小伙伴都知道,我之前肝了本《HTTP 核心總結(jié)》的 PDF,這本 PDF 是取自我 HTTP 系列文章的匯總,然而我寫的 HTTP 相關(guān)內(nèi)容都是一年前了,我回頭看了一下這本 PDF,雖然內(nèi)容不少,但是很多內(nèi)容缺少系統(tǒng)性,看起來不爽,這個有悖于我的初心,所以我打算重新搞一搞 HTTP 協(xié)議,HTTP 協(xié)議對我們程序員來說太重要了,不管你使用的是哪個語言,HTTP 都是你需要知道的重點。
這不是一篇簡單介紹 HTTP 基本概念的文章,如果你對 HTTP 基本概念不是很熟悉,推薦你去讀 cxuan 寫的關(guān)于 HTTP 基礎(chǔ)文章 - 看完這篇HTTP,跟面試官扯皮就沒問題了
所以我們假定在做的各位對 HTTP 有一定的了解和認識。
下面開始我們這篇文章。
搭載 HTTP 的 TCP
我們大家都知道,HTTP 這個應(yīng)用層協(xié)議是以 TCP 為基礎(chǔ)來傳輸數(shù)據(jù)的。當(dāng)你想訪問一個資源(資源在網(wǎng)絡(luò)中就是一個 URL)時,你需要先解析這個資源的 IP 地址和端口號,從而和這個 IP 和端口號所在的服務(wù)器建立 TCP 連接,然后 HTTP 客戶端發(fā)起服務(wù)請求(GET)報文,服務(wù)器對服務(wù)器的請求報文做出響應(yīng),等到不需要交換報文時,客戶端會關(guān)閉連接,下面我用圖很好的說明了這個過程。

上面這幅圖很好的說明了 HTTP 從建立連接開始 -> 發(fā)起請求報文 -> 關(guān)閉連接的全過程,但是上面這個過程還忽略了一個很重要的點,那就是TCP 建立連接的過程。
TCP 建立連接需要經(jīng)過三次握手,交換三個報文,我相信大家都對這個過程了然于胸了,如果你還不清楚 TCP 建立連接的過程,可以先閱讀 cxuan 的這篇文章 TCP 連接管理。
由于 HTTP 位于 TCP 的上層,所以 HTTP 的請求 -> 響應(yīng)過程的時效性(性能)很大程度上取決于底層 TCP 的性能,只有在了解了 TCP 連接的性能之后,才可以更好的理解 HTTP 連接的性能,從而才能夠?qū)崿F(xiàn)高性能的 HTTP 應(yīng)用程序。
我們通常把一次完整的請求 -> 相應(yīng)過程稱之為 HTTP 事務(wù)。
所以我后面一般會寫為 HTTP 事務(wù),你理解怎么回事就好。
我們接下來的重點要先從 TCP 的性能入手。
HTTP 時延損耗
再來回顧一下上面的 HTTP 事務(wù)的過程,你覺得有哪幾個過程會造成 HTTP 事務(wù)時延呢?如下圖所示

從圖中可以看出,主要是有下面這幾個因素影響 HTTP 事務(wù)的時延
客戶端會根據(jù) URL 確定服務(wù)器的 IP 和端口號,這里主要是 DNS 把域名轉(zhuǎn)換為 IP 地址的時延,DNS 會發(fā)起 DNS 查詢,查詢服務(wù)器的 IP 地址。 第二個時延是 TCP 建立連接時會由客戶端向服務(wù)器發(fā)送連接請求報文,并等待服務(wù)器回送響應(yīng)報文的時延。每條新的 TCP 連接建立都會有建立時延。 一旦連接建立后,客戶端就會向服務(wù)器請求數(shù)據(jù),這個時延主要是服務(wù)器從 TCP 連接中讀取請求報文,并對請求進行處理的時延。 服務(wù)器會向客戶端傳輸響應(yīng)報文的時延。 最后一個時延是 TCP 連接關(guān)閉的時延。
其中最后一點的優(yōu)化也是本文想要討論的一個重點。
HTTP 連接管理
試想一個問題,假設(shè)一個頁面有五個資源(元素),每個資源都需要客戶端打開一個 TCP 連接、獲取資源、斷開連接,而且每個連接都是串行打開的,如下圖所示:

串行的意思就是,這五個連接必須是有先后順序,不會出現(xiàn)同時有兩個以上的連接同時打開的情況。
上面五個資源就需要打開五條連接,資源少還好說,CPU 能夠處理,如果頁面資源達到上百或者更多的時候呢?每個資源還需要單獨再打開一條連接嗎?這樣顯然會急劇增加 CPU 的處理壓力,造成大量的時延,顯然是沒有必要的。
串行還有一個缺點就是,有些瀏覽器在對象加載完畢之前是無法知道對象的尺寸(size)的,并且瀏覽器需要對象尺寸信息來將他們放在屏幕中合理的位置上,所以在加載了足夠多的對象之前,屏幕是不會顯示任何內(nèi)容的,這就會造成,其實對象一直在加載,但是我們以為瀏覽器卡住了。
所以,有沒有能夠優(yōu)化 HTTP 性能的方式呢?這個問題問得好,當(dāng)然是有的。
并行連接
這是一種最常見的,也是最容易想到的一種連接方式,HTTP 允許客戶端打開多條連接,并行執(zhí)行多個 HTTP 事務(wù),加入并行連接后,整個 HTTP 事務(wù)的請求過程是這樣的。

采用并行連接這種方式會克服單條連接的空載時間和帶寬限制,因為每個事務(wù)都有連接,因此時延能夠重疊起來,會提高頁面的加載速度。
但是并行連接并不一定快,如果帶寬不夠的情況下,甚至頁面響應(yīng)速度還不如串行連接,因為在并行連接中,每個連接都會去競爭使用有效的帶寬,每個對象都會以較慢的速度加載,有可能連接 1 加載了 95% ,連接 2 占用帶寬加載了 80%,連接 3 ,連接 4 。。。。。。雖然每個對象都在加載,但是頁面上卻沒有任何響應(yīng)。
而且,打開大量連接會消耗很多內(nèi)存資源,從而出現(xiàn)性能問題,上面討論的就五個連接,這個還比較少,復(fù)雜的 web 頁面有可能會有數(shù)十甚至數(shù)百個內(nèi)嵌對象,也就是說,客戶端可以打開數(shù)百個連接,而且有許多的客戶端同時發(fā)出申請,這樣很容易會成為性能瓶頸。
這樣看來,并行連接并不一定"快",實際上并行連接并沒有加快頁面的傳輸速度,并行連接也只是造成了一種假象,這是一切并行的通病。
持久連接
Web 客戶端通常會打開到同一個站點的連接,而且初始化了對某服務(wù)器請求的應(yīng)用程序很可能會在不久的將來對這臺服務(wù)器發(fā)起更多的請求,比如獲取更多的圖片。這種特性被稱為站點局部性(site locality)。
因此,HTTP 1.1 以及 HTTP1.0 的允許 HTTP 在執(zhí)行完一次事務(wù)之后將連接繼續(xù)保持在打開狀態(tài),這個打開狀態(tài)其實指的就是 TCP 的打開狀態(tài),以便于下一次的 HTTP 事務(wù)能夠復(fù)用這條連接。
在一次 HTTP 事務(wù)結(jié)束之后仍舊保持打開狀態(tài)的 TCP 連接被稱為
持久連接。
非持久連接會在每個事務(wù)結(jié)束之后關(guān)閉,相對的,持久連接會在每個事務(wù)結(jié)束之后繼續(xù)保持打開狀態(tài)。持久連接會在不同事務(wù)之間保持打開狀態(tài),直到客戶端或者服務(wù)器決定將其關(guān)閉為止。
長連接也是有缺點的,如果單一客戶端發(fā)起請求數(shù)量不是很頻繁,但是連接的客戶端卻有很多的話,服務(wù)器早晚會有崩潰的時候。
持久連接一般有兩種選型方式,一種是 HTTP 1.0 + keep-alive ;一種是 HTTP 1.1 + persistent。
HTTP 1.1 之前的版本默認連接都是非持久連接,如果想要在舊版本的 HTTP 上使用持久連接,需要指定 Connection 的值為 Keep-Alive。
HTTP 1.1 版本都是持久性連接,如果想要斷開連接時,需要指定 Connection 的值為 close,這也是我們上面說的兩種選型方式的版本因素。
下面是使用了持久連接之后的 HTTP 事務(wù)與使用串行 HTTP 事務(wù)連接的對比圖

這張圖對比了 HTTP 事務(wù)在串行連接上和持久連接的時間損耗圖,可以看到,HTTP 持久連接省去了連接打開 - 連接關(guān)閉的時間,所以在時間損耗上有所縮減。
在持久性連接中,還有一個非常有意思的地方,就是 Connection 選項,Connection 是一個通用選項,也就是客戶端和服務(wù)端都具有的一個標(biāo)頭,下面是一個具有持久性連接的客戶端和服務(wù)端的請求-響應(yīng)圖

從這張圖可以看出,持久連接主要使用的就是 Connection 標(biāo)頭,這也就意味著,Connection 就是持久性連接的實現(xiàn)方式。所以下面我們主要討論一下 Connection 這個大佬。
Connection 標(biāo)頭
Connection 標(biāo)頭具有兩種作用
和 Upgrade 一起使用進行協(xié)議升級 管理持久連接
和 Upgrade 一起使用進行協(xié)議升級
HTTP 提供了一種特殊的機制,這一機制允許將一個已建立的連接升級成新的協(xié)議,一般寫法如下
GET /index.html HTTP/1.1
Host: www.example.com
Connection: upgrade
Upgrade: example/1, foo/2
HTTP/2 明確禁止使用此機制,這個機制只屬于HTTP/1.1
也就是說,客戶端發(fā)起 Connection:upgrade 就表明這是一個連接升級的請求,如果服務(wù)器決定升級這次連接,就會返回一個 101 Switching Protocols 響應(yīng)狀態(tài)碼,和一個要切換到的協(xié)議的頭部字段 Upgrade。如果服務(wù)器沒有(或者不能)升級這次連接,它會忽略客戶端發(fā)送的 Upgrade 頭部字段,返回一個常規(guī)的響應(yīng):例如返回 200。
管理持久連接
我們上面說持久連接有兩種方式,一種是 HTTP 1.0 + Keep-Alive ;一種是 HTTP 1.1 + persistent。
Connection: Keep-Alive
Keep-Alive: timeout=10,max=500
在 HTTP 1.0 + Keep-Alive 這種方式下,客戶端可以通過包含 Connection:Keep-Alive 首部請求將一條連接保持在打開狀態(tài)。
這里需要注意??一點:Keep-Alive 首部只是將請求保持在活躍狀態(tài),發(fā)出 Keep-Alive 請求之后,客戶端和服務(wù)器不一定會同意進行 Keep-Alive 會話。它們可以在任何時刻關(guān)閉空閑的 Keep-Alive 連接,并且客戶端和服務(wù)器可以限制 Keep-Alive 連接所處理事務(wù)的數(shù)量。
Keep-Alive 這個標(biāo)頭有下面幾種選項:
timeout:這個參數(shù)估計了服務(wù)器希望將連接保持在活躍狀態(tài)的時間。max:這個參數(shù)是跟在 timeout 參數(shù)后面的,它表示的是服務(wù)器還能夠為多少個事務(wù)打開持久連接。
Keep-Alive 這個首部是可選的,但是只有在提供 Connection:Keep-Alive 時才能使用它。
Keep-Alive 的使用有一定限制,下面我們就來討論一下 Keep-Alive 的使用限制問題。
Keep-Alive 使用限制和規(guī)則
在 HTTP/1.0 中,Keep-Alive 并不是默認使用的,客戶端必須發(fā)送一個 Connection:Keep-Alive 請求首部來激活 Keep-Alive 連接。
通過檢測響應(yīng)中是否含有 Connection:Keep-Alive 首部字段,客戶端可以判斷服務(wù)器是否在發(fā)出響應(yīng)之后關(guān)閉連接。
代理和網(wǎng)管必須執(zhí)行 Connection 首部規(guī)則,它們必須在將報文轉(zhuǎn)發(fā)出去或者將緩存之前,刪除 Connection 首部中的首部字段和 Connection 首部自身,因為 Connection 是一個
Hop-by-Hop首部,這個首部說的是只對單次轉(zhuǎn)發(fā)有效,會因為轉(zhuǎn)發(fā)給緩存/代理服務(wù)器而失效。嚴格來說,不應(yīng)該與無法確定是否支持 Connection 首部的代理服務(wù)器建立 Keep-Alive 連接,以防止出現(xiàn)
啞代理問題,啞代理問題我們下面會說。
Keep-Alive 和啞代理問題
這里我先解釋一下什么是代理服務(wù)器,然后再說啞代理問題。
什么是代理服務(wù)器?
代理服務(wù)器就是代替客戶端去獲取網(wǎng)絡(luò)信息的一種媒介,通俗一點就是網(wǎng)絡(luò)信息的中轉(zhuǎn)站。
為什么我們需要代理服務(wù)器?
最廣泛的一種用處是我們需要使用代理服務(wù)器來替我們訪問一些我們客戶端無法直接訪問的網(wǎng)站。除此之外,代理服務(wù)器還有很多功能,比如緩存功能,可以降低費用,節(jié)省帶寬;對信息的實時監(jiān)控和過濾,代理服務(wù)器相對于目標(biāo)服務(wù)器(最終獲取信息的服務(wù)器)來說,也是一個客戶端,它能夠獲取服務(wù)器提供的信息,代理服務(wù)器相對于客戶端來說,它是一個服務(wù)器,由它來決定提供哪些信息給客戶端,以此來達到監(jiān)控和過濾的功能。
啞代理問題出現(xiàn)就出現(xiàn)在代理服務(wù)器上,再細致一點就是出現(xiàn)在不能識別 Connection 首部的代理服務(wù)器,而且不知道在發(fā)出請求之后會刪除 Connection 首部的代理服務(wù)器。
假設(shè)一個 Web 客戶端正在通過一個啞代理服務(wù)器與 Web 服務(wù)器進行對話,如下圖所示

來解釋一下上面這幅圖
首先,Web 客戶端向代理發(fā)送了一條報文,其中包含了 Connection: Keep-Alive 首部,希望在這次 HTTP 事務(wù)之后繼續(xù)保持活躍狀態(tài),然后客戶端等待響應(yīng),以確定對方是否允許持久連接。 啞代理(這里先界定為啞代理是不妥的,我們往往先看做的事,再給這件事定性,現(xiàn)在這個服務(wù)器還沒做出啞代理行為呢,就給他定性了)收到了這條 HTTP 請求,但它不理解 Connection 首部,它也不知道 Keep-Alive 是什么意思,因此只是沿著轉(zhuǎn)發(fā)鏈路將報文發(fā)送給服務(wù)器,但 Connection 首部是個 Hop-by-Hop 首部,只適用于單條鏈路傳輸,所以這個代理服務(wù)器不應(yīng)該再將其發(fā)送給服務(wù)器了,但是它還是發(fā)送了,后面就會發(fā)生一些難頂?shù)氖虑椤?/section> 經(jīng)過轉(zhuǎn)發(fā)的 HTTP 請求到達服務(wù)器后,會誤以為對方希望保持 Keep-Alive 持久連接,經(jīng)過評估后,服務(wù)器作出響應(yīng),它同意進行 Keep-Alive 對話,所以它回送了一個 Connection:Keep-Alive 響應(yīng)并到達了啞代理服務(wù)器。 啞代理服務(wù)器會直接將響應(yīng)發(fā)送給客戶端,客戶端收到響應(yīng)后,就知道服務(wù)器可以使用持久連接。然而,此時客戶端和服務(wù)器都知道要使用 Keep-Alive 持久連接,但是啞代理服務(wù)器卻對 Keep-Alive 一無所知。 由于代理對 Keep-Alive 一無所知,所以會收到的所有數(shù)據(jù)都會發(fā)送給客戶端,然后等待服務(wù)器關(guān)閉連接,但是代理服務(wù)器卻認為應(yīng)該保持打開狀態(tài),所以不會去關(guān)閉連接。這樣,啞代理服務(wù)器就一直掛在那里等待連接的關(guān)閉。 等到客戶端發(fā)送下一個 HTTP 事務(wù)后,啞代理會直接忽視新的 HTTP 事務(wù),因為它并不認為一條連接上還會有其他請求的到來,所以會直接忽略新的請求。
這就是 Keep-Alive 的啞代理。
那么如何解決這個問題呢?用 Proxy-Connection
Proxy-Connection 解決啞代理
網(wǎng)景公司提出了一種使用 Proxy-Connection 標(biāo)頭的辦法,首先瀏覽器會向代理發(fā)送 Proxy-Connection 擴展首部,而不是官方支持的 Connection 首部。如果代理服務(wù)器是啞代理的話,它會直接將 Proxy-Connection 發(fā)送給服務(wù)器,而服務(wù)器收到 Proxy-Connection 的話,就會忽略這個首部,這樣不會帶來任何問題。如果是一個聰明的代理服務(wù)器,在收到 Proxy-Connection 的時候,就會直接將 Connection 替換掉 Proxy-Connection ,再發(fā)送給服務(wù)器。
HTTP/1.1 持久連接
HTTP/1.1 逐漸停止了對 Keep-Alive 連接的支持,用一種名為 persistent connection 的改進型設(shè)計取代了 Keep-Alive ,這種改進型設(shè)計也是持久連接,不過比 HTTP/1.0 的工作機制更優(yōu)。
與 HTTP/1.0 的 Keep-Alive 連接不同,HTTP/1.1 在默認情況下使用的就是持久連接。除非特別指明,否則 HTTP/1.1 會假定所有連接都是持久連接。如果想要在事務(wù)結(jié)束后關(guān)閉連接的話,就需要在報文中顯示添加一個 Connection:close 首部。這是與以前的 HTTP 協(xié)議版本很重要的區(qū)別。
使用 persistent connection 也會有一些限制和規(guī)則:
首先,發(fā)送了 Connection: close 請求后,客戶端就無法在這條連接上發(fā)送更多的請求。這同時也可以說,如果客戶端不想發(fā)送其他請求,就可以使用 Connection:close 關(guān)閉連接。 HTTP/1.1 的代理必須能夠分別管理客戶端和服務(wù)器的持久連接 ,每個持久連接都只適用于單次傳輸。 客戶端對任何服務(wù)器或者代理最好只維護兩條持久連接,以防止服務(wù)器過載。 只有實體部分的長度和相應(yīng)的 Content-Length保持一致時,或者使用分塊傳輸編碼的方式時,連接才能保持長久。
管道化連接
HTTP/1.1 允許在持久連接上使用請求管道。這是相對于 Keep-Alive 連接的又一個性能優(yōu)化。管道就是一個承載 HTTP 請求的載體,我們可以將多個 HTTP 請求放入管道,這樣能夠降低網(wǎng)絡(luò)的環(huán)回時間,提升性能。下圖是使用串行連接、并行連接、管道化連接的示意圖:

使用管道化的連接也有幾處限制:
如果 HTTP 客戶端無法確認連接是持久的,就不應(yīng)該使用管道。 必須按照與請求的相同順序回送 HTTP 響應(yīng),因為 HTTP 沒有序號這個概念,所以一旦響應(yīng)失序,就沒辦法將其與請求匹配起來了。 HTTP 客戶端必須做好連接會在任何時刻關(guān)閉的準(zhǔn)備,還要準(zhǔn)備好重發(fā)所有未完成的管道化請求。
HTTP 關(guān)閉連接
所有 HTTP 客戶端、服務(wù)器或者代理都可以在任意時刻關(guān)閉一條 HTTP 傳輸連接。通常情況下會在一次響應(yīng)后關(guān)閉連接,但是保不準(zhǔn)也會在 HTTP 事務(wù)的過程中出現(xiàn)。
但是,服務(wù)器無法確定在關(guān)閉的那一刻,客戶端有沒有數(shù)據(jù)要發(fā)送,如果出現(xiàn)這種情況,客戶端就會在進行數(shù)據(jù)傳輸?shù)倪^程中發(fā)生了寫入錯誤。
即使在不出錯的情況下,連接也可以在任意時刻關(guān)閉。如果在事務(wù)傳輸?shù)倪^程中出現(xiàn)了連接關(guān)閉情況,就需要重新打開連接進行重試。如果是單條連接還好說,如果是管道化連接,就比較糟糕,因為管道化連接會把大量的連接丟在管道中,此時如果服務(wù)器關(guān)閉,就會造成大量的連接未響應(yīng),需要重新調(diào)度。
如果一個 HTTP 事務(wù)不管執(zhí)行一次還是執(zhí)行 n 次,它得到的結(jié)果始終是一樣的,那么我們就認為這個事務(wù)是冪等的,一般 GET、HEAD、PUT、DELETE、TRACE 和 OPTIONS方法都認為是冪等的。客戶端不應(yīng)該以管道化的方式發(fā)送任何非冪等請求,比如 POST,否則就會造成不確定的后果。
由于 HTTP 使用 TCP 作為傳輸層的協(xié)議,所以 HTTP 關(guān)閉連接其實還是 TCP 關(guān)閉連接的過程。
HTTP 關(guān)閉連接一共分為三種情況:完全關(guān)閉、半關(guān)閉和正常關(guān)閉。
應(yīng)用程序可以關(guān)閉 TCP 輸入和輸出信道中的任何一個,或者將二者同時關(guān)閉。調(diào)用套接字 close() 方法會講輸入和輸出同時關(guān)閉,這就被稱為完全關(guān)閉。還可以調(diào)用套接字的 shutdown 方法單獨關(guān)閉輸入或者輸出信道,這被稱為半關(guān)閉。HTTP 規(guī)范建議當(dāng)客戶端和服務(wù)器突然需要關(guān)閉連接的時候,應(yīng)該正常關(guān)閉,但是它沒有說如何去做。
關(guān)于 TCP 一些關(guān)閉問題的深入研究,你可以閱讀 cxuan 的另一篇文章 TCP 基礎(chǔ)知識
完
往期推薦
??
我真不想學(xué) happens - before 了!

