Сжатие (ZIP) сжатия на лету в Java с использованием только абстракций InputStream и OutputStream. Возможный? - PullRequest
4 голосов
/ 17 января 2012

В настоящее время я пытаюсь написать собственный прокси потоков (давайте назовем его таким образом), который может изменить содержимое данного входного потока и произвести модифицированный, если необходимо, вывод. Это требование действительно необходимо, потому что иногда мне приходится изменять потоки в моем приложении (например, сжимать данные действительно на лету ). Следующий класс довольно прост и использует внутреннюю буферизацию.

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 предназначены для работы на лету.

Возможно, в какой-то момент я ошибаюсь со спецификой алгоритма сжатия с раздувом. Что я действительно пропускаю? .. Возможно, мог бы быть другой подход, чтобы написать «прокси потоков», чтобы вести себя точно так, как я хочу, чтобы оно работало ... Как я могу сжать данные на лету, ограничиваясь только потоками?

Заранее спасибо.


UPD: Следующая базовая версия прекрасно работает с дефлятором и инфлятором:

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;
}

}

Ответы [ 3 ]

4 голосов
/ 17 января 2012

Я не верю, что DeflaterOutputStream.flush() делает что-то значимое. дефлятор будет накапливать данные до тех пор, пока ему не будет что записать в основной поток. единственный способ вытолкнуть оставшийся бит данных - вызвать DeflaterOutputStream.finish(). однако, это не сработает для вашей текущей реализации, так как вы не можете вызвать финиш, пока не закончите писать.

на самом деле очень сложно написать сжатый поток и прочитать его в том же потоке. В проекте RMIIO я на самом деле это делаю, но вам нужен промежуточный выходной буфер произвольного размера (и вам в основном нужно помещать данные, пока на другом конце что-то не будет сжато, тогда вы сможете их прочитать). Возможно, вы сможете использовать некоторые из классов util в этом проекте, чтобы выполнить то, что вы хотите.

3 голосов
/ 17 января 2012

Почему бы не использовать GZipOutputStream?

Я немного растерялся.Но я должен просто использовать исходный outputStream, когда я не хочу сжимать, и new GZipOutputStream(outputStream), когда я действительно хочу сжимать.Это все.В любом случае, проверьте, что вы очищаете выходные потоки.

Gzip vs zip

Также: одна вещь - это GZIP (сжатие потока, это то, что вы делаете) идругое дело - написать действительный zip-файл (заголовки файлов, каталог файлов, записи (заголовок, данные) *).Чек ZipOutputStream.

1 голос
/ 25 июля 2017

Будьте осторожны, если где-то вы используете метод int read(byte b[], int off, int len) и в случае исключения в строке final int iBufferedBytes = iStream.read(iBuffer);

вы застрянете в бесконечном цикле

...