HTTP 斷點續傳基於 Range 請求頭 (RFC 7233) 實現:客戶端告知服務端「我已擁有前 N 字節,請從第 N+1 字節開始傳輸」。服務端響應 206 Partial Content 並返回 Content-Range 標識返回片段範圍。
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
public class ResumeDownloader {
private static final int BUFFER_SIZE = 8192; // 8KB 緩衝區
private final String downloadUrl;
private final File targetFile;
private final File tempFile;
public ResumeDownloader(String downloadUrl, String savePath) {
this.downloadUrl = downloadUrl;
this.targetFile = new File(savePath);
this.tempFile = new File(savePath + ".tmp"); // 臨時文件標識
}
public void download() throws IOException {
// 1. 獲取文件總大小 (HEAD 請求或首次 GET)
long totalSize = getContentLength();
System.out.println("文件總大小: " + formatSize(totalSize));
// 2. 檢查已下載大小
long downloaded = tempFile.exists() ? tempFile.length() : 0;
System.out.println("已下載: " + formatSize(downloaded));
if (downloaded >= totalSize) {
System.out.println("文件已完整,無需下載");
renameTempToTarget();
return;
}
// 3. 斷點續傳核心邏輯
try (RandomAccessFile raf = new RandomAccessFile(tempFile, "rw");
InputStream in = openRangeStream(downloaded)) {
raf.seek(downloaded); // 定位寫入位置
byte[] buffer = new byte[BUFFER_SIZE];
int len;
long startTime = System.currentTimeMillis();
long lastProgressTime = startTime;
long lastDownloaded = downloaded;
while ((len = in.read(buffer)) != -1) {
raf.write(buffer, 0, len);
downloaded += len;
// 進度顯示 (每秒更新一次)
long now = System.currentTimeMillis();
if (now - lastProgressTime >= 1000) {
double speed = (downloaded - lastDownloaded) * 1000.0 / (now - lastProgressTime);
double percent = downloaded * 100.0 / totalSize;
System.out.printf("\r進度: %.2f%% | 速度: %s/s | 已用: %ds",
percent, formatSize((long)speed), (now - startTime) / 1000);
lastProgressTime = now;
lastDownloaded = downloaded;
}
}
System.out.println("\n下載完成!");
}
// 4. 校驗完整性並重命名
if (tempFile.length() == totalSize) {
renameTempToTarget();
System.out.println("文件校驗通過: " + targetFile.getAbsolutePath());
} else {
throw new IOException("文件大小不匹配: 期望 " + totalSize + ", 實際 " + tempFile.length());
}
}
// 獲取文件總長度
private long getContentLength() throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(downloadUrl).openConnection();
conn.setRequestMethod("HEAD");
conn.connect();
long length = conn.getContentLengthLong();
conn.disconnect();
if (length <= 0) throw new IOException("無法獲取文件大小,服務器可能不支持 Range");
return length;
}
// 打開 Range 請求流
private InputStream openRangeStream(long start) throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL(downloadUrl).openConnection();
conn.setRequestProperty("Range", "bytes=" + start + "-"); // 關鍵:從 start 字節開始
conn.setRequestProperty("User-Agent", "Java-ResumeDownloader/1.0");
conn.connect();
int responseCode = conn.getResponseCode();
if (responseCode != 200 && responseCode != 206) {
conn.disconnect();
throw new IOException("服務器不支持斷點續傳,響應碼: " + responseCode);
}
return conn.getInputStream();
}
private void renameTempToTarget() {
if (targetFile.exists()) targetFile.delete();
tempFile.renameTo(targetFile);
}
private String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
if (bytes < 1024 * 1024 * 1024) return String.format("%.2f MB", bytes / (1024.0 * 1024));
return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
}
// 測試主方法
public static void main(String[] args) {
String url = "https://speed.hetzner.de/100MB.bin"; // 測試文件
String savePath = "D:/downloads/100MB.bin";
try {
new ResumeDownloader(url, savePath).download();
} catch (IOException e) {
System.err.println("下載失敗: " + e.getMessage());
e.printStackTrace();
}
}
}
conn.setRequestProperty("Range", "bytes=" + start + "-");
// 含義:請求從第 start 字節到文件結尾
// 也可指定範圍:bytes=1000-1999 (請求第 1000-1999 字節)
| 狀態碼 | 含義 | 處理 |
|---|---|---|
| 200 OK | 服務器不支持 Range,返回全文件 | 視為不支持續傳,重新下載 |
| 206 Partial Content | 支持 Range,返回片段 | 正常續傳流程 |
| 416 Range Not Satisfiable | 請求範圍無效 (超過文件大小) | 文件已完整或邏輯錯誤 |
// "rw" 模式:可讀可寫
RandomAccessFile raf = new RandomAccessFile(tempFile, "rw");
// 定位文件指針到已下載位置
raf.seek(downloaded);
// 寫入數據 (追加模式)
raf.write(buffer, 0, len);
public class MultiThreadDownloader {
private static final int THREAD_COUNT = 4;
public void download(String url, File target) throws Exception {
long totalSize = getContentLength(url);
long blockSize = totalSize / THREAD_COUNT;
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
long start = i * blockSize;
long end = (i == THREAD_COUNT - 1) ? totalSize - 1 : (start + blockSize - 1);
executor.submit(() -> {
try (RandomAccessFile raf = new RandomAccessFile(target, "rw")) {
raf.seek(start);
downloadRange(url, start, end, raf);
} catch (IOException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
}
}
// 下載前讀取進度文件
// 下載中定期保存進度 (每 1MB 或 5 秒)
// 進度文件格式: JSON { "url": "...", "downloaded": 12345, "total": 67890, "blocks": [{"start":0,"end":1000,"done":true},...] }
// 下載完成後計算文件哈希,與服務器提供的 ETag 或單獨接口對比
MessageDigest md5 = MessageDigest.getInstance("MD5");
try (FileInputStream fis = new FileInputStream(targetFile)) {
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
md5.update(buffer, 0, len);
}
}
String fileMd5 = bytesToHex(md5.digest());
// 令牌桶 / 漏桶算法限制下載速度
// 適合後台下載不佔用帶寬場景
RateLimiter limiter = RateLimiter.create(1024 * 1024); // 1MB/s
limiter.acquire(len); // 寫入前獲取令牌
raf.getFD().sync()long 而非 int 存儲文件大小、偏移量FileChannel 替代 RandomAccessFile 獲得更好大文件支持
// 測試環境信任所有證書 (生產嚴禁)
TrustManager[] trustAll = new TrustManager[]{new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAll, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
Java 實現斷點續傳的核心在於 Range: bytes=start- 請求頭與 RandomAccessFile.seek/write 的配合。掌握單線程續傳基礎後,可向多線程分塊、進度持久化、完整性校驗、限速控制等生產級特性演進。建議實際項目中優先選用成熟的 HTTP 客戶端庫,專注於業務邏輯而非底層協議處理。