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),這一點你一定要記住。
下面我們來看一下分塊傳輸的編碼規則,其實也很簡單,同樣采用了明文的方式,很類似響應頭。
聽起來好像有點難懂,看一下圖就好理解了:
實驗環境里的 URI“/16-1”簡單地模擬了分塊傳輸,可以用 Chrome 訪問這個地址看一下效果:
?不過瀏覽器在收到分塊傳輸的數據后會自動按照規則去掉分塊編碼,重新組裝出內容,所以想要看到服務器發出的原始報文形態就得用 Telnet 手工發送請求(或者用 Wireshark 抓包):
GET /16-1 HTTP/1.1 Host: www.chrono.com因為 Telnet 只是收到響應報文就完事了,不會解析分塊數據,所以可以很清楚地看到響應報文里的 chunked 數據格式:先是一行 16 進制長度,然后是數據,然后再是 16 進制長度和數據,如此重復,最后是 0 長度分塊結束。
范圍請求
有了分塊傳輸編碼,服務器就可以輕松地收發大文件了,但對于上 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 頭區別在沒有“=”,范圍后多了總長度。例如,對于“0-10”的范圍請求,值就是“bytes 0-10/100”。
最后剩下的就是發送數據了,直接把片段用 TCP 發給客戶端,一個范圍請求就算是處理完了。
你可以用實驗環境的 URI“/16-2”來測試范圍請求,它處理的對象是“/mime/a.txt”。不過我們不能用 Chrome 瀏覽器,因為它沒有編輯 HTTP 請求頭的功能(這點上不如 Firefox 方便),所以還是要用 Telnet。
例如下面的這個請求使用 Range 字段獲取了文件的前 32 個字節:
GET /16-2 HTTP/1.1 Host: www.chrono.com Range: bytes=0-31?返回的數據是(去掉了幾個無關字段):
HTTP/1.1 206 Partial Content Content-Length: 32 Accept-Ranges: bytes Content-Range: bytes 0-31/96// this is a plain text json doc有了范圍請求之后,HTTP 處理大文件就更加輕松了,看視頻時可以根據時間點計算出文件的 Range,不用下載整個文件,直接精確獲取片段所在的數據內容。
不僅看視頻的拖拽進度需要范圍請求,常用的下載工具里的多段下載、斷點續傳也是基于它實現的,要點是:
- 先發個 HEAD,看服務器是否支持范圍請求,同時獲取文件的大小;
- 開 N 個線程,每個線程使用 Range 字段劃分出各自負責下載的片段,發請求傳輸數據;
- 下載意外中斷也不怕,不必重頭再來一遍,只要根據上次的下載記錄,用 Range 請求剩下的那一部分就可以了。
多段數據
剛才說的范圍請求一次只獲取一個片段,其實它還支持在 Range 頭里使用多個“x-y”,一次性獲取多個片段數據。
這種情況需要使用一種特殊的 MIME 類型:“multipart/byteranges”,表示報文的 body 是由多段字節序列組成的,并且還要用一個參數“boundary=xxx”給出段之間的分隔標記。
多段數據的格式與分塊傳輸也比較類似,但它需要用分隔標記 boundary 來區分不同的片段,可以通過圖來對比一下。
每一個分段必須以“- -boundary”開始(前面加兩個“-”),之后要用“Content-Type”和“Content-Range”標記這段數據的類型和所在范圍,然后就像普通的響應頭一樣以回車換行結束,再加上分段數據,最后用一個“- -boundary- -”(前后各有兩個“-”)表示所有的分段結束。
例如,我們在實驗環境里用 Telnet 發出有兩個范圍的請求:
GET /16-2 HTTP/1.1 Host: www.chrono.com Range: bytes=0-9, 20-29得到的就會是下面這樣:
HTTP/1.1 206 Partial Content Content-Type: multipart/byteranges; boundary=00000000001 Content-Length: 189 Connection: keep-alive Accept-Ranges: bytes--00000000001 Content-Type: text/plain Content-Range: bytes 0-9/96// this is --00000000001 Content-Type: text/plain Content-Range: bytes 20-29/96ext json d --00000000001--報文里的“- -00000000001”就是多段的分隔符,使用它客戶端就可以很容易地區分出多段 Range 數據。
小結
今天我們學習了 HTTP 傳輸大文件相關的知識,在這里做一下簡單小結:
要注意這四種方法不是互斥的,而是可以混合起來使用,例如壓縮后再分塊傳輸,或者分段后再分塊,實驗環境的 URI“/16-3”就模擬了后一種的情形,你可以自己用 Telnet 試一下。
總結
以上是生活随笔為你收集整理的HTTP传输大文件的方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: zabbix 批量生成聚合图形
- 下一篇: 浅析JVM中常见的垃圾收集算法