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

Java 斷點續傳核心原理與 RandomAccessFile 實戰實現

     編輯:Java 相關知識

斷點續傳本質

核心只有一點:**記錄已傳輸位置,下次從該位置繼續**。類比「遊戲存檔」:玩到第 4 關存檔,下次讀檔直接從第 4 關開始。

HTTP 協議層面實現


客戶端                                    服務端
  |                                          |
  |--- GET /file.zip Range: bytes=0-999 --->|  首次請求前 1000 字節
  |<-- 206 Partial Content, Content-Range -->|
  |                                          |
  |--- GET /file.zip Range: bytes=1000- --->|  斷點續傳:從 1000 字節繼續
  |<-- 206 Partial Content, Content-Range -->|
  |                                          |

關鍵 Header:

  • 請求:Range: bytes=start-end (end 可省略表示到結尾)
  • 響應:206 Partial Content、Content-Range: bytes start-end/total、Accept-Ranges: bytes

Java 核心類:RandomAccessFile

支持隨機讀寫,內部維護文件指針,關鍵方法:

  • seek(long pos):移動指針到指定位置
  • getFilePointer():獲取當前指針位置
  • read(byte[]) / write(byte[]):從指針位置讀寫
  • 模式:"r" 只讀,"rw" 讀寫

完整斷點下載實現 (客戶端)


public class ResumableDownloader {
    
    public void download(String urlStr, File destFile) throws IOException {
        long downloaded = 0;
        if (destFile.exists()) {
            downloaded = destFile.length(); // 已下載長度 = 斷點位置
        }
        
        URL url = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        
        // 關鍵:設置 Range 頭
        if (downloaded > 0) {
            conn.setRequestProperty("Range", "bytes=" + downloaded + "-");
        }
        conn.setRequestProperty("User-Agent", "Mozilla/5.0");
        conn.connect();
        
        int responseCode = conn.getResponseCode();
        if (responseCode == 200) {
            // 服務端不支持 Range,重新下載
            downloaded = 0;
            destFile.delete();
        } else if (responseCode != 206) {
            throw new IOException("Unexpected response: " + responseCode);
        }
        
        // 獲取總長度 (用於進度條)
        String contentRange = conn.getHeaderField("Content-Range"); // bytes 1000-1999/2000
        long total = Long.parseLong(contentRange.substring(contentRange.lastIndexOf('/') + 1));
        
        // RandomAccessFile 定位寫入
        try (RandomAccessFile raf = new RandomAccessFile(destFile, "rw");
             InputStream in = conn.getInputStream()) {
            
            raf.seek(downloaded); // 移動到斷點位置
            
            byte[] buffer = new byte[8192];
            int len;
            long progress = downloaded;
            while ((len = in.read(buffer)) != -1) {
                raf.write(buffer, 0, len);
                progress += len;
                printProgress(progress, total);
            }
        }
        System.out.println("\n下載完成: " + destFile.getAbsolutePath());
    }
    
    private void printProgress(long current, long total) {
        int percent = (int) (current * 100.0 / total);
        System.out.print("\r[" + "=".repeat(percent/2) + " ".repeat(50-percent/2) + "] " + percent + "%");
    }
}

斷點上傳實現 (客戶端)


public void upload(File file, String uploadUrl) throws IOException {
    long uploaded = queryUploadedFromServer(file); // 需服務端提供查詢接口
    
    try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
        raf.seek(uploaded); // 跳過已上傳部分
        
        HttpURLConnection conn = (HttpURLConnection) new URL(uploadUrl).openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setRequestProperty("Content-Range", "bytes " + uploaded + "-" + (file.length()-1) + "/" + file.length());
        conn.setRequestProperty("Content-Type", "application/octet-stream");
        
        try (OutputStream out = conn.getOutputStream()) {
            byte[] buffer = new byte[8192];
            int len;
            while ((len = raf.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
        // 檢查響應碼 200/201
    }
}

服務端支持要點 (Spring MVC)


@GetMapping("/download/{fileId}")
public ResponseEntity download(@PathVariable String fileId,
                                         @RequestHeader(value = "Range", required = false) String rangeHeader) {
    File file = fileService.getFile(fileId);
    long fileLen = file.length();
    
    long start = 0, end = fileLen - 1;
    if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
        String[] range = rangeHeader.substring(6).split("-");
        start = Long.parseLong(range[0]);
        if (range.length > 1 && !range[1].isEmpty()) end = Long.parseLong(range[1]);
    }
    
    long contentLength = end - start + 1;
    InputStreamResource resource = new InputStreamResource(
        new FileInputStream(file) {{ skip(start); }} // 跳過已發送部分
    );
    
    return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
        .header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileLen)
        .header(HttpHeaders.ACCEPT_RANGES, "bytes")
        .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength))
        .contentLength(contentLength)
        .body(resource);
}

進階:多線程分片下載


public void multiThreadDownload(String url, File dest, int threadCount) throws Exception {
    long fileSize = getFileSize(url);
    long blockSize = fileSize / threadCount;
    
    // 預分配文件空間
    try (RandomAccessFile raf = new RandomAccessFile(dest, "rw")) {
        raf.setLength(fileSize);
    }
    
    ExecutorService pool = Executors.newFixedThreadPool(threadCount);
    CountDownLatch latch = new CountDownLatch(threadCount);
    
    for (int i = 0; i < threadCount; i++) {
        final int idx = i;
        long start = idx * blockSize;
        long end = (idx == threadCount - 1) ? fileSize - 1 : (start + blockSize - 1);
        
        pool.submit(() -> {
            try {
                downloadRange(url, dest, start, end);
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                latch.countDown();
            }
        });
    }
    latch.await();
    pool.shutdown();
    System.out.println("多線程下載完成");
}

private void downloadRange(String urlStr, File dest, long start, long end) throws IOException {
    URL url = new URL(urlStr);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
    
    try (RandomAccessFile raf = new RandomAccessFile(dest, "rw");
         InputStream in = conn.getInputStream()) {
        raf.seek(start);
        byte[] buf = new byte[8192];
        int len;
        while ((len = in.read(buf)) != -1) raf.write(buf, 0, len);
    }
}

關鍵最佳實踐

  1. 持久化斷點信息:文件/數據庫/Redis 存儲 fileId -> downloadedBytes,防止進程重啟丟失
  2. 校驗完整性:下載完成後驗證 MD5/SHA256,分片下載需合併驗證
  3. 臨時文件命名:下載中用 .tmp 後綴,完成後原子重命名
  4. 並發控制:分片下載線程數建議 4-8,避免服務端限流/帶寬爭搶
  5. 斷點查詢接口:上傳場景服務端需提供 GET /upload/status?fileId=xxx 返回已接收字節數

總結

斷點續傳核心三部曲:客戶端記錄位置 → 請求帶 Range 頭 → RandomAccessFile seek 定位讀寫。HTTP 協議原生支持 (Range/206/Content-Range),Java RandomAccessFile 提供底層隨機訪問能力。掌握這套機制,即可實現從簡單下載工具到企業級大文件傳輸系統的完整功能。

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