本文實現了一個基於Java多線程的下載器,可提供的功能有:
1. 對檔案使用多線程下載,並顯示每時刻的下載速度。
2. 對多個下載進行管理,包括線程調度,記憶體管理等。
這篇文章的結構如下:首先討論如何?利用Java多線程對單個檔案進行下載。然後討論當系統中有多個檔案下載,如何對這些下載進行管理。包括線程調度,記憶體管理等。
一:單個檔案下載的管理1. 單檔案下載類層次
首先簡要介紹一下單個檔案下載管理的類層次:
來一張圖來表示。
- 為需要下載的檔案建立一個Download類,Download負責管理該檔案下載時的線程管理、檔案管理、當前速度計算等操作。
- 根據線程的數目tNum,將該檔案分為tNum段,每段為一個DownloadBlock。在實際下載的過程中,並不是一次把所有的東西下載完,而是每次下載固定size的一段Di。所以每個DownloadBlock又會分成n段。
- 為每個DownloadBlock申請一個線程DownloadThread。其主要作用就是每次下載一段Di,並將其寫入到檔案中。
2. 單檔案下載
對於單個下載,步驟如下
- 串連資原始伺服器,擷取資源資訊,建立檔案
- 切分資源,為每個線程分配固定的下載地區。
1)封裝下載的屬性
在建立下載之前,我們把每一次下載進行抽象封裝。
首先把URL、目標檔案等封裝在一個DownloadConfig類中。
其中包含了4個屬性:
private URL url; //檔案private File file; //下載檔案儲存目標檔案private int nthread; //下載該檔案需要的線程數private int priority; //該下載的優先順序
如下如所示:
2)串連資原始伺服器,擷取資源資訊,建立檔案,並指定檔案大小
length = config.getUrl().openConnection().getContentLength();RandomAccessFile file = new RandomAccessFile(config.getFile(), "rw");file.setLength(length);file.close();
3)切分資源,為每個線程分配固定的下載地區,並將當前的下載加入到隊列中
int size = length / config.getNthread();for(int i = 0; i < config.getNthread(); i++){int start = i * size;int len;if(i == config.getNthread() - 1)len = length - start;else len = size;//並將當前的下載加入到下載隊列中addDownloadBlock(getDownloadBlock(start, len));}
3)啟動線程進行下載
下載的步驟如下:
1. 建立緩衝,建立串連。設定擷取資源資料的範圍,建立檔案,並設定寫入位置
//建立緩衝byte [] b;if(block.getLength() < Constants.BYTES_READ)b = new byte[(int)block.getLength()];elseb = new byte[Constants.BYTES_READ];//建立串連。設定擷取資源資料的範圍,從startPos到endPosURLConnection con = null;con.setRequestProperty("Range", "bytes=" + block.getStart() + "-" + block.getStart()+block.getLength()-1);RandomAccessFile file = new RandomAccessFile(block.getDownload().getConfig().getFile(), "rw");//建立RandomAccessFilefile.seek(block.getStart()); //從startPos開始寫入
2. 如果當前block的length大於0,則從URL資源處擷取固定大小的資源,並將其寫入到檔案中。
3 .更新block塊的start,以及length,如果length大於0,繼續進行2,否則則表示當前block已經下載完畢,退出該線程。
InputStream in = block.getDownload().getConfig().getUrl().openStream();int n;//對該block內的檔案進行下載,while(count < block.getLength()){if (needSplit()) { // 檢查該Block是否還需要分塊(即當前block剩餘的大小大於一次下載的量)long newLength = (block.getLength() - count) / 2;long newStart = block.getStart() + block.getLength() - newLength;DownloadBlock newBlock = block.getDownload().getDownloadBlock(newStart, newLength);block.setLength(block.getLength() - newLength);block.getDownload().addDownloadBlock(newBlock);}//寫入檔案n = in.read(b);if(n < 0){break;}else if(count + n > block.getLength()){file.write(b, 0, (int)(block.getLength() - count));count = block.getLength();}else {count += n;file.write(b, 0, n);}// set block count in downloadif(n > 0){//統計每個block中已經下載的段的個數,用於計算當前下載的速度。block.getDownload().setBlockCount(block.getStart(), count);}}in.close();file.close();
二 . 當前檔案下載速度與進度計算
如第一個圖所表示的,每個Block中又分為了很多的段D1、D2、…Dn,因此當為了計算當前下載的速度,需要將下載的段D的數量統計出來,這裡使用了一個ConcurrentHashMap<Long, Long>來儲存每個block已經下載完成的段D的數目。其中key為每個block的start值,而value為該block已經下載完的段D。
在當前時刻,我們需要統計當前Download已經下載完成段D的數量,然後再和上一時刻的相比較,則可以得出當前的下載速度。具體代碼見下:
class CheckSpeedTask extends TimerTask{private static final Log log = LogFactory.getLog(CheckSpeedTask.class);private Download download;private ConcurrentHashMap<Long, Long> blockCounts; private long speed = 0; // Byte/Sprivate long count = 0; // Total downloaded byte countprivate long lastCount = 0;private long time = 0; // Check timeprivate long lastTime = 0;public CheckSpeedTask(Download download, long startTime, ConcurrentHashMap<Long, Long> blockCounts){this.download = download;this.lastTime = startTime;this.blockCounts = blockCounts;}@Overridepublic void run() {try {time = System.currentTimeMillis();count = 0;//需要統計當前已經下載完成段D的數量。for(long c : blockCounts.values()){count += c;}speed = (count -lastCount)/((time - lastTime)/1000);log.debug(blockCounts.size() + " threads are downloading " + download + ", cuttent is " + speed + "Byte/S, " + (count * 1.0)/download.getLength()*100 + "% downloaded");download.setCount(count);download.setSpeed(speed);lastTime = time;lastCount = count;} catch (Exception e) {// TODO: handle exceptione.printStackTrace();}}}
這樣我們就可以在Thread類的run()函數中,計算當前下載的速度
while(activeThreads.size() > 0 || blockQueue.size() > 0){Thread.sleep(1000);checkSpeed();}
上面的代碼示範了如何使用Java多線程對單個檔案進行下載,接下來我們繼續討論如何對多個下載進行調度、管理等