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

Java 實現 HTTP 斷點續傳下載:支持 Range 請求、多線程下載工具兼容

     編輯:Java 相關知識

核心原理:HTTP Range 請求

斷點續傳核心是 HTTP/1.1 Range 標頭,客戶端聲明需下載的字節範圍,服務端返回 206 Partial Content 及對應數據塊。常見範圍格式:

  • bytes=0-499:前 500 字節
  • bytes=500-:第 500 字節到結尾
  • bytes=-500:最後 500 字節
  • bytes=0-499,600-1099:多範圍(本文不支持)

架構設計:三層分離

  1. FileIoServlet:入口 Servlet,解析參數、獲取文件、輸出流
  2. FileUtil:Range 解析、響應頭設置
  3. RangeSettings:範圍參數封裝實體
  4. IoUtil:MIME 類型映射、UTF-8 文件名編碼

核心代碼實現

1. Servlet 入口

public class FileIoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        download(req, resp);
    }

    private void download(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        File file = getFile(req); // 解析 uri 參數獲取文件
        long startPos = FileUtil.headerSetting(file, req, resp); // 處理 Range,返回起始位置
        
        try (RandomAccessFile raf = new RandomAccessFile(file, "r");
             BufferedOutputStream out = new BufferedOutputStream(resp.getOutputStream())) {
            
            raf.seek(startPos);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = raf.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            out.flush();
        } catch (IOException e) {
            // 客戶端斷開連接等忽略
        }
    }

    private File getFile(HttpServletRequest req) throws UnsupportedEncodingException {
        String uri = URLDecoder.decode(req.getParameter("uri"), "UTF-8");
        if (uri.startsWith("file://")) return new File(uri.substring(7));
        if (uri.startsWith("hbase://")) return new File(new URI(uri));
        throw new RuntimeException("非法 URI 協議");
    }
}

2. Range 解析與響應頭設置

public class FileUtil {
    public static long headerSetting(File file, HttpServletRequest req, HttpServletResponse resp) {
        long fileLen = file.length();
        String rangeHeader = req.getHeader("Range");
        
        if (rangeHeader == null) { // 全量下載
            setResponse(new RangeSettings(fileLen), file.getName(), resp);
            return 0;
        }
        
        String range = rangeHeader.replace("bytes=", "");
        RangeSettings settings = parseRange(fileLen, range);
        setResponse(settings, file.getName(), resp);
        return settings.getStart();
    }

    private static RangeSettings parseRange(long fileLen, String range) {
        long start, end, contentLength;
        if (range.startsWith("-")) { // -500 最後 N 字節
            contentLength = Long.parseLong(range.substring(1));
            end = fileLen - 1;
            start = fileLen - contentLength;
        } else if (range.endsWith("-")) { // 500- 從 N 到結尾
            start = Long.parseLong(range.replace("-", ""));
            end = fileLen - 1;
            contentLength = fileLen - start;
        } else { // 0-499 區間
            String[] parts = range.split("-");
            start = Long.parseLong(parts[0]);
            end = Long.parseLong(parts[1]);
            contentLength = end - start + 1;
        }
        return new RangeSettings(start, end, contentLength, fileLen);
    }

    private static void setResponse(RangeSettings s, String fileName, HttpServletResponse resp) {
        resp.setHeader("Content-Disposition", "attachment; filename=\"" + IoUtil.toUtf8String(fileName) + "\"");
        resp.setContentType(IoUtil.setContentType(fileName));
        
        if (!s.isRange()) { // 全量
            resp.setHeader("Content-Length", String.valueOf(s.getTotalLength()));
        } else { // 斷點
            resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206
            resp.setHeader("Content-Length", String.valueOf(s.getContentLength()));
            resp.setHeader("Content-Range", "bytes " + s.getStart() + "-" + s.getEnd() + "/" + s.getTotalLength());
        }
    }
}

3. 範圍實體類

public class RangeSettings {
    private long start, end, contentLength, totalLength;
    private boolean range;

    public RangeSettings(long totalLength) { // 全量構造
        this.totalLength = totalLength;
        this.range = false;
    }
    public RangeSettings(long start, long end, long contentLength, long totalLength) {
        this.start = start; this.end = end; this.contentLength = contentLength; this.totalLength = totalLength;
        this.range = true;
    }
    // Getter/Setter 省略...
}

4. MIME 類型與文件名編碼工具

public class IoUtil {
    // 根據副檔名返回 MIME,此處省略完整映射表...
    public static String setContentType(String fileName) { /* ... */ return "application/octet-stream"; }

    /** RFC 5987 編碼,解決中文文件名跨瀏覽器亂碼 */
    public static String toUtf8String(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (c <= 0x7F) sb.append(c); // ASCII 直接保留
            else {
                byte[] bytes = Character.toString(c).getBytes(StandardCharsets.UTF_8);
                for (byte b : bytes) sb.append('%').append(String.format("%02X", b));
            }
        }
        return sb.toString();
    }
}

兼容性驗證

下載工具/瀏覽器斷點續傳多線程備註
Chrome / Firefox / Edge✅❌單線程 Range
迅雷 / IDM / FDM✅✅多範圍請求需額外支持
wget -c / curl -C -✅❌命令行工具

注意:本實現僅支持單範圍 bytes=start-end,多範圍 bytes=0-499,600- 需返回 multipart/byteranges,複雜度較高,實際項目中建議使用成熟庫(如 Spring ResourceHttpRequestHandler)。

生產環境優化建議

  1. 大文件傳輸:啟用 Transfer-Encoding: chunked 或 NIO 零拷貝 (FileChannel.transferTo)
  2. 限速控制:令牌桶算法限制單連接下載速度
  3. 權限校驗:在 getFile() 中集成認證授權邏輯
  4. 緩存頭:添加 ETag/Last-Modified 支持條件請求

此實現經過迅雷 7、Firefox、Chrome 實測,可滿足大多數業務場景的斷點續傳需求。

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