漫談 HTTP 鏈接

圖片

本文首先會 HTTP 的特色和優缺點,而後會詳細介紹 HTTP 長鏈接和短鏈接的鏈接管理,經過閱讀本文可以對 HTTP 鏈接有個深刻的認識。圖片經過前面的 HTTP 系列文章,想必你們已經知道 HTTP 協議的基本知識,瞭解它的報文結構,請求頭、響應頭等細節。html

HTTP 的特色

因此接下來先是聊聊 HTTP 協議的特色、優勢和缺點。既要看到它好的一面,也要正視它很差的一面,只有全方位、多角度瞭解 HTTP,才能實現「揚長避短」,更好地利用 HTTP。後端

靈活可擴展瀏覽器

首先, HTTP 協議是一個「靈活可擴展」的傳輸協議。緩存

HTTP 協議最初誕生的時候就比較簡單,本着開放的精神只規定了報文的基本格式,好比用空格分隔單詞,用換行分隔字段,「header+body」等,報文裏的各個組成部分都沒有作嚴格的語法語義限制,能夠由開發者任意定製。性能優化

因此,HTTP 協議就隨着互聯網的發展一同成長起來了。在這個過程當中,HTTP 協議逐漸增長了請求方法、版本號、狀態碼、頭字段等特性。而 body 也再也不限於文本形式的 TXT 或 HTML,而是可以傳輸圖片、音頻視頻等任意數據,這些都是源於它的「靈活可擴展」的特色。服務器

而那些 RFC 文檔,實際上也能夠理解爲是對已有擴展的「認可和標準化」,實現了「從實踐中來,到實踐中去」的良性循環。網絡

也正是由於這個特色,HTTP 才能在三十年的歷史長河中「屹立不倒」,從最初的低速實驗網絡發展到如今的遍及全球的高速互聯網,始終保持着旺盛的生命力。數據結構

可靠傳輸架構

第二個特色, HTTP 協議是一個「可靠」的傳輸協議。併發

這個特色顯而易見,由於 HTTP 協議是基於 TCP/IP 的,而 TCP 自己是一個「可靠」的傳輸協議,因此 HTTP 天然也就繼承了這個特性,可以在請求方和應答方之間「可靠」地傳輸數據。

它的具體作法與 TCP/UDP 差很少,都是對實際傳輸的數據(entity)作了一層包裝,加上一個頭,而後調用 Socket API,經過 TCP/IP 協議棧發送或者接收。

不過咱們必須正確地理解「可靠」的含義,HTTP 並不能 100% 保證數據必定可以發送到另外一端,在網絡繁忙、鏈接質量差等惡劣的環境下,也有可能收發失敗。「可靠」只是向使用者提供了一個「承諾」,會在下層用多種手段「儘可能」保證數據的完整送達。

固然,若是遇到光纖被意外挖斷這樣的極端狀況,即便是神仙也不能發送成功。因此,「可靠」傳輸是指在網絡基本正常的狀況下數據收發一定成功,借用運維裏的術語,大概就是「3 個 9」或者「4 個 9」的程度吧。

應用層協議

第三個特色,HTTP 協議是一個應用層的協議。

這個特色也是不言自明的,但卻很重要。

在 TCP/IP 誕生後的幾十年裏,雖然出現了許多的應用層協議,但它們都僅關注很小的應用領域,侷限在不多的應用場景。例如 FTP 只能傳輸文件、SMTP 只能發送郵件、SSH 只能遠程登陸等,在通用的數據傳輸方面「徹底不能打」。

因此 HTTP 憑藉着可攜帶任意頭字段和實體數據的報文結構,以及鏈接控制、緩存代理等方便易用的特性,一出現就「技壓羣雄」,迅速成爲了應用層裏的「明星」協議。只要不太苛求性能,HTTP 幾乎能夠傳遞一切東西,知足各類需求,稱得上是一個「萬能」的協議。

套用一個網上流行的段子,HTTP 徹底能夠用開玩笑的口吻說:「不要誤會,我不是針對 FTP,我是說在座的應用層各位,都是垃圾。」

請求 - 應答

第四個特色,HTTP 協議使用的是請求 - 應答通訊模式。

這個請求 - 應答模式是 HTTP 協議最根本的通訊模型,通俗來說就是「一發一收」「有來有去」,就像是寫代碼時的函數調用,只要填好請求頭裏的字段,「調用」後就會收到答覆。

請求 - 應答模式也明確了 HTTP 協議裏通訊雙方的定位,永遠是請求方先發起鏈接和請求,是主動的,而應答方只有在收到請求後才能答覆,是被動的,若是沒有請求時不會有任何動做。

固然,請求方和應答方的角色也不是絕對的,在瀏覽器 - 服務器的場景裏,一般服務器都是應答方,但若是將它用做代理鏈接後端服務器,那麼它就可能同時扮演請求方和應答方的角色。

HTTP 的請求 - 應答模式也剛好契合了傳統的 C/S(Client/Server)系統架構,請求方做爲客戶端、應答方做爲服務器。因此,隨着互聯網的發展就出現了 B/S(Browser/Server)架構,用輕量級的瀏覽器代替笨重的客戶端應用,實現零維護的「瘦」客戶端,而服務器則擯棄私有通訊協議轉而使用 HTTP 協議。

此外,請求 - 應答模式也徹底符合 RPC(Remote Procedure Call)的工做模式,能夠把 HTTP 請求處理封裝成遠程函數調用,致使了 WebService、RESTful 和 gPRC 等的出現。

無狀態

第五個特色,HTTP 協議是無狀態的。這個所謂的「狀態」應該怎麼理解呢?

「狀態」其實就是客戶端或者服務器裏保存的一些數據或者標誌,記錄了通訊過程當中的一些變化信息。

你必定知道,TCP 協議是有狀態的,一開始處於 CLOSED 狀態,鏈接成功後是 ESTABLISHED 狀態,斷開鏈接後是 FIN-WAIT 狀態,最後又是 CLOSED 狀態。

這些「狀態」就須要 TCP 在內部用一些數據結構去維護,能夠簡單地想象成是個標誌量,標記當前所處的狀態,例如 0 是 CLOSED,2 是 ESTABLISHED 等等。

再來看 HTTP,那麼對比一下 TCP 就看出來了,在整個協議裏沒有規定任何的「狀態」,客戶端和服務器永遠是處在一種「無知」的狀態。創建鏈接前二者互不知情,每次收發的報文也都是互相獨立的,沒有任何的聯繫。收發報文也不會對客戶端或服務器產生任何影響,鏈接後也不會要求保存任何信息。

「無狀態」形象地來講就是「沒有記憶能力」。好比,瀏覽器發了一個請求,說「我是小明,請給我 A 文件。」,服務器收到報文後就會檢查一下權限,看小明確實能夠訪問 A 文件,因而把文件發回給瀏覽器。接着瀏覽器還想要 B 文件,但服務器不會記錄剛纔的請求狀態,不知道第二個請求和第一個請求是同一個瀏覽器發來的,因此瀏覽器必須還得重複一次本身的身份才行:「我是剛纔的小明,請再給我 B 文件。」

咱們能夠再對比一下 UDP 協議,不過它是無鏈接也無狀態的,順序發包亂序收包,數據包發出去後就無論了,收到後也不會順序整理。而 HTTP 是有鏈接無狀態,順序發包順序收包,按照收發的順序管理報文。

但不要忘了 HTTP 是「靈活可擴展」的,雖然標準裏沒有規定「狀態」,但徹底可以在協議的框架裏給它「打個補丁」,增長這個特性。

其餘特色

除了以上的五大特色,其實 HTTP 協議還能夠列出很是多的特色,例如傳輸的實體數據可緩存可壓縮、可分段獲取數據、支持身份認證、支持國際化語言等。但這些並不能算是 HTTP 的基本特色,由於這都是由第一個「靈活可擴展」的特色所衍生出來的。

小結

  • HTTP 是靈活可擴展的,能夠任意添加頭字段實現任意功能;
  • HTTP 是可靠傳輸協議,基於 TCP/IP 協議「儘可能」保證數據的送達;
  • HTTP 是應用層協議,比 FTP、SSH 等更通用功能更多,可以傳輸任意數據;
  • TTP 使用了請求 - 應答模式,客戶端主動發起請求,服務器被動回覆請求;
  • HTTP 本質上是無狀態的,每一個請求都是互相獨立、毫無關聯的,協議不要求客戶端或服務器記錄請求相關的信息。

HTTP的鏈接管理

HTTP 的鏈接管理也算得上是個「老生常談」的話題了,你必定曾經據說過「短鏈接」「長鏈接」之類的名詞,今天讓咱們一塊兒來把它們弄清楚。

短鏈接

HTTP 協議最初(0.9/1.0)是個很是簡單的協議,通訊過程也採用了簡單的「請求 - 應答」方式。

它底層的數據傳輸基於 TCP/IP,每次發送請求前須要先與服務器創建鏈接,收到響應報文後會當即關閉鏈接。

由於客戶端與服務器的整個鏈接過程很短暫,不會與服務器保持長時間的鏈接狀態,因此就被稱爲「短鏈接」(short-lived connections)。早期的 HTTP 協議也被稱爲是「無鏈接」的協議。

短鏈接的缺點至關嚴重,由於在 TCP 協議裏,創建鏈接和關閉鏈接都是很是「昂貴」的操做。TCP 創建鏈接要有「三次握手」,發送 3 個數據包,須要 1 個 RTT;關閉鏈接是「四次揮手」,4 個數據包須要 2 個 RTT。

而 HTTP 的一次簡單「請求 - 響應」一般只須要 4 個包,若是不算服務器內部的處理時間,最可能是 2 個 RTT。這麼算下來,浪費的時間就是「3÷5=60%」,有三分之二的時間被浪費掉了,傳輸效率低得驚人。

圖片

單純地從理論上講,TCP 協議你可能還不太好理解,我就拿打卡考勤機來作個形象的比喻吧。

假設你的公司買了一臺打卡機,放在前臺,由於這臺機器比較貴,因此專門作了一個保護罩蓋着它,公司要求每次上下班打卡時都要先打開蓋子,打卡後再蓋上蓋子。

但是恰恰這個蓋子很是牢固,打開關閉要費很大力氣,打卡可能只要 1 秒鐘,而開關蓋子卻須要四五秒鐘,大部分時間都浪費在了毫無心義的開關蓋子操做上了。

可想而知,日常還好說,一到上下班的點在打卡機前就會排起長隊,每一個人都要重複「開蓋 - 打卡 - 關蓋」的三個步驟,你說着急不着急。

在這個比喻裏,打卡機就至關於服務器,蓋子的開關就是 TCP 的鏈接與關閉,而每一個打卡的人就是 HTTP 請求,很顯然,短鏈接的缺點嚴重製約了服務器的服務能力,致使它沒法處理更多的請求。

長鏈接

針對短鏈接暴露出的缺點,HTTP 協議就提出了「長鏈接」的通訊方式,也叫「持久鏈接」(persistent connections)、「鏈接保活」(keep alive)、「鏈接複用」(connection reuse)。

其實解決辦法也很簡單,用的就是「成本均攤」的思路,既然 TCP 的鏈接和關閉很是耗時間,那麼就把這個時間成本由原來的一個「請求 - 應答」均攤到多個「請求 - 應答」上。

這樣雖然不能改善 TCP 的鏈接效率,但基於「分母效應」,每一個「請求 - 應答」的無效時間就會下降很多,總體傳輸效率也就提升了。

這裏我畫了一個短鏈接與長鏈接的對比示意圖。在短鏈接裏發送了三次 HTTP「請求 - 應答」,每次都會浪費 60% 的 RTT 時間。而在長鏈接的狀況下,一樣發送三次請求,由於只在第一次時創建鏈接,在最後一次時關閉鏈接,因此浪費率就是「3÷9≈33%」,下降了差很少一半的時間損耗。顯然,若是在這個長鏈接上發送的請求越多,分母就越大,利用率也就越高。

繼續用剛纔的打卡機的比喻,公司也以爲這種反覆「開蓋 - 打卡 - 關蓋」的操做太「反人類」了,因而頒佈了新規定,早上打開蓋子後就不用關上了,能夠自由打卡,到下班後再關上蓋子。

這樣打卡的效率(即服務能力)就大幅度提高了,原來一次打卡須要五六秒鐘,如今只要一秒就能夠了,上下班時排長隊的景象一去不返,你們都開心。

鏈接相關的頭字段

因爲長鏈接對性能的改善效果很是顯著,因此在 HTTP/1.1 中的鏈接都會默認啓用長鏈接。不須要用什麼特殊的頭字段指定,只要向服務器發送了第一次請求,後續的請求都會重複利用第一次打開的 TCP 鏈接,也就是長鏈接,在這個鏈接上收發數據。

固然,咱們也能夠在請求頭裏明確地要求使用長鏈接機制,使用的字段是 Connection,值是 「keep-alive」。

不過無論客戶端是否顯式要求長鏈接,若是服務器支持長鏈接,它總會在響應報文裏放一個 「Connection: keep-alive」 字段,告訴客戶端:「我是支持長鏈接的,接下來就用這個 TCP 一直收發數據吧」。

不過長鏈接也有一些小缺點,問題就出在它的「長」字上。

由於 TCP 鏈接長時間不關閉,服務器必須在內存裏保存它的狀態,這就佔用了服務器的資源。若是有大量的空閒長鏈接只連不發,就會很快耗盡服務器的資源,致使服務器沒法爲真正有須要的用戶提供服務。

因此,長鏈接也須要在恰當的時間關閉,不能永遠保持與服務器的鏈接,這在客戶端或者服務器均可以作到。

在客戶端,能夠在請求頭裏加上「Connection: close」字段,告訴服務器:「此次通訊後就關閉鏈接」。服務器看到這個字段,就知道客戶端要主動關閉鏈接,因而在響應報文裏也加上這個字段,發送以後就調用 Socket API 關閉 TCP 鏈接。

服務器端一般不會主動關閉鏈接,但也可使用一些策略。拿 Nginx 來舉例,它有兩種方式:

  • 使用「keepalive_timeout」指令,設置長鏈接的超時時間,若是在一段時間內鏈接上沒有任何數據收發就主動斷開鏈接,避免空閒鏈接佔用系統資源。
  • 使用「keepalive_requests」指令,設置長鏈接上可發送的最大請求次數。好比設置成 1000,那麼當 Nginx 在這個鏈接上處理了 1000 個請求後,也會主動斷開鏈接。

另外,客戶端和服務器均可以在報文裏附加通用頭字段「Keep-Alive: timeout=value」,限定長鏈接的超時時間。但這個字段的約束力並不強,通訊的雙方可能並不會遵照,因此不太常見。

隊頭阻塞

看完了短鏈接和長鏈接,接下來就要說到著名的「隊頭阻塞」(Head-of-line blocking,也叫「隊首阻塞」)了。

「隊頭阻塞」與短鏈接和長鏈接無關,而是由 HTTP 基本的「請求 - 應答」模型所致使的。

由於 HTTP 規定報文必須是「一發一收」,這就造成了一個先進先出的「串行」隊列。隊列裏的請求沒有輕重緩急的優先級,只有入隊的前後順序,排在最前面的請求被最優先處理。

若是隊首的請求由於處理的太慢耽誤了時間,那麼隊列裏後面的全部請求也不得不跟着一塊兒等待,結果就是其餘的請求承擔了不該有的時間成本。圖片仍是用打卡機作個比喻。

上班的時間點上,你們都在排隊打卡,可這個時候恰恰最前面的那我的遇到了打卡機故障,怎麼也不能打卡成功,急得滿頭大汗。等找人把打卡機修好,後面排隊的全部人全遲到了。

性能優化

由於「請求 - 應答」模型不能變,因此「隊頭阻塞」問題在 HTTP/1.1 裏沒法解決,只能緩解,有什麼辦法呢?

公司裏能夠再多買幾臺打卡機放在前臺,這樣你們能夠不用擠在一個隊伍裏,分散打卡,一個隊伍偶爾阻塞也沒關係,能夠改換到其餘不阻塞的隊伍。

這在 HTTP 裏就是「併發鏈接」(concurrent connections),也就是同時對一個域名發起多個長鏈接,用數量來解決質量的問題。

但這種方式也存在缺陷。若是每一個客戶端都想本身快,創建不少個鏈接,用戶數×併發數就會是個天文數字。服務器的資源根本就扛不住,或者被服務器認爲是惡意攻擊,反而會形成「拒絕服務」。

因此,HTTP 協議建議客戶端使用併發,但不能「濫用」併發。RFC2616 裏明確限制每一個客戶端最多併發 2 個鏈接。不過實踐證實這個數字實在是過小了,衆多瀏覽器都「無視」標準,把這個上限提升到了 6~8。後來修訂的 RFC7230 也就「順水推舟」,取消了這個「2」的限制。

但「併發鏈接」所壓榨出的性能也跟不上高速發展的互聯網無止境的需求,還有什麼別的辦法嗎?

公司發展的太快了,員工愈來愈多,上下班打卡成了迫在眉睫的大問題。前臺空間有限,放不下更多的打卡機了,怎麼辦?那就多開幾個打卡的地方,每一個樓層、辦公區的入口也放上三四臺打卡機,把人進一步分流,不要都往前臺擠。

這個就是「域名分片」(domain sharding)技術,仍是用數量來解決質量的思路。

HTTP 協議和瀏覽器不是限制併發鏈接數量嗎?好,那我就多開幾個域名,好比 shard1.chrono.com、shard2.chrono.com,而這些域名都指向同一臺服務器 www.chrono.com,這樣實際長鏈接的數量就又上去了,真是「美滋滋」。不過實在是有點「上有政策,下有對策」的味道。

小結

這一講中咱們學習了 HTTP 協議裏的短鏈接和長鏈接,簡單小結一下今天的內容:

  • 早期的 HTTP 協議使用短鏈接,收到響應後就當即關閉鏈接,效率很低;
  • HTTP/1.1 默認啓用長鏈接,在一個鏈接上收發多個請求響應,提升了傳輸效率;
  • 服務器會發送「Connection: keep-alive」字段表示啓用了長鏈接;
  • 報文頭裏若是有「Connection: close」就意味着長鏈接即將關閉;
  • 過多的長鏈接會佔用服務器資源,因此服務器會用一些策略有選擇地關閉長鏈接;
  • 「隊頭阻塞」問題會致使性能降低,能夠用「併發鏈接」和「域名分片」技術緩解。

原文:https://www.cnblogs.com/huans...

image

相關文章
相關標籤/搜索