前言
最近過了金三銀四的金三,順利拿到了暑假實習生的offer。實習部門leader給我佈置了入職前學習任務,強化多執行緒、資料庫方面的知識,並建議我實現一個和他們產品中類似的下載器。
實現思路
本文的重點在下載部分的實現。目前我也正在做單個任務下載開發與優化。後續更新完成後如果有好的思路也會分享給大家。 專案地址是:github.com/SirLYC/Yuch… (處於開發中)
斷點下載
首先,下載器有斷點續傳功能,斷點續傳實現的基礎知識就是HTTP協議中的Range頭部。比如,一個檔案有500bytes,我要從第200個bytes下載,就在請求的頭部新增一個key為Range
的項,內容是bytes=200-
。因此,在實現的時候,我們需要記錄當前的下載量,在恢復下載的時候,就可以從上次的當前下載量開始下載,節省使用者流量。
但是並不是所有的伺服器都支援斷點下載。因此,可以在正式的下載前先發一個請求,在請求中新增Range
欄位,順帶也可以通過這種方式獲取檔案長度(ContentLength
首部)。
這裡簡單說一下下載檔案的原理。在一個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…