Java顺序解压缩GZIP流

时间:2017-04-08 19:32:18

标签: java gzip zlib chunks

我的Java程序实现了一个服务器,它应该通过websockets从客户端获取一个非常大的文件,使用gzip压缩,并且应该检查文件内容中的某些字节模式。

客户端发送嵌入在专有协议中的文件块,因此我收到来自客户端的消息后的消息,解析消息并提取gzip压缩文件内容。

我无法将整个文件保存在程序存储器中,因此我尝试解压缩每个块,处理数据并继续下一个块。

我正在使用以下代码:

public static String gzipDecompress(byte[] compressed) throws IOException {
    String uncompressed;
    try (
        ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
        GZIPInputStream gis = new GZIPInputStream(bis);
        Reader reader = new InputStreamReader(gis);
        Writer writer = new StringWriter()
    ) {

      char[] buffer = new char[10240];
      for (int length = 0; (length = reader.read(buffer)) > 0; ) {
        writer.write(buffer, 0, length);
      }
      uncompressed = writer.toString();
    }

    return uncompressed;
  }

但是在使用第一个压缩块调用函数时,我得到以下异常:

java.io.EOFException: Unexpected end of ZLIB input stream
    at java.util.zip.InflaterInputStream.fill(InflaterInputStream.java:240)
    at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:158)
    at java.util.zip.GZIPInputStream.read(GZIPInputStream.java:117)
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
    at java.io.InputStreamReader.read(InputStreamReader.java:184)
    at java.io.Reader.read(Reader.java:140)

重要的是要提到我没有跳过任何块并尝试按顺序解压缩块。

我错过了什么?

2 个答案:

答案 0 :(得分:2)

问题是您手动使用这些块。

正确的方法是获取一些InputStream,用GZIPInputStream换行,然后读取数据。

    InputStream is = // obtain the original gzip stream

    GZIPInputStream gis = new GZIPInputStream(is);
    Reader reader = new InputStreamReader(gis);

    //... proceed reading and so on

GZIPInputStream以流方式工作,因此如果您只从reader一次询问10kb,则无论初始GZIP文件的大小如何,总内存占用量都会很低。

问题更新后更新

针对您的情况的一种可能的解决方案是编写一个InputStream实现,该实现会流式传输由客户端协议处理程序以块的形式放入的字节。

这是一个原型:

public class ProtocolDataInputStream extends InputStream {
    private BlockingQueue<byte[]> nextChunks = new ArrayBlockingQueue<byte[]>(100);
    private byte[] currentChunk = null;
    private int currentChunkOffset = 0;
    private boolean noMoreChunks = false;

    @Override
    public synchronized int read() throws IOException {
        boolean takeNextChunk = currentChunk == null || currentChunkOffset >= currentChunk.length;
        if (takeNextChunk) {
            if (noMoreChunks) {
                // stream is exhausted
                return -1;
            } else {
                currentChunk = nextChunks.take();
                currentChunkOffset = 0;
            }
        }
        return currentChunk[currentChunkOffset++];
    }

    @Override
    public synchronized int available() throws IOException {
        if (currentChunk == null) {
            return 0;
        } else {
            return currentChunk.length - currentChunkOffset;
        }
    }

    public synchronized void addChunk(byte[] chunk, boolean chunkIsLast) {
        nextChunks.add(chunk);
        if (chunkIsLast) {
            noMoreChunks = true;
        }
    }
}

您的客户端协议处理程序使用addChunk()添加字节块,而您的解压缩代码将数据从此流中提取出来(通过Reader)。

请注意,此代码存在一些问题:

  1. 正在使用的队列大小有限。如果过于频繁地调用addChunk(),则可能会填充队列,这将阻止addChunk()。这可能是可取的或不是。
  2. 仅为了说明目的而实施read()方法。为了提高性能,最好以相同的方式实现read(byte[])
  3. 在读者(解压缩器)和编写器(协议处理程序调用{​​{1}})是不同的线程的假设下使用保守同步化。
  4. addChunk()未在InterruptedException上处理以避免过多细节。
  5. 如果您的解压缩程序和take()在同一个线程中执行(在同一个循环中),那么在使用addChunk()或{{1}进行提取时,您可以尝试使用InputStream.available()方法用InputStream拉动时。

答案 1 :(得分:0)

来自gzip流的任意字节序列不是有效的独立gzip数据。无论如何,您必须连接所有字节块。

最简单的方法是使用简单的管道累计它们:

import java.io.PipedOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;

public class ChunkInflater {
    private final PipedOutputStream pipe;

    private final InputStream stream;

    public ChunkInflater()
    throws IOException {
        pipe = new PipedOutputStream();
        stream = new GZIPInputStream(new PipedInputStream(pipe));
    }

    public InputStream getInputStream() {
        return stream;
    }

    public void addChunk(byte[] compressedChunk)
    throws IOException {
        pipe.write(compressedChunk);
    }
}

现在您有一个InputStream,您可以按照您想要的任何增量读取。例如:

ChunkInflater inflater = new ChunkInflater();

Callable<Void> chunkReader = new Callable<Void>() {
    @Override
    public Void call()
    throws IOException {
        byte[] chunk;
        while ((chunk = readChunkFromSource()) != null) {
            inflater.addChunk(chunk);
        }

        return null;
    }
};
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(chunkReader);
executor.shutdown();

Reader reader = new InputStreamReader(inflater.getInputStream());
// read text here