java 檔案下載 斷點續傳
阿新 • • 發佈:2018-11-06
最近閒來無事,因為公司遮蔽了迅雷軟體的下載埠,所以自己寫了一個下載工具。拿過來分享下。
下載網路上的檔案肯定不能只用單執行緒下載,這樣下載太慢,網速得不多合理利用。那麼就應該用多執行緒下載和執行緒池排程執行緒。
所以我們要講檔案切分成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