標籤:java 多線程下載 urlconnection randomaccessfile io
多線程下載的原理在於,每個線程下載檔案的一部分,每個線程將自己下載的一部分寫入檔案中它應該的位置,所有線程下載完成時,檔案下載完成。其關鍵點在於:RandomAccessFile.seek(beginIndex)和URLConnection.setRequestProperty("Range", "bytes=" + beginIndex + "-" + endIndex)。
轉載請註明原創地址,請尊重原創,謝謝。
代碼如下,以下代碼copy後可以直接運行,已可直接用於Java或者Android項目中(Android項目中的API版本比較低沒有connection.getContentLengthLong()方法,需要將connection.getContentLengthLong()改成connection.getContentLength()):
import java.io.IOException;import java.io.InputStream;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;import java.util.EventObject;/** * 多線程下載器 * * @author Tang * */public final class MultiThreadDownloader {/** * 測試 * * @param args * @throws IOException */public static void main(String[] args) throws IOException {final MultiThreadDownloader downloader = new MultiThreadDownloader("http://dlsw.baidu.com/sw-search-sp/soft/3a/12350/QQ7.3.15034.0.1434079564.exe","QQInstall.exe");downloader.setDownloadListener(new DownloadListener() {public void undoneDownload(DownloadEvent event) {System.out.println("下載未完成");}public void doneDownload(DownloadEvent event) {System.out.println("下載完成");}public void progressChange(DownloadEvent event) {System.out.println();MultiThreadDownloader downloader = (MultiThreadDownloader) event.getSource();System.out.println("下載進度:" + downloader.getProgress() + "%");System.out.println("已用時:" + (downloader.getDoneTime() / 1000) + "秒");System.out.println("預計仍需用時:" + (downloader.getUndoneTime() / 1000) + "秒");System.out.println("下載速度為:" + (downloader.getDownloadSpeed() * 1000 / 1024 / 1024) + "MB/秒");System.out.println();}});downloader.startDownload();}/** * 多線程下載事件監聽器 */public static interface DownloadListener {/** * 下載完成的通知 */void doneDownload(DownloadEvent event);/** * 下載未完成的通知 */void undoneDownload(DownloadEvent event);/** * 下載進度改變的通知 */void progressChange(DownloadEvent event);}/** * 多線程下載事件來源 */public static class DownloadEvent extends EventObject {private static final long serialVersionUID = 1L;public DownloadEvent(Object source) {super(source);}}/** * 檔案下載的URL路徑 */private final String downloadPath;/** * 檔案下載的URL */private final URL downloadUrl;/** * 保持檔案到本地的檔案路徑 */private final String savePath;/** * RandomAccessFile對象構建的模式,"rwd"表示此檔案可讀可寫可刪 */private final String fileMode = "rwd";/** * 下載檔案時,每次讀取多少個位元組 */private final int bufferArrayInitialCapacity;/** * 啟動多少個線程下載這個檔案 */private final int threadCount;/** * 檔案總位元組大小 */private final long fileContentLength;/** * 已讀取的位元組大小 */private long doneByteLength;/** * 剩餘檔案總位元組大小 */private long undoneByteLength;/** * 開始下載時的時間,單位:毫秒 */private long beginDownloadTime;/** * 已完成所花費的時間,單位:毫秒 */private long doneTime;/** * 預計剩餘檔案下載時間,單位:毫秒 */private long undoneTime;/** * 下載速度,單位:位元組/毫秒 */private double downloadSpeed;/** * 下載進度,值在0~100之間,也就是0<=progress<=100 */private int progress;/** * 是否暫停下載,如果為true則暫停下載,重新設為false則繼續下載 */private boolean isPause;/** * 是否關閉下載,如果為true,下載會被終止 */private boolean isClose;/** * 下載是否完成 */private boolean isDone;/** * 用來存放所有下載檔案的線程 */private final ShareEquallyDownloadThread[] downloadThreads;/** * 下載事件監聽器 */private DownloadListener downloadListener;/** * 下載事件來源,事件來源本來是需要每次建立的,但是這個是事件來源什麼屬性都沒有,所以為了節約記憶體只建立一個 */private final DownloadEvent downloadEvent = new DownloadEvent(this);/** * @param downloadPath * 檔案下載的URL路徑 * @param savePath * 保持檔案到本地的檔案路徑 * @throws IOException */public MultiThreadDownloader(String downloadPath, String savePath) throws IOException {this(downloadPath, savePath, 2048, 10, null);}/** * @param downloadPath * 檔案下載的URL路徑 * @param savePath * 保持檔案到本地的檔案路徑 * @param threadCount * 啟動多少個線程下載這個檔案 * @throws IOException */public MultiThreadDownloader(String downloadPath, String savePath, int threadCount) throws IOException {this(downloadPath, savePath, 2048, threadCount, null);}/** * @param downloadPath * 檔案下載的URL路徑 * @param savePath * 保持檔案到本地的檔案路徑 * @param bufferArrayInitialCapacity * 每次讀取位元組的長度 * @param threadCount * 啟動多少個線程下載這個檔案 * @param downloadListener * 下載監聽器 * @throws IOException */public MultiThreadDownloader(String downloadPath, String savePath, DownloadListener downloadListener) throws IOException {this(downloadPath, savePath, 2048, 10, downloadListener);}/** * @param downloadPath * 檔案下載的URL路徑 * @param savePath * 保持檔案到本地的檔案路徑 * @param bufferArrayInitialCapacity * 每次讀取位元組的長度 * @param threadCount * 啟動多少個線程下載這個檔案 * @param downloadListener * 下載監聽器 * @throws IOException */public MultiThreadDownloader(String downloadPath, String savePath, int bufferArrayInitialCapacity, int threadCount, DownloadListener downloadListener)throws IOException {this.downloadPath = downloadPath;this.savePath = savePath;this.bufferArrayInitialCapacity = bufferArrayInitialCapacity;this.threadCount = threadCount;this.downloadListener = downloadListener;downloadThreads = new ShareEquallyDownloadThread[threadCount];downloadUrl = new URL(downloadPath);HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();int responseCode = connection.getResponseCode();if (responseCode != HttpURLConnection.HTTP_OK) {throw new IOException("URL:" + downloadPath + " responseCode is " + responseCode);}fileContentLength = connection.getContentLengthLong();if (fileContentLength < 0) {throw new IOException("URL:" + downloadPath + " download file content length less than 0!");}if (fileContentLength == 0) {throw new IOException("URL:" + downloadPath + " download file content length equals 0!");}try (RandomAccessFile randomAccessFile = new RandomAccessFile(savePath, fileMode)) {randomAccessFile.setLength(fileContentLength);// 指定建立的檔案的長度} catch (IOException e) {throw e;} finally {connection.disconnect();}// 平均每一個線程下載的檔案的大小。long threadShareEquallyByteLength = fileContentLength / threadCount;for (int i = 0; i < threadCount; i++) {// Java中都是包前不包後的原則,所以endIndex不用減1long beginIndex = i * threadShareEquallyByteLength;// 防止均分有餘數,餘數部分的檔案位元組交給最後一個線程long endIndex = i < (threadCount - 1) ? beginIndex + threadShareEquallyByteLength : fileContentLength;downloadThreads[i] = new ShareEquallyDownloadThread(beginIndex, endIndex);}}/** * 負責下載和儲存檔案中的一部分的線程 */private final class ShareEquallyDownloadThread extends Thread {private final long beginIndex;private final long endIndex;private boolean isDone;private HttpURLConnection connection;private RandomAccessFile randomAccessFile;public ShareEquallyDownloadThread(long beginIndex, long endIndex) {this.beginIndex = beginIndex;this.endIndex = endIndex;}@Overridepublic void run() {try {connection = (HttpURLConnection) downloadUrl.openConnection();connection.setRequestProperty("Range", "bytes=" + beginIndex + "-" + endIndex);// 下載一部分檔案,包前不包後try (InputStream inputStream = connection.getInputStream(); RandomAccessFile randomAccessFile = new RandomAccessFile(savePath, fileMode)) {this.randomAccessFile = randomAccessFile;randomAccessFile.seek(beginIndex);// 跳至指定的檔案位置byte[] bufferBytes = new byte[bufferArrayInitialCapacity];int readByteLength = inputStream.read(bufferBytes);// 讀while (readByteLength > 0 && !isClose) {if (isPause) {// 暫停continue;}randomAccessFile.write(bufferBytes, 0, readByteLength);// 寫doneTime = System.currentTimeMillis() - beginDownloadTime;// 統計用時// 每次將讀取的的位元組長度累加記錄下來doneByteLength += readByteLength;undoneByteLength = fileContentLength - doneByteLength;// 計算出已下載的檔案佔總檔案的百分比int percentCompleted = (int) (doneByteLength * 100.0 / fileContentLength);percentCompleted = Math.min(Math.max(percentCompleted, 0), 100);updateProgress(percentCompleted);// 更新進度readByteLength = inputStream.read(bufferBytes);// 繼續讀}isDone = true;if (downloadListener != null && isDone()) {if (isClose || isPause) {downloadListener.undoneDownload(downloadEvent);} else {downloadListener.doneDownload(downloadEvent);}}} catch (Exception e) {throw new RuntimeException(e);} finally {connection.disconnect();}} catch (IOException e) {throw new RuntimeException(e);}}/** * 強制銷毀串連和關閉檔案流 */public void destroyConnectionAndCloseStream() {if (connection != null) {connection.disconnect();try {connection.getInputStream().close();} catch (Exception e) {e.printStackTrace();}}if (randomAccessFile != null) {try {randomAccessFile.close();} catch (IOException e) {e.printStackTrace();}}}}/** * 計算下載速度和剩餘下載時間 */private void updateDownloadSpeedAndUndoneTime() {downloadSpeed = doneByteLength / doneTime * 1.0;undoneTime = (long) (undoneByteLength / downloadSpeed);}/** * 更新進度 * * @param progress */private void updateProgress(int progress) {if (this.progress != progress) {this.progress = progress;updateDownloadSpeedAndUndoneTime();if (downloadListener != null) {downloadListener.progressChange(downloadEvent);}}}/** * 開始下載 */public void startDownload() {beginDownloadTime = System.currentTimeMillis();for (int i = 0; i < downloadThreads.length; i++) {downloadThreads[i].start();}}public String getDownloadPath() {return downloadPath;}/** * 擷取檔案總位元組大小 * * @return */public long getFileContentLength() {return fileContentLength;}public String getSavePath() {return savePath;}public int getBufferArrayInitialCapacity() {return bufferArrayInitialCapacity;}public int getThreadCount() {return threadCount;}/** * 檔案下載是否全部完成 * * @return */public boolean isDone() {if (isDone) {return isDone;}for (int i = 0; i < downloadThreads.length; i++) {if (!downloadThreads[i].isDone) {return false;}}return true;}/** * 獲得下載進度,值在0~100之間,也就是0<=progress<=100 * * @return */public int getProgress() {return progress;}/** * 暫停下載 */public void pauseDownload() {this.isPause = true;}/** * 恢複下載 */public void restoreDownload() {this.isPause = false;}public boolean isPause() {return isPause;}/** * 關閉下載,下載會被終止 */public void closeDownload() {if (isClose) {return;}isClose = true;for (int i = 0; i < downloadThreads.length; i++) {downloadThreads[i].destroyConnectionAndCloseStream();}}public boolean isClose() {return isClose;}public URL getDownloadUrl() {return downloadUrl;}/** * 擷取已讀取的位元組大小 * * @return */public long getDoneByteLength() {return doneByteLength;}/** * 擷取剩餘檔案總位元組大小 * * @return */public long getUndoneByteLength() {return undoneByteLength;}/** * 擷取已完成所花費的時間,單位:毫秒 * * @return */public long getDoneTime() {return doneTime;}/** * 擷取剩餘檔案下載時間,單位:毫秒 * * @return */public long getUndoneTime() {return undoneTime;}/** * 擷取下載速度,單位:位元組/毫秒 * * @return */public double getDownloadSpeed() {return downloadSpeed;}/** * 擷取下載事件監聽器 * * @return */public DownloadListener getDownloadListener() {return downloadListener;}/** * 設定下載事件監聽器 * * @return */public void setDownloadListener(DownloadListener downloadListener) {this.downloadListener = downloadListener;}}
Java之多線程下載