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

Java 實現 HTTP 多執行緒斷點續傳下載器(核心原理與關鍵程式碼解析)

     編輯:Java 程式設計入門

整體架構設計

  1. 連接資源伺服器:取得檔案總長度(Content-Length),建立本地檔案預分配空間。
  2. 切分資源、多執行緒下載:將總長度均分 N 段,每段啟動一執行緒,透過 HTTP Range 標頭請求對應位元組區間。
  3. 斷點續傳:即時記錄每執行緒已下載位元組數(curPos),序列化整個任務物件至磁碟;重啟時反序列化恢復,續傳邏輯僅將 startPos 替換為 curPos。
  4. 進度監聽與速率統計:獨立監聽執行緒定期序列化任務、計算並輸出平均下載速率。

核心類別設計

1. 下載任務物件(可序列化)


public class DLTask implements Serializable {
    private static final long serialVersionUID = 1L;

    private URL url;
    private File file;                    // 臨時檔 (.tmp)
    private String filename;              // 最終檔名
    private int threadCount = 10;         // 執行緒數
    private int contentLength;            // 總長度
    private long completedTotal;          // 已完成總位元組
    private boolean isNewTask = true;     // 是否全新任務
    private DLThread[] dlThreads;         // 子執行緒陣列
    private transient DLListener listener; // 監聽器(不序列化)

    // getter/setter 略
}

2. 下載執行緒


public class DLThread implements Runnable {
    private DLTask task;
    private int id;
    private long startPos, endPos, curPos;
    private long readBytes;
    private boolean finished;
    private static final int BUFFER_SIZE = 4096;

    public DLThread(DLTask task, int id, long startPos, long endPos) { ... }

    @Override
    public void run() {
        try (RandomAccessFile raf = new RandomAccessFile(task.getFile(), "rw")) {
            URLConnection conn = task.getUrl().openConnection();
            conn.setAllowUserInteraction(true);

            // 斷點續傳關鍵:isNewTask 決定起始位置
            if (task.isNewTask()) {
                conn.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
                raf.seek(startPos);
                curPos = startPos;
            } else {
                conn.setRequestProperty("Range", "bytes=" + curPos + "-" + endPos);
                raf.seek(curPos);
            }

            try (BufferedInputStream bis = new BufferedInputStream(conn.getInputStream())) {
                byte[] buf = new byte[BUFFER_SIZE];
                int len;
                while (curPos < endPos && (len = bis.read(buf)) != -1) {
                    raf.write(buf, 0, len);
                    curPos += len;
                    readBytes += len;
                }
            }
            finished = true;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

3. 監聽執行緒:持久化 + 速率統計


public class DLListener implements Runnable {
    private DLTask task;
    private long startTime;

    public DLListener(DLTask task) {
        this.task = task;
        this.startTime = System.currentTimeMillis();
    }

    @Override
    public void run() {
        while (!task.isComplete()) {
            long completed = task.getCompletedTotal();
            long elapsedSec = (System.currentTimeMillis() - startTime) / 1000;
            double speedKB = elapsedSec > 0 ? (completed / 1024.0) / elapsedSec : 0;
            System.out.printf("Speed: %.1f KB/s  %.1f%%%n", speedKB, task.getCurPercent());

            persistTask();  // 序列化任務到 .tsk 檔
            try { Thread.sleep(3000); } catch (InterruptedException ignored) {}
        }
        // 完成:重命名 .tmp → 正式檔名
        task.getFile().renameTo(new File(task.getFilename()));
        System.out.println("下載完成,耗時 " + formatTime(task.getCostTime()));
    }

    private void persistTask() {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(task.getFilename() + ".tsk"))) {
            oos.writeObject(task);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4. 任務恢復入口


private void resumeTask() {
    // 反序列化 .tsk 檔還原 DLTask 物件
    try (ObjectInputStream ois = new ObjectInputStream(
            new FileInputStream(filename + ".tsk"))) {
        task = (DLTask) ois.readObject();
    } catch (IOException | ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
    task.setNewTask(false);  // 標記為續傳
    task.setListener(new DLListener(task));

    // 重新啟動所有執行緒
    for (DLThread t : task.getDlThreads()) {
        t.setTask(task);
        threadPool.execute(t);
    }
    threadPool.execute(task.getListener());
}

關鍵技術點總結

技術點實現方式
HTTP 斷點續傳Range: bytes=start-end 請求標頭(RFC 7233)
隨機寫入檔案RandomAccessFile.seek(pos) 定位寫入位置
任務狀態持久化Java 序列化 ObjectOutputStream/InputStream 整個 DLTask 物件圖
執行緒管理線程池 ExecutorService 統一調度
臨時檔命名下載中用 .tmp,完成後 renameTo() 原子改名

生產級擴充建議

  • 校驗完整性:下載完成後驗證 MD5/SHA-256。
  • 限速控制:Token Bucket 或 RateLimiter(Guava)於寫入迴圈限流。
  • 多來源下載:支援多鏡像 URL,分段並行下載合併。
  • 介面解耦:定義 DownloadListener 回調介面,支援 Swing/JavaFX/Web 前端進度顯示。
  • 錯誤重試:指數退避重試、失效分片標記重新調度。
  • 序列化替代:JSON (Jackson/Gson) 或 Protobuf 替代 Java 序列化,跨語言、版本演進更安全。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved