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

Java 實現斷點續傳下載:HttpURLConnection + RandomAccessFile 核心技術詳解

     編輯:Java 相關知識

前言

大文件下載斷點續傳是提升用戶體驗的關鍵功能。本文深入講解基於 HTTP/1.1 Range 請求頭與 RandomAccessFile 隨機讀寫的 Java 實現方案,涵蓋臨時文件管理、進度持久化、異常恢復、完整性校驗等工程級細節。

核心原理

  1. HTTP Range 請求:客戶端發送 Range: bytes=已下載字節數-,服務端響應 206 Partial Content 並返回 Content-Range 與剩餘內容。
  2. 隨機寫入:RandomAccessFile.seek(已下載長度) 定位文件指針,續寫數據塊。
  3. 狀態標識:未完成文件命名為 .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 次,記錄失敗片段

服務端配置要求

  • Nginx/Apache/Tomcat 默認支援 Accept-Ranges: bytes。
  • CDN/對象存儲 (S3/OSS) 通常原生支援 Range 請求。
  • 若服務端不支援 Range (返回 200 而非 206),客戶端需檢測並降級為普通下載 (刪除臨時文件重頭開始)。

常見坑點與解決

  1. Content-Length 為 -1:服務端未返回長度 (如動態生成流),無法斷點續傳,只能流式下載。
  2. 302 重定向丟失 Range:HttpURLConnection 自動跟隨重定向但不攜帶 Range 頭,需手動處理重定向或使用 Apache HttpClient 開啟 setRedirectStrategy。
  3. 大文件 > 2GB:32 位系統 int 溢出,必須用 long 存儲位置與長度 (代碼中已統一用 long)。
  4. 臨時文件殘留:程序異常退出留下 .part 文件,啟動時掃描清理或提示用戶繼續。

總結

Java 斷點續傳核心在於「Range 請求頭 + RandomAccessFile 隨機寫 + 臨時文件狀態機」。掌握上述完整流程與工程細節,即可構建生產級下載組件。進階方向包括多線程分片下載、限速控制、斷點上傳 (HTTP PUT Range) 等,原理同構,可舉一反三。

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