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