首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java HTTP 斷點續傳下載實戰:Range 請求與 RandomAccessFile 核心技術

     編輯:Java 程式設計入門

斷點續傳核心原理

HTTP 斷點續傳基於 Range 請求頭 (RFC 7233) 實現:客戶端告知服務端「我已擁有前 N 字節,請從第 N+1 字節開始傳輸」。服務端響應 206 Partial Content 並返回 Content-Range 標識返回片段範圍。

關鍵技術棧

  • HttpURLConnection:發送 Range 請求、讀取響應流
  • RandomAccessFile:支持文件任意位置讀寫 (seek/write)
  • .tmp 臨時文件:標識未完成下載,完成後重命名
  • Content-Length:獲取文件總大小,驗證完整性

完整實現代碼


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();
        }
    }
}

核心代碼逐行解析

1. Range 請求頭構造


conn.setRequestProperty("Range", "bytes=" + start + "-");
// 含義:請求從第 start 字節到文件結尾
// 也可指定範圍:bytes=1000-1999 (請求第 1000-1999 字節)

2. 響應碼處理

狀態碼含義處理
200 OK服務器不支持 Range,返回全文件視為不支持續傳,重新下載
206 Partial Content支持 Range,返回片段正常續傳流程
416 Range Not Satisfiable請求範圍無效 (超過文件大小)文件已完整或邏輯錯誤

3. RandomAccessFile 關鍵操作


// "rw" 模式:可讀可寫
RandomAccessFile raf = new RandomAccessFile(tempFile, "rw");

// 定位文件指針到已下載位置
raf.seek(downloaded);

// 寫入數據 (追加模式)
raf.write(buffer, 0, len);

生產級增強功能

1. 多線程分塊下載 (提速關鍵)


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();
    }
}

2. 進度持久化 (支援重啟恢復)


// 下載前讀取進度文件
// 下載中定期保存進度 (每 1MB 或 5 秒)
// 進度文件格式: JSON { "url": "...", "downloaded": 12345, "total": 67890, "blocks": [{"start":0,"end":1000,"done":true},...] }

3. 校驗完整性 (MD5/SHA256)


// 下載完成後計算文件哈希,與服務器提供的 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());

4. 限速控制


// 令牌桶 / 漏桶算法限制下載速度
// 適合後台下載不佔用帶寬場景
RateLimiter limiter = RateLimiter.create(1024 * 1024); // 1MB/s
limiter.acquire(len); // 寫入前獲取令牌

常見問題排查

❌ 服務器返回 200 而非 206

  • 服務器不支持 Range (如某些 CDN、對象存儲默認關閉)
  • 解決:聯繫運維開啟 Range 支持,或放棄續傳改用完整下載

❌ RandomAccessFile 寫入數據損壞

  • 多線程下載時區塊邊界計算錯誤 (off-by-one)
  • 緩衝區未及時刷新導致掉電丟數據 → 關鍵節點調用 raf.getFD().sync()

❌ 大文件 (>2GB) 32 位系統溢出

  • 使用 long 而非 int 存儲文件大小、偏移量
  • Java NIO FileChannel 替代 RandomAccessFile 獲得更好大文件支持

❌ HTTPS 證書驗證失敗


// 測試環境信任所有證書 (生產嚴禁)
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());

現代替代方案

  • Apache HttpClient 5:更豐富的 API、連接池、自動重試、攔截器鏈
  • OkHttp:現代 HTTP 客戶端,支持 HTTP/2、連接復用、透明 GZIP
  • Resumable Download Libraries:PRDownloader (Android)、axel (CLI) 等成熟庫

總結

Java 實現斷點續傳的核心在於 Range: bytes=start- 請求頭與 RandomAccessFile.seek/write 的配合。掌握單線程續傳基礎後,可向多線程分塊、進度持久化、完整性校驗、限速控制等生產級特性演進。建議實際項目中優先選用成熟的 HTTP 客戶端庫,專注於業務邏輯而非底層協議處理。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved