前言
最近過(guò)了金三銀四的金三阅畴,順利拿到了暑假實(shí)習(xí)生的offer。實(shí)習(xí)部門leader給我布置了入職前學(xué)習(xí)任務(wù)鳍悠,強(qiáng)化多線程哩都、數(shù)據(jù)庫(kù)方面的知識(shí)魁兼,并建議我實(shí)現(xiàn)一個(gè)和他們產(chǎn)品中類似的下載器。
實(shí)現(xiàn)思路
本文的重點(diǎn)在下載部分的實(shí)現(xiàn)漠嵌。目前我也正在做單個(gè)任務(wù)下載開發(fā)與優(yōu)化咐汞。后續(xù)更新完成后如果有好的思路也會(huì)分享給大家。
項(xiàng)目地址是:https://github.com/SirLYC/Yuchuan-Downloader
(處于開發(fā)中)
斷點(diǎn)下載
首先献雅,下載器有斷點(diǎn)續(xù)傳功能碉考,斷點(diǎn)續(xù)傳實(shí)現(xiàn)的基礎(chǔ)知識(shí)就是HTTP協(xié)議中的Range頭部。比如挺身,一個(gè)文件有500bytes,我要從第200個(gè)bytes下載锌仅,就在請(qǐng)求的頭部添加一個(gè)key為Range
的項(xiàng)章钾,內(nèi)容是bytes=200-
。因此热芹,在實(shí)現(xiàn)的時(shí)候贱傀,我們需要記錄當(dāng)前的下載量,在恢復(fù)下載的時(shí)候伊脓,就可以從上次的當(dāng)前下載量開始下載府寒,節(jié)省用戶流量。
但是并不是所有的服務(wù)器都支持?jǐn)帱c(diǎn)下載报腔。因此株搔,可以在正式的下載前先發(fā)一個(gè)請(qǐng)求,在請(qǐng)求中添加Range
字段纯蛾,順帶也可以通過(guò)這種方式獲取文件長(zhǎng)度(ContentLength
首部)纤房。
這里簡(jiǎn)單說(shuō)一下下載文件的原理。在一個(gè)GET請(qǐng)求時(shí)翻诉,服務(wù)器首先會(huì)把頭部報(bào)文全部返回給你炮姨,如果是下載文件捌刮,一般來(lái)說(shuō)都是流下載,有一個(gè)標(biāo)志會(huì)告訴你
responseBody
是流舒岸。而HTTP
又是基于TCP
的绅作,這個(gè)流實(shí)際上就是TCP
的流,在Java中對(duì)應(yīng)的就是InputStream
蛾派。流可以看作是一個(gè)只能向后走的指針俄认,指針指向下一個(gè)待讀取的字節(jié),并且讀取了一個(gè)才能讀下一個(gè)碍脏。因此梭依,如果暫停恢復(fù)不用部分請(qǐng)求的話典尾,你必須得把前面下載過(guò)的字節(jié)全部接受一遍役拴,這顯然浪費(fèi)了時(shí)間和流量。
多線程下載
首先要知道钾埂,多線程是基于斷點(diǎn)下載的原理河闰。一個(gè)文件實(shí)際上就是二進(jìn)制數(shù)據(jù),把文件拆分成多個(gè)段褥紫,每個(gè)線程下載各自的段姜性。因此每個(gè)線程在請(qǐng)求時(shí)需要控制文件起始和結(jié)尾,給每一個(gè)線程分配下載的段髓考。因此部念,不支持?jǐn)帱c(diǎn)續(xù)傳的服務(wù)器是不能用多線程下載的。
那為什么多線程下載可以提速呢氨菇?首先比較顯然的一點(diǎn)是多線程可以利用CPU多核的特性儡炼,在相同時(shí)間內(nèi)完成更多的任務(wù)。但事實(shí)上基于這一點(diǎn)不會(huì)提高多大的速度查蓉,因?yàn)榻邮斩说目値捠且欢ǖ奈谘O胂笠粋€(gè)這個(gè)場(chǎng)景:
上面的小水管就是我們的服務(wù)端連接,每個(gè)連接限制了最大帶寬豌研。大水管就是接收端妹田,接收端帶寬一定。當(dāng)我們啟用一個(gè)小水管時(shí)鹃共,我們可以獲得的最大流速是min(小水管鬼佣、大水管)。當(dāng)我們啟用多個(gè)水管時(shí)及汉,最大速度是min(小水管1+小水管2+...+小水管n沮趣,大水管)】浪妫可見房铭,在這種場(chǎng)景下的多線程驻龟,瓶頸就不會(huì)再是服務(wù)端的帶寬限制。
那線程是不是越多越好呢缸匪? 顯然這是不對(duì)的翁狐。線程本身就是一個(gè)很重的對(duì)象,創(chuàng)建線程凌蔬、多線程調(diào)度管理會(huì)占用CPU時(shí)間露懒,會(huì)減少用戶時(shí)間比例。另外就是多線程對(duì)內(nèi)存的占用也是一個(gè)問(wèn)題砂心。因此懈词,啟動(dòng)的下載線程數(shù)要有限制。
下載與寫線程分開
以前寫下載器時(shí)辩诞,常見的下載模式是
// 偽代碼
while (data remains to read) {
buffer = inputstream.read(bufferSize)
outputstream.write(buffer)
}
在多線程的情況下大概是這樣的
當(dāng)時(shí)現(xiàn)場(chǎng)面試的時(shí)候我也講下載器可以這么實(shí)現(xiàn)坎弯,結(jié)果面試官上來(lái)問(wèn)一句,讀和寫真的要放在一個(gè)線程译暂?
從目前來(lái)講抠忘,寫磁盤的速度一般都是遠(yuǎn)大于網(wǎng)絡(luò)獲取的速度的。如果我們能把寫數(shù)據(jù)放在一個(gè)單獨(dú)的線程里外永,假設(shè)3個(gè)線程以相同的速度讀取相同大小的網(wǎng)絡(luò)字節(jié)流放在緩沖區(qū)崎脉,每個(gè)線程都把各自的緩沖區(qū)送入寫線程,然后又各自去讀網(wǎng)絡(luò)數(shù)據(jù)伯顶。因?yàn)槲覀儗懙乃俣却笥诰W(wǎng)絡(luò)下載速度的囚灼,因此在下一次3個(gè)緩沖區(qū)送入前是可以寫完的,這樣在理想情況下就節(jié)省了1次寫磁盤的時(shí)間祭衩。
但在實(shí)際實(shí)現(xiàn)時(shí)啦撮,有很多需要注意的地方。首先下載線程不能無(wú)限制的下載汪厨。如果寫線程阻塞了,下載線程還在不停下載的話愉择,緩沖區(qū)會(huì)越來(lái)越大劫乱,造成OOM。另外就是緩沖區(qū)的交換锥涕,寫線程需要拿衷戈,下載線程需要送,這是一個(gè)典型的消費(fèi)者——生產(chǎn)者模式层坠。這方面的實(shí)現(xiàn)文章就多了殖妇,最終我是選用的BlockQueue
來(lái)實(shí)現(xiàn)。大致的流程如下:
上述流程中破花,還有很多未包括所有內(nèi)容谦趣,比如錯(cuò)誤處理疲吸,狀態(tài)轉(zhuǎn)換等。實(shí)際上前鹅,要寫一個(gè)用戶體驗(yàn)好摘悴,性能好的下載器是一件很不容易的事。
后續(xù)
目前舰绘,我的項(xiàng)目上實(shí)現(xiàn)的只有單任務(wù)多線程的下載蹂喻,多任務(wù)、下載信息本地保存等還未實(shí)現(xiàn)捂寿。
除了這些以外口四,我還會(huì)考慮加入多進(jìn)程的架構(gòu),可以實(shí)現(xiàn)ui退出后的離線下載秦陋。歡迎大家clone跑sample或者提一些意見蔓彩!
再次掛上項(xiàng)目地址:https://github.com/SirLYC/Yuchuan-Downloader