整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          滲透測試-請求響應(爬蟲)

          http請求方式連接網站數據


          statusCode.py


          import requests

          #在路徑中直接添加get方式獲取路徑和狀態碼

          url='https://www.baidu.com/get'

          response=requests.get(url)

          print(response)

          print(response.status_code)


          #無參數的get獲取狀態碼方式

          url='http://www.jd.com'

          r=requests.get(url=url)

          print(r.status_code)

          print(r.url)


          #有參數的get獲取狀態碼方式

          url='https://login.taobao.com/member/login.jhtml'


          #字典格式

          payload={

          'spm':'a21bo.2017.754894437.1.5af911d9IUfLcO',

          'f':'top',

          'redirectURL':'https%3A%2F%2Fwww.taobao.com%2F'

          }


          #請求方式get路徑和參數

          r=requests.get(url=url,params=payload)

          print(r)

          print(r.url)

          print(r.status_code)

          print(r.content)

          print(r.text)

          result=r.content

          if str(result).find('succ'):

          print('admin:admin'+'succeeful')


          #請求的post方式 帶參數的請求

          url='https://i.taobao.com/my_taobao.htm'

          params={

          "spm":"a21bo.2017.754894437.3.5af911d9j6H5ku",

          "ad_id":"",

          "cm_id":"",

          "pm_id":"1501036000a02c5c3739"

          }

          r=requests.post(url=url,params=params)

          print(r)

          print(r.request)

          print(r.status_code)

          print(r.url)

          print(r.text)

          print(type(r.text))


          if r.text.find('succ'):

          print("successful")

          url='http://www.baidu.com'

          r=requests.get(url)

          #得到網站請求頭

          r1=r.request.headers

          print(r1)


          #定義user-agent的值 可以改變固定值

          url="http://www.jd.com"

          headers={

          "User-Agent":"my-sql"

          }

          r=requests.get(url=url,headers=headers)

          print(r.request.headers)

          控制臺運行結果如下:

          <class 'str'>

          successful

          {'User-Agent': 'python-requests/2.23.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}

          {'User-Agent': 'my-sql', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}

          二 ** 不同的http請求的不同結果

          url='https://www.tmall.com/'

          r=requests.get(url,params={

          "ali_trackid":"2:mm_26632258_3504122_55934697:1600051047_178_2071692865",

          "clk1":"4e5c8f90d4dfbc090f056171fea55794",

          "upsid":"4e5c8f90d4dfbc090f056171fea55794",

          "bxsign":"tbk16000510475478276d5e30e98700bd0143d2d468075d8"

          })

          print(r.status_code)

          print(r.headers)

          print(r.request.headers)

          print(r.encoding)

          print(r.url)

          #此時cookies是空目錄

          print(r.cookies)

          #當https的時候cookies為有數據狀態 ]>

          url="https://www.baidu.com"

          r=requests.get(url)

          print(r.cookies)

          控制臺輸出結果

          200

          {'Server': 'Tengine', 'Content-Type': 'text/html; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding, Accept-Encoding, Origin, Ali-Detector-Type, X-Host, Accept-Encoding', 'Date': 'Mon, 14 Sep 2020 02:57:39 GMT', 'x-server-id': '28c3d6b2523ca52cb704b8b5dcd97677d231532c71c47d1b0f87559eae61f07c8bb00e660f25c2b1', 'realpath': 'page/portal/act/fp', 'Cache-Control': 'max-age=0, s-maxage=116', 'ETag': 'W/"36e89-v0m9Q3I/yOxHAam8+4z7Cz3ZtiI"', 'x-readtime': '85', 'x-via': 'cn2460.l1, bcache8.cn2460, l2cn859.l2, cache17.l2cn859, wormholesource011088033031.center.na61', 'EagleEye-TraceId': '7beb211c16000522590061050e', 'Strict-Transport-Security': 'max-age=0, max-age=31536000', 'Timing-Allow-Origin': '*, *', 'Ali-Swift-Global-Savetime': '1600052259', 'Via': 'cache17.l2cn859[176,200-0,C], cache35.l2cn859[15,0], bcache2.cn2583[0,200-0,H], bcache4.cn2583[1,0]', 'Age': '99', 'X-Cache': 'HIT TCP_MEM_HIT dirn:-2:-2', 'X-Swift-SaveTime': 'Mon, 14 Sep 2020 02:57:39 GMT', 'X-Swift-CacheTime': '116', 'EagleId': '3cdd499816000523589496905e', 'Content-Encoding': 'gzip'}

          {'User-Agent': 'python-requests/2.23.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}

          utf-8

          https://www.tmall.com/?ali_trackid=2%3Amm_26632258_3504122_55934697%3A1600051047_178_2071692865&clk1=4e5c8f90d4dfbc090f056171fea55794&upsid=4e5c8f90d4dfbc090f056171fea55794&bxsign=tbk16000510475478276d5e30e98700bd0143d2d468075d8

          <RequestsCookieJar[]>

          <RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>

          載說明:原創不易,未經授權,謝絕任何形式的轉載

          一個吸引人的網頁頁眉對于給訪問者留下良好的第一印象至關重要。一個設計精良的頁眉不僅能夠吸引注意力,還能為整個網站設定基調。借助CSS,創建現代化和視覺吸引力的網頁頁眉比以往任何時候都更加容易。

          在本文中,我們將探索一些基本的技巧和提示,以幫助您使用CSS創建令人驚艷的頁眉布局。我們并不過多關注設計,而是專注于創建布局,并了解創建布局時可能遇到的困難。

          Space-Between在一個三列的頁眉中無法居中

          首先,讓我們談談三列頁眉,因為這是我最常見到實現錯誤的一種情況。

          標記相對簡單:

          <header>
            <nav>
              <a href="/">ABC</a>
          
              <div>
                <a href="#">Features</a>
                <a href="#">Pricing</a>
                <a href="#">About us</a>
              </div>
          
              <div>
                <a href="#">Login</a>
              </div>
            </nav>
          </header>


          我將所有鏈接放在頁眉的導航標簽中。因此,這是一個非常簡單的標記。通常情況下,導航應該在第一個和最后一個項之間居中對齊。

          根據我的觀察,這個問題已經成為前端社區中的新問題,類似于“居中一個div”的問題。因為許多開發人員會使用justify-content屬性的space-between值來解決這個問題,但它實際上并不能將中間元素居中對齊。下面是使用justify-content屬性的space-between值的相同導航標記,供比較參考:

          造成這種效果的原因是左側比右側更寬。我們的中間元素在左側和右側元素之間居中對齊,但在頁面的上下文中,中間元素并沒有真正居中。

          這是創建頁眉時的第一個挑戰:正確設置基本布局。在您確定要實現的布局以及如何實現之前,不要試圖添加更多內容。

          在我們繼續之前,我在ProductHunt上花了幾個小時尋找和評估三列頁眉。它們中的大多數使用了我展示的將justify-content屬性設置為space-between的技巧(因此,它們的導航并沒有真正居中)。有些人試圖繞過這個問題(例如,通過添加外邊距),而其他人則通過絕對定位放棄了。當然,這些"hack"可以"解決"問題,但它們增加了復雜性。您的頁眉將變得難以維護,當您再次回到頁眉時會產生不好的感覺。話雖如此,這個"真正的解決方案"也有些技巧性。

          下面是如何實現的方法:

          header > nav {
            display: flex;
          }
          
          header > nav > * {
            display: flex;
          }
          
          header > nav > :first-child,
          header > nav > :last-child {
            flex: 1 1 0;
          }
          
          header > nav > :last-child {
            justify-content: flex-end;
          }

          效果如下圖所示:

          讓我們來談談這個解決方案。

          首先,我使用的選擇器過于具體化。這樣做是為了使嵌套關系更加清晰。

          然后,頁眉下的每個元素都是一個彈性容器。這也是不必要的。目前,它僅用于導航的最后一個子元素,以將其子元素移動到右側。

          這只留下了這條規則:flex: 1 1 0; 這是我們在這里的主要關注點。我將這條規則應用于第一個和最后一個元素。它允許它們增長和收縮,并將它們的基準大小設置為0像素。這就是整個"hack"的全部內容。因為我們將它們的基準大小設置為0,它們將等比增長,從而使我們的中間元素居中對齊。

          當創建頁眉布局時,當然,將頁眉的中間元素居中對齊并不是我們面臨的唯一挑戰。

          在較小的屏幕上隱藏導航欄

          與使用justify-content屬性的space-between值一樣,上述模式使我們能夠在布局保持完整的同時隱藏中間導航。當我們隱藏中間元素時,效果如下所示:

          當然,將登錄替換為按鈕是很簡單的。所以,我們來談談其他的事情吧。

          假設我們的頁眉看起來像這樣:

          <header>
            <nav>
              <a href="/">ABC Company</a>
          
              <div class="desktop-navigation">
                <a href="#">Features</a>
                <a href="#">Pricing</a>
                <a href="#">About us</a>
                <a href="#">Terms of Service</a>
              </div>
          
              <div class="action-navigation">
                <input type="search" aria-label="search" />
                <a href="#">Sign Up</a>
                <a href="#">Login</a>
              </div>
            </nav>
          </header>

          現在,當我們的視口變小時,我們的頁眉遇到了一個問題:

          我們可以為此添加一個媒體查詢,在其中用圖標替換某些元素,或者簡單地隱藏搜索。但是現代的CSS也允許使用不同的解決方案。

          例如,我們可以創建一個容器查詢。以下是更新后的代碼示例:

          * {
            padding: 0;
            margin: 0;
            box-sizing: border-box;
          }
          
          a {
            color: #000;
            text-decoration: none;
          }
          
          header {
            padding: 1.5rem;
          }
          
          header > nav {
            display: flex;
          }
          
          header > nav > * {
            display: flex;
          }
          
          header > nav > :first-child,
          header > nav > :last-child {
            flex: 1 1 0;
          }
          
          header > nav > :last-child {
            justify-content: flex-end;
          }
          
          header > nav > :last-child,
          .desktop-navigation {
            gap: 1.5rem;
          }
          
          .action-navigation {
                container-type: inline-size;
                container-name: action-navigation;
              }
          
          @container action-navigation (max-width: 400px) {
                input {
                  display: none;
                }
              }

          我只添加了這些行:

          .action-navigation {
            container-type: inline-size;
            container-name: action-navigation;
          }
          
          @container action-navigation (max-width: 400px) {
            input {
              display: none;
            }
          }


          當然,你可能會說你也可以用媒體查詢來做到這一點。沒什么了不起的。但容器查詢的優勢在于我們可以為容器指定最小寬度。我們不關心視口有多大,但我們知道:如果我們的容器寬度小于400像素,它會變得非常難看。這是我真正期待被廣泛支持的功能之一。

          粘性頂部導航欄

          我仍然看到一些使用position: fixed實現頂部導航欄,即使sticky是更好的解決方案。

          為什么sticky更好呢?請考慮以下代碼:

          <style>
            * {
              padding: 0;
              margin: 0;
              box-sizing: border-box;
            }
          
            a {
              color: #000;
              text-decoration: none;
            }
          
            header {
              padding: 1.5rem;
              width: 100%;
              position: fixed;
            }
          
            header > nav {
              display: flex;
            }
          
            header > nav > * {
              display: flex;
            }
          
            header > nav > :first-child,
            header > nav > :last-child {
              flex: 1 1 0;
            }
          
            header > nav > :last-child {
              justify-content: flex-end;
            }
          
            .desktop-navigation {
              gap: 1.5rem;
            }
          </style>
          
          <header>
            <nav>
              <a href="/">ABC Company</a>
          
              <div class="desktop-navigation">
                <a href="#">Features</a>
                <a href="#">Pricing</a>
                <a href="#">About us</a>
              </div>
          
              <div class="action-navigation">
                <a href="#">Login</a>
              </div>
            </nav>
          </header>
          
          <main>
            Some content
          </main>

          在這種情況下,頁眉具有position: fixed。結果,主要內容區域移動到網站的頂部,因為文檔中沒有為頁眉保留空間。它處于流動之外。

          在這種情況下,解決方法是使用margin-top對主要內容區域進行偏移,將其移動到頁眉下方。您可能會經常看到這種解決方法,即使在較新的網站上也是如此。問題在于,sticky屬性并不總是存在的。它是比較新的屬性。這就是為什么您仍然可以找到一些使用position: fixed而不是sticky的教程的原因。但是使用sticky,我們就不需要margin-top的偏移了。頁眉元素仍然會占用空間,就好像它在文檔中一樣。

          以下是帶有position: sticky的更新代碼示例:

          * {
            padding: 0;
            margin: 0;
            box-sizing: border-box;
          }
          
          a {
            color: #000;
            text-decoration: none;
          }
          
          header {
            padding: 1.5rem;
            position: sticky;
            top: 0;
          }
          
          header > nav {
            display: flex;
          }
          
          header > nav > * {
            display: flex;
          }
          
          header > nav > :first-child,
          header > nav > :last-child {
            flex: 1 1 0;
          }
          
          header > nav > :last-child {
            justify-content: flex-end;
          }
          
          .desktop-navigation {
            gap: 1.5rem;
          }

          正如您所看到的,這種方法要簡單得多。我們不需要為內容設置任意的偏移量。

          就是這樣了,朋友們!非常感謝您的閱讀!

          結束

          您是否知道關于頁眉布局的其他常見錯誤?或者您是否了解其他具有挑戰性的元素?我很樂意在評論中了解更多!

          由于文章內容篇幅有限,今天的內容就分享到這里,文章結尾,我想提醒您,文章的創作不易,如果您喜歡我的分享,請別忘了點贊和轉發,讓更多有需要的人看到。同時,如果您想獲取更多前端技術的知識,歡迎關注我,您的支持將是我分享最大的動力。我會持續輸出更多內容,敬請期待。

          譯)Robin Marx: QUIC 和 HTTP/3 隊頭阻塞的細節

          你可能已經聽說,經過4年的工作,新的 HTTP/3 和 QUIC 協議終于接近正式標準化。預覽版現在可以在服務器和瀏覽器中進行測試。

          與 HTTP/2 相比,HTTP/3 有很大的性能改進,這主要是因為它將底層傳輸協議從 TCP 改為基于 UDP 的 QUIC。在這篇文章中,我們將深入了解其中的一項改進,即消除隊頭阻塞(Head-of-Line blocking, 簡寫:HOL blocking)問題。這很有用,因為我讀過很多關于這實際上意味著什么以及它在現實中有多大幫助的誤解。解決隊頭阻塞也是 HTTP/3 和 QUIC 以及 HTTP/2 背后的主要動機之一,因此它也為協議演進的原因提供了一個極好的視角。

          我將首先介紹隊頭阻塞問題,然后在整個 HTTP 歷史中跟蹤它的不同形式。我們還將研究它如何與其他系統交互,如優先級和擁塞控制。我們的目標是幫助人們對 HTTP/3 的性能改進做出正確的判斷,而這(劇透)可能不像其他介紹的文章中所說的那樣令人驚訝。

          目錄:

          什么是隊頭阻塞?
          HTTP/1.1 的隊頭阻塞
          HTTP/2(基于 TCP)的隊頭阻塞
          HTTP/3(基于 QUIC)的隊頭阻塞
          總結與結論

          彩蛋內容:

          彩蛋:HTTP/1.1 管道
          彩蛋:TLS 隊頭阻塞
          彩蛋:傳輸擁堵控制
          彩蛋:多路復用是否重要?

          什么是隊頭阻塞(Head-of-Line blocking)?

          很難給你一個單一的隊頭阻塞(HOL blocking) 的技術定義,因為這篇博客文章單獨描述了它的四個不同變體。然而,一個簡單的定義是:

          當單個(慢)對象阻止其他/后續的對象前進時

          現實生活中一個很好的比喻就是只有一個收銀臺的雜貨店。一個顧客買了很多東西,最后會耽誤排在他后面的人,因為顧客是以先進先出(First In, First Out)的方式服務的。另一個例子是只有單行道的高速公路。在這條路上發生一起車禍,可能會使整個通道堵塞很長一段時間。因此,即使是在“頭部(head)”一個單一的問題可以“阻塞(block)”整條“線(line)”。

          這個概念一直是最難解決的 Web 性能問題之一。為了理解這一點,讓我們從 HTTP/1.1 開始講起。

          HTTP/1.1 的隊頭阻塞

          HTTP/1.1(默認開啟keep-alive)是一種更簡單的協議。一個協議仍然可以基于文本并在網絡上可讀的時代。如下圖1所示:

          圖1

          在本例中,瀏覽器基于 HTTP/1.1上 請求簡單的script.js文件(綠色),圖1顯示了服務器對該請求的響應。我們可以看到 HTTP 方面本身很簡單:它只是在明文文件內容或“有效荷載”(payload)前面直接添加一些文本“headers”(紅色)。然后,頭(Headers)+ 有效荷載(payload)被傳遞到底層 TCP(橙色),以便真實傳輸到客戶端。對于這個例子,假設我們不能將整個文件放入一個 TCP 包中,并且必須將它分成兩部分。

          注意:實際上,當使用 HTTPS 時,HTTP 和 TCP 之間有另一個安全層,通常使用 TLS 協議。不過,為了清晰起見,我們在這里省略了這一點。我在結尾加入了一個額外的彩蛋部分,詳細說明了 TLS 特定的隊頭阻塞變體以及 QUIC 如何避免它。閱讀完正文后,請隨意閱讀它(以及其他的彩蛋部分)。

          現在讓我們看看當瀏覽器也請求style.css時發生了什么,如圖2:

          圖2

          在本例中,當script.js的響應傳輸之后,我們發送style.css(紫色)。style.css的頭部(headers)和內容只是附加在 JavaScript(JS)文件之后。接收者使用Content-Length header 來知道每個響應的結束位置和另一個響應的開始位置(在我們的簡化示例中,script.js是1000字節,而style.css只有600字節)。

          在這個包含兩個小文件的簡單示例中,所有這些似乎都很合理。但是,假設 JS 文件比 CSS 大得多(比如說 1MB 而不是 1KB)。這種情況下,在下載整個JS文件之前,CSS 必須等待,盡管它要小得多,其實可以更早地解析/使用。更直接地將其可視化,使用數字 1 表示large_script.js和 2 表示style.css,我們會得到這樣的結果:

          11111111111111111111111111111111111111122
          

          你可以看到這是一個隊頭阻塞問題的例子!現在你可能會想:這很容易解決!只需讓瀏覽器在JS文件之前請求CSS文件!然而,至關重要的是,瀏覽器無法預先知道這兩個文件中的哪一個在請求時會成為更大的文件。這是因為沒有辦法在HTML中指明文件有多大(類似這樣的東西很不錯,HTML工作組:<img src="thisisfine.jpg" size="15000" />)

          這個問題的“真正”解決方案是采用多路復用(multiplexing)。如果我們可以將每個文件的有效荷載(header)分成更小的片(pieces)或“塊”(chunks),我們就可以在網絡上混合或“交錯”(interleave)這些塊:為 JS 發送一個塊,為 CSS 發送一個塊,然后再發送另一個用于 JS,等等,直到文件被下載為止。使用這種方法,較小的CSS文件將更早地下載(并且可用),同時只將較大的JS文件延遲一點。用數字形象化我們會得到:

          12121111111111111111111111111111111111111
          

          然而不幸的是,由于協議存在一些基礎的限制,這種多路復用在 HTTP/1.1 中是不可能的。為了理解這一點,我們甚至不需要繼續查看大資源對小資源(large-vs-small)場景,因為它已經在我們的示例中顯示了兩個較小的文件。如圖3,我們只為兩個資源交錯4個塊:

          圖3

          這里的主要問題是 HTTP/1.1 是一個純文本協議,它只在有效荷載(payload)的前面附加頭(headers)。它不會進一步區分單個(大塊)資源與其他資源。讓我們用一個例子來說明這一點,如果我們嘗試了它會發生什么。在圖3中,瀏覽器開始分析script.js并期望后面有1000個字節(Content-Length)的有效荷載。但是,它只接收450個 JS 字節(第一個塊),然后開始讀取sytle.css的頭部。它最終將 CSS 頭部和第一個 CSS 塊解釋為JS的一部分,因為這兩個文件的有效荷載和頭都是純文本。更糟糕的是,它在讀取1000個字節后停止,直到第二個script.js塊的一半。此時,它看不到有效的新報頭,必須刪除 TCP 數據包3(packet 3)的其余部分。然后瀏覽器傳遞它認為的內容script.js到JS解析器,它失敗了因為不是有效的 JavaScript:

          function first() { return "hello"; }
          HTTP/1.1 200 OK
          Content-Length: 600
          
          .h1 { font-size: 4em; }
          func
          

          同樣,你可以說有一個簡單的解決方案:讓瀏覽器查找HTTP/1.1 {statusCode} {statusString}\n模式來查看新的頭塊何時開始。這可能適用于 TCP 數據包2(packet 2),但在數據包3(packet 3)中會失?。簽g覽器如何知道綠色的script.js塊在哪里結束和紫色style.css塊從哪里開始?

          這是 HTTP/1.1 協議設計方式的一個基礎限制。如果您只有一個 HTTP/1.1 連接,那么在你切換到發送新資源之前,必須完整地傳輸資源響應。如果前面的資源創建緩慢(例如,從數據庫查詢動態生成的index.html)或者,如上所述,如果前面的資源很大。這些問題可能會引起隊頭阻塞問題。

          這就是為什么瀏覽器開始為 HTTP/1.1 上的每個頁面加載打開多個并行 TCP 連接(通常為6個)。這樣,請求可以分布在這些單獨的連接上,并且不再有隊頭阻塞。也就是說,除非每頁有超過6個資源…這當然是很常見的。這就是在多個域名上“分片”(sharding)資源的實踐(img.mysite.com, static.mysite.com, 等)和 CDN 的由來。由于每個單獨的域名有6個連接,瀏覽器將為每個頁面加載總共打開 30-ish 個 TCP 連接。這是可行的,但有相當大的開銷:建立一個新的 TCP 連接可能是昂貴的(例如在服務器上的狀態和內存方面,以及設置 TLS 加密的計算),并且需要消耗一些時間(特別是對于 HTTPS 連接,因為 TLS 需要自己的握手)。

          由于這個問題不能用 HTTP/1.1 解決,而且并行 TCP 連接的補丁解決方案也不能隨著時間的推移擴展得太好,很明顯需要一種全新的方法,這就是后來的 HTTP/2。

          注意:閱讀本文的老哥可能會表示想知道 HTTP/1.1 管道(pipelining)。我決定不在這里討論這一點,以保持整個故事的流暢性,但對更深入的技術感興趣的人可以閱讀結尾的彩蛋部分。

          HTTP/2(基于 TCP)的隊頭阻塞

          那么,讓我們回顧一下。HTTP/1.1 有一個隊頭阻塞問題,一個大的或慢的響應會延遲后面的其他響應。這主要是因為協議本質上是純文本的,在資源塊(resource chunks)之間不使用分隔符。作為一種解決辦法,瀏覽器打開許多并行TCP連接,這既不高效,也不可擴展。

          因此,HTTP/2 的目標非常明確:我們能夠回到單個 TCP 連接,解決隊頭阻塞問題。換一種說法:我們希望能夠正確地復用資源塊(resource chunks)。這在 HTTP/1.1 中是不可能的,因為沒有辦法分辨一個塊屬于哪個資源,或者它在哪里結束,另一個塊從哪里開始。HTTP/2 非常優雅地解決了這一問題,它在資源塊之前添加了幀(frames)。如圖4所示:

          圖4

          HTTP/2 在每個塊前面放置一個所謂的數據幀(DATA frame)。這些數據幀主要包含兩個關鍵的元數據。首先:下面的塊屬于哪個資源。每個資源的“字節流(bytestream)”都被分配了一個唯一的數字,即流id(stream id)第二:塊的大小是多少。協議還有許多其他幀類型,圖5也顯示了頭部幀(HEADERS frame)。這再次使用流id(stream id來指出這些頭(headers)屬于哪個響應,這樣甚至可以將頭(headers)從它們的實際響應數據中分離出來。

          圖5

          與圖3中的示例不同,瀏覽器現在可以完美地處理這種情況。它首先處理script.js的頭部幀(HEADERS frame),然后是第一個JS塊的數據幀(DATA frame)。從數據幀(DATA frame)中包含的塊長度來看,瀏覽器知道它只延伸到 TCP 數據包1的末尾,并且需要從 TCP 數據包2開始尋找一個全新的幀。在那里它確實找到了style.css的頭(HEADERS), 下一個數據幀(DATA frame)含有與第一個數據幀(1)不同的流 id(2),因此瀏覽器知道這屬于不同的資源。同樣的情況也適用于 TCP 數據包3,其中數據幀(DATA frame)流 id 用于將響應塊“解復用”(de-multiplex)到正確的資源“流”(streams)。

          因此,通過“framing”單個消息,HTTP/2 比 HTTP/1.1 更加靈活。它允許在單個 TCP 連接上通過交錯排列塊來多路傳輸多個資源。它還解決了第一個資源緩慢時的隊頭阻塞問題:而不必等待查詢數據庫生成的index.html,服務器可以在等待index.html時開始發送其他資源。

          HTTP/2 的方式一個重要結果是,我們突然需要一種方法讓瀏覽器與服務器通信,單個連接的帶寬如何跨資源分布(distributed)。換一種說法:資源塊應該如何“調度(scheduled)”或交錯(interleaved)。如果我們再次用 1 和 2 來表示,我們會發現對于 HTTP/1.1,唯一的選項是11112222(我們稱之為順序的(sequential))。然而, HTTP/2 有更多的自由:

          • 公平多路復用(例如兩個漸進的 JPEGs):12121212
          • 加權多路復用(2是1的兩倍):22122122121
          • 反向順序調度(例如2是密鑰服務器推送的資源):22221111
          • 部分調度(流1被中止且未完整發送):112222

          使用哪種方法是由 HTTP/2 中所謂的“優先級(prioritization)”系統驅動的,所選擇的方法對Web 性能有很大的影響。然而,這本身就是一個非常復雜的話題,你不需要在接下來的博客文章中理解它,所以我不把它放在這里講了(盡管我在YouTube上有一個關于這個的講座)。

          我想你會同意,通過 HTTP/2 的幀(frames)及其優先級設置,它確實解決了 HTTP/1.1 的隊頭阻塞問題。這意味著我在這里的工作完成了,我們都可以回家了。對嗎?好吧,沒那么簡單。我們已經解決了 HTTP/1.1 的隊頭阻塞,是的,但是 TCP 的隊頭阻塞呢?

          TCP 隊頭阻塞

          事實證明,HTTP/2 只解決了 HTTP 級別的隊頭阻塞,我們可以稱之為應用層隊頭阻塞。然而,在典型的網絡模型中,還需要考慮下面的其他層。您可以在圖6中清楚地看到這一點:

          圖6

          HTTP 位于頂層,但首先由安全層的 TLS 支持(請參閱“彩蛋 TLS”部分),然后接著再由傳輸層的 TCP 傳輸。這些協議中的每一層都用一些元數據包裝來自其上一層的數據。例如,在我們的 HTTP(S) 數據中預先加上 TCP 包頭(packet header),然后將其放入 IP 包等,這樣就可以在協議之間實現相對簡潔的分離。這反過來又有利于它們的可重用性:像 TCP 這樣的傳輸層協議不必關心它正在傳輸什么類型的數據(可以是 HTTP,也可以是 FTP,也可以是 SSH,誰知道呢),而且 IP 對于 TCP 和 UDP 都能很好地工作。

          然而,如果我們想將多個 HTTP/2 資源多路傳輸到一個 TCP 連接上,這確實會產生重要的后果。如圖7:

          圖7

          雖然我們和瀏覽器都知道我們正在獲取 JavaScript 和 CSS 文件,但 HTTP/2 不需要知道這一點。它只知道它在使用來自不同資源流 id (stream id)的塊。然而,TCP 甚至不知道它在傳輸 HTTP!TCP 所知道的就是它被賦予了一系列字節,它必須從一臺計算機傳輸另一臺計算機。為此,它使用特定最大大?。╩aximum size)的數據包,通常大約為1450字節。每個數據包只跟蹤它攜帶的數據的那一部分(字節范圍),這樣原始數據就可以按照正確的順序重建。

          換言之,這兩個層之間的透視圖是不匹配的:HTTP/2 可以看到多個獨立的資源字節流(bytestream),而 TCP 只看到一個不透明的字節流(bytestreams)。圖7的TCP數據包3就是一個例子:TCP 只知道它正在傳輸的任何內容的字節 750 到字節1 599。另一方面,HTTP/2 知道數據包3中實際上有兩個獨立資源的兩個塊。(注意:實際上,每個 HTTP/2 幀(如 DATA 和 HEADERS)的大小也有幾個字節。為了簡單起見,我沒有計算額外的開銷或這里的 HEADERS 幀,以使數字更直觀。

          所有這些看起來都是不必要的細節,直到你意識到互聯網是一個根本不可靠的網絡。在從一個端點到另一個端點的傳輸過程中,數據包會丟失和延遲。TCP 的可靠性正是其最受歡迎的原因之一。它只需重新傳輸丟失數據包的副本就可以做到這一點。

          我們現在可以理解傳輸層是如何導致隊頭阻塞的。再次思考下圖7并問自己:如果 TCP 數據包2在網絡中丟失,但數據包1數據包3已經到達,會發生什么情況?請記住,TCP并不知道它正在承載 HTTP/2,只知道它需要按順序傳遞數據。因此,它知道數據包1的內容可以安全使用,并將這些內容傳遞給瀏覽器。然而,它發現數據包1中的字節和數據包3中的字節(放數據包2 的地方)之間存在間隙,因此還不能將數據包3傳遞給瀏覽器。TCP 將數據包3保存在其接收緩沖區(receive buffer)中,直到它接收到數據包2的重傳副本(這至少需要往返服務器一次),之后它可以按照正確的順序將這兩個數據包都傳遞給瀏覽器。換個說法:丟失的數據包2 隊頭阻塞(HOL blocking)數據包3!

          你可能不清楚為什么這是個問題,所以讓我們更深入地研究圖7中 HTTP 層的 TCP 包中的實際內容。我們可以看到,TCP 數據包2只攜帶流id 2(CSS文件)的數據,數據包3同時攜帶流1(JS文件)流2的數據。在 HTTP 級別,我們知道這兩個流是獨立的,并且由數據幀(DATA frame)清楚地描述出來。因此,理論上我們可以完美地將數據包3傳遞給瀏覽器,而不必等待數據包2到達。瀏覽器將看到流id為1的數據幀,并且能夠直接使用它。只有流2必須被掛起,等待數據包2的重新傳輸。這將比我們從 TCP 的方式中得到的效率更高,TCP 的方式最終會阻塞流1和流2

          另一個例子是數據包1丟失,但是接收到23的情況。TCP將再次阻止數據包23,等待1。但是,我們可以看到,在HTTP/2級別,流2的數據(CSS文件)完全存在于數據包2和3中,不必等待數據包1的重新傳輸。瀏覽器本可以完美地解析/處理/使用 CSS 文件,但卻被困在等待 JS 文件的重新傳輸。

          總之,TCP 不知道 HTTP/2 的獨立流(streams)這一事實意味著TCP 層隊頭阻塞(由于丟失或延遲的數據包)也最終導致 HTTP 隊頭阻塞!

          現在,您可能會問自己:那重點是什么?如果我們仍然有 TCP 隊頭阻塞,為什么還要使用HTTP/2 呢?好吧,主要原因是雖然數據包丟失確實發生在網絡上,但還是比較少見的。特別是在有線網絡中,包丟失率只有 0.01%。即使是在最差的蜂窩網絡上,在現實中,您也很少看到丟包率高于2%。這與數據包丟失和抖動(網絡中的延遲變化)通常是突發性的這一事實結合在一起的。包丟失率為2%并不意味著每100個包中總是有2個包丟失(例如數據包 42 和 96)。實際上,可能更像是在總共500個包中丟失10個連續的包(例如數據包255到265)。這是因為數據包丟失通常是由網絡路徑中的路由器內存緩沖區暫時溢出引起的,這些緩沖區開始丟棄無法存儲的數據包。不過,細節在這里并不重要(如果你想知道更多,可以在其他地方找到)。重要的是:是的,TCP 隊頭阻塞是真實存在的,但是它對 Web 性能的影響要比HTTP/1.1 隊頭阻塞小得多,HTTP/1.1 隊頭阻塞幾乎可以保證每次都會遇到它,而且它也會受到 TCP 隊頭阻塞的影響!

          然而,當比較單個連接上的 HTTP/2 和單個連接上的 HTTP/1.1 時,這個基本上是真的。正如我們之前所看到的,實際上它并不是這樣工作的,因為 HTTP/1.1 通常會打開多個連接。這使得 HTTP/1.1 不僅在一定程度上減輕了 HTTP 級別,而且減輕了 TCP 級別的隊頭阻塞。因此,在某些情況下,單個連接上的 HTTP/2 很難比6個連接上的 HTTP/1.1 快,甚至與 HTTP/1.1 一樣快。這主要是由于 TCP 的擁塞控制(congestion control機制。然而,這是另一個非常深入的話題,并不是我們討論隊頭阻塞(HOL blocking)的核心,所以我把它移到了末尾的另一個彩蛋部分。

          總之,事實上,我們看到(也許出乎意料),HTTP/2 目前部署在瀏覽器和服務器中,在大多數情況下通常與 HTTP/1.1 一樣快或略快。在我看來,這部分是因為網站在優化 HTTP/2 方面做得更好,部分原因是瀏覽器仍然經常打開多個并行 HTTP/2 連接(要么是因為站點仍然在不同的服務器上共享資源,要么是因為與安全相關的副作用),從而使兩者兼得。

          然而,也有一些情況(特別是在數據包丟失率較高的低速網絡上),6個連接的 HTTP/1.1 仍然比一個連接的 HTTP/2 更為出色,這通常是由于 TCP 級別的隊頭阻塞問題造成的。正是這個事實極大地推動了新的 QUIC 傳輸協議的開發,以取代 TCP。

          HTTP/3(基于 QUIC)的隊頭阻塞

          在那之后,我們終于可以開始談論新的東西了!但首先,讓我們總結一下我們目前所學到的:

          • HTTP/1.1 有隊頭阻塞,因為它需要完整地發送響應,并且不能多路復用它們
          • HTTP/2 通過引入幀(frames)標識每個資源塊屬于哪個流(stream)來解決這個問題
          • 然而,TCP 不知道這些單獨的“流”(streams),只是把所有的東西看作一個大流(1 big stream)
          • 如果一個 TCP 包丟失,所有后續的包都需要等待它的重傳,即使它們包含來自不同流的無關聯數據。TCP 具有傳輸層隊頭阻塞。

          我敢肯定你現在可以預測我們如何解決 TCP 的問題,對吧?畢竟,解決方案很簡單:我們“只是”需要讓傳輸層知道不同的、獨立的流!這樣,如果一個流的數據丟失,傳輸層本身就知道它不需要阻塞其他流。

          盡管這個解決方案概念簡單,但在現實中卻很難實現。由于各種原因,不可能改變 TCP 本身使其具有流意識(stream-aware)。選擇的替代方法是以 QUIC 的形式實現一個全新的傳輸層協議。為了使 QUIC 現實中可以部署在因特網上,它運行在不可靠的 UDP 協議之上。然而,非常重要的是,這并不意味著 QUIC 本身也是不可靠的!在許多方面,QUIC 應該被看作是一個 TCP 2.0。它包括 TCP 的所有特性(可靠性、擁塞控制、流量控制、排序等)的最佳版本,以及更多其他特性。QUIC還完全集成了TLS(參見圖6),并且不允許未加密的連接。因為 QUICTCP 如此不同,這也意味著我們不能僅僅在其上運行 HTTP/2,這就是為什么創建了 HTTP/3(稍后我們將詳細討論這個問題)。這篇博文已經足夠長了,不需要更詳細地討論QUIC(請參閱其他來源),因此我將只關注我們需要了解當前隊頭阻塞討論的幾個部分。如圖8所示:

          圖8

          我們觀察到,讓 QUIC 知道不同的流(streams)是非常簡單的。QUIC 受到 HTTP/2 幀方式(framing-approach)的啟發,還添加了自己的幀(frames);在本例中是流幀(STREAM frame)。流id(stream id)以前在 HTTP/2 的數據幀(DATA frame)中,現在被下移到傳輸層的 QUIC 流幀(STREAM frame)中。這也說明了如果我們想使用 QUIC,我們需要一個新版本的 HTTP 的原因之一:如果我們只在 QUIC 之上運行 HTTP/2,那么我們將有兩個(可能沖突的)“流層”(stream layers)。相反,HTTP/3 從 HTTP 層刪除了流的概念(它的數據幀(DATA frames)沒有流id),而是重新使用底層的 QUIC 流。

          注意:這并不意味著 QUIC 突然知道 JS 或 CSS 文件,甚至知道它正在傳輸 HTTP;和 TCP 一樣,QUIC 應該是一個通用的、可重用的協議。它只知道有獨立的流(streams),它可以單獨處理,而不必知道它們到底是什么。

          現在我們了解了QUIC的流幀(STREAM frames),也很容易看出它們如何幫助解決圖9中的傳輸層隊頭阻塞:

          圖9

          HTTP/2數據幀(DATA frames)非常相似,QUIC 的流幀(STREAM frames)分別跟蹤每個流的字節范圍。這與 TCP 不同,TCP 只是將所有流數據附加到一個大 blob 中。像以前一樣,讓我們考慮一下如果 QUIC 數據包2丟失,而 13 到達會發生什么。與 TCP 類似,數據包1流1(stream 1)的數據可以直接傳遞到瀏覽器。然而,對于數據包3,QUIC 可以比 TCP 更聰明。它查看流1的字節范圍,發現這個流幀(STREAM frame)完全遵循流id 1的第一個流幀 STREAM frame字節 450 跟在字節 449 之后,因此數據中沒有字節間隙)。它可以立即將這些數據提供給瀏覽器進行處理。然而,對于流id 2,QUIC確實看到了一個缺口(它還沒有接收到字節0-299,這些字節在丟失的QUIC 數據包2中)。它將保存該流幀(STREAM frame),直到 QUIC數據包2的重傳(retransmission)到達。再次將其與 TCP 進行對比,后者也將數據流1的數據保留在數據包3中!

          類似的情況發生在另一種情形下,數據包1丟失,但23到達。QUIC 知道它已經接收到流2(stream 2)的所有預期數據,并將其傳遞給瀏覽器,只保留流1(stream 1)。我們可以看到,對于這個例子,QUIC 確實解決了 TCP 的隊頭阻塞!

          不過,這種方式有幾個重要的后果。最有影響的是 QUIC 數據可能不再以與發送時完全相同的順序發送到瀏覽器。對于 TCP,如果你發送數據包1、2和3,它們的內容將以完全相同的順序發送到瀏覽器(這就是導致隊頭阻塞的第一個原因)。然而,對于 QUIC,在上面的第二個示例中,在數據包1丟失的情況下,瀏覽器首先看到數據包2的內容,然后是數據包3的最后一部分,然后是數據包1的(重傳),然后是數據包3的第一部分。換言之:QUIC 在單個資源流中保留了順序,但不再跨單個流(individual streams)進行排序

          這是需要 HTTP/3 的第二個也是最重要的原因,因為事實證明,HTTP/2 中的幾個系統非常嚴重地依賴于 TCP 跨流(across streams)的完全確定性排序。例如,HTTP/2 的優先級系統通過傳輸更改樹數據結構(tree data structure )布局的操作(例如,將資源5添加為資源6的子級)工作的。如果這些操作應用的順序與發送順序不同(現在通過 QUIC 是可能出現的),客戶端和服務端的優先級狀態可能不同。HTTP/2 的頭壓縮系統 HPACK也會發生類似的情況。理解這里的細節并不重要,只需要得出結論:要讓這些 HTTP/2 系統直接應用 QUIC 是非常困難的。因此,對于 HTTP/3,有些系統使用完全不同的方法。例如,QPACK 是 HTTP/3 的 HPACK 版本,它允許在潛在的隊頭阻塞和壓縮性能之間進行自我選擇的權衡。HTTP/2 的優先級系統甚至被完全刪除,很可能會被 HTTP/3 的簡化版本所取代。所有這些都是因為,與 TCP 不同,QUIC 不能完全保證首先發送的數據也會首先被接收。

          所以,所有 QUIC 和重新設想 HTTP 版本的這些工作都是為了消除傳輸層隊頭阻塞。我當然希望這是值得的…

          QUIC 和 HTTP/3 真的完全消除了隊頭阻塞?如果你允許我說一點不好的話,我想引用自己幾個段落前的話:

          QUIC在單個資源流中保留排序

          你想想,這很符合邏輯。它基本上是這樣說的:如果你有一個 JavaScript 文件,該文件需要重新組裝(re-assembled),就像它是由開發人員創建的一樣(或者,老實說,通過 webpack),否則代碼將無法工作。任何類型的文件都是一樣的:把圖片隨機地放在一起意味著你阿姨寄來的一些奇怪的電子圣誕卡(甚至更奇怪的)。這意味著,即使在QUIC中,我們仍然有一種隊頭阻塞的形式:如果在單個流中有一個字節間隙,那么流的后面部分仍然會被阻塞,直到這個間隙被填滿

          這有一個關鍵的含義:QUIC 的隊頭阻塞移除只有在多個資源流同時活動時才有效。這樣,如果其中一個流上有包丟失,其他流仍然可以繼續。這就是我們在上面圖9的例子中看到的。然而,如果在某一時刻只有一個流在活動,任何丟包都會影響到這條孤獨的流,我們仍然會被阻塞,即使在 QUIC。所以,真正的問題是:我們會經常有多個并發流(simultaneous streams)嗎?

          正如對 HTTP/2 所解釋的,這是可以通過使用適當的資源調度器/多路復用方法來配置的。流1和流2可以被發送 1122、2121、1221 等,并且瀏覽器可以使用優先級系統指定它希望服務器遵循的方案(對于 HTTP/3 仍然如此)。所以瀏覽器可以說:嘿!我注意到這個連接有嚴重的數據包丟失。我將讓服務器以 121212 模式而不是 111222 向我發送資源。這樣,如果1的一個數據包丟失,2仍然可以繼續工作。然而,這種模式的問題是,121212 模式(或者類似的)對資源加載性能通常不是最優的。

          這是另一個復雜的話題,我現在不想太深入(我在 YouTube 上有一個關于這個的討論,供感興趣的人了解)。但是,通過我們的 JS 和 CSS 文件的簡單示例,基本概念很容易理解。正如您可能知道的那樣,瀏覽器需要接收整個 JS 或 CSS 文件,然后才能實際執行/應用它(雖然有些瀏覽器已經可以開始編譯/解析部分下載的文件,但它們仍然需要等待它們完整后才能實際使用它們)。但是,大量多路復用這些文件的資源塊最終會延遲它們:

          使用多路復用(較慢):
          ---------------------------
                                        流1(Stream 1)只有到這里才能使用
                                        ▼  
          12121212121212121212121212121212
                                         ▲
                                         流2(Stream 2)在這里下載完畢
          
          未使用多路復用/順序(流1(Stream 1)更快):
          ------------------------------------------------------
                           流1(Stream 1)在這里下載完畢,可以更早地使用
                           ▼      
          11111111111111111122222222222222
                                         ▲
                                         流2(Stream 2)還是在這里下載完畢
          

          現在,這個話題有很多細微差別,當然也存在多路復用方法更快的情況(例如,如果其中一個文件比另一個文件小得多,正如本文前面討論的那樣)。然而,一般來說,對于大多數頁面和大多數資源類型,我們可以說順序方法最有效(再次,請參閱上面的YouTube鏈接以獲取更多信息)。

          現在,這是什么意思?對于最佳性能,我們有兩個相互沖突的性能優化建議:

          • 從 QUIC 的隊頭阻塞移除中獲利:多路復用發送資源(12121212)
          • 為了確保瀏覽器能夠盡快處理核心資源:按順序發送資源(11112222)

          那么,哪一個是正確的?或者至少:哪一個應該優先于另一個?可悲的是,目前我還不能給你一個明確的答案,因為這是我正在研究的一個主題。這之所以困難,主要是因為數據包丟失模式很難預測。

          正如我們在上面討論過的,包丟失通常是突發性的和分組的。這意味著我們上面 12121212 的例子已經過于簡化了。圖10給出了一個更真實的概述。在這里,我們假設在下載2個流(綠色和紫色)時,我們有一個8個丟失包的突發事件:

          圖10

          在圖10的頂部第一行中,我們可以看到(通常)對資源加載性能更好的順序情況。在這里,我們看到 QUIC 對消除隊頭阻塞確實沒有那么大的幫助:在丟包之后收到的綠包不能被瀏覽器處理,因為它們屬于經歷丟包的同一個流。第二個(紫色)流的數據尚未收到,因此無法處理。

          這與中間一行不同,中間一行(偶然?。﹣G失的8個數據包都來自綠色流。這意味著瀏覽器可以處理最后收到的紫色數據包。然而,正如前面所討論的,如果瀏覽器是 JS 或 CSS 文件,如果有更多的紫色數據出現,瀏覽器可能不會從中受益太多。因此,在這里,我們從 QUIC 的隊頭阻塞移除中獲得了一些好處(因為紫色沒有被綠色阻止),但是可能會犧牲整體資源加載性能(因為多路復用會導致文件稍后完成)。

          最下面一行幾乎是最糟糕的情況。8個丟失的數據包分布在兩個流中。這意味著這兩個流現在都被隊頭阻塞了:不是因為它們像TCP那樣在等待對方,而是因為每個流仍然需要自己排序。

          注意:這也是為什么大多數 QUIC 實現很少同時創建包含來自多個流(streams)的數據包(packets)的原因。如果其中一個數據包丟失,則會立即導致單個數據包中所有流的隊頭阻塞!

          因此,我們看到可能存在某種最佳位置(中間一行),在這里,隊頭阻塞預防和資源加載性能之間的權衡可能是值得的。然而,正如我們所說,丟包模式很難預測。不會總是8個數據包。它們不會總是一樣的8個數據包。如果我們搞錯了,丟失的數據包只向左移動了一個,我們突然也少了一個紫色的包,這基本上使我們降級到與最下面一行相似的位置…

          我想你會同意我的觀點,那聽起來很復雜,甚至可能太復雜了。即便如此,問題是這會有多大幫助。如前所述,包丟失在許多網絡類型中通常比較少見,可能(也許?)太罕見了,看不到 QUIC 移除隊頭阻塞的影響。另一方面,已經有很好的文檔證明,無論您使用的是 HTTP/2 還是 HTTP/3,每個資源的數據包(圖10的最后一行)對資源加載性能都是相當不利的。

          因此,有人可能會說,雖然 QUIC 和 HTTP/3 不再受到應用層或傳輸層隊頭阻塞的影響,但這在現實中可能并不重要。我不能確定這一點,因為我們還沒有完全完成 QUIC 和 HTTP/3 的實現,所以我也沒有最后的度量(measurements)。然而,我個人的直覺(這是由我的幾個早期實驗的結果支持的)說,QUIC 消除隊頭阻塞可能實際上對 Web 性能沒有太大幫助,因為理想情況下,您不希望為了資源加載性能而對許多流進行多路復用。而且,如果你真的想讓它工作得很好,你就必須非常巧妙地調整你的多路復用方式來適應連接類型,因為你絕對不想在包丟失非常低的快速網絡上進行大量的多路復用(因為它們無論如何都不會遭受太多的隊頭阻塞)。就我個人而言,我不認為會發生這種事。

          注意:在這里,在結尾,你可能已經注意到我的故事有點不一致。一開始,我說 HTTP/1.1 的問題是它不允許多路復用。最后,我說多路復用在現實中并不那么重要。為了幫助解決這個明顯的矛盾,我添加了另一個額外的彩蛋部分

          在這篇(很長,我知道)的文章中,我們一直在追蹤隊頭阻塞。我們首先討論了為什么 HTTP/1.1 會受到應用層隊頭阻塞的影響。這主要是因為 HTTP/1.1 沒有識別單個資源塊的方法HTTP/2 使用幀來標記這些塊并啟用多路復用。這解決了 HTTP/1.1 的問題,但遺憾的是HTTP/2 仍然受到底層 TCP 的限制。由于 TCP 將 HTTP/2 數據抽象為一個單一的、有序的、但不透明的流,因此如果數據包在網絡上丟失或嚴重延遲,它將遭受隊頭阻塞。QUIC 通過將 HTTP/2 的一些概念引入傳輸層來解決這個問題。這反過來會產生嚴重的影響,因為跨流的數據不再是完全有序的。這最終導致需要一個全新的版本 HTTP/3,它只運行在 QUIC 之上(而 HTTP/2 只運行在 TCP 之上,請參見圖6)。

          我們需要所有這些上下文來批判性地思考 QUIC(以及 HTTP/3)中的隊頭阻塞移除在現實中對 Web 性能的實際幫助有多大。我們看到它可能只會對有大量數據包丟失的網絡產生很大的影響。我們還討論了為什么即使這樣,你仍然需要多路復用資源,并看運氣丟包對多路復用的影響怎么樣。我們看到了為什么這樣做實際上弊大于利,因為資源多路復用通常不是 Web 性能的最佳方案。我們得出的結論是,雖然現在確定這一點還為時過早,但在大多數情況下,QUIC 和 HTTP/3 的隊頭阻塞移除可能不會對 Web 性能起到多大作用。

          那么…這又給我們留下什么樣的 Web 性能評價呢?忽略 QUIC 和 HTTP/3,堅持 HTTP/2 + TCP?我當然不希望!我仍然相信 HTTP/3 總體上將比 HTTP/2 快,因為 QUIC 還包括其他性能改進。例如,它比 TCP 在網絡上的開銷更小,在擁塞控制方面更加靈活,而且最重要的是,它具有 0-RTT 連接建立特性。我覺得特別是 0-RTT 將提供最多的Web性能好處,盡管也有很多挑戰。以后我會寫另一篇關于 0-RTT 的博客文章,但是如果你迫不及待想知道更多關于放大攻擊預防、重放攻擊、初始擁塞窗口大小等的信息,請看我的另一篇 YouTube 講座或閱讀我最近的論文。

          感謝您的閱讀!

          以下是彩蛋,感興趣的話可以閱讀

          彩蛋:HTTP/1.1 管道(pipelining)

          HTTP/1.1 包含了一個名為“管道“(pipelining)的特性,在我看來這是經常被誤解的。我看過很多文章,甚至書籍中都有人聲稱 HTTP/1.1 管道解決了隊頭阻塞問題。我甚至見過一些人說管道和正確的多路復用是一樣的。兩種說法都是錯誤的。

          我發現用類似彩蛋圖1中的插圖來解釋 HTTP/1.1 管道是最簡單的:

          彩蛋圖1:HTTP/1.1 管道 如果沒有管道(pipelining)(彩蛋圖1的左側),瀏覽器必須等待發送第二個資源請求,直到第一個請求的響應被完全接收(同樣使用 Content-Length)。這會為每個請求增加一個往返時間(RTT)延遲,這對Web性能不利。

          有了管道(彩蛋圖1的中間部分),瀏覽器不必等待任何響應數據,現在可以背靠背地發送請求。這樣,我們在連接過程中節省了一些 RTTs,使得加載過程更快。另請注意,請回顧圖2:您會看到實際上也在使用管道,因為服務器在 TCP 數據包2中打包 script.js 以及 style.css 的響應數據。當然,只有在服務器同時接收到這兩個請求時,這才是可能的。

          然而,最重要的是,這種管道只適用于來自瀏覽器的請求。正如[HTTP/1.1 規范][h1spec]所說:

          服務器必須按照接收請求的順序發送對這些[管道化]請求的響應。因此,我們看到,實際的響應塊(response chunks)多路復用(如彩蛋圖1右側所示)在 HTTP/1.1 管道中仍然是不可能的。換一種說法:管道解決了請求的隊頭阻塞,而不是響應的隊頭阻塞??杀氖?,響應隊頭阻塞是導致 Web 性能問題最多的原因。

          更糟糕的是,大多數瀏覽器實際上并沒有在現實中使用 HTTP/1.1 管道,因為這會使隊頭阻塞在多個并行 TCP 連接的設置中變得更加不可預測。為了理解這一點,讓我們設想一個設置,其中通過兩個 TCP 連接從服務器請求三個文件 A(大)、B(小)和 C(小)。A 和 B 在不同的連接上被請求?,F在,瀏覽器應該在哪個連接上傳輸對 C 的請求?正如我們之前所說,它不知道 A 還是 B 將成為最大/最慢的資源。

          如果它猜對了(B),它就可以在傳輸 A 所需的時間內同時下載 B 和 C,從而獲得了很好的加速效果。但是,如果猜測是錯誤的(A),B 的連接將長時間處于空閑狀態,而 C 則在 A 后面被阻塞。這是因為 HTTP/1.1 也沒有提供一種在請求被發送后“中止”的方法(HTTP/2 和 HTTP/3 允許這樣做)。因此,瀏覽器不能簡單地通過 B 的連接請求 C,因為它最終會請求兩次 C。

          為了解決所有這些問題,現代瀏覽器不使用管道,甚至會主動延遲對某些已發現資源(例如圖像)的請求一段時間,以查看是否找到更重要的文件(例如 JS 和 CSS),以確保高優先級資源不會被阻塞。

          很明顯,HTTP/1.1 管道的失敗是 HTTP/2 使用截然不同方法的另一個動機。然而,由于 HTTP/2 的優先級系統指導多路復用在現實中常常無法執行,一些瀏覽器甚至采取了延遲 HTTP/2 資源請求的方式來獲得最佳性能。

          彩蛋:TLS 隊頭阻塞

          如上所述,TLS 協議為應用層協議(如 HTTP)提供加密(和其他功能)。它通過將從 HTTP 獲取的數據包裝到 TLS 記錄中,TLS 記錄在概念上類似于 HTTP/2 幀(frames)或 TCP 數據包(packets)。例如,它們在開始時包含一些元數據,以標識記錄的長度。然后,對該記錄及其 HTTP 內容進行加密并傳遞給 TCP 進行傳輸。

          就 CPU 使用率而言,加密可能是一項昂貴的操作,因此一次加密一個好數據塊通常是一個好主意,因為這通常更有效。實際上,TLS 可以以高達 16KB 的塊加密資源,這足以填充大約 11 個典型的 TCP 包(give 或 take)。

          然而,至關重要的是,TLS 只能對整個記錄進行解密,這就是為什么會出現 TLS 隊頭阻塞的情況。假設 TLS 記錄分散在 11 個 TCP 包上,最后一個 TCP 包丟失。由于 TLS 記錄是不完整的,它不能被解密,因此被卡在等待最后一個 TCP 包的重傳。注意,在這個特定的情況下,沒有 TCP 隊頭阻塞:在編號11之后沒有數據包被卡住等待重新傳輸。換言之,如果我們在本例中使用純 HTTP 而不是 HTTPS,那么前10個包中的 HTTP 數據可能已經被移動到瀏覽器中進行處理。然而,因為我們需要整個11包的 TLS 記錄才能解密它,所以我們有了一種新形式的隊頭阻塞。

          雖然這是一個非常具體的情況,在現實中可能不會經常發生,但在設計 QUIC 協議時,它仍然被考慮在內。因為那里的目標是徹底消除所有形式的隊頭阻塞(或至少盡可能多地消除),甚至這種邊緣情況也必須被移除。這就是為什么 QUIC 集成了 TLS,它總是以每個包為基礎加密數據,并且不直接使用 TLS 記錄。正如我們所看到的,與使用更大的塊相比,這效率更低,需要更多的 CPU,這也是為什么 QUIC 在當前實現中仍然比 TCP 慢的主要原因之一。

          彩蛋:傳輸擁塞控制

          傳輸層協議如 TCP 和 QUIC 包括一種稱為擁塞控制(Congestion Control)的機制。擁塞控制器的主要工作是確保網絡不會同時被過多的數據過載。如果沒有緩沖區的話,數據包就會溢出。所以,它通常只發送一點數據(通常是 14KB),看看是否能通過。如果數據到達,接收方將確認發送回發送方。只要所有發送的數據都得到確認,發送方就在每次 RTT 時將其發送速率加倍,直到觀察到丟包事件(這意味著網絡過載(1 位),它需要后退(1 位))。這就是 TCP 連接如何“探測”其可用帶寬。

          注:以上描述只是擁塞控制的一種方法。目前,其他方法也越來越流行,其中主要是 BBR 算法。BBR 并沒有直接觀察數據包丟失,而是大量考慮 RTT 波動來確定網絡是否過載,這意味著它通常通過探測帶寬來減少數據包丟失。

          關鍵是:擁塞控制機制對每個 TCP(和 QUIC)連接都是獨立的! 這反過來也會影響到 HTTP層 的 Web 性能。首先,這意味著 HTTP/2 的單個連接最初只發送 14KB。然而,HTTP/1.1 的6個連接在它們的第一次傳輸中發送 14KB,大約是 84KB!隨著時間的推移,這將變得復雜,因為每個 HTTP/1.1 連接使用每個 RTT 將其數據加倍。第二,只有在數據包丟失的情況下,連接才會降低其發送速率。對于 HTTP/2 的單個連接,即使是一個包丟失也意味著它將減慢速度(除了導致 TCP 隊頭阻塞之外!)。然而,對于 HTTP/1.1,只有一個連接上的一個包丟失只會減慢這一個連接的速度:其他5個連接可以保持正常的發送和增長。

          這一切使一件事變得非常清楚:HTTP/2 的多路復用與 HTTP/1.1 的同時下載資源是不一樣的(我還看到一些人聲稱這一點)。單個 HTTP/2 連接的可用帶寬只是在不同文件之間分布/共享,但是塊仍然是按順序發送的。這與 HTTP/1.1 不同,后者以真正的并行方式發送內容。

          現在,您可能會想知道:那么,HTTP/2 怎么可能比 HTTP/1.1 快呢? 這是一個很好的問題,也是我斷斷續續問自己很久的問題。一個明顯的答案是,如果你有超過6個文件。這就是 HTTP/2 在當時的市場營銷方式:將一個圖像分割成小正方形,然后通過 HTTP/1.1 vs HTTP/2 加載它們。這主要展示了 HTTP/2 的隊頭阻塞移除。然而,對于普通/真實的網站來說,事情很快就會變得更加微妙。這取決于資源的數量、大小、使用的優先級/多路復用方案、到服務器的 RTT、實際有多少丟包以及何時發生、同時鏈路上有多少其他流量、使用的擁塞控制器邏輯,等等。HTTP/1.1 可能會丟失的一個例子是在可用帶寬有限的網絡上:6個 HTTP/1.1 連接各自增加其發送速率,導致它們很快使網絡過載,之后它們都必須后退,必須通過反復試驗找到它們共存的帶寬限制(在 HTTP/2 之前,人們認為 HTTP/1.1 的并行連接可能是因特網上數據包丟失的主要原因)。單個 HTTP/2 連接增長較慢,但在包丟失事件后恢復速度更快,并且更快地找到最佳帶寬。另一個帶有注釋的擁塞窗口的更詳細的,HTTP/2 更快的示例可以看這張圖片(不適用于膽小的人)。

          QUIC 和 HTTP/3 將面臨類似的挑戰,就像 HTTP/2 一樣,HTTP/3 將使用單一的底層 QUIC 連接。然后,您可能會說 QUIC 連接在概念上有點像多個 TCP 連接,因為每個 QUIC 流都可以看作一個TCP連接,因為丟失檢測是在每個流的基礎上完成的。然而,關鍵的是,QUIC 的擁塞控制仍然是在連接級別完成的,而不是針對每個流。這意味著,即使流在概念上是獨立的,它們仍然會影響 QUIC 的單連接擁塞控制器,如果流中有任何一個丟失,就會導致速度減慢。換一種說法:單個 HTTP/3 + QUIC 連接的增長速度仍不及6個 HTTP/1.1 連接,這與 HTTP/2 + TCP 在一個連接上的速度不一樣。

          彩蛋:多路復用是否重要?

          如上所述,并在本演示文稿中進行了深入解釋,通常建議以順序方式而不是多路傳輸方式發送大多數網頁資源。換一種說法,如果你有兩個文件,你通常最好發送 11112222 而不是 12121212。對于需要在應用之前完全接收的資源,如 JS、CSS 和字體,尤其如此。

          如果是這樣的話,我們可能會想為什么我們需要多路復用?通過擴展:HTTP/2 甚至 HTTP/3,因為多路復用是 HTTP/1.1 沒有的主要特性之一。首先,一些可以增量處理/呈現的文件確實從多路復用中獲益。例如,漸進式圖像就是這樣。第二,如上所述,如果其中一個文件比其他文件小得多,那么它可能會很有用,因為它將更早地下載,而不會對其他文件造成太多的延遲。第三,多路復用允許改變響應的順序,并為高優先級的響應中斷低優先級的響應

          現實中出現的一個很好的例子是在源服務器前面使用 CDN 緩存。假設瀏覽器從 CDN 請求兩個文件。第一個(1)沒有被緩存,需要從源文件中獲取,這需要一段時間。第二個資源(2)緩存在 CDN 中,因此可以直接傳輸回瀏覽器。

          在一個連接上使用 HTTP/1.1,由于隊頭阻塞,我們必須等待隊頭完全發送(1),然后才能開始發送(2)。這將給我們帶來 11112222,但需要很長的前期等待時間。然而,使用HTTP/2,我們可以立即開始發送(2),利用 CDN 和源節點之間的“思考時間”,并“預熱”連接的擁塞控制器。重要的是,如果(1)在(2)完成之前開始到達,我們現在可以簡單地開始將(1)的數據注入到響應流中。這樣我們就可以得到 22111122,等待的時間要短得多。甚至可以在連接開始時使用服務器推送(Server Push)或103早期提示(103 early hints)等功能。

          因此,雖然像 12121212 這樣的完全“輪詢”多路復用很少是您想要的 Web 性能,但是多路復用在總體上絕對是一個有用的特性。


          作者:Robin Marx
          原文:Head-of-Line Blocking in QUIC and HTTP/3: The Details
          GitHub:rmarx/holblocking-blogpost
          譯文來自:https://zhuanlan.zhihu.com/p/330300133

          如有涉及侵權,請聯系刪除!


          主站蜘蛛池模板: 亚洲AV无码一区二区三区牲色| 国产成人高清亚洲一区91| 成人无号精品一区二区三区| 亚洲精品伦理熟女国产一区二区 | 内射女校花一区二区三区| 亚洲视频一区二区三区| 国产精品久久一区二区三区| 亚洲视频一区在线观看| 四虎一区二区成人免费影院网址 | 国产精品一区三区| 亚洲一区二区三区四区在线观看| 日韩精品无码一区二区三区免费| 亚洲一区二区三区高清| 精品一区二区三区AV天堂| 亚洲一区二区三区无码中文字幕| 亚洲av成人一区二区三区| 无码国产精品一区二区高潮| 精品亚洲综合在线第一区| 精品国产一区二区三区香蕉事| 日本道免费精品一区二区| 97久久精品无码一区二区天美| 日本伊人精品一区二区三区| 无码丰满熟妇浪潮一区二区AV | 国产午夜精品一区二区三区极品| 激情综合一区二区三区| 日本精品啪啪一区二区三区| 国产高清视频一区三区| 亚洲国产成人久久一区WWW | 伊人久久大香线蕉av一区| 日韩精品一区二区三区在线观看 | 精品在线视频一区| 精品国产一区二区三区免费看 | 成人无码精品一区二区三区| 日韩精品无码一区二区中文字幕 | 在线观看国产一区亚洲bd| 人妻夜夜爽天天爽爽一区| 美女视频在线一区二区三区| 亚洲国产成人一区二区精品区| 色狠狠色噜噜Av天堂一区| 久久精品一区二区| 精品伦精品一区二区三区视频|