真正“搞”懂HTTP協議07之隊頭阻塞真的很煩人

  這一篇文章,我們核心要聊的事情就是HTTP的對頭阻塞問題,因爲HTTP的核心改進其實就是在解決HTTP的隊頭阻塞。所以,我們會講的理論多一些,而實踐其實很少,要學習的頭字段也只有一個,我會在最開始就講完這個頭字段,然後我們安心的去學習接下來的理論知識,嗯……這些理論知識很重要。

  那我們就先來看看我們本篇要學的這個唯一的頭字段是什麼吧。

一、Connection頭字段及其示例

  其實在聊這個字段之前,我們要先學一些前置知識才好,但是我想先講這個字段,後面再帶着疑問去學理論,嗯……就這樣。

  Connection字段其實很好理解,就是用來開啓長鏈接的,長鏈接的意思就是會重複利用TCP開啓的通道,不會在請求一次後關閉。長鏈接可以這樣開啓Connection: keep-alive。這個東東在HTTP/1.1中是默認開啓的,也就是說你啥也不干它就開啓,當然,如果你想關閉的話可以這樣Connection: close

  不僅僅如此,客戶端和服務器都可以通過Keep-alive: timeout=value來限定長鏈接的超時時間,但是服務器和客戶端往往都不一定遵守,約束力並不是很強。大家瞭解下就好了,另外,一些代理服務器比如Nginx,也會針對該字段有一些特殊的策略,比如該通道多長時間沒有發送數據就關閉,比如該通道發送了多少次數據後就關閉等等。

  那麼下面我們就來看看具體的例子,來實踐一下。客戶端和服務器的代碼都很簡單,基本的代碼在上一篇文章中都接觸過,我就不再複製代碼了,可以在這裏看。我們直接看下請求的結果。

  關於Connection的有三個字段,其中Proxy-Connection從詞意上來講就是指代理的通道連接方式。然後你看,Connection是Keep-alive,Keep-Alive字段的超時時間設置爲4,也就是默認設置了四秒沒有在該通道上傳輸數據就關閉TCP通道。那怎麼驗證呢?我們還得用下Wireshark。先請求下數據,然後看看四秒後會不會有TCP的四次揮手斷開連接。

   我們可以清楚的看到三次握手後(也就是96、97、98三個id的tcp)的HTTP請求,過了四秒,就四次揮手(就是344、345、346、347四次)斷開連接了,大家有興趣自行嘗試體驗一下。

  例子就這麼簡單~我們接下來要學習理論知識了,這些理論知識很重要,這個例子就當個開胃小菜吧。

二、長短連接說短長

  我在上面的例子中說到,HTTP/1.1會默認開啓長鏈接,那爲什麼要開啓長鏈接?什麼是長鏈接?那既然有長鏈接是不是還有短連接呢?嗯……你聽我慢慢說。

一)短連接是什麼? 

   我們知道,HTTP/0.9和HTTP/1.0都是十分簡單的協議,它的底層是基於TCP的,在每次請求發送前都需要通過三次握手來和服務器建立連接,響應結束後會通過四次揮手斷開連接。這就是短連接,在早期的時候也會被稱爲是無連接的。而這種操作是十分浪費資源的,效率就很低下。

  爲什麼早期的HTTP會是這個樣子呢?因爲在當時,大家知道大多數的網站都是靜態頁面,一個頁面上能放幾個gif圖那都是很酷炫的事情了。所以,在這樣的場景下,沒有那麼多的請求需要,這樣設計似乎也無可厚非,但是隨着互聯網頁面的極速發展,一個頁面可能有幾個甚至幾十個請求,幾百個外部資源文件,每次都開啓、關閉、開啓、關閉,浪費的資源可不是一點半點。

  所以,我們就需要對短連接進行改進,於是長連接出現了。

二)時代的寵兒:長連接

  因爲短連接實在是無法適應時代的需要,太浪費了,所以爲了解決短連接帶來問題,在HTTP/1.1中就增加了持久鏈接的方法,它的特點就是可以在一個TCP連接上傳輸多個HTTP請求,只要瀏覽器或者服務器沒有明確斷開,那麼就會一直保持連接狀態。雖然這樣做並沒有改善TCP的連接效率,但是由於開啓和斷開的次數少了,把整個開啓和斷開的時間平均到了多次請求中,每個請求和應答的無效時間就少了很多,從而增加了整體傳輸的效率。

  在目前的瀏覽器中,同一個域名可以開啓六個TCP連接,不過這裏稍微要注意的是,其實HTTP規範約定的TCP連接數量是2個,但是各大瀏覽器廠商覺得肯定不夠用,所以在實現層面上來說,每個域名可以開啓6個TCP連接,HTTP規範在新的版本RFC7230中也就順水推舟,約定可以是6到8個連接。

  那如果六個TCP連接還是不夠用呢?嗯……我們可以多開幾個域名,比如a.zaking.com,b.zaking.com,c.zaking.com,每一個域名都指向同一臺服務器,說白了就是用數量來解決質量的方式,而這種解決思路也有個高大上的名詞,叫做“域名分片”。

三、初識隊頭阻塞

  隊頭阻塞是本篇的重點,也是一件比較有趣的事情,有趣在哪裏呢?因爲它解決不了。我們下面就來看看什麼是隊頭阻塞。

  因爲HTTP是基於“請求—應答”模型的,在這個模型的基礎上,HTTP規定報文必須是一發一收的,這就形成了一個先進先出的串行隊列,如果你不知道什麼是隊列的話,請看這裏。既然是隊列,就存在一個這樣的問題,隊列裏的請求沒有優先級,誰先進來誰就先出去。但是假如某一個排在前面的請求卡住了,沒有返回,那後面的所有隊列中的請求都要等着那個卡住的請求結束,結果就是我分擔了本來不應該由我來承擔的時間損耗。

  那要怎麼解決這個問題呢?誒?你不是說這個問題是解決不了的麼?嗯……從規範上,從設計上來說確實無法解決,既然是隊列就必然是這樣的,但是上有政策下有對策,我大不了多開幾個域名唄,多開幾個隊列,讓它堵的可能性小點,你是不是想到了啥?嗯,就是我們上面說到的“域名分片”技術。

  其實很好理解,就好像我們在一個汽車在單車道上跑,堵車的可能性就很大,堵車了我也沒辦法,只能等前面解決了繼續走,但是假設我是6車道,18車道,是不是就能在一定程度上解決這個問題了(其實用車道比喻HTTP的隊頭阻塞並不是十分恰當,比喻TCP的隊頭阻塞更恰當一點,但是這樣好理解)。

  當然,你並沒有從根本解決隊頭阻塞。只是使了點小手段罷了。

  我在demo代碼裏寫了點小例子,大家可以點擊試試。坦白說我並不知道底層的實現是什麼,但是大概能猜到原因。

 

  當你發送很多無響應的HTTP請求後,等一會,再點有響應的HTTP請求,你會發現卡死了,我猜就是因爲那些無響應的HTTP請求佔用全部六個TCP連接。當然,你也可以通過Wireshark來驗證這一點。不多說啦~

  完了嘛?還沒……

  在HTTP/1.1中,也曾試圖通過“管線化” 的技術來解決隊頭阻塞的問題,管線化就是指將多個HTTP請求整批發送給服務器的技術,雖然可以整批發送,但是服務器還是要按照隊列的順序返回結果,得~~~白玩了。所以最後這玩意沒啥用,大家瞭解下就行了

四、多路複用的HTTP/2

   我們回顧一下上面的三個部分,發現HTTP/1.1爲了優化做了哪些努力,一個是長連接,一個是每個域名可以同時維護6個TCP長連接,一個是就是域名分片技術。一共三種,但是這些手段都沒有從根本上解決隊頭阻塞的問題,HTTP數據報文在傳輸的某一條連接上堵塞了,還是要等待,沒辦法。

  雖然使用這些手段一定程度上緩解了HTTP/1.0和HTTP/0.9所帶來的問題,但是其實問題還是不少的,性能還可以進一步的壓縮。

  其中關於TCP的問題有慢啓動問題,以及帶寬競爭問題。在TCP進入到傳輸數據的狀態時,會處於一種遞增的狀態,就像開車一樣,緩慢的從0加速到多少時速,這樣做是爲了減少網絡擁塞,但是有些數據本身就很小,等着你慢慢啓動就很浪費時間。

  而帶寬競爭,則是指當帶寬充足的時候,每條連接都會緩慢的增加發送速度,而一旦帶寬不足時,這些連接傳輸數據的速度則會減慢,這樣就回帶來一個問題,就是優先級的問題,重要資源隨着普通資源一起減慢了,真的是很苦惱。

  這兩個問題是TCP引起的,HTTP想改變也改變不了,只能接受,所以HTTP/3的時候乾脆不用TCP了。

  但是,隊頭阻塞的問題,則是HTTP可以進一步優化和解決的,想辦法在一定程度上規避TCP的這兩個問題,什麼意思呢?

  HTTP/2的思路是一個域名只採用一個TCP連接,這樣就能儘可能的減少TCP的慢啓動和帶寬競爭問題,就一個TCP連接,你也不用競爭了,就一個TCP連接,你就算啓動的很慢,平分到每一個連接好像也還可以。你看,好像所有的解決思路都類似,解決不了就平分。

  基於這樣的思路,HTTP/2針對隊頭阻塞的問題提出了多路複用的解決方案。什麼意思呢,HTTP/2實現了資源的並行請求,也就是你在任何時候都可以發送請求,不用管前一個請求是否堵塞,服務器會在處理好數據後就返回給你。

  那,核心的問題來了,多路複用是如何實現的呢?

多路複用的實現

  HTTP/2在HTTP和TCP的中間又加了一層,也就是二進制分幀層:

 

   就像上圖這樣,其實二進制分幀層屬於應用層,這個二進制分幀層做了什麼呢?就是把發送的HTTP數據包拆成一個一個帶有id的幀,服務器收到這些幀後,會把有同一個id的幀合併成一條完整的信息,那麼同樣的,服務器發送給客戶端的數據也要這樣經過二進制分幀層的分幀處理,瀏覽器會根據對應的id發送給請求的數據源頭。

  HTTP/2就是通過這樣的形式,引入了多路複用的機制,來解決隊頭阻塞的問題。

  看起來似乎很美好了是吧,HTTP/2可以說是HTTP目前爲止最完美的解決方案了。但是故事並沒有就此停止。

五、TCP也有隊頭阻塞

   雖然,HTTP/2解決了HTTP的隊頭阻塞,但是TCP也有隊頭阻塞,雖然你把HTTP數據包拆分成了一個又一個的幀,但是你還是傳輸在一條通道上,一旦某一個數據幀丟失了,那TCP就得等丟失的數據包重新傳過來纔行,臥槽,問題又回到了原點。那咋整?我們改一改TCP協議?

  抱歉,你改不了,主要的原因在於僵化,一個是中間設備的僵化,一個是操作系統的僵化。

  中間設備其實就是指數據在互聯網中傳輸的過程中,所遇到的各種設備,比如路由器,網關,代理服務器,服務器等等等等,很多很多,這些東西比較硬性,一旦安裝軟件後很少升級,所以你改了客戶端的TCP,這一連串的設備,甚至說全球的設備都要改,你想想,是不是很誇張。

  而操作系統僵化,則是因爲TCP的核心實現是由操作系統底層來處理的,所以你看,要改TCP就要改操作系統,想想就頭大。

  所以,由於僵化的原因,TCP改不了。那咋整?嗯……那就不用他了唄,我們用UDP好了。

  HTTP/3選擇用UDP作爲傳輸協議,並且在UDP和HTTP/3中又加了QUIC層。QUIC層則針對UDP區別於TCP的一些特性進行了處理,從而讓UDP的傳輸像TCP一樣完整和安全,並且像HTTP/2那樣採用多路複用機制,來解決TCP的隊頭阻塞。

  關於QUIC或者HTTP/3的更多內容,會在後面HTTP/3的部分詳細講解,本篇就不再過多的闡述了。

  嗯……本篇結束了~

六、總結

  這篇文章並不長,理論知識稍微多一點,而其中最核心的點就是隊頭阻塞和多路複用,大家一定要着重學習。那麼在本篇的最後,留給大家兩個小問題。

  1. 長連接和短連接是啥?長連接出現的原因是什麼?解決了什麼問題呢?
  2. 關於HTTP的隊頭阻塞,你都有哪些瞭解?HTTP解決了隊頭阻塞的問題麼?如果解決了,又是如何解決的?如果沒解決,爲什麼沒解決呢?
  3. HTTP的隊頭阻塞和TCP的對頭阻塞有什麼區別?都是怎麼解決的?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章