三:多個檔案下載的管理
這一節我們主要來講一下如何對多個檔案的下載進行管理
首先來看一下整個系統的UML圖
從最下面開始說起:
Download代表一個下載類,對每一個檔案都需要建立一個Download執行個體,用於對該檔案下載線程的管理。其中每個Download中都有以下幾個對象:
private ConcurrentLinkedQueue<DownloadBlock> blockQueue;private ConcurrentLinkedQueue<DownloaBlock> blockCache;private ConcurrentHashMap<Long, Long> blockCounts;private ConcurrentLinkedQueue<DownloadThread> activeThreads;
其中
- blockQueue是一個隊列,用於儲存當前需要下載的DownloadBlock。Download對檔案進行切分形成的DownloadBlock會被放入到放入到blockQueue中,供以後的下載。
- blockCache為block記憶體緩衝池,主要是為了能夠複用已經建立好的DownloadBlock。
- blockCounts為一個Map,其中key為每個block的start值,而value為該block已經下載完的段D。主要作用是統計出當前已經每個Block已經下載完的段D,以計算即時下載速度
- activeThreads 主要是為了儲存該Thread中所有的活躍線程。
DownloadBlock是一個下載塊,其裡面有3個成員變數
private Download download; //其所屬的Downloadprivate long start; //下載檔案起始處private long length; //下載檔案的長度
DownloadThread是指下載進程,每個DownloadBlock都需要啟動一個DownloadThread去進行下載。即
new DownloadThread(block).start()
DownloadDeamon為了一個守護線程。其內部主要為了下載所有的需要下載DownloadBlock
private DownloadList downloads; //當前系統中所有的下載列表private ExecutorService threadPool; //線程池
Downloader 代表整個下載系統,整個系統中只有一個執行個體對象,因此我們需要保證系統中只有一個執行個體對象。
private DownloaderConfig config; // Downloader配置private DownloadList downloads; //當前系統所有的下載列表private Thread deamon; //守護進程private ConcurrentLinkedQueue<DownloadBlock> blockCache; //當前系統的緩衝private Timer timer; //
看了上面一大堆的東西,我保證你現在很暈,OK,我們從使用的角度來看整個系統是如何啟動並執行。
下面是範例程式碼。
public static void main(String[] args) {Downloader downloader = Downloader.getInstance();//下載第一個檔案String url1 = "https://tmsvm.googlecode.com/files/tmsvm_src_v1.1.0.rar";String saveFile1 = "data/tmsvm_src_v1.1.0.rar";DownloadConfig config = new DownloadConfig();try {config.setUrl(new URL(url1));config.setFile(new File(saveFile1));config.setNthread(new Integer(5));config.setPriority(new Integer(6));//將第一個下載加入到下載列表中downloader.addDownload(new Download(config, downloader.getTimer()));} catch (MalformedURLException e) {// TODO Auto-generated catch blocke.printStackTrace();}//下載第二個檔案String url2 = "https://tmsvm.googlecode.com/files/Tmsvm%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3%28v1.1.0%29.rar";String saveFile2 = "data/Tmsvm參考文檔(v1.1.0).rar";try {config.setUrl(new URL(url2));config.setFile(new File(saveFile2));config.setNthread(new Integer(5));config.setPriority(new Integer(6));//將第二個下載加入到下載列表中downloader.addDownload(new Download(config, downloader.getTimer()));} catch (MalformedURLException e) {// TODO Auto-generated catch blocke.printStackTrace();}
1. 系統初始化
首先來看這一行行:
Downloader downloader = Downloader.getInstance();
Downloader是這個下載器的總調度師,一山不容二虎,當然在系統運行過程中,只能有一個Downloader的執行個體,因此我們需要用單例模式來保證這一點。
首先要取得downloader執行個體,即系統的初始化。我們看系統初始化需要做什嗎?
public static Downloader getInstance(){if(downloader == null)downloader = new Downloader(new DownloaderConfig());return downloader;}private Downloader(DownloaderConfig config) {super();this.config = config;start();}
上面的代碼中的start()函數中到底做了什麼呢?
- 初始化blockCache緩衝,其中blockCache為ConcurrentLinkedQueue<DownloadBlock>類型。
- 啟動守護進程DownloadDeamon
具體代碼如下:
private void start(){blockCache = new ConcurrentLinkedQueue<DownloadBlock>(); //初始化緩衝downloads = new DownloadList();deamon = new Thread(new DownloadDeamon(downloads)); //初始化守護進程deamon.setDaemon(true); deamon.start();timer = new Timer(Constants.TIMER_NAME, true);}
上面代碼中啟動了一個守護進程。那麼這個守護進程在啟動的時候在做什麼事情呢?
我們來看一下他的run()函數
public void run() {System.out.println("Create thread pool");threadPool = Executors.newCachedThreadPool(); //初始化線程池DownloadBlock block;while(true){block = getDownloadBlock(); //不斷從當前系統中擷取待下載的DownloadBlockif(block != null){log.info("Create new download thread for " + block);//啟動線程執行下載threadPool.execute(new DownloadThread(block)); //將當前Block從其所在的Download中移除block.getDownload().removeDownloadBlock(block);}try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blockSystem.out.println("Download deamon stoped by user");break;}}}
守護進程所做的事情就是不斷擷取將要進行下載的Block,然後啟動線程去進行下載。
來看一下擷取Block的策略:這裡不斷的從當前下載列表中擷取所有的Download,然後從裡面選取最需要下載的檔案,“最需要下載”定義為剩餘的待下載量最多。其具體的代碼看下方:
private DownloadBlock getDownloadBlock(){downs= downloads.toArray();if(downs== null || downs.length == 0)return null;Downloaddownload = downs[0];for(Downloaddown : downs){//找最需要下載的Download進行下載 if(down.getRemainThread()> download.getRemainThread())download= down;}download.descRemainThread();returndownload.getDownloadBlock();}
2. 建立下載
上面講解的是系統初始化所做的事情。那麼當我們把開始下載一個檔案時系統是怎麼啟動並執行呢?
//下載第一個檔案String url1 = "https://tmsvm.googlecode.com/files/tmsvm_src_v1.1.0.rar";String saveFile1 = "data/tmsvm_src_v1.1.0.rar";DownloadConfig config = new DownloadConfig();try {config.setUrl(new URL(url1));config.setFile(new File(saveFile1));config.setNthread(new Integer(5));config.setPriority(new Integer(6));//將第一個下載加入到下載列表中downloader.addDownload(new Download(config, downloader.getTimer()));} catch (MalformedURLException e) {// TODO Auto-generated catch blocke.printStackTrace();}
我們重點來看這一句:
//將第一個下載加入到下載列表中downloader.addDownload(new Download(config, downloader.getTimer()));
addDownload的定義如下
public boolean addDownload(Download download){new Thread(download).start();return downloads.add(download);}
這段代碼做了兩件事情:
- 為Download啟動一下線程。Download線程所做的事情就是把當前的檔案根據線程數目進行切分。
- 把當前Download加入到DownloadList中。
OK,我們看看,Download是切分檔案時是如何與整個系統聯絡在一起。
public void run(){try {begin = System.currentTimeMillis();// get lengthlog.info("Begin download " + config.getUrl());length = config.getUrl().openConnection().getContentLength();log.info("Total size : " + length);// create filelog.info("Create file " + config.getFile());RandomAccessFile file = new RandomAccessFile(config.getFile(), "rw");file.setLength(length);log.info("Created with length = " + length);file.close();int size = length / config.getNthread();// add initial blockslog.debug("Add initial " + config.getNthread() + " download blocks");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));}// set task that checks speed every 1 secondlog.debug("Set task for speed check");checkSpeedTask = new CheckSpeedTask(this, System.currentTimeMillis()-10, blockCounts);//timer.schedule(checkSpeedTask, 1000, 1000);// set task that creates new blocks every 1 minutelog.debug("Set task for split blocks");splitBlockTask = new SplitBlockTask(this, System.currentTimeMillis()-10, blockCounts, activeThreads);timer.schedule(splitBlockTask, 60*1000, 60*1000);// wait for all blocks completelog.debug("Waiting for all blocks to complete");while(activeThreads.size() > 0 || blockQueue.size() > 0){Thread.sleep(1000);checkSpeed();}// stop the taskscheckSpeedTask.cancel();splitBlockTask.cancel();long total = System.currentTimeMillis() - begin;speed = length/(total/1000);log.info("Complete download " + config.getUrl() + "\n"+ "Total time : " + total + " ms" + "\n"+ "Average speed: " + speed + "Byte/s");log.debug(this + " put all block in blockCache back to downloader system");for(DownloadBlock block : blockCache){Downloader.getInstance().putDownloadBlock(block);}} catch (FileNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}
上面的代碼,我們在第一篇中已經講解過了,這裡我們會重點看
addDownloadBlock(getDownloadBlock(start, len));
其意思是將當前的切分出來的Block放入到待下載隊列中去。
而我們在守護進程那裡,看到他不斷的會從當前系統中找最需要下載的Download,然後再從Download中取出下載隊列的Block進行下載
//DownloadDemen不斷的擷取DownloadBlockwhile(true){block = getDownloadBlock();if(block != null){System.out.println("Create new download thread for " + block);threadPool.execute(new DownloadThread(block));block.getDownload().removeDownloadBlock(block);}}
//getDownloadBlock()定義如下:private DownloadBlock getDownloadBlock(){ downs= downloads.toArray(); if(downs== null || downs.length == 0) return null; Downloaddownload = downs[0]; for(Downloaddown : downs){ if(down.getRemainThread()> download.getRemainThread()) download= down; } download.descRemainThread(); return download.getDownloadBlock();}
//而download.getDownloadBlock()定義如下所示:public DownloadBlock getDownloadBlock(){return blockQueue.peek();}
寫到這裡,整個的系統架構目錄就非常清晰了:Downloader, DownloadDemen, Download 之間是通過DownloadBlock聯絡起來的。
當有一個檔案需要下載時,Downloader 把該Download加入到DownloadList中。而Download自身會通過切分檔案建立出多個DownloadBlock。DownloadDemen每時每刻都在擷取DownloadBlock,賦予其線程進行下載。
下一節,我們會重點講解一下Downloader如何系統中的緩衝進行處理的。