大文件下載斷點續傳是提升用戶體驗的關鍵功能。本文深入講解基於 HTTP/1.1 Range 請求頭與 RandomAccessFile 隨機讀寫的 Java 實現方案,涵蓋臨時文件管理、進度持久化、異常恢復、完整性校驗等工程級細節。
Range: bytes=已下載字節數-,服務端響應 206 Partial Content 並返回 Content-Range 與剩餘內容。RandomAccessFile.seek(已下載長度) 定位文件指針,續寫數據塊。.tmp 或 .part,完成後重命名為目標文件名。import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
public class ResumeDownloader {
private static final int BUFFER_SIZE = 8192;
public static void download(String urlStr, String destPath) throws IOException {
File destFile = new File(destPath);
File tmpFile = new File(destPath + ".part"); // 臨時文件
long downloaded = 0;
// 1. 檢查臨時文件是否存在,獲取已下載長度
if (tmpFile.exists()) {
downloaded = tmpFile.length();
System.out.println("檢測到未完成下載,續傳位置: " + downloaded + " bytes");
}
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
conn.setRequestProperty("Accept", "*/*");
// 2. 設置 Range 請求頭
if (downloaded > 0) {
conn.setRequestProperty("Range", "bytes=" + downloaded + "-");
}
// 3. 獲取總大小 (首次請求或 HEAD 請求)
long totalSize;
if (downloaded == 0) {
// 首次請求獲取 Content-Length
conn.connect();
totalSize = conn.getContentLengthLong();
System.out.println("文件總大小: " + totalSize + " bytes");
} else {
// 續傳請求從 Content-Range 解析總大小
// Content-Range: bytes 1024-2047/5000 -> 總大小 5000
String contentRange = conn.getHeaderField("Content-Range");
if (contentRange != null && contentRange.contains("/")) {
totalSize = Long.parseLong(contentRange.substring(contentRange.lastIndexOf('/') + 1));
} else {
// 回退:重新發 HEAD 請求
HttpURLConnection headConn = (HttpURLConnection) url.openConnection();
headConn.setRequestMethod("HEAD");
totalSize = headConn.getContentLengthLong();
headConn.disconnect();
}
}
// 4. 打開隨機訪問文件流 (rw 模式)
try (RandomAccessFile raf = new RandomAccessFile(tmpFile, "rw");
InputStream in = conn.getInputStream()) {
raf.seek(downloaded); // 定位寫入位置
byte[] buffer = new byte[BUFFER_SIZE];
int len;
long startTime = System.currentTimeMillis();
// 5. 循環讀寫
while ((len = in.read(buffer)) != -1) {
raf.write(buffer, 0, len);
downloaded += len;
// 可選:進度回調/打印
if (downloaded % (1024 * 1024) == 0) { // 每 1MB 打印一次
double speed = downloaded / 1024.0 / 1024 / ((System.currentTimeMillis() - startTime) / 1000.0);
System.out.printf("進度: %.2f%% (%.2f MB/s)%n",
downloaded * 100.0 / totalSize, speed);
}
}
}
// 6. 校驗完整性並重命名
if (tmpFile.length() == totalSize) {
if (destFile.exists()) destFile.delete();
tmpFile.renameTo(destFile);
System.out.println("下載完成: " + destFile.getAbsolutePath());
} else {
throw new IOException("文件不完整: 預期 " + totalSize + " 實際 " + tmpFile.length());
}
}
public static void main(String[] args) {
try {
download("https://example.com/largefile.zip", "D:/downloads/largefile.zip");
} catch (IOException e) {
e.printStackTrace();
}
}
}
| 環節 | 最佳實踐 |
|---|---|
| 連接復用 | 首次 HEAD 請求獲取總大小 + 支援 Range,再發 GET Range 請求,避免重複握手 |
| 緩衝區大小 | 8KB~64KB 平衡內存與系統調用開銷 |
| 進度持久化 | 定期將 downloaded 寫入單獨 .progress 文件或數據庫,防止進程崩潰丟失進度 |
| 多線程分片 | 將文件分為 N 段,各線程下載不同 Range,最後合併 (需服務端支援多 Range) |
| 校驗機制 | 下載完成後計算 MD5/SHA-256 與服務端提供值比對 |
| 錯誤重試 | 網絡異常時指數退避重試 3 次,記錄失敗片段 |
Accept-Ranges: bytes。HttpURLConnection 自動跟隨重定向但不攜帶 Range 頭,需手動處理重定向或使用 Apache HttpClient 開啟 setRedirectStrategy。int 溢出,必須用 long 存儲位置與長度 (代碼中已統一用 long)。Java 斷點續傳核心在於「Range 請求頭 + RandomAccessFile 隨機寫 + 臨時文件狀態機」。掌握上述完整流程與工程細節,即可構建生產級下載組件。進階方向包括多線程分片下載、限速控制、斷點上傳 (HTTP PUT Range) 等,原理同構,可舉一反三。