HTTP 如何傳輸大文件?


 

上面我們談到了 HTTP 報文里的 body,知道了 HTTP 可以傳輸很多種類的數據,不僅是文本,也能傳輸圖片、音頻和視頻。早期互聯網上傳輸的基本上都是只有幾 K 大小的文本和小圖片,現在的情況則大有不同。網頁里包含的信息實在是太多了,隨隨便便一個主頁 HTML 就有可能上百 K,高質量的圖片都以 M 論,更不要說那些電影、電視劇了,幾 G、幾十 G 都有可能。相比之下,100M 的光纖固網或者 4G 移動網絡在這些大文件的壓力下都變成了小水管,無論是上傳還是下載,都會把網絡傳輸鏈路擠的滿滿當當。所以如何在有限的帶寬下高效快捷地傳輸這些大文件就成了一個重要的課題,下面我們就一起看看 HTTP 協議里有哪些手段能解決這個問題。

數據壓縮

首先我們想到的就是數據壓縮,通常瀏覽器在發送請求時都會帶着 Accept-Encoding 頭字段,里面是瀏覽器支持的壓縮格式列表,例如 gzip、deflate、br 等,這樣服務器就可以從中選擇一種壓縮算法,放進 Content-Encoding 響應頭里,再把原數據壓縮后發給瀏覽器。如果壓縮率能有 50%,也就是說 100K 的數據能夠壓縮成 50K 的大小,那么就相當於在帶寬不變的情況下網速提升了一倍,加速的效果是非常明顯的。

不過這個解決方法也有個缺點,gzip 等壓縮算法通常只對文本文件有較好的壓縮率,而圖片、音頻視頻等多媒體數據本身就已經是高度壓縮的,再用 gzip 處理也不會變小(甚至還有可能會增大一點),所以它就失效了。不過數據壓縮在處理文本的時候效果還是很好的,所以各大網站的服務器都會使用這個手段作為保底。例如,在 Nginx 里就會使用 gzip on 指令,啟用對 text/html 的壓縮。

分塊傳輸

在數據壓縮之外,還能有什么辦法來解決大文件的問題呢?壓縮是把大文件整體變小,我們可以反過來思考,如果大文件整體不能變小,那就把它拆開,分解成多個小塊,把這些小塊分批發給瀏覽器,瀏覽器收到后再組裝復原。這樣瀏覽器和服務器都不用在內存里保存文件的全部,每次只收發一小部分,網絡也不會被大文件長時間占用,內存、帶寬等資源也就節省下來了。

這種化整為零的思路在 HTTP 協議里就是 chunked 分塊傳輸編碼,在響應報文里用頭字段 Transfer-Encoding: chunked 來表示,意思是報文里的 body 部分不是一次性發過來的,而是分成了許多的塊(chunk)逐個發送。分塊傳輸也可以用於流式數據,例如由數據庫動態生成的表單頁面,這種情況下 body 數據的長度是未知的,無法在頭字段 Content-Length 里給出確切的長度,所以也只能用 chunked 方式分塊發送。

Transfer-Encoding: chunked 和 Content-Length 這兩個字段是互斥的,也就是說響應報文里這兩個字段不能同時出現,一個響應報文的傳輸要么是長度已知,要么是長度未知(chunked),這一點一定要記住。下面我們來看一下分塊傳輸的編碼規則,其實也很簡單,同樣采用了明文的方式,很類似響應頭。

  • 每個分塊包含兩個部分,長度頭和數據塊
  • 長度頭是以 CRLF(回車換行,即\r\n)結尾的一行明文,用 16 進制數字表示長度
  • 數據塊緊跟在長度頭后,最后也用 CRLF 結尾
  • 最后用一個長度為 0 的塊表示結束,即 0\r\n\r\n

看一張圖:

瀏覽器在收到分塊傳輸的數據后會自動按照規則去掉分塊編碼,重新組裝出內容。

范圍請求

有了分塊傳輸編碼,服務器就可以輕松地收發大文件了,但對於上 G 的超大文件,還有一些問題需要考慮。比如,你在看當下正熱播的某穿越劇,想跳過片頭,直接看正片,或者有段劇情很無聊,想拖動進度條快進幾分鍾,這實際上是想獲取一個大文件其中的片段數據,而分塊傳輸並沒有這個能力。

HTTP 協議為了滿足這樣的需求,提出了范圍請求(range requests)的概念,允許客戶端在請求頭里使用專用字段來表示只獲取文件的一部分,相當於是客戶端的化整為零。范圍請求不是 Web 服務器必備的功能,可以實現也可以不實現,所以服務器必須在響應頭里使用字段 Accept-Ranges: bytes 明確告知客戶端:我是支持范圍請求的。如果不支持的話該怎么辦呢?服務器可以發送 Accept-Ranges: none,或者干脆不發送 Accept-Ranges 字段,這樣客戶端就認為服務器沒有實現范圍請求功能,只能老老實實地收發整塊文件了。

請求頭 Range 是 HTTP 范圍請求的專用字段,格式是 bytes=x-y,其中的 x 和 y 是以字節為單位的數據范圍。要注意 x、y 表示的是偏移量,范圍必須從 0 計數,例如前 10 個字節表示為 0-9,第二個 10 字節表示為 10-19,0-10 表示前 11 個字節。Range 的格式也很靈活,起點 x 和終點 y 可以省略,能夠很方便地表示正數或者倒數的范圍。假設文件是 100 個字節,那么:

  • "0-" 表示從文檔起點到文檔終點,相當於 "0-99",即整個文件
  • "10-" 是從第 10 個字節開始到文檔末尾,相當於 "10-99"
  • "-1" 是文檔的最后一個字節,相當於 "99-99"
  • "-10" 是從文檔末尾倒數 10 個字節,相當於 "90-99"

服務器收到 Range 字段后,需要做四件事。

第一,它必須檢查范圍是否合法,比如文件只有 100 個字節,但請求 200-300,這就是范圍越界了。服務器就會返回狀態碼 416,意思是:你的范圍請求有誤,我無法處理,請再檢查一下。

第二,如果范圍正確,服務器就可以根據 Range 頭計算偏移量,讀取文件的片段了,返回狀態碼 206 Partial Content,和 200 的意思差不多,但表示 body 只是原數據的一部分。

第三,服務器要添加一個響應頭字段 Content-Range,告訴片段的實際偏移量和資源的總大小,格式是 bytes x-y/length,與 Range 頭區別是 bytes 后面沒有 =,范圍后多了總長度。例如,對於 0-10 的范圍請求,值就是 bytes 0-10/100。

最后剩下的就是發送數據了,直接把片段用 TCP 發給客戶端,一個范圍請求就算是處理完了。有了范圍請求之后,HTTP 處理大文件就更加輕松了,看視頻時可以根據時間點計算出文件的 Range,不用下載整個文件,直接精確獲取片段所在的數據內容。當然現在不僅看視頻的拖拽進度需要范圍請求,常用的下載工具里的多段下載、斷點續傳也是基於它實現的,要點是:

  • 先發個 HEAD,看服務器是否支持范圍請求,同時獲取文件的大小
  • 開 N 個線程,每個線程使用 Range 字段划分出各自負責下載的片段,發請求傳輸數據
  • 下載意外中斷也不怕,不必重頭再來一遍,只要根據上次的下載記錄,用 Range 請求剩下的那一部分就可以了

多段數據

剛才說的范圍請求一次只獲取一個片段,其實它還支持在 Range 頭里使用多個 x-y,一次性獲取多個片段數據。這種情況需要使用一種特殊的 MIME 類型:multipart/byteranges,表示報文的 body 是由多段字節序列組成的,並且還要用一個參數 boundary=xxx 給出段之間的分隔標記。

多段數據的格式與分塊傳輸也比較類似,但它需要用分隔標記 boundary 來區分不同的片段,可以通過圖來對比一下。

每一個分段必須以 - -boundary 開始(前面加兩個 -),之后要用 Content-Type 和 Content-Range 標記這段數據的類型和所在范圍,然后就像普通的響應頭一樣以回車換行結束,再加上分段數據,最后用一個 - -boundary- -(前后各有兩個 -)表示所有的分段結束。

補充

gzip 的壓縮率通常能夠超過 60%,而 br 算法是專門會 HTML 設計的,壓縮效率和性能比 gzip 還要好,能夠再提高 20% 的壓縮密度。

Nginx 的 gzip on 非常智能,只會壓縮文本數據,不會壓縮圖片、音頻、視頻。

Transfer-Encoding 字段最常見的值是 chunked,但也可以使用 gzip、deflate 等,表示傳輸時使用了壓縮編碼。注意:這個 Content-Encoding 不同,Transfer-Encoding 在傳輸之后會被自動解碼還原出原始數據,而 Content-Encoding 則必須由應用自行解碼。

分塊傳輸在末尾還允許有拖尾數據,由響應頭字段 Trailer 指定。

與 Range 有關的還有一個 If-Range,即條件范圍請求,后面說。

問題

分塊傳輸數據的時候,如果數據里含有換行(\r\n)是否會影響分塊的處理呢?

答案是不會的,因為分塊前面有數據長度。

如果對一個被 gzip 的文件執行范圍請求,比如 Range: bytes=10-19,那么這個范圍是應用於原文件還是壓縮后的文件呢?

不用想,肯定是原文件。想象一下我們看視頻拖動進度條,如果是應用於壓縮后的文件,那么就會造成拖拽范圍和響應范圍不一致。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM