久久国产乱子伦精品免费M,亚洲一区二区三区91,欧美国产在线视频,国产精品视频久久

大話 HTTP 協議前世今生

HTTP 全稱 Hypertext Transfer Protocol,中文是超文本傳輸協議。網上講 HTTP 協議的資料可以說是五花八門,但大多數都在羅列 HTTP 協議具體的規定,很少有講 HTTP 協議這樣設計的原因。今天我就嘗試從解決問題的角度分析 HTTP 協議主要特性,希望能幫助大家快速理解 HTTP 協議。

HTTP 是一種通過網絡傳輸數據的協議。我們不希望數據在傳輸的過程中出現丟失或者損壞的問題。所以 HTTP 選用 TCP 作為底層網絡協議,因為 TCP 是一種可靠的傳輸層協議。

通信雙方就建立 TCP 連接后立馬發現一個新問題:服務端要給客戶端發送什么數據呢?所以客戶端必需在連接建立后將自己想要的內容發送給服務端,這就是所謂的「請求」,也就 HTTP Request。由此就確立了 HTTP 協議最根本的設計,即由客戶端主導的請求應答式協議。

客戶端上來就給服務端發了一個「請求」。但服務端有可能收到的內容跟客戶端并不完全一樣。等等,TCP不是可靠傳輸協議嗎?接收到的數據怎么會不一樣?這就涉及到數據分段的問題。比如客戶端發送”abcdef”,底層 TCP 協議可能分兩次傳輸 “abc” 和 “def”,也可能分好多次傳輸。不論分幾次,它們的順序是固定的,跟客戶端發送的順序完全一致。服務端可能會收到多段數據,所以服務端需要把收到的數據「攢」起來,等到客戶端的數據全部收到之后才能看到客戶端「請求」的全貌。

那到什么時候算全部收到呢?這是 TCP 通信的一個基本問題。解決這個問題有兩個流派:長度流和分隔符流。

所謂長度流就是在實際發送數據之前,先發送數據的長度。服務端先讀取長度信息,然后再根據長度來「攢」后面的數據。那服務端在讀取長度的時候不會碰到分段問題嗎?其實不會,因為 TCP 只會對比較長的數據做分段。前面說的”abcdef”分兩段只是一種極端的例子,實際上很難發生。所以,只要先發送的長度數據不要太長,服務端就能一次性收到。退一步,即便是真的會分段,這類長度流協議都會規定長度數據自身的長度。比如用兩個字節表示長度,那范圍就是數據長度的范圍就是0-65535。服務端可以先收兩個字節,然后再根據數據長度來接收后面的內容。

長度流最大的優點就是實現簡單,內存效率高,服務端不用事先分配很多內存。但缺點也比較突出,長度的范圍不夠靈活。如果我們規定長度字段為兩個字節,但就不能傳輸超過64k的數據。但如果規定長度字段為八個字節,那在傳輸比較短的數據時就造成浪費。如何設置最優長度字段,大家可以參考我的另一篇文章。

此外,長度流的擴展性也比較差。如果我們想在長度之外傳輸其他信息,比如數據類型、版本號之類,我們都需要提前規定好這些數據的長度。長度一旦定好,以后就很難擴展了。最典型的長度流協議就是 IP 報文。有興趣的朋友可以去看看 IP 協議是怎么規定數據長度的。

有鑒于長度流的不足,人們又搞出了分割符流。簡單來說就是用一個特殊的分割符表示數據的結尾。最經典的例子就是C語言的字符串,結尾用\0來表示。使用這個流派的服務端程序要不停地從客戶端接收數據,直到收到某一個分割符,就表明已經收到了完整的「請求」。

因為不需要事先指定數據的長度,所以分割符流派一下子就解決了長度流長度范圍不靈活的問題。分割符流派的協議可以接收任意長度的數據。但是,分割符流派為些也付出了代價。因為長度不固定,服務端必須分配比較大的內存或者多次動態分配內存,這會產生比較大的資源消耗。惡意用戶可能通過構造很長的數據來占滿服務器的內存。

但是 HTTP 協議還是加入了這個流派,它用的分割符是\r\n。這里的\r表示回車,就是讓打印機把打印頭回到最左邊的位置。\n表示換行,就是讓打印機把紙向上挪一行,準備打印新的實符。上古時代的電腦沒用現在的液晶屏,用電傳打印機來「顯示」內容,所以需要傳輸\r\n兩個字符。現在這些都淘汰了,理論上用\n也可以,像 Nginx 就支持只用\n。

所以,一個最簡單的 HTTP 請求長這個樣子:

GET?/mypage.html\r\n

這里的GET是一種擬人的說法,從服務拿什么東西。這也是 HTTP 語義化設計的開端(所謂語義化就是普通人能看懂)。后面跟一個空格,再后面是文件的路徑。最后是分割符\r\n。因為最后是\r\n,所以上面的數據也叫請求行(request line)。

客戶端跟服務器建立連接后就立即發送上面的數據。服務端等收到\r\n后開始解析,也就是把/mypage.html提取出來,然后找到對應的文件,把文件內容發送給客戶端。

到這里,客戶端就收到了服務端發送的文件內容,也叫「響應」。但是,客戶端馬上面臨服務端同樣的問題:如何確定已經收到了 mypage.html 的完整的內容呢?服務端要不要在最后發送分割符\r\n呢?不能!因為 mypage.html 的內容里本身就可能包含\r\n。如果客戶端還是以\r\n當作結束標記,那可能會丟失數據。

為此 Tim Berners-Lee (HTTP 協議之父) 采用了更簡單的辦法——關閉連接。也就是說,服務器在傳輸完成之后要主動關閉 TCP 連接,這樣客戶端就明確知道所有的內容已經傳輸完成了。

以上就是最原始的 HTTP 協議,大約在1990發布?,F在稱這個時代的 HTTP 協議為 HTTP/0.9,主要是跟后面標準化之后的 1.x 進行區分。就這樣,萬維網的時代開啟了。

HTTP/0.9 發布后得到了廣泛的應用。但它的功能太簡單了,所以很多瀏覽器都在它的基礎上做了擴展。最主要的擴展功能有如下幾個:

  • 添加版本信息
  • 添加擴展頭信息
  • 添加返回狀態信息

添加版本信息是為了方便客戶端和服務端相互識別,這樣才能開啟擴展功能。添加之后的請求行如下:

GET?/mypage.html?HTTP/1.0\r\n

添加擴展頭信息是為了傳遞更多的擴展信息。比如,這時候不同的瀏覽器會在請求中標記自己的身份。為方便后續添加各種不同的擴展信息,HTTP協議繼續使用「行」和分割符的概念。

首先,跟請求行保持一致,每一條擴展信息占一行,以冒號分割,以\r\n結尾,比如:

User-Agent:?NCSA_Mosaic/2.0?(Windows?3.1)\r\n

其次,這種信息可以有多行。那服務端怎么確定到底有幾行呢,這還得用到分割符\r\n。HTTP 協議用一個空行表示后面擴展信息都結束了。所以完整的請求是:

GET?/mypage.html?HTTP/1.0\r\n
Host:?taoshu.in\r\n
User-Agent:?NCSA_Mosaic/2.0?(Windows?3.1)\r\n
\r\n

服務端先接收一行,提取文件路程,然后再根據\r\n逐行提取擴展信息。如果收到一個空行,則說明擴展信息接收完成。這些擴展信息也叫頭信息(header),后續 HTTP 協議的各種特性都是基于它來實現。

HTTP/0.9 收到請求后直接傳輸文件內容。但用些場景需要返回其他信息,比如文件不存在之類的,所以人們給它添加了返回狀態信息。此外,擴展后的 HTTP 協議也支持服務端在發送數據前返回多個頭信息。一個典型的擴展響應為:

200?OK\r\n
Date:?Tue,?15?Nov?1994?08:12:32?GMT\r\n
Server:?CERN/3.0?libwww/2.17\r\n
Content-Type:?image/gif\r\n
\r\n
(image?content)

服務器首先會發一行數據200 OK\r\n。這里的200是狀態碼,表示成功。后面的OK是給人看的語義部分。這一行也叫 status code line。緊接著就是擴展信息,形式跟請求里的一模一樣,每行一條,以空行表示結束。最后才是文件內容。

因為有了頭信息,HTTP協議的擴展性直接起飛。人們不斷給 HTTP 協議添加各種種樣的特性。

HTTP/0.9 只能傳輸純文本文件。因為有了 Header,我們可以傳輸更多的描述信息,比如文件在的類型、長度、更新時間等等。這些傳輸數據的描述信息也被稱為 Entity Header,數據本身稱為 Entiy。

常見的 Entiy Header 有:

  • Content-Type 內容類型
  • Content-Length 內容長度
  • Content-Encoding 數據編碼

Content-Type 表示數據類型,比如 gif 的類型是image/gif。類型的取值最終被標準化為 Multipurpose Internet Mail Extensions(MIME)。

Content-Length 表示數據長度。但我們前面說過,HTTP/0.9 的服務器不需要返回文件長度,等傳輸完畢后關閉 TCP 連接就好了。為什么又要定義長度信息呢?

這里有兩個問題。第一個是在請求里支持上傳內容,第二個是連接優化問題。

HTTP/0.9 只有一種 GET 請求。顯然光下載是不夠的。人們陸續引入了 HEAD 和 POST 等請求,用來給服務器提交數據。一但要提交數據,光用分割符就不夠了。因為提交的數據本身就可能包含分割符。所以需要事先指定數據的長度。這個長度用的就是 Content-Length 頭來指定。

另外一個是連接優化問題。其實 HTTP 協議的發展史很大程度上就是傳輸性能的優化史。

HTTP/0.9每次請求都會創建一個 TCP 連接,讀取結束后連接就會被關閉。如果一次只下載一個文件也沒什么問題。但后來 HTML 頁面支持嵌入圖片等內容,一個頁面可能有多個圖片。這樣瀏覽器打開一個 HTML 頁面的時候就需要發起多次 HTTP 請求,每次請求都要反復建立和關閉 TCP 連接。不但浪費服務器資源,還會拖慢頁面的加載速度。

所以,大家就想辦法復用底層的 TCP 連接。簡單來說就是服務器在內容發送完成后不主動關閉連接。但不關閉就會出現前面說的問題,客戶端不知道響應內容什么時候傳輸完畢。所以需要事先指定數據的長度。因為 HTTP 協議已經有了 header 機制,所以添加 Content-Length 就是最自然的辦法。

這里還有一個兼容性問題。如果客戶端不支持復用 TCP 連接,那服務端不關閉連接的話客戶端就會一直在等待。所以復用 TCP 連接這個功能不能默認開啟,而是應該由客戶端決定要不要使用。這就引出了Connection:Keep-Alive這個頭信息。如果客戶在請求中指定 Keep-Alive,服務端才不會主動關閉 TCP 連接。

除了復用 TCP 連接之外,HTTP/0.9 另一個值得優化的地方就是數據壓縮。那個時代網速很慢,如果能把數據壓縮之后再傳輸可以顯著降低傳輸耗時。服務端不能隨意壓縮,因為有的客戶端可能不支持。所以就先引入了Accept-Encoding這個頭,可能的取值如compress或者gzip。服務端收到這個請求之后才對內容做壓縮。因為瀏覽器可能支持多種壓縮算法,瀏覽器需要選擇一種自己也支持的來壓縮數據,所以就需要在返回內容的時候指定自己用了哪種算法。這就是Content-Encoding頭的用途。

不論是前面的 Connection 還是后面的 Accept-Encoding,為了盡可能地兼容不同客戶端,HTTP 協議會通過添加新的 header 來協商是否使用擴展特性。這種協商由客戶端來主導,服務器需要根據客戶端的請求來配合完成。

還是因為網絡比較慢而且成本很高,HTTP協議需要進一步優化數據傳輸效率。一個典型的場景是客戶端已經下載過某文件內容。當客戶端再次請求的時候,服務端還要不要返回。如果不返回,則客戶端拿不到最新的內容;如果返回,當服務端的文件沒有變化的時候,客戶端會花很長時間加載一個已經下載過的文件。怎么優化這個問題呢?

人們引入了如下 Entity Header:

  • Last-Modified 最近修改時間
  • Expires 過期時間

如果文件不經常改動,服務器可以對過 Last-Modified 把最近修改時間發送給瀏覽器。瀏覽器如果支持,可以在下次請求該資源的時候帶上這個時間,也就是在請求里添加下面的頭:

If-Modified-Since:?Sat,?29?Oct?1994?19:43:31?GMT\r\n

服務器收到后會跟文件的當前修改時間做對比,如果沒有修改則直接返回304:

304?Not?Modified\r\n

這種叫作條件請求,可以顯著減少不必要的網絡傳輸。

即使如此,客戶端還是發起一次 HTTP 請求才能拿到 304 響應,也會產生網絡傳輸和服務端開銷。為了進一步優化,HTTP又引入了 Expires 頭,它的含義是一個未來的過期時間。在這個時間之前瀏覽器可以安全使用本地緩存的副本,不需要從服務器下載。這樣連條件請求都不需要發起了。

不過 Expires 特性有一個副作用,文件一旦下發,在過期之前根本無法修改。

大約是在1991-1995這個時間,各瀏覽器廠商陸續實現了上述功能。但不同瀏覽器和服務端軟件支持的功能不同,帶來各種兼容問題。于是到 1996 年,IETF 發布 RFC1945。RFC1945 只能說是當前最佳實踐的總結,并不是推薦標準。但人們還是稱它為 HTTP/1.0。

沒過一年,也就是1997年,IETF就發布了RFC2068,也就是大名鼎鼎的 HTTP/1.1 協議規范。

HTTP/1.1 是對 HTTP/1.0 的梳理和擴展。核心的改動有:

  • 默認開啟 TCP 連接復用,客戶端不需要再發送 Connection:Keep-Alive
  • 添加了所謂 pipeline 特性,進一步優化傳輸效率
  • 支持 chunked 傳輸編碼
  • 擴展緩存控制
  • 內容協商,包括語言、傳輸編碼、類型等
  • 在同一IP上建立多個 HTTP 網站

所謂的 pipeline 特性是對 HTTP 協議傳輸效率的進一步優化,但最終失敗了。

HTTP 協議是請求應答式協議。客戶端發一個請求,然后等待服務端返回內容。雖然在 HTTP/1.0 時代就有了 TCP 連接復用、內容壓縮和條件請求等優化機制,但客戶端發起新請求之前必須等待服務器返回內容。換言之就是客戶端無法在一個連接上并行發起多個請求。為此,HTTP/1.1 的 pipeline 就規定客戶端可以依次發起多個 HTTP 請求,然后等待服務器返回結果。服務器需要按照請求順序依次返回對應的響應內容。

??c????????s??????????????c????????s
??|??req1??|??????????????|??req1??|
??|------->|??????????????|------->|
??|??resp1?|??????????????|??req2??|
??|<-------|??????????????|------->|
??|??req2??|??????????????|??req3??|
??|------->|??????????????|------->|
??|??resp2?|??????????????|??resp1?|
??|<-------|??????????????|<-------|
??|??req3??|??????????????|??resp2?|
??|------->|??????????????|<-------|
??|??resp3?|??????????????|??resp3?|
??|<-------|??????????????|<-------|
??
without?pipeline?????????with?pipeline

雖然服務器收到多個請求的時候可以并發處理,這種并發帶來的優化有限,而且 pipeline 特性并沒有減少實際的網絡傳輸。幾乎沒有軟件實現 pipeline 特性,所以這個優化設計以失敗告終。

chunked 編碼是一項非常成功的優化,主要解決服務端動態生成響應內容的情況。

HTTP/1.0 只能使用 Content-Length 指定內容長度,而且是先發送 header 再發送 body。這就要求必須在傳輸內容之前確定內容的長度。對于靜態文件,這當然不是問題。但如果要加載一個由 PHP 動態渲染的 HTML 就有問題了。因為 HTML 是程序動態生成的,沒法事先確定內容長度。如果還用原來的辦法,只能先把內容生成好保存到一個臨時文件,再發送給客戶端。顯然這種性能太差。

為了解決這個問題,HTTP/1.1 引入 chunked 編碼。簡單來說就是回到之前的長度流,將數據逐段發送給客戶端,每一段前面加上長度信息:

HTTP/1.1?200?OK\r\n
Content-Type:?text/plain\r\n
Transfer-Encoding:?chunked\r\n

7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n

Transfer-Encoding 指定為 chunked。接下來的數據也是分行傳輸。一行長度,一行數據。結束的時候長度指定為零,然后再加一個空行。這樣服務端就不需要事先確定響應內容的長度,PHP 就可以有邊渲染一邊發送。這個特性還是 WebSocket 沒有普及的年代被用于實現消息推送。大家可以搜索 Comet 或者 HTTP 長輪詢了解更多信息。

HTTP/1.1 對緩存做了更粗細化的定義,引入了 Cache-Control 擴展信息。這一部分內容比較復雜,除了會影響瀏覽器的緩存行為之外,還會影響 CDN 節點的行為。部分 CDN 廠商還會擴展 標準緩存指令的語義。限于篇幅,在此就不展開了。

但 HTTP/1.1 對條件請求做了擴展,可以說一下。

操作系統會自動記錄文件的修改時間,讀取該時間也非常方便,但 Last-Modified 不能覆蓋所有情況。有時候我們需要用程序定時生成某些文件,它的修改時間會周期性變化,但內容不一定有改變。所以光用 Last-Modified 還是可能產生不必要的網絡傳輸。于是 HTTP 協議引入了一個新的頭信息 Etag。

Etag 的語義是根據文件內容計算一個值,只有在修改內容的時候才會產生新的 Etag??蛻舳嗣看握埱蟮臅r候把上一次的 Etag 帶回來,也就是添加下面的頭:

If-None-Match:?"c3piozzzz"\r\n

服務端收到后會對比 Etag,只有發生變化的時候才會返回新的文件內容。

那個時候的網絡很不穩定,斷網是家常便飯。想想一個文件下載到99%然后斷網了是一種怎樣的體驗。為了減少不必要的數據傳輸,人們很快就給 HTTP 協議添加了「斷點續傳」功能。其實斷點續傳是從客戶端視角來看的。從協議角度來看,需要添加的功能是根據指定范圍傳輸數據。也就是說原來的文件是100字節,客戶端可以指定只下載最后的10字節:

Content-Range: bytes 91-100/100\r\n 這里的91-100表示要下載的范圍,后面的100表示整個文件的長度。如果服務器支持,則會返回:

HTTP/1.1?206?Partial?content\r\n
Date:?Wed,?15?Nov?1995?06:25:24?GMT\r\n
Last-modified:?Wed,?15?Nov?1995?04:58:08?GMT\r\n
Content-Range:?bytes?91-100/100\r\n
Content-Length:?10\r\n
Content-Type:?image/gif\r\n
\r\n
(image?data)

該功能除了用于斷點續傳外,還可以實現并行下載加速??蛻舳丝梢云鸲鄠€線程,建立多條 TCP 連接,每個線程下載一部分,最后把有的內容連到一直。就這么簡單。

另外,HTTP/1.1 還要求客戶端在請求的時候必須發送 Host 頭信息。這里面保存著當前請求對應的網站域名。服務器收到請求后會根據 Host 里的域名和請求行里的路徑來確定需要返回的內容。這樣就能實現在同一個 IP 上搭建不同域名的網站,也就是所謂的虛擬主機。這大大降低了網站的建設成本,對 Web 生態的發展起到了至關重要的作用。

除了擴展 HTTP/1.0 原來的功能外,HTTP/1.1 還引入了連接升級功能。其實這個功能后面用的不多,但有一個重量級的協議 WebSocket 在用,所以不得不說。

所以連接升級就是把當前用于 HTTP 會話的 TCP 連接切換到其他協議。以 WebSocket 為例:

GET?/chat?HTTP/1.1
Host:?taoshu.in
Upgrade:?websocket
Connection:?Upgrade

這里把 Connection 設成了 Upgrade,表示希望切換協議。而 Upgrade:websocket 表示要切換到 websocket 協議。在切換之前,這還是一個普通的 HTTP 請求。服務器可以對該請求做各種鑒權等 HTTP 動作。服務器如果接受用戶的請求,則會返回:

HTTP/1.1?101?Switching?Protocols
Upgrade:?websocket
Connection:?Upgrade

從這一該起,雙方就不能在該 TCP 連接上發送 HTTP 協議數據了。因為協議已經切換到 WebSocket。

從 1999 年開始,到 2015 年 HTTP/2 發布,HTTP 協議有15年的時候沒有大的變化。與此同時,互聯網蓬勃發展,從 Web 1.0 過渡到 Web 2.0,從 PC 互聯網發展到移動互聯網,從明文 HTTP 也切換到加密 HTTPS。整個過程 HTTP 協議都發揮了核心作用。這從側面也說明 HTTP 協議是一種擴展性非常好的協議。

但 HTTP/1.1 畢竟是九十年代設計的協議。2010年之后,移動互聯網興起,業界希望對 HTTP 的問題做夠進一步優化。那還有哪些問題可以優化呢?主要有幾個方面:

  1. 協議使用文本格式,傳輸和解析效率都比較低
  2. Header 部分信息無法壓縮,但現實情況是 Header 體積也不小(比如 cookie)
  3. 無法在單一 TCP 連接上并發請求資源(pipeline 失敗了)
  4. 服務端無法主動給客戶發送內容

文本格式其實是 HTTP 的一大特色。我們在調試的時候可以直接使用 telnet 連接服務器,然后用肉眼看服務器的返回結果。但對人類友好的設計對機器一定不友好。HTTP協議使用\r\n作為分割符,雙不限制頭信息的數量,這必然導致解析的時候需要動態分配內存。而且還要把數字、日期等信息轉換成對應的二進制格式,這都需要額外的解析成本。

HTTP/1.x 支持壓縮數據內容,而且使用頭信息保存壓縮算法。所以就不能用相同的算法壓縮頭信息了。只能另辟蹊徑。

HTTP/1.1 的 pipeline 已然失敗,無法充分復用 TCP 連接。HTTP 從一開始就是請求應答式的設計,服務器沒辦法主動推送內容到客戶端。

為了解決這幾個問題,Google 挾 YouTube 和 Chrome 兩大殺器,推出了 SPDY 協議。該協議有兩個特點:

  1. 兼容 HTTP 語義
  2. 使用二進行格式傳輸數據 SPDY 引入了幀做為最小的傳輸單位:
+-----------------------------------------------+
|?????????????????Length?(24)???????????????????|
+---------------+---------------+---------------+
|???Type?(8)????|???Flags?(8)???|
+-+-------------+---------------+-------------------------------+
|R|?????????????????Stream?Identifier?(31)??????????????????????|
+=+=============================================================+
|???????????????????Frame?Payload?(0...)??????????????????????...
+---------------------------------------------------------------+

??????????????????????Figure?1:?Frame?Layout

每一幀前三個字節表示數據長度,然后用一個字節表示類型,再用一個字節保存一些擴展標記。然后就是四個字節的 stream ID,最后是真正的數據。這其實就表明 HTTP 協議從分割符流轉向了長度流。

在同一個 TCP 連接上,數據幀可以交替發送,不再受請求應答模式制約。也就是說服務端也可以主動給客戶端發消息了。同一個請求的 header 和數據部分也可以分開發送,不再要求先發 header 再發 body。也正是因為數據幀交錯傳輸,同一個會話下的數據需要能關聯起來,所以 SPDY 給每一幀添加了 stram ID。換句話說 SPDY 在一個 TCP 連接上虛擬出了多個 stream,每一個 stream 從效果看都是一個 TCP 連接。不同的 HTTP 請求和響應數據可以使用自己的 stream 并發傳輸,互不影響。這樣一下子就解決了上面的一、三和四這三個問題。

第二個問題比較麻煩。但解決思路也很簡單。HTTP/1.x 的頭信息都是 K-V 型的,而且都是字符串。這里的 K-V 都很少變化。比如只要是訪問我的博客,不論有多少請求,都得發送 Host: taoshu.in。對于這種不變的,我們完全可以在兩端各保存一張映射表,給每個 Key 和 Value 都指定一個編號。這樣后續的請求只要傳 Key 和 Value 的編號就行了,從而實現壓縮的效果。單看 Host 可能不覺得有多少進步。但大家想想自己的 cookie,里面有登錄會話信息,每次都重復發送浪費相當驚人。所以壓縮頭信息帶來的優化還是驚人的。

因為谷歌一邊控制著市場份額最大的 Chrome 瀏覽器,另一邊又控制像 Google/YouTube 這樣的內容服務,所以開發下一代 HTTP 協議便一件非常容易的事情。SPDY 于 2012 年發布,最終在 IETF 完成標準化,并于 2015 年發布,也就是RFC7540。

隨著社會的發展,隱私保護成了人們關注的重要課題。為了保護用戶信息,業界一真在推動 HTTP + TLS 也就是 HTTPS 的普及。HTTPS 服務使用 443 端口。我們前面講過,HTTP/2 使用二進制編碼,跟 HTTP/1.x 并不兼容。但客戶端又不會一夜之間都升級的 HTTP/2。那怎么才能在一個端口上同時支持兩種 HTTP 協議呢?這就用到了 TLS 協議的 ALPN 擴展。簡單來說就是客戶端在發起 TLS 會話的時候會通過 ALPN 擴展附帶自己支持的應用層協議,比如 http/1.1 和 h2。服務端收到后會把自己支持的應用層協議返回給客戶端。這樣雙方就能確定接下來在 TLS 會話是使用什么協議。

理論上 HTTP/2 可以通過 HTTP/1.1 的升級機制來協商,這樣也能解決兩個版本共用 TLS 會話的問題。但這種升級會再來額外的延遲,所以主流的瀏覽器都不支持。

HTTP/2 發布之后,整個業界都在積極遷移到新的協議。但實踐證明,HTTP/2并沒有想象中的那么好。為什么呢?因為對于同一個域名,瀏覽器默認只會開一個連接,所有請求都使用一個TCP連接收發。雖然不同的請求使用不同的 stream,但底層的連接只有一個。如果網絡出現抖動,不論是哪一個請求的數據需要重傳,其他請求的數據都必須等待。這就是所謂的 Head of Line blocking 問題。HTTP/2 非但沒有優化,甚至還比 HTTP/1.x 還要差。因為在 HTTP/1.x 時代,瀏覽器自知 HTTP 無法復用連接,所以會為同一個域名創建多個 TCP 連接。不同的請求可能會分布到不同的連接上,出現網絡抖動的影響比只用一個連接要好一點。

HTTP/2 的另一個問題就是功能太復雜。比如它支持在服務器主動推送資源(比如 CSS 文件)到瀏覽器,這樣客戶端在加載的時候就需要等待網絡傳輸。但該功能非常復雜,而且效果有限,最終連 Chrome 自己都放棄支持該功能了。這部分功能被 HTTP 103 Early Hints 狀態碼代替,具體可以參考RFC8297。

一計不成,再生一計。谷歌的工程師跟 Head of Line blocking 問題死磕。這次他們把矛頭指向了問題的根源 TCP 協議。因為 TCP 是可靠傳輸協議,數據必須按順序收發,而且要邊確認邊發送。如果底層用 TCP 連接,就不可能解決 Head of Line blocking 問題。為此,他們基于 UDP 協議設計了 QUIC 協議。

QUIC 協議簡單來說就是一種面向消息的傳輸協議(TCP 是面向數據流的傳輸協議)。QUIC 也有 stream 的概念,每個會話可以有多個流。不同的流的數據都使用 UDP 收發,互不干擾。跟 TCP 一樣,數據發出后也需要對方確認。然后再把 QUIC 跟 HTTP/2 的幀映射到一起,最終形成 HTTP/3 協議,也就是RFC9114。

那 QUIC 有沒有問題呢?也有,但基本都不是設計上的問題。

第一個問題就是運營商可能對 UDP 流量做限流,很多防火墻可能會阻止 QUIC 流量。這是之前 UDP 通信使用不廣泛導致的。隨著 HTTP/3 技術的普及,這些問題會逐漸改善。

第二個問題是 HTTP/3 啟動延遲的問題。HTTP/3 使用 UDP 通信,跟 HTTP/1.x 和 HTTP/2 不兼容,所以瀏覽器沒法判斷服務器是否支持 HTTP/3。

目前主流的做法是網站同時支持 HTTP/2 和 HTTP/3。瀏覽器先通過過 TCP 連接訪問服務器。服務器在第一個響應中返回一個特殊的 Header:

Alt-Svc:?h3=":4430";?ma=3600

這里的意思是在 UDP 的 4430 端口提供 HTTP/3 服務,該信息的有效時間為 3600 秒。后面瀏覽器就可以使用 QUIC 連接 4430 端口了。

明眼人一看就知道這里有問題,建立 HTTP/3 會話之前還得先用一下 HTTP/2 啟動有把。這不科學??而且這會帶來額外的耗時。為此,人們又開始想別的辦法,這就是 DNS SVCB/HTTPS 記錄。

DNS SVCB/HTTPS 簡單來說就是用一種特殊的 DNS 記錄把前面的 Alt-Svc 信息曝露出來。瀏覽器在訪問網站之前先通過 DNS 查詢是否支持 HTTP/3 以及對應的 UDP 端口,然后就直接發起 HTTP/3 會話就好。這樣就完全不依賴 TCP 連接了。關于 DNS SVCB/HTTPS 記錄的更多信息請看我的專門文章。

順便說一句,HTTP/3 默認可以工作在任意 UDP 端口,不像 HTTPS 那樣默認工作在 443 端口。如果運營商封掉 443 就沒法對外服務。等 HTTP/3 普及了,所有人都可以使用自家的寬帶搭建網站??具體做法可以參考我的這篇文章。

好了,到現在快肝了一萬字了。我認為基本講清楚了 HTTP 協議的發展脈絡?,F于篇幅,沒能詳細討論 HTTP/2 和 HTTP/3 的技術細節,不能說不是個遺憾。先開個坑,后面有時間再補上。希望本文能幫助你更好地理解 HTTP 協議。

鏈接:https://taoshu.in/net/http.html

(版權歸原作者所有,侵刪)

相關新聞

歷經多年發展,已成為國內好評如潮的Linux云計算運維、SRE、Devops、網絡安全、云原生、Go、Python開發專業人才培訓機構!

    1. 主站蜘蛛池模板: 宜丰县| 宿州市| 大丰市| 新巴尔虎左旗| 澄迈县| 札达县| 永胜县| 富川| 合山市| 惠水县| 东海县| 江华| 南汇区| 卓尼县| 阜宁县| 辽阳市| 延川县| 渑池县| 灌云县| 罗源县| 海淀区| 杂多县| 石台县| 潮安县| 大厂| 威信县| 阿拉善右旗| 滦南县| 桐梓县| 洛浦县| 沂南县| 左贡县| 连江县| 乡宁县| 海宁市| 潼关县| 高邑县| 郓城县| 宣武区| 临猗县| 江永县|