io.netty.handler.codec.http2.Http2MultiplexCodec Maven / Gradle / Ivy
/*
* Copyright 2016 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:
*
* http://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.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.handler.codec.UnsupportedMessageTypeException;
import io.netty.handler.codec.http2.Http2Exception.StreamException;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import io.netty.util.collection.IntObjectMap.PrimitiveEntry;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.internal.UnstableApi;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import static io.netty.handler.codec.http2.AbstractHttp2StreamChannel.CLOSE_MESSAGE;
import static io.netty.handler.codec.http2.Http2CodecUtil.isOutboundStream;
import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
import static java.lang.String.format;
/**
* An HTTP/2 handler that creates child channels for each stream.
*
* When a new stream is created, a new {@link Channel} is created for it. Applications send and
* receive {@link Http2StreamFrame}s on the created channel. {@link ByteBuf}s cannot be processed by the channel;
* all writes that reach the head of the pipeline must be an instance of {@link Http2StreamFrame}. Writes that reach
* the head of the pipeline are processed directly by this handler and cannot be intercepted.
*
*
The child channel will be notified of user events that impact the stream, such as {@link
* Http2GoAwayFrame} and {@link Http2ResetFrame}, as soon as they occur. Although {@code
* Http2GoAwayFrame} and {@code Http2ResetFrame} signify that the remote is ignoring further
* communication, closing of the channel is delayed until any inbound queue is drained with {@link
* Channel#read()}, which follows the default behavior of channels in Netty. Applications are
* free to close the channel in response to such events if they don't have use for any queued
* messages.
*
*
Outbound streams are supported via the {@link Http2StreamChannelBootstrap}.
*
*
{@link ChannelConfig#setMaxMessagesPerRead(int)} and {@link ChannelConfig#setAutoRead(boolean)} are supported.
*/
@UnstableApi
public final class Http2MultiplexCodec extends ChannelDuplexHandler {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2MultiplexCodec.class);
private final Http2StreamChannelBootstrap bootstrap;
private final List channelsToFireChildReadComplete = new ArrayList();
private final boolean server;
private ChannelHandlerContext ctx;
private volatile Runnable flushTask;
private final IntObjectMap childChannels = new IntObjectHashMap();
/**
* Construct a new handler whose child channels run in a different event loop.
*
* @param server {@code true} this is a server
* @param bootstrap bootstrap used to instantiate child channels for remotely-created streams.
*/
public Http2MultiplexCodec(boolean server, Http2StreamChannelBootstrap bootstrap) {
if (bootstrap.parentChannel() != null) {
throw new IllegalStateException("The parent channel must not be set on the bootstrap.");
}
this.server = server;
this.bootstrap = new Http2StreamChannelBootstrap(bootstrap);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
this.ctx = ctx;
bootstrap.parentChannel(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (!(cause instanceof StreamException)) {
ctx.fireExceptionCaught(cause);
return;
}
StreamException streamEx = (StreamException) cause;
try {
Http2StreamChannel childChannel = childChannels.get(streamEx.streamId());
if (childChannel != null) {
childChannel.pipeline().fireExceptionCaught(streamEx);
} else {
logger.warn(format("Exception caught for unknown HTTP/2 stream '%d'", streamEx.streamId()),
streamEx);
}
} finally {
onStreamClosed(streamEx.streamId());
}
}
// Override this to signal it will never throw an exception.
@Override
public void flush(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof Http2Frame)) {
ctx.fireChannelRead(msg);
return;
}
if (msg instanceof Http2StreamFrame) {
Http2StreamFrame frame = (Http2StreamFrame) msg;
int streamId = frame.streamId();
Http2StreamChannel childChannel = childChannels.get(streamId);
if (childChannel == null) {
// TODO: Combine with DefaultHttp2ConnectionDecoder.shouldIgnoreHeadersOrDataFrame logic.
ReferenceCountUtil.release(msg);
throw new StreamException(streamId, STREAM_CLOSED, format("Received %s frame for an unknown stream %d",
frame.name(), streamId));
}
fireChildReadAndRegister(childChannel, frame);
} else if (msg instanceof Http2GoAwayFrame) {
Http2GoAwayFrame goAwayFrame = (Http2GoAwayFrame) msg;
for (PrimitiveEntry entry : childChannels.entries()) {
Http2StreamChannel childChannel = entry.value();
int streamId = entry.key();
if (streamId > goAwayFrame.lastStreamId() && isOutboundStream(server, streamId)) {
childChannel.pipeline().fireUserEventTriggered(goAwayFrame.retainedDuplicate());
}
}
goAwayFrame.release();
} else {
// It's safe to release, as UnsupportedMessageTypeException just calls msg.getClass()
ReferenceCountUtil.release(msg);
throw new UnsupportedMessageTypeException(msg);
}
}
private void fireChildReadAndRegister(Http2StreamChannel childChannel, Http2StreamFrame frame) {
// Can't use childChannel.fireChannelRead() as it would fire independent of whether
// channel.read() had been called.
childChannel.fireChildRead(frame);
if (!childChannel.inStreamsToFireChildReadComplete) {
channelsToFireChildReadComplete.add(childChannel);
childChannel.inStreamsToFireChildReadComplete = true;
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2StreamActiveEvent) {
Http2StreamActiveEvent activeEvent = (Http2StreamActiveEvent) evt;
onStreamActive(activeEvent.streamId(), activeEvent.headers());
} else if (evt instanceof Http2StreamClosedEvent) {
onStreamClosed(((Http2StreamClosedEvent) evt).streamId());
} else {
ctx.fireUserEventTriggered(evt);
}
}
private void onStreamActive(int streamId, Http2HeadersFrame headersFrame) {
final Http2StreamChannel childChannel;
if (isOutboundStream(server, streamId)) {
if (!(headersFrame instanceof ChannelCarryingHeadersFrame)) {
throw new IllegalArgumentException("needs to be wrapped");
}
childChannel = ((ChannelCarryingHeadersFrame) headersFrame).channel();
childChannel.streamId(streamId);
} else {
ChannelFuture future = bootstrap.connect(streamId);
childChannel = (Http2StreamChannel) future.channel();
}
Http2StreamChannel existing = childChannels.put(streamId, childChannel);
assert existing == null;
}
private void onStreamClosed(int streamId) {
final Http2StreamChannel childChannel = childChannels.remove(streamId);
if (childChannel != null) {
final EventLoop eventLoop = childChannel.eventLoop();
if (eventLoop.inEventLoop()) {
onStreamClosed0(childChannel);
} else {
eventLoop.execute(new Runnable() {
@Override
public void run() {
onStreamClosed0(childChannel);
}
});
}
}
}
private void onStreamClosed0(Http2StreamChannel childChannel) {
assert childChannel.eventLoop().inEventLoop();
childChannel.onStreamClosedFired = true;
childChannel.fireChildRead(CLOSE_MESSAGE);
}
void flushFromStreamChannel() {
EventExecutor executor = ctx.executor();
if (executor.inEventLoop()) {
flush(ctx);
} else {
Runnable task = flushTask;
if (task == null) {
task = flushTask = new Runnable() {
@Override
public void run() {
flush(ctx);
}
};
}
executor.execute(task);
}
}
void writeFromStreamChannel(Object msg, boolean flush) {
writeFromStreamChannel(msg, ctx.newPromise(), flush);
}
void writeFromStreamChannel(final Object msg, final ChannelPromise promise, final boolean flush) {
EventExecutor executor = ctx.executor();
if (executor.inEventLoop()) {
writeFromStreamChannel0(msg, flush, promise);
} else {
try {
executor.execute(new Runnable() {
@Override
public void run() {
writeFromStreamChannel0(msg, flush, promise);
}
});
} catch (Throwable cause) {
promise.setFailure(cause);
}
}
}
private void writeFromStreamChannel0(Object msg, boolean flush, ChannelPromise promise) {
try {
write(ctx, msg, promise);
} catch (Throwable cause) {
promise.tryFailure(cause);
}
if (flush) {
flush(ctx);
}
}
/**
* Notifies any child streams of the read completion.
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
for (int i = 0; i < channelsToFireChildReadComplete.size(); i++) {
Http2StreamChannel childChannel = channelsToFireChildReadComplete.get(i);
// Clear early in case fireChildReadComplete() causes it to need to be re-processed
childChannel.inStreamsToFireChildReadComplete = false;
childChannel.fireChildReadComplete();
}
channelsToFireChildReadComplete.clear();
}
ChannelFuture createStreamChannel(Channel parentChannel, EventLoopGroup group, ChannelHandler handler,
Map, Object> options,
Map, Object> attrs,
int streamId) {
final Http2StreamChannel channel = new Http2StreamChannel(parentChannel);
if (isStreamIdValid(streamId)) {
assert !isOutboundStream(server, streamId);
assert ctx.channel().eventLoop().inEventLoop();
channel.streamId(streamId);
}
channel.pipeline().addLast(handler);
initOpts(channel, options);
initAttrs(channel, attrs);
ChannelFuture future = group.register(channel);
// Handle any errors that occurred on the local thread while registering. Even though
// failures can happen after this point, they will be handled by the channel by closing the
// channel.
if (future.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return future;
}
@SuppressWarnings("unchecked")
private static void initOpts(Channel channel, Map, Object> opts) {
if (opts != null) {
for (Entry, Object> e: opts.entrySet()) {
try {
if (!channel.config().setOption((ChannelOption