1. 程式人生 > >java 檔案下載 斷點續傳

java 檔案下載 斷點續傳

最近閒來無事,因為公司遮蔽了迅雷軟體的下載埠,所以自己寫了一個下載工具。拿過來分享下。

下載網路上的檔案肯定不能只用單執行緒下載,這樣下載太慢,網速得不多合理利用。那麼就應該用多執行緒下載和執行緒池排程執行緒。

所以我們要講檔案切分成N段下載。用到了RandomAccessFile 隨機訪問檔案。

首先我們寫一個主執行緒,用來管理下載的子執行緒:

package org.app.download.component;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Map;
import java.util.Set;

import org.app.download.core.EngineCore;

/**
 * 
 * @Title: DLTask.java
 * @Description: 本類對應一個下載任務,每個下載任務包含多個下載執行緒,預設最多包含十個下載執行緒
 * @Package org.app.download.component
 * @author 
[email protected]
* @date 2012-8-1 * @version V1.0 * */ public class DLTask extends Thread implements Serializable { private static final long serialVersionUID = 126148287461276024L; // 下載臨時檔案字尾,下載完成後將自動被刪除 public final static String FILE_POSTFIX = ".tmp"; // URL地址 private URL url; // 檔案物件 private File file; // 檔名 private String filename; // 下載執行緒數量,使用者可定製 private int threadQut; // 下載檔案長度 private int contentLen; // 當前下載完成總數 private long completedTot; // 下載時間計數,記錄下載耗費的時間 private int costTime; // 下載百分比 private String curPercent; // 是否新建下載任務,可能是斷點續傳任務 private boolean isNewTask; // 儲存當前任務的執行緒 private DLThread[] dlThreads; // 當前任務的監聽器,用於即時獲取相關下載資訊 transient private DLListener listener; public DLTask(int threadQut, String url, String filename) { this.threadQut = threadQut; this.filename = filename; costTime = 0; curPercent = "0"; isNewTask = true; this.dlThreads = new DLThread[threadQut]; this.listener = new DLListener(this); try { this.url = new URL(url); } catch (MalformedURLException ex) { ex.printStackTrace(); throw new RuntimeException(ex); } } @Override public void run() { if (isNewTask) { newTask(); return; } resumeTask(); } /** * 恢復任務時被呼叫,用於斷點續傳時恢復各個執行緒。 */ private void resumeTask() { listener = new DLListener(this); file = new File(filename + FILE_POSTFIX); for (int i = 0; i < threadQut; i++) { dlThreads[i].setDlTask(this); EngineCore.pool.execute(dlThreads[i]); } EngineCore.pool.execute(listener); } /** * 新建任務時被呼叫,通過連線資源獲取資源相關資訊,並根據具體長度建立執行緒塊, 執行緒建立完畢後,即刻通過執行緒池進行排程 * * @throws RuntimeException */ @SuppressWarnings({ "rawtypes", "unchecked" }) private void newTask() throws RuntimeException { try { isNewTask = false; URLConnection con = url.openConnection(); Map map = con.getHeaderFields(); Set<String> set = map.keySet(); for (String key : set) { System.out.println(key + " : " + map.get(key)); } contentLen = con.getContentLength(); if (contentLen <= 0) { System.out.println("Unable to get resources length, the interrupt download process!"); return; } file = new File(filename + FILE_POSTFIX); int fileCnt = 1; while (file.exists()) { file = new File(filename += (fileCnt + FILE_POSTFIX)); fileCnt++; } int subLenMore = contentLen % threadQut; int subLen = (contentLen - subLenMore) / threadQut; for (int i = 0; i < threadQut; i++) { DLThread thread; if (i == threadQut - 1) { thread = new DLThread(this, i + 1, subLen * i, (subLen * (i + 1) - 1) + subLenMore); } else { thread = new DLThread(this, i + 1, subLen * i, subLen * (i + 1) - 1); } dlThreads[i] = thread; EngineCore.pool.execute(dlThreads[i]); } EngineCore.pool.execute(listener); } catch (IOException ex) { ex.printStackTrace(); throw new RuntimeException(ex); } } /** * 計算當前已經完成的長度並返回下載百分比的字串表示,目前百分比均為整數 * * @return */ public String getCurPercent() { this.completeTot(); curPercent = new BigDecimal(completedTot).divide(new BigDecimal(this.contentLen), 2, BigDecimal.ROUND_HALF_EVEN).divide(new BigDecimal(0.01), 0, BigDecimal.ROUND_HALF_EVEN).toString(); return curPercent; } /** * 獲取當前下載的位元組 */ private void completeTot() { completedTot = 0; for (DLThread t : dlThreads) { completedTot += t.getReadByte(); } } /** * 判斷全部執行緒是否已經下載完成,如果完成則返回true,相反則返回false * * @return */ public boolean isComplete() { boolean completed = true; for (DLThread t : dlThreads) { completed = t.isFinished(); if (!completed) { break; } } return completed; } /** * 下載完成後重新命名檔案 */ public void rename() { this.file.renameTo(new File(filename)); } public DLThread[] getDlThreads() { return dlThreads; } public void setDlThreads(DLThread[] dlThreads) { this.dlThreads = dlThreads; } public File getFile() { return file; } public URL getUrl() { return url; } public int getContentLen() { return contentLen; } public String getFilename() { return filename; } public int getThreadQut() { return threadQut; } public long getCompletedTot() { return completedTot; } public int getCostTime() { return costTime; } public void setCostTime(int costTime) { this.costTime = costTime; } }


那麼有了主執行緒之後,我們還需要子執行緒,來下載各自負責下載的部分檔案:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.app.download.component;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.net.URL;
import java.net.URLConnection;

/**
 * 
 * @Title: DLThread.java
 * @Description: 下載執行緒類
 * @Package org.app.download.component
 * @author 
[email protected]
* @date 2012-8-1 * @version V1.0 * */ public class DLThread extends Thread implements Serializable { private static final long serialVersionUID = -3317849201046281359L; //  緩衝位元組 private static int BUFFER_SIZE = 8096; // 當前任務物件 transient private DLTask dlTask; // 任務ID private int id; // URL下載地址 private URL url; // 開始下載點 private int startPos; // 結束下載點 private int endPos; // 當前下載點 private int curPos; // 讀入位元組 private long readByte; // 檔案 transient private File file; // 當前執行緒是否下載完成 private boolean finished; // 是否是新建下載任務 private boolean isNewThread; public DLThread(DLTask dlTask, int id, int startPos, int endPos) { this.dlTask = dlTask; this.id = id; this.url = dlTask.getUrl(); this.curPos = this.startPos = startPos; this.endPos = endPos; this.file = dlTask.getFile(); finished = false; readByte = 0; } public void run() { System.out.println("Tread - " + id + " start......"); BufferedInputStream bis = null; RandomAccessFile fos = null; byte[] buf = new byte[BUFFER_SIZE]; URLConnection con = null; try { con = url.openConnection(); con.setAllowUserInteraction(true); if (isNewThread) { con.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos); fos = new RandomAccessFile(file, "rw"); fos.seek(startPos); } else { con.setRequestProperty("Range", "bytes=" + curPos + "-" + endPos); fos = new RandomAccessFile(dlTask.getFile(), "rw"); fos.seek(curPos); } bis = new BufferedInputStream(con.getInputStream()); while (curPos < endPos) { int len = bis.read(buf, 0, BUFFER_SIZE); if (len == -1) { break; } fos.write(buf, 0, len); curPos = curPos + len; if (curPos > endPos) { // 獲取正確讀取的位元組數 readByte += len - (curPos - endPos) + 1; } else { readByte += len; } } System.out.println("Tread - " + id + " Has the download is complete!"); this.finished = true; bis.close(); fos.close(); } catch (IOException ex) { ex.printStackTrace(); throw new RuntimeException(ex); } } public boolean isFinished() { return finished; } public long getReadByte() { return readByte; } public void setDlTask(DLTask dlTask) { this.dlTask = dlTask; } }

為了達到檔案續傳的目的,我們要把主執行緒類的相關資訊寫到磁碟上,待檔案下載完之後就可以刪除這個檔案,這樣我們就需要一個記錄器,同樣也是執行緒,

在主執行緒中執行,隔3秒儲存主執行緒類的相關資訊,序列化到磁碟上:

package org.app.download.component;

import java.io.File;
import java.math.BigDecimal;

import org.app.download.util.DownUtils;
import org.app.download.util.FileOperation;

/**
 * 
 * @Title: DLListener.java
 * @Description: 儲存當前任務的及時資訊
 * @Package org.app.download.component
 * @author [email protected]
 * @date 2012-8-1
 * @version V1.0
 * 
 */
public class DLListener extends Thread {
	// 當前下載任務
	private DLTask dlTask;
	// 當前記錄器(儲存當前任務的下載物件,用於任務恢復)
	private Recorder recoder;

	DLListener(DLTask dlTask) {
		this.dlTask = dlTask;
		this.recoder = new Recorder(dlTask);
	}

	@Override
	public void run() {
		int i = 0;
		BigDecimal completeTot = null;
		long start = System.currentTimeMillis();
		long end = start;

		while (!dlTask.isComplete()) {
			i++;
			String percent = dlTask.getCurPercent();

			completeTot = new BigDecimal(dlTask.getCompletedTot());

			end = System.currentTimeMillis();
			if (end - start > 1000) {
				BigDecimal pos = new BigDecimal(((end - start) / 1000) * 1024);
				System.out.println("Speed :" + completeTot.divide(pos, 0, BigDecimal.ROUND_HALF_EVEN) + "k/s   " + percent + "% completed. ");
			}
			recoder.record();
			try {
				sleep(3000);
			} catch (InterruptedException ex) {
				ex.printStackTrace();
				throw new RuntimeException(ex);
			}

		}

		// 計算下載花費時間
		int costTime = +(int) ((System.currentTimeMillis() - start) / 1000);
		dlTask.setCostTime(costTime);
		String time = DownUtils.changeSecToHMS(costTime);

		// 對檔案重新命名
		dlTask.getFile().renameTo(new File(dlTask.getFilename()));
		System.out.println("Download finished. " + time);

		// 刪除記錄物件狀態的檔案
		String tskFileName = dlTask.getFilename() + ".tsk";
		try {
			FileOperation.delete(tskFileName);
		} catch (Exception e) {
			System.out.println("Delete tak file fail!");
			e.printStackTrace();
		}
	}
}

最後我們要做的就是啟動main方法建立下載的資料夾,判斷是否下載過,呼叫主執行緒等等:

package org.app.download.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.app.download.component.DLTask;
import org.app.download.util.FileOperation;

/**
 * 下載核心引擎類
 * 
 * @Title: Engine.java
 * @Description: org.app.download.core
 * @Package org.app.download.core
 * @author [email protected]
 * @date 2012-7-31
 * @version V1.0
 * 
 */
public class EngineCore {
	// 下載最大執行緒數
	private static final int MAX_DLINSTANCE_QUT = 10;
	// 下載任務物件
	private DLTask[] dlTask;
	// 執行緒池
	public static ExecutorService pool = Executors.newCachedThreadPool();

	public DLTask[] getDlTask() {
		return dlTask;
	}

	public void setDlTask(DLTask[] dlInstance) {
		this.dlTask = dlInstance;
	}

	/**
	 * 建立新下載任務
	 * 
	 * @param threadQut
	 *            執行緒數
	 * @param url
	 *            下載地址
	 * @param path
	 *            檔案存放地址
	 * @param filename
	 *            檔名
	 */
	public void createDLTask(int threadQut, String url, String filePath) {
		DLTask task = new DLTask(threadQut, url, filePath);
		pool.execute(task);
	}

	/**
	 * 斷點續傳下載任務
	 * 
	 * @param threadQut
	 *            執行緒數
	 * @param url
	 *            下載地址
	 * @param path
	 *            檔案存放地址
	 * @param filename
	 *            檔名
	 */
	public void resumeDLTask(int threadQut, String url, String path) {
		ObjectInputStream in = null;
		try {
			in = new ObjectInputStream(new FileInputStream(path + ".tsk"));
			DLTask task = (DLTask) in.readObject();
			pool.execute(task);
		} catch (ClassNotFoundException ex) {
			Logger.getLogger(EngineCore.class.getName()).log(Level.SEVERE, null, ex);
		} catch (IOException ex) {
			Logger.getLogger(EngineCore.class.getName()).log(Level.SEVERE, null, ex);
		} finally {
			try {
				in.close();
			} catch (IOException ex) {
				Logger.getLogger(EngineCore.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
	}

	/**
	 * 啟動下載
	 * 
	 * @param url
	 *            下載地址
	 * @param path
	 *            本地存放目錄
	 * @param threadQut
	 *            執行緒數量
	 * @throws Exception
	 */
	public void startDownLoad(String url, String path, int threadQut) throws Exception {
		// 建立資料夾
		FileOperation.createFolder(path);
		// 獲取當前檔案中所有的檔名
		Map<String, String> map = FileOperation.listFile(path);
		// 檢視下載檔案是否已經下載過
		String filePath = existFile(path + File.separator + url.substring(url.lastIndexOf("/") + 1, url.length()));
		// 儲存下載物件的檔名
		String takFileName = filePath.substring(filePath.lastIndexOf("\\") + 1, filePath.length()) + ".tsk";
		boolean isexist = false;
		for (Map.Entry<String, String> key : map.entrySet()) {
			if (takFileName.equals(key.getKey())) {
				isexist = true;
			}
		}

		if (isexist) {
			System.out.println("Restore download task - taskName is " + takFileName);
			resumeDLTask(threadQut, url, filePath);
		} else {
			System.out.println("start downloading task -taskName is " + takFileName);
			createDLTask(threadQut, url, filePath);
		}
	}

	/**
	 * 檢測檔案是否存在
	 */
	private String existFile(String filePath) {
		File file = new File(filePath);
		int fileCnt = 1;
		while (file.exists()) {
			String fileFirst = filePath.substring(0, filePath.lastIndexOf(".")) + "(" + fileCnt + ")";
			String fileSecond = filePath.substring(filePath.lastIndexOf("."), filePath.length());
			filePath = fileFirst + fileSecond;
			fileCnt++;
			filePath = existFile(filePath);
			break;
		}
		return filePath;
	}

	/**
	 * 主程式入口
	 * 
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
		EngineCore engine = new EngineCore();
		try {
			engine.startDownLoad(args[0], args[1], Integer.parseInt(args[2]));
		} catch (ArrayIndexOutOfBoundsException e) {
			engine.startDownLoad(args[0], args[1], MAX_DLINSTANCE_QUT);
		}
	}
}

最後要說的是,我借鑑了網上一哥們的程式碼,但是下載下來的檔案全是破損,因為那哥們沒有算好檔案的位元組,導致執行緒在現在的時候丟失了位元組,所以檔案破損,

打個比喻:假如檔案有899個位元組,用10執行緒下載 ,每個執行緒就是要下載89.9個位元組,因為位元組是整數,用int存放的話就是89個位元組,每個執行緒丟失了0.9個位元組,

所以檔案就破損了。個人的解決方法就是取模,總位元組減去取模的位元組,讓第十個執行緒多分配幾個下載位元組。

最重要的請大神不要噴我,本人純粹自娛自樂。3Q.

不知道csdn怎樣在文章中打包下載,這裡貼出我google code裡面的下載地址。裡面還有一個生成mybatis程式碼的工具。

地址:http://code.google.com/p/mybatis-generator/downloads/list