All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.websocket.common.extensions.compress.CompressExtension Maven / Gradle / Ivy

There is a newer version: 3.1.1
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.websocket.common.extensions.compress;

import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import java.util.zip.ZipException;

import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.ConcurrentArrayQueue;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.BadPayloadException;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
import org.eclipse.jetty.websocket.common.frames.DataFrame;

public abstract class CompressExtension extends AbstractExtension
{
    protected static final byte[] TAIL_BYTES = new byte[]{0x00, 0x00, (byte)0xFF, (byte)0xFF};
    private static final Logger LOG = Log.getLogger(CompressExtension.class);

    private final Queue entries = new ConcurrentArrayQueue<>();
    private final IteratingCallback flusher = new Flusher();
    private final Deflater compressor;
    private final Inflater decompressor;

    protected CompressExtension()
    {
        compressor = new Deflater(Deflater.BEST_COMPRESSION, true);
        decompressor = new Inflater(true);
    }

    public Deflater getDeflater()
    {
        return compressor;
    }

    public Inflater getInflater()
    {
        return decompressor;
    }

    /**
     * Indicates use of RSV1 flag for indicating deflation is in use.
     */
    @Override
    public boolean isRsv1User()
    {
        return true;
    }

    protected void forwardIncoming(Frame frame, ByteAccumulator accumulator)
    {
        DataFrame newFrame = new DataFrame(frame);
        // Unset RSV1 since it's not compressed anymore.
        newFrame.setRsv1(false);

        ByteBuffer buffer = getBufferPool().acquire(accumulator.getLength(), false);
        try
        {
            BufferUtil.flipToFill(buffer);
            accumulator.transferTo(buffer);
            newFrame.setPayload(buffer);
            nextIncomingFrame(newFrame);
        }
        finally
        {
            getBufferPool().release(buffer);
        }
    }

    protected ByteAccumulator decompress(byte[] input)
    {
        // Since we don't track text vs binary vs continuation state, just grab whatever is the greater value.
        int maxSize = Math.max(getPolicy().getMaxTextMessageSize(), getPolicy().getMaxBinaryMessageBufferSize());
        ByteAccumulator accumulator = new ByteAccumulator(maxSize);

        decompressor.setInput(input, 0, input.length);
        LOG.debug("Decompressing {} bytes", input.length);

        try
        {
            // It is allowed to send DEFLATE blocks with BFINAL=1.
            // For such blocks, getRemaining() will be > 0 but finished()
            // will be true, so we need to check for both.
            // When BFINAL=0, finished() will always be false and we only
            // check the remaining bytes.
            while (decompressor.getRemaining() > 0 && !decompressor.finished())
            {
                byte[] output = new byte[Math.min(input.length * 2, 32 * 1024)];
                int decompressed = decompressor.inflate(output);
                if (decompressed == 0)
                {
                    if (decompressor.needsInput())
                    {
                        throw new BadPayloadException("Unable to inflate frame, not enough input on frame");
                    }
                    if (decompressor.needsDictionary())
                    {
                        throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary");
                    }
                }
                else
                {
                    accumulator.addChunk(output, 0, decompressed);
                }
            }
            LOG.debug("Decompressed {}->{} bytes", input.length, accumulator.getLength());
            return accumulator;
        }
        catch (DataFormatException x)
        {
            throw new BadPayloadException(x);
        }
    }

    @Override
    public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode)
    {
        // We use a queue and an IteratingCallback to handle concurrency.
        // We must compress and write atomically, otherwise the compression
        // context on the other end gets confused.

        if (flusher.isFailed())
        {
            notifyCallbackFailure(callback, new ZipException());
            return;
        }

        FrameEntry entry = new FrameEntry(frame, callback, batchMode);
        LOG.debug("Queuing {}", entry);
        entries.offer(entry);
        flusher.iterate();
    }

    protected void notifyCallbackSuccess(WriteCallback callback)
    {
        try
        {
            if (callback != null)
                callback.writeSuccess();
        }
        catch (Throwable x)
        {
            LOG.debug("Exception while notifying success of callback " + callback, x);
        }
    }

    protected void notifyCallbackFailure(WriteCallback callback, Throwable failure)
    {
        try
        {
            if (callback != null)
                callback.writeFailed(failure);
        }
        catch (Throwable x)
        {
            LOG.debug("Exception while notifying failure of callback " + callback, x);
        }
    }

    @Override
    public String toString()
    {
        return getClass().getSimpleName();
    }

    private static class FrameEntry
    {
        private final Frame frame;
        private final WriteCallback callback;
        private final BatchMode batchMode;

        private FrameEntry(Frame frame, WriteCallback callback, BatchMode batchMode)
        {
            this.frame = frame;
            this.callback = callback;
            this.batchMode = batchMode;
        }

        @Override
        public String toString()
        {
            return frame.toString();
        }
    }

    private class Flusher extends IteratingCallback implements WriteCallback
    {
        private FrameEntry current;
        private ByteBuffer payload;
        private boolean finished = true;

        @Override
        protected Action process() throws Exception
        {
            if (finished)
            {
                current = entries.poll();
                LOG.debug("Processing {}", current);
                if (current == null)
                    return Action.IDLE;
                deflate(current);
            }
            else
            {
                compress(current, false);
            }
            return Action.SCHEDULED;
        }

        private void deflate(FrameEntry entry)
        {
            Frame frame = entry.frame;
            BatchMode batchMode = entry.batchMode;
            if (OpCode.isControlFrame(frame.getOpCode()) || !frame.hasPayload())
            {
                nextOutgoingFrame(frame, this, batchMode);
                return;
            }

            compress(entry, true);
        }

        private void compress(FrameEntry entry, boolean first)
        {
            // Get a chunk of the payload to avoid to blow
            // the heap if the payload is a huge mapped file.
            Frame frame = entry.frame;
            ByteBuffer data = frame.getPayload();
            int remaining = data.remaining();
            int inputLength = Math.min(remaining, 32 * 1024);
            LOG.debug("Compressing {}: {} bytes in {} bytes chunk", entry, remaining, inputLength);

            // Avoid to copy the bytes if the ByteBuffer
            // is backed by an array.
            int inputOffset;
            byte[] input;
            if (data.hasArray())
            {
                input = data.array();
                int position = data.position();
                inputOffset = position + data.arrayOffset();
                data.position(position + inputLength);
            }
            else
            {
                input = new byte[inputLength];
                inputOffset = 0;
                data.get(input, 0, inputLength);
            }
            finished = inputLength == remaining;

            compressor.setInput(input, inputOffset, inputLength);

            // Use an additional space in case the content is not compressible.
            byte[] output = new byte[inputLength + 64];
            int outputOffset = 0;
            int outputLength = 0;
            while (true)
            {
                int space = output.length - outputOffset;
                int compressed = compressor.deflate(output, outputOffset, space, Deflater.SYNC_FLUSH);
                outputLength += compressed;
                if (compressed < space)
                {
                    // Everything was compressed.
                    break;
                }
                else
                {
                    // The compressed output is bigger than the uncompressed input.
                    byte[] newOutput = new byte[output.length * 2];
                    System.arraycopy(output, 0, newOutput, 0, output.length);
                    outputOffset += output.length;
                    output = newOutput;
                }
            }

            // Skip the last tail bytes bytes generated by SYNC_FLUSH.
            payload = ByteBuffer.wrap(output, 0, outputLength - TAIL_BYTES.length);
            LOG.debug("Compressed {}: {}->{} chunk bytes", entry, inputLength, outputLength);

            boolean continuation = frame.getType().isContinuation() || !first;
            DataFrame chunk = new DataFrame(frame, continuation);
            chunk.setRsv1(true);
            chunk.setPayload(payload);
            boolean fin = frame.isFin() && finished;
            chunk.setFin(fin);

            nextOutgoingFrame(chunk, this, entry.batchMode);
        }

        @Override
        protected void completed()
        {
            // This IteratingCallback never completes.
        }

        @Override
        public void writeSuccess()
        {
            if (finished)
                notifyCallbackSuccess(current.callback);
            succeeded();
        }

        @Override
        public void writeFailed(Throwable x)
        {
            notifyCallbackFailure(current.callback, x);
            // If something went wrong, very likely the compression context
            // will be invalid, so we need to fail this IteratingCallback.
            failed(x);
            // Now no more frames can be queued, fail those in the queue.
            FrameEntry entry;
            while ((entry = entries.poll()) != null)
                notifyCallbackFailure(entry.callback, x);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy