如何使用Java多線程下載網絡文件,並實現斷點續傳
在現代網絡應用中,多線程下載是一種常見的技術,它可以顯著提高下載速度並提供更好的用户體驗。本篇文章將介紹如何使用Java實現多線程下載,並結合項目中的代碼作為示例進行講解。
1. 多線程下載的基本原理
多線程下載的基本思想是將一個文件分成多個部分,每個部分由一個線程獨立下載,最後將這些部分合併成完整的文件。這樣可以充分利用帶寬和計算資源,提高下載速度。
使用Http請求頭的Range字段可以實現文件的分段下載,服務器會根據Range字段返回指定範圍的文件內容。例如,請求頭Range: bytes=0-1023表示獲取文件的前1024字節。
斷點續傳是多線程下載的一個重要功能,它可以在下載中斷後繼續從中斷的地方繼續下載,避免重新下載整個文件。斷點續傳的實現方法是在下載過程中保存下載進度,例如保存已下載的字節數,以便在下次下載時繼續下載。
注意⚠️: 後續示例代碼為了方便閲讀,省略細節處理,只貼出核心代碼。完整代碼請參考文章最後給的項目地址。
2. 創建下載器類
首先,我們需要創建一個下載器類,用於管理下載任務。以下是項目中的Downloader類的基本框架:
public class Downloader {
private String url;
private String fileName;
private int threadCount;
private long fileSize;
private List<DownloadThread> threads = new ArrayList<>();
public Downloader(String url, String fileName, int threadCount) {
this.url = url;
this.fileName = fileName;
this.threadCount = threadCount;
}
public void download() throws Exception {
// 省略具體實現
}
// 其他方法
}
3. 獲取文件大小
在開始下載之前,需要獲取文件的大小,以便確定每個線程下載的範圍。可以使用HttpURLConnection來實現:
public void getFileSize() throws IOException {
URL url = new URL(this.url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("HEAD");
this.fileSize = conn.getContentLengthLong();
conn.disconnect();
}
4. 創建下載線程
接下來,我們需要創建下載線程,每個線程負責下載文件的一部分。以下是DownloadThread類的基本實現:
public class DownloadThread extends Thread {
private String url;
private String fileName;
private long start;
private long end;
public DownloadThread(String url, String fileName, long start, long end) {
this.url = url;
this.fileName = fileName;
this.start = start;
this.end = end;
}
@Override
public void run() {
try {
URL url = new URL(this.url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 主要是這一行代碼,設置Range頭部信息,告訴服務器要獲取文件的哪一部分
conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
InputStream in = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile(this.fileName, "rw");
raf.seek(start);
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
raf.write(buffer, 0, len);
}
raf.close();
in.close();
conn.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5. 啓動下載線程
在Downloader類中,我們需要根據文件大小和線程數量來啓動多個下載線程:
public void download() throws Exception {
getFileSize();
long partSize = fileSize / threadCount;
for (int i = 0; i < threadCount; i++) {
long start = i * partSize;
long end = (i == threadCount - 1) ? fileSize - 1 : (i + 1) * partSize - 1;
DownloadThread thread = new DownloadThread(url, fileName, start, end);
threads.add(thread);
thread.start();
}
for (DownloadThread thread : threads) {
thread.join();
}
}
6. 斷點續傳
在文件下載過程中,斷點續傳功能非常重要。它可以在下載中斷後(例如暫停或網絡中斷)繼續從中斷的地方繼續下載,避免重新下載整個文件。以下是項目中實現斷點續傳的關鍵代碼片段。
1. 檢查文件是否已經下載
在開始下載之前,需要檢查目標文件是否已經存在。如果文件已經存在且下載未完成,則繼續下載:
private void checkFile(File target, File tempFile) throws StoppedException {
if (target.exists()) {
if (!tempFile.exists()) {
System.out.println(target.getAbsoluteFile().getPath() + "文件已經存在,是否覆蓋 ? y/n ");
var scanner = new Scanner(System.in);
var s = scanner.next();
if (!Objects.equals("y", s)) {
throw new StoppedException();
}
return;
}
System.out.println(target.getAbsoluteFile().getPath() + "文件存在,下載未完成,繼續下載");
}
}
2. 設置文件大小和文件名
獲取文件的大小和文件名,並設置目標文件的地址:
private void setTotalAndFileName(DownFileBO downFileBO) throws IOException {
URL url = new URL(downFileBO.getUrl());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
int contentLength = conn.getContentLength();
downFileBO.setTotalSize(contentLength);
total = contentLength;
String fileName = "";
String contentDisposition = conn.getHeaderField("Content-Disposition");
if (contentDisposition != null && contentDisposition.indexOf("=") != -1) {
fileName = contentDisposition.split("=")[1];
} else {
fileName = url.getPath().substring(url.getPath().lastIndexOf("/") + 1);
}
downFileBO.setFileName(fileName);
var targetFolder = FileUtil.getTargetFolder(downFileBO.getTargetLocalPath());
downFileBO.setTargetLocalPath(targetFolder);
conn.disconnect();
}
3. 等待下載完成並獲取下載結果
使用多線程下載文件,並等待所有線程完成下載。期間可以保存臨時文件以記錄下載進度:
private boolean waitDownAndGetResult(List<Future<Boolean>> future, DownFileBO downFileBO,
RandomAccessFile tempRandomAccessFile) throws IOException, InterruptedException {
while (true) {
var finish = future.stream().allMatch(Future::isDone);
if (finish) {
break;
}
saveTempFile(tempRandomAccessFile, downFileBO);
FileUtil.printLog(total, progressSize.get());
Thread.sleep(500 * 1);
}
return future.stream().allMatch(futureItem -> {
try {
return futureItem.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
}
private void saveTempFile(RandomAccessFile tempRandomAccessFile, DownFileBO downFileBO)
throws IOException {
var jsonString = JSON.toJSONString(downFileBO);
var length = jsonString.length();
var tempStr = length + TEMP_LEN_FLAG + jsonString;
tempRandomAccessFile.seek(0);
tempRandomAccessFile.write(tempStr.getBytes());
}
結語
通過以上步驟,我們實現了一個簡單的Java多線程下載器。你可以根據實際需求進行擴展和優化,例如添加下載進度顯示、錯誤處理等功能。
希望這篇文章對你有所幫助!如果你覺得這個文章有用,幫忙點贊、收藏,謝謝!
需要源碼的可以去這裏clone 項目地址