netty5/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java

439 lines
18 KiB
Java
Raw Normal View History

/*
2012-06-04 22:31:44 +02:00
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
2011-12-09 04:38:59 +01:00
package io.netty.handler.codec.http;
import java.util.HashMap;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
Revamp the core API to reduce memory footprint and consumption The API changes made so far turned out to increase the memory footprint and consumption while our intention was actually decreasing them. Memory consumption issue: When there are many connections which does not exchange data frequently, the old Netty 4 API spent a lot more memory than 3 because it always allocates per-handler buffer for each connection unless otherwise explicitly stated by a user. In a usual real world load, a client doesn't always send requests without pausing, so the idea of having a buffer whose life cycle if bound to the life cycle of a connection didn't work as expected. Memory footprint issue: The old Netty 4 API decreased overall memory footprint by a great deal in many cases. It was mainly because the old Netty 4 API did not allocate a new buffer and event object for each read. Instead, it created a new buffer for each handler in a pipeline. This works pretty well as long as the number of handlers in a pipeline is only a few. However, for a highly modular application with many handlers which handles connections which lasts for relatively short period, it actually makes the memory footprint issue much worse. Changes: All in all, this is about retaining all the good changes we made in 4 so far such as better thread model and going back to the way how we dealt with message events in 3. To fix the memory consumption/footprint issue mentioned above, we made a hard decision to break the backward compatibility again with the following changes: - Remove MessageBuf - Merge Buf into ByteBuf - Merge ChannelInboundByte/MessageHandler and ChannelStateHandler into ChannelInboundHandler - Similar changes were made to the adapter classes - Merge ChannelOutboundByte/MessageHandler and ChannelOperationHandler into ChannelOutboundHandler - Similar changes were made to the adapter classes - Introduce MessageList which is similar to `MessageEvent` in Netty 3 - Replace inboundBufferUpdated(ctx) with messageReceived(ctx, MessageList) - Replace flush(ctx, promise) with write(ctx, MessageList, promise) - Remove ByteToByteEncoder/Decoder/Codec - Replaced by MessageToByteEncoder<ByteBuf>, ByteToMessageDecoder<ByteBuf>, and ByteMessageCodec<ByteBuf> - Merge EmbeddedByteChannel and EmbeddedMessageChannel into EmbeddedChannel - Add SimpleChannelInboundHandler which is sometimes more useful than ChannelInboundHandlerAdapter - Bring back Channel.isWritable() from Netty 3 - Add ChannelInboundHandler.channelWritabilityChanges() event - Add RecvByteBufAllocator configuration property - Similar to ReceiveBufferSizePredictor in Netty 3 - Some existing configuration properties such as DatagramChannelConfig.receivePacketSize is gone now. - Remove suspend/resumeIntermediaryDeallocation() in ByteBuf This change would have been impossible without @normanmaurer's help. He fixed, ported, and improved many parts of the changes.
2013-05-28 13:40:19 +02:00
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.compression.Brotli;
import io.netty.handler.codec.compression.BrotliEncoder;
import io.netty.handler.codec.compression.BrotliOptions;
import io.netty.handler.codec.compression.CompressionOptions;
import io.netty.handler.codec.compression.DeflateOptions;
import io.netty.handler.codec.compression.GzipOptions;
import io.netty.handler.codec.compression.StandardCompressionOptions;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibEncoder;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.handler.codec.compression.Zstd;
import io.netty.handler.codec.compression.ZstdEncoder;
import io.netty.handler.codec.compression.ZstdOptions;
import io.netty.util.internal.ObjectUtil;
/**
* Compresses an {@link HttpMessage} and an {@link HttpContent} in {@code gzip} or
2009-11-03 08:11:52 +01:00
* {@code deflate} encoding while respecting the {@code "Accept-Encoding"} header.
* If there is no matching encoding, no compression is done. For more
* information on how this handler modifies the message, please refer to
* {@link HttpContentEncoder}.
*/
public class HttpContentCompressor extends HttpContentEncoder {
private final boolean supportsCompressionOptions;
private final BrotliOptions brotliOptions;
private final GzipOptions gzipOptions;
private final DeflateOptions deflateOptions;
private final ZstdOptions zstdOptions;
private final int compressionLevel;
private final int windowBits;
private final int memLevel;
private final int contentSizeThreshold;
private ChannelHandlerContext ctx;
private final Map<String, CompressionEncoderFactory> factories;
2009-11-03 08:11:52 +01:00
/**
* Creates a new handler with the default compression level (<tt>6</tt>),
* default window size (<tt>15</tt>) and default memory level (<tt>8</tt>).
2009-11-03 08:11:52 +01:00
*/
public HttpContentCompressor() {
this(6);
}
2009-11-03 08:11:52 +01:00
/**
* Creates a new handler with the specified compression level, default
* window size (<tt>15</tt>) and default memory level (<tt>8</tt>).
2009-11-03 08:11:52 +01:00
*
* @param compressionLevel
* {@code 1} yields the fastest compression and {@code 9} yields the
* best compression. {@code 0} means no compression. The default
* compression level is {@code 6}.
*/
@Deprecated
public HttpContentCompressor(int compressionLevel) {
this(compressionLevel, 15, 8, 0);
}
/**
* Creates a new handler with the specified compression level, window size,
* and memory level..
*
* @param compressionLevel
* {@code 1} yields the fastest compression and {@code 9} yields the
* best compression. {@code 0} means no compression. The default
* compression level is {@code 6}.
* @param windowBits
* The base two logarithm of the size of the history buffer. The
* value should be in the range {@code 9} to {@code 15} inclusive.
* Larger values result in better compression at the expense of
* memory usage. The default value is {@code 15}.
* @param memLevel
* How much memory should be allocated for the internal compression
* state. {@code 1} uses minimum memory and {@code 9} uses maximum
* memory. Larger values result in better and faster compression
* at the expense of memory usage. The default value is {@code 8}
*/
@Deprecated
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
this(compressionLevel, windowBits, memLevel, 0);
}
/**
* Creates a new handler with the specified compression level, window size,
* and memory level..
*
* @param compressionLevel
* {@code 1} yields the fastest compression and {@code 9} yields the
* best compression. {@code 0} means no compression. The default
* compression level is {@code 6}.
* @param windowBits
* The base two logarithm of the size of the history buffer. The
* value should be in the range {@code 9} to {@code 15} inclusive.
* Larger values result in better compression at the expense of
* memory usage. The default value is {@code 15}.
* @param memLevel
* How much memory should be allocated for the internal compression
* state. {@code 1} uses minimum memory and {@code 9} uses maximum
* memory. Larger values result in better and faster compression
* at the expense of memory usage. The default value is {@code 8}
* @param contentSizeThreshold
* The response body is compressed when the size of the response
* body exceeds the threshold. The value should be a non negative
* number. {@code 0} will enable compression for all responses.
*/
@Deprecated
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
this.brotliOptions = null;
this.gzipOptions = null;
this.deflateOptions = null;
this.zstdOptions = null;
this.factories = null;
this.supportsCompressionOptions = false;
}
/**
* Create a new {@link HttpContentCompressor} Instance with specified
* {@link CompressionOptions}s and contentSizeThreshold set to {@code 0}
*
* @param compressionOptions {@link CompressionOptions} or {@code null} if the default
* should be used.
*/
public HttpContentCompressor(CompressionOptions... compressionOptions) {
this(0, compressionOptions);
}
/**
* Create a new {@link HttpContentCompressor} instance with specified
* {@link CompressionOptions}s
*
* @param contentSizeThreshold
* The response body is compressed when the size of the response
* body exceeds the threshold. The value should be a non negative
* number. {@code 0} will enable compression for all responses.
* @param compressionOptions {@link CompressionOptions} or {@code null}
* if the default should be used.
*/
public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
BrotliOptions brotliOptions = null;
GzipOptions gzipOptions = null;
DeflateOptions deflateOptions = null;
ZstdOptions zstdOptions = null;
if (compressionOptions == null || compressionOptions.length == 0) {
brotliOptions = Brotli.isAvailable() ? StandardCompressionOptions.brotli() : null;
gzipOptions = StandardCompressionOptions.gzip();
deflateOptions = StandardCompressionOptions.deflate();
zstdOptions = Zstd.isAvailable() ? StandardCompressionOptions.zstd() : null;
} else {
ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
for (CompressionOptions compressionOption : compressionOptions) {
if (compressionOption instanceof BrotliOptions) {
// if we have BrotliOptions, it means Brotli is available
brotliOptions = (BrotliOptions) compressionOption;
} else if (compressionOption instanceof GzipOptions) {
gzipOptions = (GzipOptions) compressionOption;
} else if (compressionOption instanceof DeflateOptions) {
deflateOptions = (DeflateOptions) compressionOption;
} else if (compressionOption instanceof ZstdOptions) {
zstdOptions = (ZstdOptions) compressionOption;
} else {
throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
": " + compressionOption);
}
}
}
this.gzipOptions = gzipOptions;
this.deflateOptions = deflateOptions;
this.brotliOptions = brotliOptions;
this.zstdOptions = zstdOptions;
this.factories = new HashMap<String, CompressionEncoderFactory>();
if (this.gzipOptions != null) {
this.factories.put("gzip", new GzipEncoderFactory());
}
if (this.deflateOptions != null) {
this.factories.put("deflate", new DeflateEncoderFactory());
}
if (this.brotliOptions != null) {
this.factories.put("br", new BrEncoderFactory());
}
if (this.zstdOptions != null) {
this.factories.put("zstd", new ZstdEncoderFactory());
}
this.compressionLevel = -1;
this.windowBits = -1;
this.memLevel = -1;
supportsCompressionOptions = true;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
}
@Override
protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
if (this.contentSizeThreshold > 0) {
if (httpResponse instanceof HttpContent &&
((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
return null;
}
}
String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
if (contentEncoding != null) {
// Content-Encoding was set, either as something specific or as the IDENTITY encoding
// Therefore, we should NOT encode here
return null;
}
2012-05-31 21:03:01 +02:00
if (supportsCompressionOptions) {
String targetContentEncoding = determineEncoding(acceptEncoding);
if (targetContentEncoding == null) {
return null;
}
CompressionEncoderFactory encoderFactory = factories.get(targetContentEncoding);
if (encoderFactory == null) {
throw new Error();
}
return new Result(targetContentEncoding,
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
ctx.channel().config(), encoderFactory.createEncoder()));
} else {
ZlibWrapper wrapper = determineWrapper(acceptEncoding);
if (wrapper == null) {
return null;
}
String targetContentEncoding;
switch (wrapper) {
case GZIP:
targetContentEncoding = "gzip";
break;
case ZLIB:
targetContentEncoding = "deflate";
break;
default:
throw new Error();
}
return new Result(
targetContentEncoding,
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
wrapper, compressionLevel, windowBits, memLevel)));
}
}
@SuppressWarnings("FloatingPointEquality")
protected String determineEncoding(String acceptEncoding) {
float starQ = -1.0f;
float brQ = -1.0f;
float zstdQ = -1.0f;
float gzipQ = -1.0f;
float deflateQ = -1.0f;
for (String encoding : acceptEncoding.split(",")) {
float q = 1.0f;
int equalsPos = encoding.indexOf('=');
if (equalsPos != -1) {
try {
q = Float.parseFloat(encoding.substring(equalsPos + 1));
} catch (NumberFormatException e) {
// Ignore encoding
q = 0.0f;
}
}
if (encoding.contains("*")) {
starQ = q;
} else if (encoding.contains("br") && q > brQ) {
brQ = q;
} else if (encoding.contains("zstd") && q > zstdQ) {
zstdQ = q;
} else if (encoding.contains("gzip") && q > gzipQ) {
gzipQ = q;
} else if (encoding.contains("deflate") && q > deflateQ) {
deflateQ = q;
}
}
if (brQ > 0.0f || zstdQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
return "br";
} else if (zstdQ != -1.0f && zstdQ >= gzipQ && this.zstdOptions != null) {
return "zstd";
} else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
return "gzip";
} else if (deflateQ != -1.0f && this.deflateOptions != null) {
return "deflate";
}
}
if (starQ > 0.0f) {
if (brQ == -1.0f && this.brotliOptions != null) {
return "br";
}
if (zstdQ == -1.0f && this.zstdOptions != null) {
return "zstd";
}
if (gzipQ == -1.0f && this.gzipOptions != null) {
return "gzip";
}
if (deflateQ == -1.0f && this.deflateOptions != null) {
return "deflate";
}
}
return null;
}
@Deprecated
@SuppressWarnings("FloatingPointEquality")
protected ZlibWrapper determineWrapper(String acceptEncoding) {
float starQ = -1.0f;
float gzipQ = -1.0f;
float deflateQ = -1.0f;
for (String encoding : acceptEncoding.split(",")) {
float q = 1.0f;
int equalsPos = encoding.indexOf('=');
if (equalsPos != -1) {
try {
q = Float.parseFloat(encoding.substring(equalsPos + 1));
} catch (NumberFormatException e) {
// Ignore encoding
q = 0.0f;
}
}
if (encoding.contains("*")) {
starQ = q;
} else if (encoding.contains("gzip") && q > gzipQ) {
gzipQ = q;
} else if (encoding.contains("deflate") && q > deflateQ) {
deflateQ = q;
}
}
if (gzipQ > 0.0f || deflateQ > 0.0f) {
if (gzipQ >= deflateQ) {
return ZlibWrapper.GZIP;
} else {
return ZlibWrapper.ZLIB;
}
}
if (starQ > 0.0f) {
if (gzipQ == -1.0f) {
return ZlibWrapper.GZIP;
}
if (deflateQ == -1.0f) {
return ZlibWrapper.ZLIB;
}
}
return null;
}
/**
* Compression Encoder Factory that creates {@link ZlibEncoder}s
* used to compress http content for gzip content encoding
*/
private final class GzipEncoderFactory implements CompressionEncoderFactory {
@Override
public MessageToByteEncoder<ByteBuf> createEncoder() {
return ZlibCodecFactory.newZlibEncoder(
ZlibWrapper.GZIP, gzipOptions.compressionLevel(),
gzipOptions.windowBits(), gzipOptions.memLevel());
}
}
/**
* Compression Encoder Factory that creates {@link ZlibEncoder}s
* used to compress http content for deflate content encoding
*/
private final class DeflateEncoderFactory implements CompressionEncoderFactory {
@Override
public MessageToByteEncoder<ByteBuf> createEncoder() {
return ZlibCodecFactory.newZlibEncoder(
ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
deflateOptions.windowBits(), deflateOptions.memLevel());
}
}
/**
* Compression Encoder Factory that creates {@link BrotliEncoder}s
* used to compress http content for br content encoding
*/
private final class BrEncoderFactory implements CompressionEncoderFactory {
@Override
public MessageToByteEncoder<ByteBuf> createEncoder() {
return new BrotliEncoder(brotliOptions.parameters());
}
}
/**
* Compression Encoder Factory for create {@link ZstdEncoder}
* used to compress http content for zstd content encoding
*/
private final class ZstdEncoderFactory implements CompressionEncoderFactory {
@Override
public MessageToByteEncoder<ByteBuf> createEncoder() {
return new ZstdEncoder(zstdOptions.compressionLevel(),
zstdOptions.blockSize(), zstdOptions.maxEncodeSize());
}
}
}