圖解 HTTP 連接管理
Hey guys ,這里是程序員cxuan,歡迎你收看我最新一期的文章。
熟悉我的小伙伴都知道,我之前肝了本《HTTP 核心總結》的 PDF,這本 PDF 是取自我 HTTP 系列文章的匯總,然而我寫的 HTTP 相關內容都是一年前了,我回頭看了一下這本 PDF,雖然內容不少,但是很多內容缺少系統(tǒng)性,看起來不爽,這個有悖于我的初心,所以我打算重新搞一搞 HTTP 協(xié)議,HTTP 協(xié)議對我們程序員來說太重要了,不管你使用的是哪個語言,HTTP 都是你需要知道的重點。
這不是一篇簡單介紹 HTTP 基本概念的文章,如果你對 HTTP 基本概念不是很熟悉,推薦你去讀 cxuan 寫的關于 HTTP 基礎文章 - 看完這篇HTTP,跟面試官扯皮就沒問題了
所以我們假定在做的各位對 HTTP 有一定的了解和認識。
下面開始我們這篇文章。
搭載 HTTP 的 TCP
我們大家都知道,HTTP 這個應用層協(xié)議是以 TCP 為基礎來傳輸數據的。當你想訪問一個資源(資源在網絡中就是一個 URL)時,你需要先解析這個資源的 IP 地址和端口號,從而和這個 IP 和端口號所在的服務器建立 TCP 連接,然后 HTTP 客戶端發(fā)起服務請求(GET)報文,服務器對服務器的請求報文做出響應,等到不需要交換報文時,客戶端會關閉連接,下面我用圖很好的說明了這個過程。
上面這幅圖很好的說明了 HTTP 從建立連接開始 -> 發(fā)起請求報文 -> 關閉連接的全過程,但是上面這個過程還忽略了一個很重要的點,那就是TCP 建立連接的過程。
TCP 建立連接需要經過三次握手,交換三個報文,我相信大家都對這個過程了然于胸了,如果你還不清楚 TCP 建立連接的過程,可以先閱讀 cxuan 的這篇文章 TCP 連接管理。
由于 HTTP 位于 TCP 的上層,所以 HTTP 的請求 -> 響應過程的時效性(性能)很大程度上取決于底層 TCP 的性能,只有在了解了 TCP 連接的性能之后,才可以更好的理解 HTTP 連接的性能,從而才能夠實現高性能的 HTTP 應用程序。
我們通常把一次完整的請求 -> 相應過程稱之為 HTTP 事務。
所以我后面一般會寫為 HTTP 事務,你理解怎么回事就好。
我們接下來的重點要先從 TCP 的性能入手。
HTTP 時延損耗
再來回顧一下上面的 HTTP 事務的過程,你覺得有哪幾個過程會造成 HTTP 事務時延呢?如下圖所示
從圖中可以看出,主要是有下面這幾個因素影響 HTTP 事務的時延
- 客戶端會根據 URL 確定服務器的 IP 和端口號,這里主要是 DNS 把域名轉換為 IP 地址的時延,DNS 會發(fā)起 DNS 查詢,查詢服務器的 IP 地址。
- 第二個時延是 TCP 建立連接時會由客戶端向服務器發(fā)送連接請求報文,并等待服務器回送響應報文的時延。每條新的 TCP 連接建立都會有建立時延。
- 一旦連接建立后,客戶端就會向服務器請求數據,這個時延主要是服務器從 TCP 連接中讀取請求報文,并對請求進行處理的時延。
- 服務器會向客戶端傳輸響應報文的時延。
- 最后一個時延是 TCP 連接關閉的時延。
其中最后一點的優(yōu)化也是本文想要討論的一個重點。
HTTP 連接管理
試想一個問題,假設一個頁面有五個資源(元素),每個資源都需要客戶端打開一個 TCP 連接、獲取資源、斷開連接,而且每個連接都是串行打開的,如下圖所示:
串行的意思就是,這五個連接必須是有先后順序,不會出現同時有兩個以上的連接同時打開的情況。
上面五個資源就需要打開五條連接,資源少還好說,CPU 能夠處理,如果頁面資源達到上百或者更多的時候呢?每個資源還需要單獨再打開一條連接嗎?這樣顯然會急劇增加 CPU 的處理壓力,造成大量的時延,顯然是沒有必要的。
串行還有一個缺點就是,有些瀏覽器在對象加載完畢之前是無法知道對象的尺寸(size)的,并且瀏覽器需要對象尺寸信息來將他們放在屏幕中合理的位置上,所以在加載了足夠多的對象之前,屏幕是不會顯示任何內容的,這就會造成,其實對象一直在加載,但是我們以為瀏覽器卡住了。
所以,有沒有能夠優(yōu)化 HTTP 性能的方式呢?這個問題問得好,當然是有的。
并行連接
這是一種最常見的,也是最容易想到的一種連接方式,HTTP 允許客戶端打開多條連接,并行執(zhí)行多個 HTTP 事務,加入并行連接后,整個 HTTP 事務的請求過程是這樣的。
采用并行連接這種方式會克服單條連接的空載時間和帶寬限制,因為每個事務都有連接,因此時延能夠重疊起來,會提高頁面的加載速度。
但是并行連接并不一定快,如果帶寬不夠的情況下,甚至頁面響應速度還不如串行連接,因為在并行連接中,每個連接都會去競爭使用有效的帶寬,每個對象都會以較慢的速度加載,有可能連接 1 加載了 95% ,連接 2 占用帶寬加載了 80%,連接 3 ,連接 4 。。。。。。雖然每個對象都在加載,但是頁面上卻沒有任何響應。
而且,打開大量連接會消耗很多內存資源,從而出現性能問題,上面討論的就五個連接,這個還比較少,復雜的 web 頁面有可能會有數十甚至數百個內嵌對象,也就是說,客戶端可以打開數百個連接,而且有許多的客戶端同時發(fā)出申請,這樣很容易會成為性能瓶頸。
這樣看來,并行連接并不一定"快",實際上并行連接并沒有加快頁面的傳輸速度,并行連接也只是造成了一種假象,這是一切并行的通病。
持久連接
Web 客戶端通常會打開到同一個站點的連接,而且初始化了對某服務器請求的應用程序很可能會在不久的將來對這臺服務器發(fā)起更多的請求,比如獲取更多的圖片。這種特性被稱為站點局部性(site locality)。
因此,HTTP 1.1 以及 HTTP1.0 的允許 HTTP 在執(zhí)行完一次事務之后將連接繼續(xù)保持在打開狀態(tài),這個打開狀態(tài)其實指的就是 TCP 的打開狀態(tài),以便于下一次的 HTTP 事務能夠復用這條連接。
在一次 HTTP 事務結束之后仍舊保持打開狀態(tài)的 TCP 連接被稱為持久連接。
非持久連接會在每個事務結束之后關閉,相對的,持久連接會在每個事務結束之后繼續(xù)保持打開狀態(tài)。持久連接會在不同事務之間保持打開狀態(tài),直到客戶端或者服務器決定將其關閉為止。
長連接也是有缺點的,如果單一客戶端發(fā)起請求數量不是很頻繁,但是連接的客戶端卻有很多的話,服務器早晚會有崩潰的時候。
持久連接一般有兩種選型方式,一種是 HTTP 1.0 + keep-alive ;一種是 HTTP 1.1 + persistent。
HTTP 1.1 之前的版本默認連接都是非持久連接,如果想要在舊版本的 HTTP 上使用持久連接,需要指定 Connection 的值為 Keep-Alive。
HTTP 1.1 版本都是持久性連接,如果想要斷開連接時,需要指定 Connection 的值為 close,這也是我們上面說的兩種選型方式的版本因素。
下面是使用了持久連接之后的 HTTP 事務與使用串行 HTTP 事務連接的對比圖
這張圖對比了 HTTP 事務在串行連接上和持久連接的時間損耗圖,可以看到,HTTP 持久連接省去了連接打開 - 連接關閉的時間,所以在時間損耗上有所縮減。
在持久性連接中,還有一個非常有意思的地方,就是 Connection 選項,Connection 是一個通用選項,也就是客戶端和服務端都具有的一個標頭,下面是一個具有持久性連接的客戶端和服務端的請求-響應圖
從這張圖可以看出,持久連接主要使用的就是 Connection 標頭,這也就意味著,Connection 就是持久性連接的實現方式。所以下面我們主要討論一下 Connection 這個大佬。
Connection 標頭
Connection 標頭具有兩種作用
- 和 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 就表明這是一個連接升級的請求,如果服務器決定升級這次連接,就會返回一個 101 Switching Protocols 響應狀態(tài)碼,和一個要切換到的協(xié)議的頭部字段 Upgrade。如果服務器沒有(或者不能)升級這次連接,它會忽略客戶端發(fā)送的 Upgrade 頭部字段,返回一個常規(guī)的響應:例如返回 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 請求之后,客戶端和服務器不一定會同意進行 Keep-Alive 會話。它們可以在任何時刻關閉空閑的 Keep-Alive 連接,并且客戶端和服務器可以限制 Keep-Alive 連接所處理事務的數量。
Keep-Alive 這個標頭有下面幾種選項:
- timeout:這個參數估計了服務器希望將連接保持在活躍狀態(tài)的時間。
- max :這個參數是跟在 timeout 參數后面的,它表示的是服務器還能夠為多少個事務打開持久連接。
Keep-Alive 這個首部是可選的,但是只有在提供 Connection:Keep-Alive 時才能使用它。
Keep-Alive 的使用有一定限制,下面我們就來討論一下 Keep-Alive 的使用限制問題。
Keep-Alive 使用限制和規(guī)則
- 在 HTTP/1.0 中,Keep-Alive 并不是默認使用的,客戶端必須發(fā)送一個 Connection:Keep-Alive 請求首部來激活 Keep-Alive 連接。
- 通過檢測響應中是否含有 Connection:Keep-Alive 首部字段,客戶端可以判斷服務器是否在發(fā)出響應之后關閉連接。
- 代理和網管必須執(zhí)行 Connection 首部規(guī)則,它們必須在將報文轉發(fā)出去或者將緩存之前,刪除 Connection 首部中的首部字段和 Connection 首部自身,因為 Connection 是一個 Hop-by-Hop 首部,這個首部說的是只對單次轉發(fā)有效,會因為轉發(fā)給緩存/代理服務器而失效。
- 嚴格來說,不應該與無法確定是否支持 Connection 首部的代理服務器建立 Keep-Alive 連接,以防止出現啞代理問題,啞代理問題我們下面會說。
Keep-Alive 和啞代理問題
這里我先解釋一下什么是代理服務器,然后再說啞代理問題。
什么是代理服務器?
代理服務器就是代替客戶端去獲取網絡信息的一種媒介,通俗一點就是網絡信息的中轉站。
為什么我們需要代理服務器?
最廣泛的一種用處是我們需要使用代理服務器來替我們訪問一些我們客戶端無法直接訪問的網站。除此之外,代理服務器還有很多功能,比如緩存功能,可以降低費用,節(jié)省帶寬;對信息的實時監(jiān)控和過濾,代理服務器相對于目標服務器(最終獲取信息的服務器)來說,也是一個客戶端,它能夠獲取服務器提供的信息,代理服務器相對于客戶端來說,它是一個服務器,由它來決定提供哪些信息給客戶端,以此來達到監(jiān)控和過濾的功能。
啞代理問題出現就出現在代理服務器上,再細致一點就是出現在不能識別 Connection 首部的代理服務器,而且不知道在發(fā)出請求之后會刪除 Connection 首部的代理服務器。
假設一個 Web 客戶端正在通過一個啞代理服務器與 Web 服務器進行對話,如下圖所示
來解釋一下上面這幅圖
- 首先,Web 客戶端向代理發(fā)送了一條報文,其中包含了 Connection: Keep-Alive 首部,希望在這次 HTTP 事務之后繼續(xù)保持活躍狀態(tài),然后客戶端等待響應,以確定對方是否允許持久連接。
- 啞代理(這里先界定為啞代理是不妥的,我們往往先看做的事,再給這件事定性,現在這個服務器還沒做出啞代理行為呢,就給他定性了)收到了這條 HTTP 請求,但它不理解 Connection 首部,它也不知道 Keep-Alive 是什么意思,因此只是沿著轉發(fā)鏈路將報文發(fā)送給服務器,但 Connection 首部是個 Hop-by-Hop 首部,只適用于單條鏈路傳輸,所以這個代理服務器不應該再將其發(fā)送給服務器了,但是它還是發(fā)送了,后面就會發(fā)生一些難頂的事情。
- 經過轉發(fā)的 HTTP 請求到達服務器后,會誤以為對方希望保持 Keep-Alive 持久連接,經過評估后,服務器作出響應,它同意進行 Keep-Alive 對話,所以它回送了一個 Connection:Keep-Alive 響應并到達了啞代理服務器。
- 啞代理服務器會直接將響應發(fā)送給客戶端,客戶端收到響應后,就知道服務器可以使用持久連接。然而,此時客戶端和服務器都知道要使用 Keep-Alive 持久連接,但是啞代理服務器卻對 Keep-Alive 一無所知。
- 由于代理對 Keep-Alive 一無所知,所以會收到的所有數據都會發(fā)送給客戶端,然后等待服務器關閉連接,但是代理服務器卻認為應該保持打開狀態(tài),所以不會去關閉連接。這樣,啞代理服務器就一直掛在那里等待連接的關閉。
- 等到客戶端發(fā)送下一個 HTTP 事務后,啞代理會直接忽視新的 HTTP 事務,因為它并不認為一條連接上還會有其他請求的到來,所以會直接忽略新的請求。
這就是 Keep-Alive 的啞代理。
那么如何解決這個問題呢?用 Proxy-Connection
Proxy-Connection 解決啞代理
網景公司提出了一種使用 Proxy-Connection 標頭的辦法,首先瀏覽器會向代理發(fā)送 Proxy-Connection 擴展首部,而不是官方支持的 Connection 首部。如果代理服務器是啞代理的話,它會直接將 Proxy-Connection 發(fā)送給服務器,而服務器收到 Proxy-Connection 的話,就會忽略這個首部,這樣不會帶來任何問題。如果是一個聰明的代理服務器,在收到 Proxy-Connection 的時候,就會直接將 Connection 替換掉 Proxy-Connection ,再發(fā)送給服務器。
HTTP/1.1 持久連接
HTTP/1.1 逐漸停止了對 Keep-Alive 連接的支持,用一種名為 persistent connection 的改進型設計取代了 Keep-Alive ,這種改進型設計也是持久連接,不過比 HTTP/1.0 的工作機制更優(yōu)。
與 HTTP/1.0 的 Keep-Alive 連接不同,HTTP/1.1 在默認情況下使用的就是持久連接。除非特別指明,否則 HTTP/1.1 會假定所有連接都是持久連接。如果想要在事務結束后關閉連接的話,就需要在報文中顯示添加一個 Connection:close 首部。這是與以前的 HTTP 協(xié)議版本很重要的區(qū)別。
使用 persistent connection 也會有一些限制和規(guī)則:
- 首先,發(fā)送了 Connection: close 請求后,客戶端就無法在這條連接上發(fā)送更多的請求。這同時也可以說,如果客戶端不想發(fā)送其他請求,就可以使用 Connection:close 關閉連接。
- HTTP/1.1 的代理必須能夠分別管理客戶端和服務器的持久連接 ,每個持久連接都只適用于單次傳輸。
- 客戶端對任何服務器或者代理最好只維護兩條持久連接,以防止服務器過載。
- 只有實體部分的長度和相應的 Content-Length保持一致時,或者使用分塊傳輸編碼的方式時,連接才能保持長久。
管道化連接
HTTP/1.1 允許在持久連接上使用請求管道。這是相對于 Keep-Alive 連接的又一個性能優(yōu)化。管道就是一個承載 HTTP 請求的載體,我們可以將多個 HTTP 請求放入管道,這樣能夠降低網絡的環(huán)回時間,提升性能。下圖是使用串行連接、并行連接、管道化連接的示意圖:
使用管道化的連接也有幾處限制:
- 如果 HTTP 客戶端無法確認連接是持久的,就不應該使用管道。
- 必須按照與請求的相同順序回送 HTTP 響應,因為 HTTP 沒有序號這個概念,所以一旦響應失序,就沒辦法將其與請求匹配起來了。
- HTTP 客戶端必須做好連接會在任何時刻關閉的準備,還要準備好重發(fā)所有未完成的管道化請求。
HTTP 關閉連接
所有 HTTP 客戶端、服務器或者代理都可以在任意時刻關閉一條 HTTP 傳輸連接。通常情況下會在一次響應后關閉連接,但是保不準也會在 HTTP 事務的過程中出現。
但是,服務器無法確定在關閉的那一刻,客戶端有沒有數據要發(fā)送,如果出現這種情況,客戶端就會在進行數據傳輸的過程中發(fā)生了寫入錯誤。
即使在不出錯的情況下,連接也可以在任意時刻關閉。如果在事務傳輸的過程中出現了連接關閉情況,就需要重新打開連接進行重試。如果是單條連接還好說,如果是管道化連接,就比較糟糕,因為管道化連接會把大量的連接丟在管道中,此時如果服務器關閉,就會造成大量的連接未響應,需要重新調度。
如果一個 HTTP 事務不管執(zhí)行一次還是執(zhí)行 n 次,它得到的結果始終是一樣的,那么我們就認為這個事務是冪等的,一般 GET、HEAD、PUT、DELETE、TRACE 和 OPTIONS方法都認為是冪等的。客戶端不應該以管道化的方式發(fā)送任何非冪等請求,比如 POST,否則就會造成不確定的后果。
由于 HTTP 使用 TCP 作為傳輸層的協(xié)議,所以 HTTP 關閉連接其實還是 TCP 關閉連接的過程。
HTTP 關閉連接一共分為三種情況:完全關閉、半關閉和正常關閉。
應用程序可以關閉 TCP 輸入和輸出信道中的任何一個,或者將二者同時關閉。調用套接字 close() 方法會講輸入和輸出同時關閉,這就被稱為完全關閉。還可以調用套接字的 shutdown 方法單獨關閉輸入或者輸出信道,這被稱為半關閉。HTTP 規(guī)范建議當客戶端和服務器突然需要關閉連接的時候,應該正常關閉,但是它沒有說如何去做。
本文轉載自微信公眾號「程序員cxuan」,可以通過以下二維碼關注。轉載本文請聯系程序員cxuan公眾號。