Существует ли GunzipOutputStream или что-то подобное?

Что касается обработки дефляции HTTP ContentEncoding, я хотел бы знать, как использовать OutputStream для раздувания потоков gzip и deflate. Вот почему:

У меня есть класс, который извлекает ресурсы с веб-сервера (думаю, wget, но на Java). У меня есть строгое соблюдение Content-Length ответа, и я хотел бы сохранить это соблюдение. Итак, что я хотел бы сделать, так это прочитать определенное количество байтов из ответа (что я уже делаю), но сгенерировать больше байтов, если ответ был сжат.

У меня это работает для deflate таких ответов:

OutputStream out = System.out;
out = new InflateOutputStream(out);
// repeatedly:
out.write(compressedBytesFromResponse);

Я хотел бы сделать то же самое с ответами gzip, но без GunzipOutputStream я не знаю, что делать дальше.

Обновлять

Я рассматривал создание чего-то вроде это, но это казалось совершенно безумным. Возможно, это единственный способ использовать OutputStream для увеличения моих данных.


person Christopher Schultz    schedule 08.08.2012    source источник
comment
Потенциальный дубликат для stackoverflow.com/questions/2474193 /   -  person Adam Zalcman    schedule 09.08.2012
comment
С уважением, @AdamZalcman, этот вопрос касается использования GZIPInputStream. Я ищу что-то, что распаковывает данные, но существует как OutputStream.   -  person Christopher Schultz    schedule 09.08.2012


Ответы (3)


Для deflate в Java есть InflaterOutputStream, который делает то, что вам нужно: подает сжатые дефлированные данные, а несжатые данные отправляет в базовый выходной поток.


Для gzip... не могу найти эквивалент. Компаньон InflaterOutputStream, InflaterInputStream, имеет подкласс GZipInputStream, который обрабатывает все заголовки, но нет эквивалентного класса распаковки выходного потока, который потенциально мог бы быть подклассом InflaterOutputStream.

Создавать подкласс InflaterOutputStream самостоятельно для GZIP было бы довольно сложно, глядя на исходный код для GZipInputStream (имеющий дело с заголовками, трейлерами и т. д.).

Использование конвейерных потоков кажется меньшим из двух зол.

person prunge    schedule 09.08.2012
comment
Да, я воспользуюсь парой Piped(Input|Output)Stream и потоком помпы, прежде чем напишу свой собственный "родной" GunzipOutputStream. - person Christopher Schultz; 09.08.2012

Отвечая на мой собственный вопрос:

Здесь есть две возможности: gunzip на выходе (например, используйте GunzipOutputStream, не предоставляемый Java API) или gunzip на входе (например, используйте GZIPInputStream, предоставляемый Java API) плюс принудительное использование Content- Длина во время чтения.

Я сделал и то, и другое, и я думаю, что предпочитаю последнее, потому что а) он не требует запуска отдельного потока для перекачки байтов из PipedOutputStream в PipedIOnputStream и б) (следствие, я думаю) он не имеет такой угрозы условий гонки и других проблем синхронизации.

Во-первых, вот моя реализация LimitedInputStream, которая позволяет мне обернуть входной поток и установить ограничение на количество считываемых данных. Обратите внимание, что у меня также есть BigLimitedInputStream, который использует счетчик BigInteger для поддержки значений Content-Length больше Long.MAX_LONG:

public class LimitedInputStream
    extends InputStream
{
    private long _limit;
    private long _read;
    private InputStream _in;

    public LimitedInputStream(InputStream in, long limit)
    {
        _limit= limit;
        _in = in;
        _read = 0;
    }
    @Override
    public int available()
        throws IOException
    {
        return _in.available(); // sure?
    }

    @Override
    public void close()
        throws IOException
    {
        _in.close();
    }

    @Override
    public boolean markSupported()
    {
        return false;
    }

    @Override
    public int read()
        throws IOException
    {
        int read = _in.read();

        if(-1 == read)
            return -1;

        ++_read;

        if(_read > _limit)
            return -1;
            // throw new IOException("Read limit reached: " + _limit);

        return read;
    }

    @Override
    public int read(byte[] b)
        throws IOException
    {
        return read(b, 0, b.length);
    }

    @Override
    public int read(byte[] b, int off, int len)
        throws IOException
    {
        // 'len' is an int, so 'max' is an int; narrowing cast is safe
        int max = (int)Math.min((long)(_limit - _read), (long)len);

        if(0 == max && len > 0)
            return -1;
            //throw new IOException("Read limit reached: " + _limit);

        int read = _in.read(b, off, max);

        _read += read;

        // This should never happen
        if(_read > _limit)
            return -1;
            //throw new IOException("Read limit reached: " + _limit);

        return read;
    }

    @Override
    public long skip(long n)
        throws IOException
    {
        long max = Math.min((long)(_limit - _read), n);

        if(0 == max)
            return 0;

        long read = _in.skip(max);

        _read += read;

        return read;
    }
}

Использование вышеприведенного класса для переноса InputStream, полученного из HttpURLConnection, позволяет мне упростить существующий код, который мне приходилось читать точное количество байтов, упомянутых в заголовке Content-Length, и просто слепо копировать ввод в вывод. Затем я оборачиваю входной поток (уже завернутый в LimitedInputStream) в GZIPInputStream для распаковки и просто перекачиваю байты из (дважды завернутого) ввода в вывод.

Менее прямой путь состоит в том, чтобы следовать моей первоначальной линии: обернуть OutputStream, используя (как оказалось) неуклюжий класс: GunzipOutputStream. Я написал GunzipOutputStream, который использует внутренний поток для перекачки байтов через пару конвейерных потоков. Это уродливо и основано на коде из OpenRDF GunzipOutputStream. Я думаю, что мой немного проще:

public class GunzipOutputStream
    extends OutputStream
{
    final private Thread _pump;

    // Streams
    final private PipedOutputStream _zipped;  // Compressed bytes are written here (by clients)
    final private PipedInputStream _pipe; // Compressed bytes are read (internally) here
    final private OutputStream _out; // Uncompressed data is written here (by the pump thread)

    // Internal state
    private IOException _e;

    public GunzipOutputStream(OutputStream out)
        throws IOException
    {
        _zipped = new PipedOutputStream();
        _pipe = new PipedInputStream(_zipped);
        _out = out;
        _pump = new Thread(new Runnable() {
            public void run() {
                InputStream in = null;
                try
                {
                    in = new GZIPInputStream(_pipe);

                    pump(in, _out);
                }
                catch (IOException e)
                {
                    _e = e;
                    System.err.println(e);
                    _e.printStackTrace();
                }
                finally
                {
                    try { in.close(); } catch (IOException ioe)
                    { ioe.printStackTrace(); }
                }
            }

            private void pump(InputStream in, OutputStream out)
                throws IOException
            {
                long count = 0;

                byte[] buf = new byte[4096];

                int read;
                while ((read = in.read(buf)) >= 0) {
                    System.err.println("===> Pumping " + read + " bytes");
                    out.write(buf, 0, read);
                    count += read;
                }
                out.flush();
                System.err.println("===> Pumped a total of " + count + " bytes");
            }
        }, "GunzipOutputStream stream pump " + GunzipOutputStream.this.hashCode());

        _pump.start();
    }

    public void close() throws IOException {
        throwIOException();
        _zipped.close();
        _pipe.close();
        _out.close();
    }

    public void flush() throws IOException {
        throwIOException();
        _zipped.flush();
    }

    public void write(int b) throws IOException {
        throwIOException();
        _zipped.write(b);
    }

    public void write(byte[] b) throws IOException {
        throwIOException();
        _zipped.write(b);
    }

    public void write(byte[] b, int off, int len) throws IOException {
        throwIOException();
        _zipped.write(b, off, len);
    }

    public String toString() {
        return _zipped.toString();
    }

    protected void finish()
        throws IOException
    {
        try
        {
            _pump.join();
            _pipe.close();
            _zipped.close();
        }
        catch (InterruptedException ie)
        {
            // Ignore
        }
    }

    private void throwIOException()
        throws IOException
    {
        if(null != _e)
        {
            IOException e = _e;
            _e = null; // Clear the existing error
            throw e;
        }
    }
}

Опять же, это работает, но кажется довольно... хрупким.

В конце концов, я реорганизовал свой код, чтобы использовать LimitedInputStream и GZIPInputStream и не использовать GunzipOutputStream. Если бы Java API предоставлял GunzipOutputStream, было бы здорово. Но это не так, и без написания «родного» алгоритма gunzip реализация собственного GunzipOutputStream расширяет границы допустимого.

person Christopher Schultz    schedule 09.08.2012

Если вы используете HttpURLConnection, все это происходит автоматически.

person user207421    schedule 08.08.2012
comment
Как ни странно, я использую HttpURLConnection, и нет, это не происходит автоматически. - person Christopher Schultz; 09.08.2012