更新了一下斷點下載的實現部分,根據你們的評論作了一些更正和完善。git
最近過了金三銀四的金三,順利拿到了暑假實習生的offer。實習部門leader給我佈置了入職前學習任務,強化多線程、數據庫方面的知識,並建議我實現一個和他們產品中相似的下載器。github
本文的重點在下載部分的實現。目前我也正在作單個任務下載開發與優化。後續更新完成後若是有好的思路也會分享給你們。 項目地址是:github.com/SirLYC/Yuch… (處於開發中)面試
首先,下載器有斷點續傳功能,斷點續傳實現的基礎知識就是HTTP協議中的Range頭部。好比,一個文件有500bytes,我要從第200個bytes下載,就在請求的頭部添加一個key爲Range
的項,內容是bytes=200-
。所以,在實現的時候,咱們須要記錄當前的下載量,在恢復下載的時候,就能夠從上次的當前下載量開始下載,節省用戶流量。數據庫
可是並非全部的服務器都支持斷點下載。所以,能夠在正式的下載前先發一個請求,在請求中添加Range
字段,順帶也能夠經過這種方式獲取文件長度(ContentLength
首部)。bash
而以前在評論區有小夥伴說添加Range
怎樣獲取文件文件長度的問題。我發送的請求Range
字段的值是bytes=0-
,是從第0個字節開始請求文件。所以,若是這個請求可以正常的返回,而且有contentLength
頭部,那就必定是文件總長度。bytes=0-
表示請求所有文件,可是對於支持斷點續傳的服務器,也是會返回206 partial content
(我測試過幾個連接,都是這樣)。服務器
關於這一點我也不敢說很是確定,但按照協議,在有
Range
字段時,服務器,服務器應該作的是檢查Range
是否合理,只要合理而且支持就是206
返回。網絡
但若是服務器返回416表示不支持呢?這個時候咱們就不能獲取到不支持斷點的文件長度了,所以我以前的代碼實現可能會有問題。實際上還有這個字段If-Range
,若是服務器支持斷點,會返回206,不支持的話就會返回200並附帶所有內容,這樣就能夠解決這個問題了。多線程
對於bytes=0-
可能支持斷點的服務器會判斷一下返回200的狀況,我也想了另外一個方法:請求一個字節。使用If-Range=0-0
去請求,支持斷點時返回Content-Range
獲取文件總長度。這種方式下,對於下載文件只有1個字節的狀況,就算返回的不是206是200(所有返回),是否用斷點無差異。架構
評論區還有小夥伴問到,萬一服務器不支持怎麼辦?我認爲,首先,對於產品來說,首先要適配大部分的狀況,而下載的例子,大部分狀況就是網絡協議,咱們認爲服務器會按照協議要求來實現,這也是爲何我直接使用前面的方法去檢測是否支持斷點續傳。在實際生產環境中,若是遇到了部分不遵照協議的服務器,就只能作特殊處理了,但實際上這個特殊處理有沒有必要呢?這就仁者見仁智者見智了。性能
這裏簡單說一下下載文件的原理。在一個GET請求時,服務器首先會把頭部報文所有返回給你,若是是下載文件,通常來講都是流下載,有一個標誌會告訴你
responseBody
是流。而HTTP
又是基於TCP
的,這個流實際上就是TCP
的流,在Java中對應的就是InputStream
。流能夠看做是一個只能向後走的指針,指針指向下一個待讀取的字節,而且讀取了一個才能讀下一個。所以,若是暫停恢復不用部分請求的話,你必須得把前面下載過的字節所有接受一遍,這顯然浪費了時間和流量。
首先要知道,多線程是基於斷點下載的原理。一個文件實際上就是二進制數據,把文件拆分紅多個段,每一個線程下載各自的段。所以每一個線程在請求時須要控制文件起始和結尾,給每個線程分配下載的段。所以,不支持斷點續傳的服務器是不能用多線程下載的。
那爲何多線程下載能夠提速呢?首先比較顯然的一點是多線程能夠利用CPU多核的特性,在相同時間內完成更多的任務。但事實上基於這一點不會提升多大的速度,由於接收端的總帶寬是必定的。想象一個這個場景:
上面的小水管就是咱們的服務端鏈接,每一個鏈接限制了最大帶寬。大水管就是接收端,接收端帶寬必定。當咱們啓用一個小水管時,咱們能夠得到的最大流速是min(小水管、大水管)。當咱們啓用多個水管時,最大速度是min(小水管1+小水管2+...+小水管n,大水管)。可見,在這種場景下的多線程,瓶頸就不會再是服務端的帶寬限制。那線程是否是越多越好呢? 顯然這是不對的。線程自己就是一個很重的對象,建立線程、多線程調度管理會佔用CPU時間,會減小用戶時間比例。另外就是多線程對內存的佔用也是一個問題。所以,啓動的下載線程數要有限制。
之前寫下載器時,常見的下載模式是
// 僞代碼
while (data remains to read) {
buffer = inputstream.read(bufferSize)
outputstream.write(buffer)
}
複製代碼
在多線程的狀況下大概是這樣的
當時現場面試的時候我也講下載器能夠這麼實現,結果面試官上來問一句,讀和寫真的要放在一個線程? 從目前來說,寫磁盤的速度通常都是遠大於網絡獲取的速度的。若是咱們能把寫數據放在一個單獨的線程裏,假設3個線程以相同的速度讀取相同大小的網絡字節流放在緩衝區,每一個線程都把各自的緩衝區送入寫線程,而後又各自去讀網絡數據。由於咱們寫的速度大於網絡下載速度的,所以在下一次3個緩衝區送入前是能夠寫完的,這樣在理想狀況下就節省了1次寫磁盤的時間。
但在實際實現時,有不少須要注意的地方。首先下載線程不能無限制的下載。若是寫線程阻塞了,下載線程還在不停下載的話,緩衝區會愈來愈大,形成OOM。另外就是緩衝區的交換,寫線程須要拿,下載線程須要送,這是一個典型的消費者——生產者模式。這方面的實現文章就多了,最終我是選用的BlockQueue
來實現。大體的流程以下:
上述流程中,還有不少未包括全部內容,好比錯誤處理,狀態轉換等。實際上,要寫一個用戶體驗好,性能好的下載器是一件很不容易的事。
目前,個人項目上實現的只有單任務多線程的下載,多任務、下載信息本地保存等還未實現。
除了這些之外,我還會考慮加入多進程的架構,能夠實現ui退出後的離線下載。歡迎你們clone跑sample或者提一些意見!
再次掛上項目地址:github.com/SirLYC/Yuch…