Java中是否可以使用InputStream和OutputStream抽象来进行Deflate(ZIP)实时压缩?

4

我目前正在尝试编写一个自定义流代理(我们称之为这样),它可以更改给定输入流的内容并生成修改后的输出(如果需要)。这个要求非常必要,因为有时我需要在我的应用程序中修改流(例如,在飞行中真正压缩数据)。下面的类相当简单,并且使用内部缓冲。

private static class ProxyInputStream extends InputStream {

    private final InputStream iStream;
    private final byte[] iBuffer = new byte[512];

    private int iBufferedBytes;

    private final ByteArrayOutputStream oBufferStream;
    private final OutputStream oStream;

    private byte[] oBuffer = emptyPrimitiveByteArray;
    private int oBufferIndex;

    ProxyInputStream(InputStream iStream, IFunction<OutputStream, ByteArrayOutputStream> oStreamFactory) {
        this.iStream = iStream;
        oBufferStream = new ByteArrayOutputStream(512);
        oStream = oStreamFactory.evaluate(oBufferStream);
    }

    @Override
    public int read() throws IOException {
        if ( oBufferIndex == oBuffer.length ) {
            iBufferedBytes = iStream.read(iBuffer);
            if ( iBufferedBytes == -1 ) {
                return -1;
            }
            oBufferIndex = 0;
            oStream.write(iBuffer, 0, iBufferedBytes);
            oStream.flush();
            oBuffer = oBufferStream.toByteArray();
            oBufferStream.reset();
        }
        return oBuffer[oBufferIndex++];
    }

}

假设我们还有一个示例测试输出流,它会在每个写入的字节前添加一个空格字符("abc" -> " a b c"),就像这样:

private static class SpacingOutputStream extends OutputStream {

    private final OutputStream outputStream;

    SpacingOutputStream(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public void write(int b) throws IOException {
        outputStream.write(' ');
        outputStream.write(b);
    }

}

以下是测试方法:

private static void test(final boolean useDeflater) throws IOException {
    final FileInputStream input = new FileInputStream(SOURCE);
    final IFunction<OutputStream, ByteArrayOutputStream> outputFactory = new IFunction<OutputStream, ByteArrayOutputStream>() {
        @Override
        public OutputStream evaluate(ByteArrayOutputStream outputStream) {
            return useDeflater ? new DeflaterOutputStream(outputStream) : new SpacingOutputStream(outputStream);
        }
    };
    final InputStream proxyInput = new ProxyInputStream(input, outputFactory);
    final OutputStream output = new FileOutputStream(SOURCE + ".~" + useDeflater);
    int c;
    while ( (c = proxyInput.read()) != -1 ) {
        output.write(c);
    }
    output.close();
    proxyInput.close();
}

这个测试方法只是读取文件内容并将其写入另一个流中,可能可以进行某种修改。如果测试方法以useDeflater=false运行,则预期的方法可以正常工作。但是,如果以设置useDeflater的方式调用测试方法,则它会表现出非常奇怪的行为,并且几乎什么都不写(如果省略头部78 9C)。我怀疑deflater类可能不是设计成满足我想使用的方法,但我一直认为ZIP格式和deflate压缩被设计为实时工作。
也许我在某些特定于deflate压缩算法的细节上错了。我真正错过了什么?也许可以采用另一种方法编写“流代理”以完全按照我想要的方式工作...如何在仅限于流的情况下压缩数据?
提前感谢你的帮助。
更新:以下基本版本与deflater和inflater非常配合。
public final class ProxyInputStream<OS extends OutputStream> extends InputStream {

private static final int INPUT_BUFFER_SIZE = 512;
private static final int OUTPUT_BUFFER_SIZE = 512;

private final InputStream iStream;
private final byte[] iBuffer = new byte[INPUT_BUFFER_SIZE];
private final ByteArrayOutputStream oBufferStream;
private final OS oStream;
private final IProxyInputStreamListener<OS> listener;

private byte[] oBuffer = emptyPrimitiveByteArray;
private int oBufferIndex;
private boolean endOfStream;

private ProxyInputStream(InputStream iStream, IFunction<OS, ByteArrayOutputStream> oStreamFactory, IProxyInputStreamListener<OS> listener) {
    this.iStream = iStream;
    oBufferStream = new ByteArrayOutputStream(OUTPUT_BUFFER_SIZE);
    oStream = oStreamFactory.evaluate(oBufferStream);
    this.listener = listener;
}

public static <OS extends OutputStream> ProxyInputStream<OS> proxyInputStream(InputStream iStream, IFunction<OS, ByteArrayOutputStream> oStreamFactory, IProxyInputStreamListener<OS> listener) {
    return new ProxyInputStream<OS>(iStream, oStreamFactory, listener);
}

@Override
public int read() throws IOException {
    if ( oBufferIndex == oBuffer.length ) {
        if ( endOfStream ) {
            return -1;
        } else {
            oBufferIndex = 0;
            do {
                final int iBufferedBytes = iStream.read(iBuffer);
                if ( iBufferedBytes == -1 ) {
                    if ( listener != null ) {
                        listener.afterEndOfStream(oStream);
                    }
                    endOfStream = true;
                    break;
                }
                oStream.write(iBuffer, 0, iBufferedBytes);
                oStream.flush();
            } while ( oBufferStream.size() == 0 );
            oBuffer = oBufferStream.toByteArray();
            oBufferStream.reset();
        }
    }
    return !endOfStream || oBuffer.length != 0 ? (int) oBuffer[oBufferIndex++] & 0xFF : -1;
}

}


1
我有点迷茫。但是当我不想压缩时,应该简单地使用原始的outputStream,而在我想要压缩时使用new GZipOutputStream(outputStream)。就这样。无论如何,请确保刷新输出流。 - helios
ByteArrayOutputStream 不等于 BufferedOutputStream。确实如此。 - Viruzzo
3个回答

4
我不相信DeflaterOutputStream.flush()有任何实际意义。压缩器会累积数据直到有东西可以写入底层流。强制剩余的数据出来的唯一方法是调用DeflaterOutputStream.finish()。然而,这对于您当前的实现无法使用,因为您不能在完全完成写入之前调用finish。
实际上,在同一个线程中编写压缩流并读取它非常困难。在RMIIO项目中,我实际上做到了这一点,但您需要一个任意大小的中间输出缓冲区(并且您基本上需要将数据推入,直到另一端压缩后输出,然后您才能读取它)。您可能可以使用该项目中的某些实用程序类来完成您想要做的事情。

你基本上需要将数据推入,直到另一端压缩出某些内容,这是最大的问题之一(除非你能负担得起一次性压缩整个内容);一种低效(但简单)的解决方案是在离散的“数据包”中压缩数据,前提是你能够进行解压缩。 - Viruzzo
刚刚在代码示例中添加了监听器 void afterFlush(O outputStream) throws IOException;,最后压缩了“lorem ipsum”文本示例。感谢指出 .finish() 的用法。 :) - Lyubomyr Shaydariv
@LyubomyrShaydariv - 你要知道,一旦你调用了finish方法,你的压缩流就结束了。你将永远无法处理超过512字节的压缩数据。你当前的代码并不是一个真正的“通用”解决方案。 - jtahlborn
是的,那是我第一次想到的“没错,它可以工作”。然而,它只能处理446个字符长度的“Lorem ipsum…”文本等。当我将文本加倍压缩(892 b)时,由于你提到的原因,它失败了。无论如何,我们今天完全重新设计了read()方法,最终它可以在deflater和inflater上实时工作。我只是感谢你指出了finish()方法,现在我在输入流结束后调用它。此外,问题中的源代码忽略了结果字节数组缓冲区中的-1被视为流的结尾而不是实际的0xFF数据。 - Lyubomyr Shaydariv

3

为什么不使用GZipOutputStream?

我有点迷惑。但是当我不想压缩时,应该简单地使用原始的outputStream,并且当我想要压缩时使用new GZipOutputStream(outputStream)。就这样。无论如何,请检查是否刷新了输出流。

Gzip vs zip

此外:GZIP是压缩流的一种方式(这就是你正在做的),而写入有效的zip文件则是另一回事(文件头、文件目录、条目(标题,数据)*)。请查看ZipOutputStream


谢谢您的回复。使用GZipOutputStream和ZipOutputStream都没有效果。我只是得到了完全被修剪的输出流:“Lorem ipsum…”从446变成了我在问题中提到的两个字节。我不能直接使用OutputStream,因为要求是获取一个InputStream来委托给JDBC准备语句(因为可能有大量的传入数据)。这就是为什么我正在寻找一个类来充当代理,比如MyApp(inputStream) --> [compressor] --> JDBC(inputStream)。 - Lyubomyr Shaydariv

1

请注意,如果你在某处使用方法 int read(byte b[], int off, int len) 并且在代码行 final int iBufferedBytes = iStream.read(iBuffer); 出现异常,你将会陷入无限循环。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接