Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 99精品在线播放,九九精品免费视频,国产精品一区二区三区四区

          整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          滲透測(cè)試-請(qǐng)求響應(yīng)(爬蟲)

          http請(qǐng)求方式連接網(wǎng)站數(shù)據(jù)


          statusCode.py


          import requests

          #在路徑中直接添加get方式獲取路徑和狀態(tài)碼

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

          response=requests.get(url)

          print(response)

          print(response.status_code)


          #無(wú)參數(shù)的get獲取狀態(tài)碼方式

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

          r=requests.get(url=url)

          print(r.status_code)

          print(r.url)


          #有參數(shù)的get獲取狀態(tài)碼方式

          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'

          }


          #請(qǐng)求方式get路徑和參數(shù)

          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')


          #請(qǐng)求的post方式 帶參數(shù)的請(qǐng)求

          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)

          #得到網(wǎng)站請(qǐng)求頭

          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)

          控制臺(tái)運(yùn)行結(jié)果如下:

          <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請(qǐng)求的不同結(jié)果

          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)

          #此時(shí)cookies是空目錄

          print(r.cookies)

          #當(dāng)https的時(shí)候cookies為有數(shù)據(jù)狀態(tài) ]>

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

          r=requests.get(url)

          print(r.cookies)

          控制臺(tái)輸出結(jié)果

          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/>]>

          載說(shuō)明:原創(chuàng)不易,未經(jīng)授權(quán),謝絕任何形式的轉(zhuǎn)載

          一個(gè)吸引人的網(wǎng)頁(yè)頁(yè)眉對(duì)于給訪問(wèn)者留下良好的第一印象至關(guān)重要。一個(gè)設(shè)計(jì)精良的頁(yè)眉不僅能夠吸引注意力,還能為整個(gè)網(wǎng)站設(shè)定基調(diào)。借助CSS,創(chuàng)建現(xiàn)代化和視覺(jué)吸引力的網(wǎng)頁(yè)頁(yè)眉比以往任何時(shí)候都更加容易。

          在本文中,我們將探索一些基本的技巧和提示,以幫助您使用CSS創(chuàng)建令人驚艷的頁(yè)眉布局。我們并不過(guò)多關(guān)注設(shè)計(jì),而是專注于創(chuàng)建布局,并了解創(chuàng)建布局時(shí)可能遇到的困難。

          Space-Between在一個(gè)三列的頁(yè)眉中無(wú)法居中

          首先,讓我們談?wù)勅许?yè)眉,因?yàn)檫@是我最常見(jiàn)到實(shí)現(xiàn)錯(cuò)誤的一種情況。

          標(biāo)記相對(duì)簡(jiǎn)單:

          <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>


          我將所有鏈接放在頁(yè)眉的導(dǎo)航標(biāo)簽中。因此,這是一個(gè)非常簡(jiǎn)單的標(biāo)記。通常情況下,導(dǎo)航應(yīng)該在第一個(gè)和最后一個(gè)項(xiàng)之間居中對(duì)齊。

          根據(jù)我的觀察,這個(gè)問(wèn)題已經(jīng)成為前端社區(qū)中的新問(wèn)題,類似于“居中一個(gè)div”的問(wèn)題。因?yàn)樵S多開發(fā)人員會(huì)使用justify-content屬性的space-between值來(lái)解決這個(gè)問(wèn)題,但它實(shí)際上并不能將中間元素居中對(duì)齊。下面是使用justify-content屬性的space-between值的相同導(dǎo)航標(biāo)記,供比較參考:

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

          這是創(chuàng)建頁(yè)眉時(shí)的第一個(gè)挑戰(zhàn):正確設(shè)置基本布局。在您確定要實(shí)現(xiàn)的布局以及如何實(shí)現(xiàn)之前,不要試圖添加更多內(nèi)容。

          在我們繼續(xù)之前,我在ProductHunt上花了幾個(gè)小時(shí)尋找和評(píng)估三列頁(yè)眉。它們中的大多數(shù)使用了我展示的將justify-content屬性設(shè)置為space-between的技巧(因此,它們的導(dǎo)航并沒(méi)有真正居中)。有些人試圖繞過(guò)這個(gè)問(wèn)題(例如,通過(guò)添加外邊距),而其他人則通過(guò)絕對(duì)定位放棄了。當(dāng)然,這些"hack"可以"解決"問(wèn)題,但它們?cè)黾恿藦?fù)雜性。您的頁(yè)眉將變得難以維護(hù),當(dāng)您再次回到頁(yè)眉時(shí)會(huì)產(chǎn)生不好的感覺(jué)。話雖如此,這個(gè)"真正的解決方案"也有些技巧性。

          下面是如何實(shí)現(xiàn)的方法:

          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;
          }

          效果如下圖所示:

          讓我們來(lái)談?wù)勥@個(gè)解決方案。

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

          然后,頁(yè)眉下的每個(gè)元素都是一個(gè)彈性容器。這也是不必要的。目前,它僅用于導(dǎo)航的最后一個(gè)子元素,以將其子元素移動(dòng)到右側(cè)。

          這只留下了這條規(guī)則:flex: 1 1 0; 這是我們?cè)谶@里的主要關(guān)注點(diǎn)。我將這條規(guī)則應(yīng)用于第一個(gè)和最后一個(gè)元素。它允許它們?cè)鲩L(zhǎng)和收縮,并將它們的基準(zhǔn)大小設(shè)置為0像素。這就是整個(gè)"hack"的全部?jī)?nèi)容。因?yàn)槲覀儗⑺鼈兊幕鶞?zhǔn)大小設(shè)置為0,它們將等比增長(zhǎng),從而使我們的中間元素居中對(duì)齊。

          當(dāng)創(chuàng)建頁(yè)眉布局時(shí),當(dāng)然,將頁(yè)眉的中間元素居中對(duì)齊并不是我們面臨的唯一挑戰(zhàn)。

          在較小的屏幕上隱藏導(dǎo)航欄

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

          當(dāng)然,將登錄替換為按鈕是很簡(jiǎn)單的。所以,我們來(lái)談?wù)勂渌氖虑榘伞?/p>

          假設(shè)我們的頁(yè)眉看起來(lái)像這樣:

          <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>

          現(xiàn)在,當(dāng)我們的視口變小時(shí),我們的頁(yè)眉遇到了一個(gè)問(wèn)題:

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

          例如,我們可以創(chuàng)建一個(gè)容器查詢。以下是更新后的代碼示例:

          * {
            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;
            }
          }


          當(dāng)然,你可能會(huì)說(shuō)你也可以用媒體查詢來(lái)做到這一點(diǎn)。沒(méi)什么了不起的。但容器查詢的優(yōu)勢(shì)在于我們可以為容器指定最小寬度。我們不關(guān)心視口有多大,但我們知道:如果我們的容器寬度小于400像素,它會(huì)變得非常難看。這是我真正期待被廣泛支持的功能之一。

          粘性頂部導(dǎo)航欄

          我仍然看到一些使用position: fixed實(shí)現(xiàn)頂部導(dǎo)航欄,即使sticky是更好的解決方案。

          為什么sticky更好呢?請(qǐng)考慮以下代碼:

          <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>

          在這種情況下,頁(yè)眉具有position: fixed。結(jié)果,主要內(nèi)容區(qū)域移動(dòng)到網(wǎng)站的頂部,因?yàn)槲臋n中沒(méi)有為頁(yè)眉保留空間。它處于流動(dòng)之外。

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

          以下是帶有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;
          }

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

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

          結(jié)束

          您是否知道關(guān)于頁(yè)眉布局的其他常見(jiàn)錯(cuò)誤?或者您是否了解其他具有挑戰(zhàn)性的元素?我很樂(lè)意在評(píng)論中了解更多!

          由于文章內(nèi)容篇幅有限,今天的內(nèi)容就分享到這里,文章結(jié)尾,我想提醒您,文章的創(chuàng)作不易,如果您喜歡我的分享,請(qǐng)別忘了點(diǎn)贊和轉(zhuǎn)發(fā),讓更多有需要的人看到。同時(shí),如果您想獲取更多前端技術(shù)的知識(shí),歡迎關(guān)注我,您的支持將是我分享最大的動(dòng)力。我會(huì)持續(xù)輸出更多內(nèi)容,敬請(qǐng)期待。

          譯)Robin Marx: QUIC 和 HTTP/3 隊(duì)頭阻塞的細(xì)節(jié)

          你可能已經(jīng)聽說(shuō),經(jīng)過(guò)4年的工作,新的 HTTP/3 和 QUIC 協(xié)議終于接近正式標(biāo)準(zhǔn)化。預(yù)覽版現(xiàn)在可以在服務(wù)器和瀏覽器中進(jìn)行測(cè)試。

          與 HTTP/2 相比,HTTP/3 有很大的性能改進(jìn),這主要是因?yàn)樗鼘⒌讓觽鬏攨f(xié)議從 TCP 改為基于 UDP 的 QUIC。在這篇文章中,我們將深入了解其中的一項(xiàng)改進(jìn),即消除隊(duì)頭阻塞(Head-of-Line blocking, 簡(jiǎn)寫:HOL blocking)問(wèn)題。這很有用,因?yàn)槲易x過(guò)很多關(guān)于這實(shí)際上意味著什么以及它在現(xiàn)實(shí)中有多大幫助的誤解。解決隊(duì)頭阻塞也是 HTTP/3 和 QUIC 以及 HTTP/2 背后的主要?jiǎng)訖C(jī)之一,因此它也為協(xié)議演進(jìn)的原因提供了一個(gè)極好的視角。

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

          目錄:

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

          彩蛋內(nèi)容:

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

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

          很難給你一個(gè)單一的隊(duì)頭阻塞(HOL blocking) 的技術(shù)定義,因?yàn)檫@篇博客文章單獨(dú)描述了它的四個(gè)不同變體。然而,一個(gè)簡(jiǎn)單的定義是:

          當(dāng)單個(gè)(慢)對(duì)象阻止其他/后續(xù)的對(duì)象前進(jìn)時(shí)

          現(xiàn)實(shí)生活中一個(gè)很好的比喻就是只有一個(gè)收銀臺(tái)的雜貨店。一個(gè)顧客買了很多東西,最后會(huì)耽誤排在他后面的人,因?yàn)轭櫩褪且韵冗M(jìn)先出(First In, First Out)的方式服務(wù)的。另一個(gè)例子是只有單行道的高速公路。在這條路上發(fā)生一起車禍,可能會(huì)使整個(gè)通道堵塞很長(zhǎng)一段時(shí)間。因此,即使是在“頭部(head)”一個(gè)單一的問(wèn)題可以“阻塞(block)”整條“線(line)”。

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

          HTTP/1.1 的隊(duì)頭阻塞

          HTTP/1.1(默認(rèn)開啟keep-alive)是一種更簡(jiǎn)單的協(xié)議。一個(gè)協(xié)議仍然可以基于文本并在網(wǎng)絡(luò)上可讀的時(shí)代。如下圖1所示:

          圖1

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

          注意:實(shí)際上,當(dāng)使用 HTTPS 時(shí),HTTP 和 TCP 之間有另一個(gè)安全層,通常使用 TLS 協(xié)議。不過(guò),為了清晰起見(jiàn),我們?cè)谶@里省略了這一點(diǎn)。我在結(jié)尾加入了一個(gè)額外的彩蛋部分,詳細(xì)說(shuō)明了 TLS 特定的隊(duì)頭阻塞變體以及 QUIC 如何避免它。閱讀完正文后,請(qǐng)隨意閱讀它(以及其他的彩蛋部分)。

          現(xiàn)在讓我們看看當(dāng)瀏覽器也請(qǐng)求style.css時(shí)發(fā)生了什么,如圖2:

          圖2

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

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

          11111111111111111111111111111111111111122
          

          你可以看到這是一個(gè)隊(duì)頭阻塞問(wèn)題的例子!現(xiàn)在你可能會(huì)想:這很容易解決!只需讓瀏覽器在JS文件之前請(qǐng)求CSS文件!然而,至關(guān)重要的是,瀏覽器無(wú)法預(yù)先知道這兩個(gè)文件中的哪一個(gè)在請(qǐng)求時(shí)會(huì)成為更大的文件。這是因?yàn)闆](méi)有辦法在HTML中指明文件有多大(類似這樣的東西很不錯(cuò),HTML工作組:<img src="thisisfine.jpg" size="15000" />)

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

          12121111111111111111111111111111111111111
          

          然而不幸的是,由于協(xié)議存在一些基礎(chǔ)的限制,這種多路復(fù)用在 HTTP/1.1 中是不可能的。為了理解這一點(diǎn),我們甚至不需要繼續(xù)查看大資源對(duì)小資源(large-vs-small)場(chǎng)景,因?yàn)樗呀?jīng)在我們的示例中顯示了兩個(gè)較小的文件。如圖3,我們只為兩個(gè)資源交錯(cuò)4個(gè)塊:

          圖3

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

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

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

          這是 HTTP/1.1 協(xié)議設(shè)計(jì)方式的一個(gè)基礎(chǔ)限制。如果您只有一個(gè) HTTP/1.1 連接,那么在你切換到發(fā)送新資源之前,必須完整地傳輸資源響應(yīng)。如果前面的資源創(chuàng)建緩慢(例如,從數(shù)據(jù)庫(kù)查詢動(dòng)態(tài)生成的index.html)或者,如上所述,如果前面的資源很大。這些問(wèn)題可能會(huì)引起隊(duì)頭阻塞問(wèn)題。

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

          由于這個(gè)問(wèn)題不能用 HTTP/1.1 解決,而且并行 TCP 連接的補(bǔ)丁解決方案也不能隨著時(shí)間的推移擴(kuò)展得太好,很明顯需要一種全新的方法,這就是后來(lái)的 HTTP/2

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

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

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

          因此,HTTP/2 的目標(biāo)非常明確:我們能夠回到單個(gè) TCP 連接,解決隊(duì)頭阻塞問(wèn)題。換一種說(shuō)法:我們希望能夠正確地復(fù)用資源塊(resource chunks)。這在 HTTP/1.1 中是不可能的,因?yàn)闆](méi)有辦法分辨一個(gè)塊屬于哪個(gè)資源,或者它在哪里結(jié)束,另一個(gè)塊從哪里開始。HTTP/2 非常優(yōu)雅地解決了這一問(wèn)題,它在資源塊之前添加了幀(frames)。如圖4所示:

          圖4

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

          圖5

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

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

          HTTP/2 的方式一個(gè)重要結(jié)果是,我們突然需要一種方法讓瀏覽器與服務(wù)器通信,單個(gè)連接的帶寬如何跨資源分布(distributed)。換一種說(shuō)法:資源塊應(yīng)該如何“調(diào)度(scheduled)”或交錯(cuò)(interleaved)。如果我們?cè)俅斡?1 和 2 來(lái)表示,我們會(huì)發(fā)現(xiàn)對(duì)于 HTTP/1.1,唯一的選項(xiàng)是11112222(我們稱之為順序的(sequential))。然而, HTTP/2 有更多的自由:

          • 公平多路復(fù)用(例如兩個(gè)漸進(jìn)的 JPEGs):12121212
          • 加權(quán)多路復(fù)用(2是1的兩倍):22122122121
          • 反向順序調(diào)度(例如2是密鑰服務(wù)器推送的資源):22221111
          • 部分調(diào)度(流1被中止且未完整發(fā)送):112222

          使用哪種方法是由 HTTP/2 中所謂的“優(yōu)先級(jí)(prioritization)”系統(tǒng)驅(qū)動(dòng)的,所選擇的方法對(duì)Web 性能有很大的影響。然而,這本身就是一個(gè)非常復(fù)雜的話題,你不需要在接下來(lái)的博客文章中理解它,所以我不把它放在這里講了(盡管我在YouTube上有一個(gè)關(guān)于這個(gè)的講座)。

          我想你會(huì)同意,通過(guò) HTTP/2 的幀(frames)及其優(yōu)先級(jí)設(shè)置,它確實(shí)解決了 HTTP/1.1 的隊(duì)頭阻塞問(wèn)題。這意味著我在這里的工作完成了,我們都可以回家了。對(duì)嗎?好吧,沒(méi)那么簡(jiǎn)單。我們已經(jīng)解決了 HTTP/1.1 的隊(duì)頭阻塞,是的,但是 TCP 的隊(duì)頭阻塞呢?

          TCP 隊(duì)頭阻塞

          事實(shí)證明,HTTP/2 只解決了 HTTP 級(jí)別的隊(duì)頭阻塞,我們可以稱之為應(yīng)用層隊(duì)頭阻塞。然而,在典型的網(wǎng)絡(luò)模型中,還需要考慮下面的其他層。您可以在圖6中清楚地看到這一點(diǎn):

          圖6

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

          然而,如果我們想將多個(gè) HTTP/2 資源多路傳輸?shù)揭粋€(gè) TCP 連接上,這確實(shí)會(huì)產(chǎn)生重要的后果。如圖7:

          圖7

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

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

          所有這些看起來(lái)都是不必要的細(xì)節(jié),直到你意識(shí)到互聯(lián)網(wǎng)是一個(gè)根本不可靠的網(wǎng)絡(luò)。在從一個(gè)端點(diǎn)到另一個(gè)端點(diǎn)的傳輸過(guò)程中,數(shù)據(jù)包會(huì)丟失和延遲。TCP 的可靠性正是其最受歡迎的原因之一。它只需重新傳輸丟失數(shù)據(jù)包的副本就可以做到這一點(diǎn)。

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

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

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

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

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

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

          總之,事實(shí)上,我們看到(也許出乎意料),HTTP/2 目前部署在瀏覽器和服務(wù)器中,在大多數(shù)情況下通常與 HTTP/1.1 一樣快或略快。在我看來(lái),這部分是因?yàn)榫W(wǎng)站在優(yōu)化 HTTP/2 方面做得更好,部分原因是瀏覽器仍然經(jīng)常打開多個(gè)并行 HTTP/2 連接(要么是因?yàn)檎军c(diǎn)仍然在不同的服務(wù)器上共享資源,要么是因?yàn)榕c安全相關(guān)的副作用),從而使兩者兼得。

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

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

          在那之后,我們終于可以開始談?wù)撔碌臇|西了!但首先,讓我們總結(jié)一下我們目前所學(xué)到的:

          • HTTP/1.1 有隊(duì)頭阻塞,因?yàn)樗枰暾匕l(fā)送響應(yīng),并且不能多路復(fù)用它們
          • HTTP/2 通過(guò)引入幀(frames)標(biāo)識(shí)每個(gè)資源塊屬于哪個(gè)流(stream)來(lái)解決這個(gè)問(wèn)題
          • 然而,TCP 不知道這些單獨(dú)的“流”(streams),只是把所有的東西看作一個(gè)大流(1 big stream)
          • 如果一個(gè) TCP 包丟失,所有后續(xù)的包都需要等待它的重傳,即使它們包含來(lái)自不同流的無(wú)關(guān)聯(lián)數(shù)據(jù)。TCP 具有傳輸層隊(duì)頭阻塞。

          我敢肯定你現(xiàn)在可以預(yù)測(cè)我們?nèi)绾谓鉀Q TCP 的問(wèn)題,對(duì)吧?畢竟,解決方案很簡(jiǎn)單:我們“只是”需要讓傳輸層知道不同的、獨(dú)立的流!這樣,如果一個(gè)流的數(shù)據(jù)丟失,傳輸層本身就知道它不需要阻塞其他流。

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

          圖8

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

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

          現(xiàn)在我們了解了QUIC的流幀(STREAM frames),也很容易看出它們?nèi)绾螏椭鉀Q圖9中的傳輸層隊(duì)頭阻塞:

          圖9

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

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

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

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

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

          QUIC 和 HTTP/3 真的完全消除了隊(duì)頭阻塞?如果你允許我說(shuō)一點(diǎn)不好的話,我想引用自己幾個(gè)段落前的話:

          QUIC在單個(gè)資源流中保留排序

          你想想,這很符合邏輯。它基本上是這樣說(shuō)的:如果你有一個(gè) JavaScript 文件,該文件需要重新組裝(re-assembled),就像它是由開發(fā)人員創(chuàng)建的一樣(或者,老實(shí)說(shuō),通過(guò) webpack),否則代碼將無(wú)法工作。任何類型的文件都是一樣的:把圖片隨機(jī)地放在一起意味著你阿姨寄來(lái)的一些奇怪的電子圣誕卡(甚至更奇怪的)。這意味著,即使在QUIC中,我們?nèi)匀挥幸环N隊(duì)頭阻塞的形式:如果在單個(gè)流中有一個(gè)字節(jié)間隙,那么流的后面部分仍然會(huì)被阻塞,直到這個(gè)間隙被填滿

          這有一個(gè)關(guān)鍵的含義:QUIC 的隊(duì)頭阻塞移除只有在多個(gè)資源流同時(shí)活動(dòng)時(shí)才有效。這樣,如果其中一個(gè)流上有包丟失,其他流仍然可以繼續(xù)。這就是我們?cè)谏厦鎴D9的例子中看到的。然而,如果在某一時(shí)刻只有一個(gè)流在活動(dòng),任何丟包都會(huì)影響到這條孤獨(dú)的流,我們?nèi)匀粫?huì)被阻塞,即使在 QUIC。所以,真正的問(wèn)題是:我們會(huì)經(jīng)常有多個(gè)并發(fā)流(simultaneous streams)嗎?

          正如對(duì) HTTP/2 所解釋的,這是可以通過(guò)使用適當(dāng)?shù)馁Y源調(diào)度器/多路復(fù)用方法來(lái)配置的。流1和流2可以被發(fā)送 1122、2121、1221 等,并且瀏覽器可以使用優(yōu)先級(jí)系統(tǒng)指定它希望服務(wù)器遵循的方案(對(duì)于 HTTP/3 仍然如此)。所以瀏覽器可以說(shuō):嘿!我注意到這個(gè)連接有嚴(yán)重的數(shù)據(jù)包丟失。我將讓服務(wù)器以 121212 模式而不是 111222 向我發(fā)送資源。這樣,如果1的一個(gè)數(shù)據(jù)包丟失,2仍然可以繼續(xù)工作。然而,這種模式的問(wèn)題是,121212 模式(或者類似的)對(duì)資源加載性能通常不是最優(yōu)的。

          這是另一個(gè)復(fù)雜的話題,我現(xiàn)在不想太深入(我在 YouTube 上有一個(gè)關(guān)于這個(gè)的討論,供感興趣的人了解)。但是,通過(guò)我們的 JS 和 CSS 文件的簡(jiǎn)單示例,基本概念很容易理解。正如您可能知道的那樣,瀏覽器需要接收整個(gè) JS 或 CSS 文件,然后才能實(shí)際執(zhí)行/應(yīng)用它(雖然有些瀏覽器已經(jīng)可以開始編譯/解析部分下載的文件,但它們?nèi)匀恍枰却鼈兺暾蟛拍軐?shí)際使用它們)。但是,大量多路復(fù)用這些文件的資源塊最終會(huì)延遲它們:

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

          現(xiàn)在,這個(gè)話題有很多細(xì)微差別,當(dāng)然也存在多路復(fù)用方法更快的情況(例如,如果其中一個(gè)文件比另一個(gè)文件小得多,正如本文前面討論的那樣)。然而,一般來(lái)說(shuō),對(duì)于大多數(shù)頁(yè)面和大多數(shù)資源類型,我們可以說(shuō)順序方法最有效(再次,請(qǐng)參閱上面的YouTube鏈接以獲取更多信息)。

          現(xiàn)在,這是什么意思?對(duì)于最佳性能,我們有兩個(gè)相互沖突的性能優(yōu)化建議:

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

          那么,哪一個(gè)是正確的?或者至少:哪一個(gè)應(yīng)該優(yōu)先于另一個(gè)?可悲的是,目前我還不能給你一個(gè)明確的答案,因?yàn)檫@是我正在研究的一個(gè)主題。這之所以困難,主要是因?yàn)?/span>數(shù)據(jù)包丟失模式很難預(yù)測(cè)

          正如我們?cè)谏厦嬗懻撨^(guò)的,包丟失通常是突發(fā)性的和分組的。這意味著我們上面 12121212 的例子已經(jīng)過(guò)于簡(jiǎn)化了。圖10給出了一個(gè)更真實(shí)的概述。在這里,我們假設(shè)在下載2個(gè)流(綠色和紫色)時(shí),我們有一個(gè)8個(gè)丟失包的突發(fā)事件:

          圖10

          在圖10的頂部第一行中,我們可以看到(通常)對(duì)資源加載性能更好的順序情況。在這里,我們看到 QUIC 對(duì)消除隊(duì)頭阻塞確實(shí)沒(méi)有那么大的幫助:在丟包之后收到的綠包不能被瀏覽器處理,因?yàn)樗鼈儗儆诮?jīng)歷丟包的同一個(gè)流。第二個(gè)(紫色)流的數(shù)據(jù)尚未收到,因此無(wú)法處理。

          這與中間一行不同,中間一行(偶然!)丟失的8個(gè)數(shù)據(jù)包都來(lái)自綠色流。這意味著瀏覽器可以處理最后收到的紫色數(shù)據(jù)包。然而,正如前面所討論的,如果瀏覽器是 JS 或 CSS 文件,如果有更多的紫色數(shù)據(jù)出現(xiàn),瀏覽器可能不會(huì)從中受益太多。因此,在這里,我們從 QUIC 的隊(duì)頭阻塞移除中獲得了一些好處(因?yàn)樽仙珱](méi)有被綠色阻止),但是可能會(huì)犧牲整體資源加載性能(因?yàn)槎嗦窂?fù)用會(huì)導(dǎo)致文件稍后完成)。

          最下面一行幾乎是最糟糕的情況。8個(gè)丟失的數(shù)據(jù)包分布在兩個(gè)流中。這意味著這兩個(gè)流現(xiàn)在都被隊(duì)頭阻塞了:不是因?yàn)樗鼈兿馮CP那樣在等待對(duì)方,而是因?yàn)槊總€(gè)流仍然需要自己排序。

          注意:這也是為什么大多數(shù) QUIC 實(shí)現(xiàn)很少同時(shí)創(chuàng)建包含來(lái)自多個(gè)流(streams)的數(shù)據(jù)包(packets)的原因。如果其中一個(gè)數(shù)據(jù)包丟失,則會(huì)立即導(dǎo)致單個(gè)數(shù)據(jù)包中所有流的隊(duì)頭阻塞!

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

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

          因此,有人可能會(huì)說(shuō),雖然 QUIC 和 HTTP/3 不再受到應(yīng)用層或傳輸層隊(duì)頭阻塞的影響,但這在現(xiàn)實(shí)中可能并不重要。我不能確定這一點(diǎn),因?yàn)槲覀冞€沒(méi)有完全完成 QUIC 和 HTTP/3 的實(shí)現(xiàn),所以我也沒(méi)有最后的度量(measurements)。然而,我個(gè)人的直覺(jué)(這是由我的幾個(gè)早期實(shí)驗(yàn)的結(jié)果支持的)說(shuō),QUIC 消除隊(duì)頭阻塞可能實(shí)際上對(duì) Web 性能沒(méi)有太大幫助,因?yàn)槔硐肭闆r下,您不希望為了資源加載性能而對(duì)許多流進(jìn)行多路復(fù)用。而且,如果你真的想讓它工作得很好,你就必須非常巧妙地調(diào)整你的多路復(fù)用方式來(lái)適應(yīng)連接類型,因?yàn)槟憬^對(duì)不想在包丟失非常低的快速網(wǎng)絡(luò)上進(jìn)行大量的多路復(fù)用(因?yàn)樗鼈儫o(wú)論如何都不會(huì)遭受太多的隊(duì)頭阻塞)。就我個(gè)人而言,我不認(rèn)為會(huì)發(fā)生這種事。

          注意:在這里,在結(jié)尾,你可能已經(jīng)注意到我的故事有點(diǎn)不一致。一開始,我說(shuō) HTTP/1.1 的問(wèn)題是它不允許多路復(fù)用。最后,我說(shuō)多路復(fù)用在現(xiàn)實(shí)中并不那么重要。為了幫助解決這個(gè)明顯的矛盾,我添加了另一個(gè)額外的彩蛋部分

          在這篇(很長(zhǎng),我知道)的文章中,我們一直在追蹤隊(duì)頭阻塞。我們首先討論了為什么 HTTP/1.1 會(huì)受到應(yīng)用層隊(duì)頭阻塞的影響。這主要是因?yàn)?HTTP/1.1 沒(méi)有識(shí)別單個(gè)資源塊的方法HTTP/2 使用幀來(lái)標(biāo)記這些塊并啟用多路復(fù)用。這解決了 HTTP/1.1 的問(wèn)題,但遺憾的是HTTP/2 仍然受到底層 TCP 的限制。由于 TCP 將 HTTP/2 數(shù)據(jù)抽象為一個(gè)單一的、有序的、但不透明的流,因此如果數(shù)據(jù)包在網(wǎng)絡(luò)上丟失或嚴(yán)重延遲,它將遭受隊(duì)頭阻塞。QUIC 通過(guò)將 HTTP/2 的一些概念引入傳輸層來(lái)解決這個(gè)問(wèn)題。這反過(guò)來(lái)會(huì)產(chǎn)生嚴(yán)重的影響,因?yàn)榭缌鞯臄?shù)據(jù)不再是完全有序的。這最終導(dǎo)致需要一個(gè)全新的版本 HTTP/3,它只運(yùn)行在 QUIC 之上(而 HTTP/2 只運(yùn)行在 TCP 之上,請(qǐng)參見(jiàn)圖6)。

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

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

          感謝您的閱讀!

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

          彩蛋:HTTP/1.1 管道(pipelining)

          HTTP/1.1 包含了一個(gè)名為“管道“(pipelining)的特性,在我看來(lái)這是經(jīng)常被誤解的。我看過(guò)很多文章,甚至?xí)卸加腥寺暦Q HTTP/1.1 管道解決了隊(duì)頭阻塞問(wèn)題。我甚至見(jiàn)過(guò)一些人說(shuō)管道和正確的多路復(fù)用是一樣的。兩種說(shuō)法都是錯(cuò)誤的。

          我發(fā)現(xiàn)用類似彩蛋圖1中的插圖來(lái)解釋 HTTP/1.1 管道是最簡(jiǎn)單的:

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

          有了管道(彩蛋圖1的中間部分),瀏覽器不必等待任何響應(yīng)數(shù)據(jù),現(xiàn)在可以背靠背地發(fā)送請(qǐng)求。這樣,我們?cè)谶B接過(guò)程中節(jié)省了一些 RTTs,使得加載過(guò)程更快。另請(qǐng)注意,請(qǐng)回顧圖2:您會(huì)看到實(shí)際上也在使用管道,因?yàn)榉?wù)器在 TCP 數(shù)據(jù)包2中打包 script.js 以及 style.css 的響應(yīng)數(shù)據(jù)。當(dāng)然,只有在服務(wù)器同時(shí)接收到這兩個(gè)請(qǐng)求時(shí),這才是可能的。

          然而,最重要的是,這種管道只適用于來(lái)自瀏覽器的請(qǐng)求。正如[HTTP/1.1 規(guī)范][h1spec]所說(shuō):

          服務(wù)器必須按照接收請(qǐng)求的順序發(fā)送對(duì)這些[管道化]請(qǐng)求的響應(yīng)。因此,我們看到,實(shí)際的響應(yīng)塊(response chunks)多路復(fù)用(如彩蛋圖1右側(cè)所示)在 HTTP/1.1 管道中仍然是不可能的。換一種說(shuō)法:管道解決了請(qǐng)求的隊(duì)頭阻塞,而不是響應(yīng)的隊(duì)頭阻塞。可悲的是,響應(yīng)隊(duì)頭阻塞是導(dǎo)致 Web 性能問(wèn)題最多的原因。

          更糟糕的是,大多數(shù)瀏覽器實(shí)際上并沒(méi)有在現(xiàn)實(shí)中使用 HTTP/1.1 管道,因?yàn)檫@會(huì)使隊(duì)頭阻塞在多個(gè)并行 TCP 連接的設(shè)置中變得更加不可預(yù)測(cè)。為了理解這一點(diǎn),讓我們?cè)O(shè)想一個(gè)設(shè)置,其中通過(guò)兩個(gè) TCP 連接從服務(wù)器請(qǐng)求三個(gè)文件 A(大)、B(小)和 C(小)。A 和 B 在不同的連接上被請(qǐng)求。現(xiàn)在,瀏覽器應(yīng)該在哪個(gè)連接上傳輸對(duì) C 的請(qǐng)求?正如我們之前所說(shuō),它不知道 A 還是 B 將成為最大/最慢的資源。

          如果它猜對(duì)了(B),它就可以在傳輸 A 所需的時(shí)間內(nèi)同時(shí)下載 B 和 C,從而獲得了很好的加速效果。但是,如果猜測(cè)是錯(cuò)誤的(A),B 的連接將長(zhǎng)時(shí)間處于空閑狀態(tài),而 C 則在 A 后面被阻塞。這是因?yàn)?HTTP/1.1 也沒(méi)有提供一種在請(qǐng)求被發(fā)送后“中止”的方法(HTTP/2 和 HTTP/3 允許這樣做)。因此,瀏覽器不能簡(jiǎn)單地通過(guò) B 的連接請(qǐng)求 C,因?yàn)樗罱K會(huì)請(qǐng)求兩次 C。

          為了解決所有這些問(wèn)題,現(xiàn)代瀏覽器不使用管道,甚至?xí)鲃?dòng)延遲對(duì)某些已發(fā)現(xiàn)資源(例如圖像)的請(qǐng)求一段時(shí)間,以查看是否找到更重要的文件(例如 JS 和 CSS),以確保高優(yōu)先級(jí)資源不會(huì)被阻塞。

          很明顯,HTTP/1.1 管道的失敗是 HTTP/2 使用截然不同方法的另一個(gè)動(dòng)機(jī)。然而,由于 HTTP/2 的優(yōu)先級(jí)系統(tǒng)指導(dǎo)多路復(fù)用在現(xiàn)實(shí)中常常無(wú)法執(zhí)行,一些瀏覽器甚至采取了延遲 HTTP/2 資源請(qǐng)求的方式來(lái)獲得最佳性能。

          彩蛋:TLS 隊(duì)頭阻塞

          如上所述,TLS 協(xié)議為應(yīng)用層協(xié)議(如 HTTP)提供加密(和其他功能)。它通過(guò)將從 HTTP 獲取的數(shù)據(jù)包裝到 TLS 記錄中,TLS 記錄在概念上類似于 HTTP/2 幀(frames)或 TCP 數(shù)據(jù)包(packets)。例如,它們?cè)陂_始時(shí)包含一些元數(shù)據(jù),以標(biāo)識(shí)記錄的長(zhǎng)度。然后,對(duì)該記錄及其 HTTP 內(nèi)容進(jìn)行加密并傳遞給 TCP 進(jìn)行傳輸。

          就 CPU 使用率而言,加密可能是一項(xiàng)昂貴的操作,因此一次加密一個(gè)好數(shù)據(jù)塊通常是一個(gè)好主意,因?yàn)檫@通常更有效。實(shí)際上,TLS 可以以高達(dá) 16KB 的塊加密資源,這足以填充大約 11 個(gè)典型的 TCP 包(give 或 take)。

          然而,至關(guān)重要的是,TLS 只能對(duì)整個(gè)記錄進(jìn)行解密,這就是為什么會(huì)出現(xiàn) TLS 隊(duì)頭阻塞的情況。假設(shè) TLS 記錄分散在 11 個(gè) TCP 包上,最后一個(gè) TCP 包丟失。由于 TLS 記錄是不完整的,它不能被解密,因此被卡在等待最后一個(gè) TCP 包的重傳。注意,在這個(gè)特定的情況下,沒(méi)有 TCP 隊(duì)頭阻塞:在編號(hào)11之后沒(méi)有數(shù)據(jù)包被卡住等待重新傳輸。換言之,如果我們?cè)诒纠惺褂眉?HTTP 而不是 HTTPS,那么前10個(gè)包中的 HTTP 數(shù)據(jù)可能已經(jīng)被移動(dòng)到瀏覽器中進(jìn)行處理。然而,因?yàn)槲覀冃枰麄€(gè)11包的 TLS 記錄才能解密它,所以我們有了一種新形式的隊(duì)頭阻塞。

          雖然這是一個(gè)非常具體的情況,在現(xiàn)實(shí)中可能不會(huì)經(jīng)常發(fā)生,但在設(shè)計(jì) QUIC 協(xié)議時(shí),它仍然被考慮在內(nèi)。因?yàn)槟抢锏哪繕?biāo)是徹底消除所有形式的隊(duì)頭阻塞(或至少盡可能多地消除),甚至這種邊緣情況也必須被移除。這就是為什么 QUIC 集成了 TLS,它總是以每個(gè)包為基礎(chǔ)加密數(shù)據(jù),并且不直接使用 TLS 記錄。正如我們所看到的,與使用更大的塊相比,這效率更低,需要更多的 CPU,這也是為什么 QUIC 在當(dāng)前實(shí)現(xiàn)中仍然比 TCP 慢的主要原因之一。

          彩蛋:傳輸擁塞控制

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

          注:以上描述只是擁塞控制的一種方法。目前,其他方法也越來(lái)越流行,其中主要是 BBR 算法。BBR 并沒(méi)有直接觀察數(shù)據(jù)包丟失,而是大量考慮 RTT 波動(dòng)來(lái)確定網(wǎng)絡(luò)是否過(guò)載,這意味著它通常通過(guò)探測(cè)帶寬來(lái)減少數(shù)據(jù)包丟失。

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

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

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

          QUIC 和 HTTP/3 將面臨類似的挑戰(zhàn),就像 HTTP/2 一樣,HTTP/3 將使用單一的底層 QUIC 連接。然后,您可能會(huì)說(shuō) QUIC 連接在概念上有點(diǎn)像多個(gè) TCP 連接,因?yàn)槊總€(gè) QUIC 流都可以看作一個(gè)TCP連接,因?yàn)閬G失檢測(cè)是在每個(gè)流的基礎(chǔ)上完成的。然而,關(guān)鍵的是,QUIC 的擁塞控制仍然是在連接級(jí)別完成的,而不是針對(duì)每個(gè)流。這意味著,即使流在概念上是獨(dú)立的,它們?nèi)匀粫?huì)影響 QUIC 的單連接擁塞控制器,如果流中有任何一個(gè)丟失,就會(huì)導(dǎo)致速度減慢。換一種說(shuō)法:?jiǎn)蝹€(gè) HTTP/3 + QUIC 連接的增長(zhǎng)速度仍不及6個(gè) HTTP/1.1 連接,這與 HTTP/2 + TCP 在一個(gè)連接上的速度不一樣。

          彩蛋:多路復(fù)用是否重要?

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

          如果是這樣的話,我們可能會(huì)想為什么我們需要多路復(fù)用?通過(guò)擴(kuò)展:HTTP/2 甚至 HTTP/3,因?yàn)槎嗦窂?fù)用是 HTTP/1.1 沒(méi)有的主要特性之一。首先,一些可以增量處理/呈現(xiàn)的文件確實(shí)從多路復(fù)用中獲益。例如,漸進(jìn)式圖像就是這樣。第二,如上所述,如果其中一個(gè)文件比其他文件小得多,那么它可能會(huì)很有用,因?yàn)樗鼘⒏绲叵螺d,而不會(huì)對(duì)其他文件造成太多的延遲。第三,多路復(fù)用允許改變響應(yīng)的順序,并為高優(yōu)先級(jí)的響應(yīng)中斷低優(yōu)先級(jí)的響應(yīng)

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

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

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


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

          如有涉及侵權(quán),請(qǐng)聯(lián)系刪除!


          主站蜘蛛池模板: 尤物精品视频一区二区三区| 久久久久人妻精品一区三寸| 国产精品免费视频一区| 日韩精品一区二区午夜成人版 | 鲁大师成人一区二区三区| 无码少妇精品一区二区免费动态| 国产亚洲福利精品一区二区| 国产精品高清一区二区三区不卡| 在线精品国产一区二区三区| 国产一区高清视频| 日韩在线一区视频| 亚洲一区二区免费视频| 久久人妻av一区二区软件| 国产A∨国片精品一区二区| 国产一区二区三区不卡在线看 | 一区二区三区在线播放视频| 黑巨人与欧美精品一区| 波多野结衣一区二区| 亚洲AV成人精品日韩一区| 亚洲视频一区网站| 亚洲日韩AV无码一区二区三区人| 亚洲香蕉久久一区二区三区四区 | 一区二区三区高清视频在线观看| 另类ts人妖一区二区三区| 日韩在线视频不卡一区二区三区| 成人区人妻精品一区二区三区| 国产伦精品一区二区三区不卡| 久久久国产精品无码一区二区三区| 亚洲乱码av中文一区二区| 欧美激情国产精品视频一区二区| 精品无码国产一区二区三区51安| 精品国产一区在线观看 | 无码一区二区三区在线| 国产成人免费一区二区三区| 麻豆视传媒一区二区三区| 国产一区二区三区乱码| 少妇激情一区二区三区视频| 影音先锋中文无码一区| 动漫精品专区一区二区三区不卡 | 中文字幕精品一区二区| 精品国产区一区二区三区在线观看 |