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

Java 伺服器端串流式 ZIP 打包下載實作:邊壓縮邊傳輸,零等待體驗

     編輯:Java 相關知識

前言

在 Web 開發中,提供大量檔案下載時,傳統做法通常是先在伺服器端完整打包成 ZIP 檔,再讓使用者下載。這不僅佔用大量暫存空間,也會讓使用者在打包過程中白白等待。本文介紹一種「邊打包、邊下載」的串流式實作,利用 HTTP Chunked Transfer Encoding,將壓縮資料即時推送給客戶端,實現零緩衝、零等待的下載體驗。

核心架構

整個流程包含三個關鍵角色:

  • Servlet/Handler:負責建立 HTTP 回應標頭,設定 Transfer-Encoding: chunked,並取得 OutputStream。
  • MyOutputStream:繼承 FilterOutputStream,攔截寫入操作,自動將資料包裝成 Chunk 格式(長度(16進位) + CRLF + 資料 + CRLF)。
  • ZipCompressor:封裝 ZipOutputStream,遞迴遍歷目錄/檔案,將內容寫入上述串流。

關鍵程式碼解析

1. 下載入口點:建立 Chunked 回應

private void down(File file, String fileName) throws IOException {
    OutputStream outputStream = mySocket.getOutputStream();
    StringBuffer sb = new StringBuffer("HTTP/1.1 200 OK\r\n");
    sb.append("Server: java/1.1\r\n");
    sb.append("Content-Type: application/octet-stream;charset=UTF-8\r\n");
    sb.append("Content-Disposition: attachment; filename=" + fileName + "\r\n");
    sb.append("Transfer-Encoding: chunked\r\n");
    sb.append("Connection: Keep-Alive\r\n\r\n");
    outputStream.write(sb.toString().getBytes());
    outputStream.flush();

    // 關鍵:用 MyOutputStream 包裝原始輸出串流
    ZipCompressor zipCompressor = new ZipCompressor(new MyOutputStream(outputStream));
    zipCompressor.compress(file);

    // 發送 Chunked 結束標記:0\r\n\r\n
    outputStream.write("0\r\n\r\n".getBytes());
    outputStream.flush();
    outputStream.close();
    // ... 關閉 socket
}

重點:Transfer-Encoding: chunked 告訴瀏覽器資料分塊傳輸,瀏覽器會邊接收邊寫入磁碟,無需等待完整檔案。

2. MyOutputStream:Chunk 封裝邏輯

public class MyOutputStream extends FilterOutputStream {
    private final byte[] rnBytes = "\r\n".getBytes();

    public MyOutputStream(OutputStream out) { super(out); }

    @Override
    public void write(byte[] b) throws IOException {
        // 長度用 16 進位
        out.write(Integer.toHexString(b.length).getBytes());
        out.write(rnBytes);
        out.write(b);
        out.write(rnBytes);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        out.write(Integer.toHexString(len).getBytes());
        out.write(rnBytes);
        out.write(b, off, len);
        out.write(rnBytes);
    }

    /** 重寫 close() 避免關閉底層串流,否則無法發送結束標記 */
    @Override
    public void close() throws IOException { }
}

3. ZipCompressor:遞迴壓縮寫入

public class ZipCompressor {
    private static final int BUFFER = 8192;
    private final OutputStream outputStream;

    public ZipCompressor(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    public void compress(File file) {
        if (!file.exists()) throw new RuntimeException("檔案不存在:" + file.getAbsolutePath());
        try (CheckedOutputStream cos = new CheckedOutputStream(outputStream, new CRC32());
             ZipOutputStream out = new ZipOutputStream(cos)) {
            compress(file, out, "");
            // 關閉 ZipOutputStream 會寫入 Central Directory,但因為我們重寫了 close(),這裡需手動 finish
            out.finish(); 
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void compress(File file, ZipOutputStream out, String basedir) throws IOException {
        if (file.isDirectory()) {
            compressDirectory(file, out, basedir);
        } else {
            compressFile(file, out, basedir);
        }
    }

    private void compressDirectory(File dir, ZipOutputStream out, String basedir) throws IOException {
        File[] files = dir.listFiles();
        if (files == null) return;
        for (File f : files) {
            compress(f, out, basedir + dir.getName() + "/");
        }
    }

    private void compressFile(File file, ZipOutputStream out, String basedir) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
            ZipEntry entry = new ZipEntry(basedir + file.getName());
            out.putNextEntry(entry);
            byte[] buffer = new byte[BUFFER];
            int count;
            while ((count = bis.read(buffer)) != -1) {
                out.write(buffer, 0, count); // 實際寫入會經由 MyOutputStream 變成 Chunk
            }
        }
    }
}

優缺點與適用場景

優點缺點/注意事項
零暫存檔,不佔伺服器磁碟空間若壓縮過程發生錯誤,客戶端已接收部分資料,難以重試
使用者立即看到下載進度,體驗極佳無法預先得知最終檔案大小,瀏覽器進度條可能顯示「未知大小」
適用於大檔案、大目錄打包下載需確保網路連線穩定,中斷會導致 ZIP 損毀

總結

透過 FilterOutputStream 實作 Chunked 編碼,搭配 ZipOutputStream 的串流特性,即可輕鬆打造「邊打包、邊下載」的高效下載功能。此模式同樣適用於 CSV 匯出、報表產生等需即時串流輸出的場景,是提升使用者體驗的利器。

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