cd3254df88
Motivation: We need to update to a new checkstyle plugin to allow the usage of lambdas. Modifications: - Update to new plugin version. - Fix checkstyle problems. Result: Be able to use checkstyle plugin which supports new Java syntax.
1074 lines
38 KiB
Java
1074 lines
38 KiB
Java
/*
|
|
* Copyright 2014 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.ChannelPromise;
|
|
import io.netty.handler.codec.http2.Http2Stream.State;
|
|
import io.netty.util.collection.IntObjectHashMap;
|
|
import io.netty.util.collection.IntObjectMap;
|
|
import io.netty.util.collection.IntObjectMap.PrimitiveEntry;
|
|
import io.netty.util.concurrent.Future;
|
|
import io.netty.util.concurrent.Promise;
|
|
import io.netty.util.concurrent.UnaryPromiseNotifier;
|
|
import io.netty.util.internal.EmptyArrays;
|
|
import io.netty.util.internal.UnstableApi;
|
|
import io.netty.util.internal.logging.InternalLogger;
|
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
|
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Queue;
|
|
import java.util.Set;
|
|
|
|
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
|
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS;
|
|
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
|
|
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
|
|
import static io.netty.handler.codec.http2.Http2Error.REFUSED_STREAM;
|
|
import static io.netty.handler.codec.http2.Http2Exception.closedStreamError;
|
|
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
|
|
import static io.netty.handler.codec.http2.Http2Exception.streamError;
|
|
import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED;
|
|
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
|
|
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE;
|
|
import static io.netty.handler.codec.http2.Http2Stream.State.IDLE;
|
|
import static io.netty.handler.codec.http2.Http2Stream.State.OPEN;
|
|
import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_LOCAL;
|
|
import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_REMOTE;
|
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
|
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
|
|
import static java.lang.Integer.MAX_VALUE;
|
|
|
|
/**
|
|
* Simple implementation of {@link Http2Connection}.
|
|
*/
|
|
@UnstableApi
|
|
public class DefaultHttp2Connection implements Http2Connection {
|
|
private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultHttp2Connection.class);
|
|
// Fields accessed by inner classes
|
|
final IntObjectMap<Http2Stream> streamMap = new IntObjectHashMap<Http2Stream>();
|
|
final PropertyKeyRegistry propertyKeyRegistry = new PropertyKeyRegistry();
|
|
final ConnectionStream connectionStream = new ConnectionStream();
|
|
final DefaultEndpoint<Http2LocalFlowController> localEndpoint;
|
|
final DefaultEndpoint<Http2RemoteFlowController> remoteEndpoint;
|
|
|
|
/**
|
|
* We chose a {@link List} over a {@link Set} to avoid allocating an {@link Iterator} objects when iterating over
|
|
* the listeners.
|
|
* <p>
|
|
* Initial size of 4 because the default configuration currently has 3 listeners
|
|
* (local/remote flow controller and {@link StreamByteDistributor}) and we leave room for 1 extra.
|
|
* We could be more aggressive but the ArrayList resize will double the size if we are too small.
|
|
*/
|
|
final List<Listener> listeners = new ArrayList<Listener>(4);
|
|
final ActiveStreams activeStreams;
|
|
Promise<Void> closePromise;
|
|
|
|
/**
|
|
* Creates a new connection with the given settings.
|
|
* @param server whether or not this end-point is the server-side of the HTTP/2 connection.
|
|
*/
|
|
public DefaultHttp2Connection(boolean server) {
|
|
this(server, DEFAULT_MAX_RESERVED_STREAMS);
|
|
}
|
|
|
|
/**
|
|
* Creates a new connection with the given settings.
|
|
* @param server whether or not this end-point is the server-side of the HTTP/2 connection.
|
|
* @param maxReservedStreams The maximum amount of streams which can exist in the reserved state for each endpoint.
|
|
*/
|
|
public DefaultHttp2Connection(boolean server, int maxReservedStreams) {
|
|
activeStreams = new ActiveStreams(listeners);
|
|
// Reserved streams are excluded from the SETTINGS_MAX_CONCURRENT_STREAMS limit according to [1] and the RFC
|
|
// doesn't define a way to communicate the limit on reserved streams. We rely upon the peer to send RST_STREAM
|
|
// in response to any locally enforced limits being exceeded [2].
|
|
// [1] https://tools.ietf.org/html/rfc7540#section-5.1.2
|
|
// [2] https://tools.ietf.org/html/rfc7540#section-8.2.2
|
|
localEndpoint = new DefaultEndpoint<Http2LocalFlowController>(server, server ? MAX_VALUE : maxReservedStreams);
|
|
remoteEndpoint = new DefaultEndpoint<Http2RemoteFlowController>(!server, maxReservedStreams);
|
|
|
|
// Add the connection stream to the map.
|
|
streamMap.put(connectionStream.id(), connectionStream);
|
|
}
|
|
|
|
/**
|
|
* Determine if {@link #close(Promise)} has been called and no more streams are allowed to be created.
|
|
*/
|
|
final boolean isClosed() {
|
|
return closePromise != null;
|
|
}
|
|
|
|
@Override
|
|
public Future<Void> close(final Promise<Void> promise) {
|
|
checkNotNull(promise, "promise");
|
|
// Since we allow this method to be called multiple times, we must make sure that all the promises are notified
|
|
// when all streams are removed and the close operation completes.
|
|
if (closePromise != null) {
|
|
if (closePromise == promise) {
|
|
// Do nothing
|
|
} else if ((promise instanceof ChannelPromise) && ((ChannelPromise) closePromise).isVoid()) {
|
|
closePromise = promise;
|
|
} else {
|
|
closePromise.addListener(new UnaryPromiseNotifier<Void>(promise));
|
|
}
|
|
} else {
|
|
closePromise = promise;
|
|
}
|
|
if (isStreamMapEmpty()) {
|
|
promise.trySuccess(null);
|
|
return promise;
|
|
}
|
|
|
|
Iterator<PrimitiveEntry<Http2Stream>> itr = streamMap.entries().iterator();
|
|
// We must take care while iterating the streamMap as to not modify while iterating in case there are other code
|
|
// paths iterating over the active streams.
|
|
if (activeStreams.allowModifications()) {
|
|
activeStreams.incrementPendingIterations();
|
|
try {
|
|
while (itr.hasNext()) {
|
|
DefaultStream stream = (DefaultStream) itr.next().value();
|
|
if (stream.id() != CONNECTION_STREAM_ID) {
|
|
// If modifications of the activeStream map is allowed, then a stream close operation will also
|
|
// modify the streamMap. Pass the iterator in so that remove will be called to prevent
|
|
// concurrent modification exceptions.
|
|
stream.close(itr);
|
|
}
|
|
}
|
|
} finally {
|
|
activeStreams.decrementPendingIterations();
|
|
}
|
|
} else {
|
|
while (itr.hasNext()) {
|
|
Http2Stream stream = itr.next().value();
|
|
if (stream.id() != CONNECTION_STREAM_ID) {
|
|
// We are not allowed to make modifications, so the close calls will be executed after this
|
|
// iteration completes.
|
|
stream.close();
|
|
}
|
|
}
|
|
}
|
|
return closePromise;
|
|
}
|
|
|
|
@Override
|
|
public void addListener(Listener listener) {
|
|
listeners.add(listener);
|
|
}
|
|
|
|
@Override
|
|
public void removeListener(Listener listener) {
|
|
listeners.remove(listener);
|
|
}
|
|
|
|
@Override
|
|
public boolean isServer() {
|
|
return localEndpoint.isServer();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream connectionStream() {
|
|
return connectionStream;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream stream(int streamId) {
|
|
return streamMap.get(streamId);
|
|
}
|
|
|
|
@Override
|
|
public boolean streamMayHaveExisted(int streamId) {
|
|
return remoteEndpoint.mayHaveCreatedStream(streamId) || localEndpoint.mayHaveCreatedStream(streamId);
|
|
}
|
|
|
|
@Override
|
|
public int numActiveStreams() {
|
|
return activeStreams.size();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream forEachActiveStream(Http2StreamVisitor visitor) throws Http2Exception {
|
|
return activeStreams.forEachActiveStream(visitor);
|
|
}
|
|
|
|
@Override
|
|
public Endpoint<Http2LocalFlowController> local() {
|
|
return localEndpoint;
|
|
}
|
|
|
|
@Override
|
|
public Endpoint<Http2RemoteFlowController> remote() {
|
|
return remoteEndpoint;
|
|
}
|
|
|
|
@Override
|
|
public boolean goAwayReceived() {
|
|
return localEndpoint.lastStreamKnownByPeer >= 0;
|
|
}
|
|
|
|
@Override
|
|
public void goAwayReceived(final int lastKnownStream, long errorCode, ByteBuf debugData) throws Http2Exception {
|
|
if (localEndpoint.lastStreamKnownByPeer() >= 0 && localEndpoint.lastStreamKnownByPeer() < lastKnownStream) {
|
|
throw connectionError(PROTOCOL_ERROR, "lastStreamId MUST NOT increase. Current value: %d new value: %d",
|
|
localEndpoint.lastStreamKnownByPeer(), lastKnownStream);
|
|
}
|
|
|
|
localEndpoint.lastStreamKnownByPeer(lastKnownStream);
|
|
for (int i = 0; i < listeners.size(); ++i) {
|
|
try {
|
|
listeners.get(i).onGoAwayReceived(lastKnownStream, errorCode, debugData);
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable from listener onGoAwayReceived.", cause);
|
|
}
|
|
}
|
|
|
|
closeStreamsGreaterThanLastKnownStreamId(lastKnownStream, localEndpoint);
|
|
}
|
|
|
|
@Override
|
|
public boolean goAwaySent() {
|
|
return remoteEndpoint.lastStreamKnownByPeer >= 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean goAwaySent(final int lastKnownStream, long errorCode, ByteBuf debugData) throws Http2Exception {
|
|
if (remoteEndpoint.lastStreamKnownByPeer() >= 0) {
|
|
// Protect against re-entrancy. Could happen if writing the frame fails, and error handling
|
|
// treating this is a connection handler and doing a graceful shutdown...
|
|
if (lastKnownStream == remoteEndpoint.lastStreamKnownByPeer()) {
|
|
return false;
|
|
}
|
|
if (lastKnownStream > remoteEndpoint.lastStreamKnownByPeer()) {
|
|
throw connectionError(PROTOCOL_ERROR, "Last stream identifier must not increase between " +
|
|
"sending multiple GOAWAY frames (was '%d', is '%d').",
|
|
remoteEndpoint.lastStreamKnownByPeer(), lastKnownStream);
|
|
}
|
|
}
|
|
|
|
remoteEndpoint.lastStreamKnownByPeer(lastKnownStream);
|
|
for (int i = 0; i < listeners.size(); ++i) {
|
|
try {
|
|
listeners.get(i).onGoAwaySent(lastKnownStream, errorCode, debugData);
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable from listener onGoAwaySent.", cause);
|
|
}
|
|
}
|
|
|
|
closeStreamsGreaterThanLastKnownStreamId(lastKnownStream, remoteEndpoint);
|
|
return true;
|
|
}
|
|
|
|
private void closeStreamsGreaterThanLastKnownStreamId(final int lastKnownStream,
|
|
final DefaultEndpoint<?> endpoint) throws Http2Exception {
|
|
forEachActiveStream(new Http2StreamVisitor() {
|
|
@Override
|
|
public boolean visit(Http2Stream stream) {
|
|
if (stream.id() > lastKnownStream && endpoint.isValidStreamId(stream.id())) {
|
|
stream.close();
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Determine if {@link #streamMap} only contains the connection stream.
|
|
*/
|
|
private boolean isStreamMapEmpty() {
|
|
return streamMap.size() == 1;
|
|
}
|
|
|
|
/**
|
|
* Remove a stream from the {@link #streamMap}.
|
|
* @param stream the stream to remove.
|
|
* @param itr an iterator that may be pointing to the stream during iteration and {@link Iterator#remove()} will be
|
|
* used if non-{@code null}.
|
|
*/
|
|
void removeStream(DefaultStream stream, Iterator<?> itr) {
|
|
final boolean removed;
|
|
if (itr == null) {
|
|
removed = streamMap.remove(stream.id()) != null;
|
|
} else {
|
|
itr.remove();
|
|
removed = true;
|
|
}
|
|
|
|
if (removed) {
|
|
for (int i = 0; i < listeners.size(); i++) {
|
|
try {
|
|
listeners.get(i).onStreamRemoved(stream);
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable from listener onStreamRemoved.", cause);
|
|
}
|
|
}
|
|
|
|
if (closePromise != null && isStreamMapEmpty()) {
|
|
closePromise.trySuccess(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
static State activeState(int streamId, State initialState, boolean isLocal, boolean halfClosed)
|
|
throws Http2Exception {
|
|
switch (initialState) {
|
|
case IDLE:
|
|
return halfClosed ? isLocal ? HALF_CLOSED_LOCAL : HALF_CLOSED_REMOTE : OPEN;
|
|
case RESERVED_LOCAL:
|
|
return HALF_CLOSED_REMOTE;
|
|
case RESERVED_REMOTE:
|
|
return HALF_CLOSED_LOCAL;
|
|
default:
|
|
throw streamError(streamId, PROTOCOL_ERROR, "Attempting to open a stream in an invalid state: "
|
|
+ initialState);
|
|
}
|
|
}
|
|
|
|
void notifyHalfClosed(Http2Stream stream) {
|
|
for (int i = 0; i < listeners.size(); i++) {
|
|
try {
|
|
listeners.get(i).onStreamHalfClosed(stream);
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable from listener onStreamHalfClosed.", cause);
|
|
}
|
|
}
|
|
}
|
|
|
|
void notifyClosed(Http2Stream stream) {
|
|
for (int i = 0; i < listeners.size(); i++) {
|
|
try {
|
|
listeners.get(i).onStreamClosed(stream);
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable from listener onStreamClosed.", cause);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public PropertyKey newKey() {
|
|
return propertyKeyRegistry.newKey();
|
|
}
|
|
|
|
/**
|
|
* Verifies that the key is valid and returns it as the internal {@link DefaultPropertyKey} type.
|
|
*
|
|
* @throws NullPointerException if the key is {@code null}.
|
|
* @throws ClassCastException if the key is not of type {@link DefaultPropertyKey}.
|
|
* @throws IllegalArgumentException if the key was not created by this connection.
|
|
*/
|
|
final DefaultPropertyKey verifyKey(PropertyKey key) {
|
|
return checkNotNull((DefaultPropertyKey) key, "key").verifyConnection(this);
|
|
}
|
|
|
|
/**
|
|
* Simple stream implementation. Streams can be compared to each other by priority.
|
|
*/
|
|
private class DefaultStream implements Http2Stream {
|
|
private static final byte META_STATE_SENT_RST = 1;
|
|
private static final byte META_STATE_SENT_HEADERS = 1 << 1;
|
|
private static final byte META_STATE_SENT_TRAILERS = 1 << 2;
|
|
private static final byte META_STATE_SENT_PUSHPROMISE = 1 << 3;
|
|
private static final byte META_STATE_RECV_HEADERS = 1 << 4;
|
|
private static final byte META_STATE_RECV_TRAILERS = 1 << 5;
|
|
private final int id;
|
|
private final PropertyMap properties = new PropertyMap();
|
|
private State state;
|
|
private byte metaState;
|
|
|
|
DefaultStream(int id, State state) {
|
|
this.id = id;
|
|
this.state = state;
|
|
}
|
|
|
|
@Override
|
|
public final int id() {
|
|
return id;
|
|
}
|
|
|
|
@Override
|
|
public final State state() {
|
|
return state;
|
|
}
|
|
|
|
@Override
|
|
public boolean isResetSent() {
|
|
return (metaState & META_STATE_SENT_RST) != 0;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream resetSent() {
|
|
metaState |= META_STATE_SENT_RST;
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream headersSent(boolean isInformational) {
|
|
if (!isInformational) {
|
|
metaState |= isHeadersSent() ? META_STATE_SENT_TRAILERS : META_STATE_SENT_HEADERS;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public boolean isHeadersSent() {
|
|
return (metaState & META_STATE_SENT_HEADERS) != 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean isTrailersSent() {
|
|
return (metaState & META_STATE_SENT_TRAILERS) != 0;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream headersReceived(boolean isInformational) {
|
|
if (!isInformational) {
|
|
metaState |= isHeadersReceived() ? META_STATE_RECV_TRAILERS : META_STATE_RECV_HEADERS;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public boolean isHeadersReceived() {
|
|
return (metaState & META_STATE_RECV_HEADERS) != 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean isTrailersReceived() {
|
|
return (metaState & META_STATE_RECV_TRAILERS) != 0;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream pushPromiseSent() {
|
|
metaState |= META_STATE_SENT_PUSHPROMISE;
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public boolean isPushPromiseSent() {
|
|
return (metaState & META_STATE_SENT_PUSHPROMISE) != 0;
|
|
}
|
|
|
|
@Override
|
|
public final <V> V setProperty(PropertyKey key, V value) {
|
|
return properties.add(verifyKey(key), value);
|
|
}
|
|
|
|
@Override
|
|
public final <V> V getProperty(PropertyKey key) {
|
|
return properties.get(verifyKey(key));
|
|
}
|
|
|
|
@Override
|
|
public final <V> V removeProperty(PropertyKey key) {
|
|
return properties.remove(verifyKey(key));
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream open(boolean halfClosed) throws Http2Exception {
|
|
state = activeState(id, state, isLocal(), halfClosed);
|
|
if (!createdBy().canOpenStream()) {
|
|
throw connectionError(PROTOCOL_ERROR, "Maximum active streams violated for this endpoint.");
|
|
}
|
|
|
|
activate();
|
|
return this;
|
|
}
|
|
|
|
void activate() {
|
|
// If the stream is opened in a half-closed state, the headers must have either
|
|
// been sent if this is a local stream, or received if it is a remote stream.
|
|
if (state == HALF_CLOSED_LOCAL) {
|
|
headersSent(/*isInformational*/ false);
|
|
} else if (state == HALF_CLOSED_REMOTE) {
|
|
headersReceived(/*isInformational*/ false);
|
|
}
|
|
activeStreams.activate(this);
|
|
}
|
|
|
|
Http2Stream close(Iterator<?> itr) {
|
|
if (state == CLOSED) {
|
|
return this;
|
|
}
|
|
|
|
state = CLOSED;
|
|
|
|
--createdBy().numStreams;
|
|
activeStreams.deactivate(this, itr);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream close() {
|
|
return close(null);
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream closeLocalSide() {
|
|
switch (state) {
|
|
case OPEN:
|
|
state = HALF_CLOSED_LOCAL;
|
|
notifyHalfClosed(this);
|
|
break;
|
|
case HALF_CLOSED_LOCAL:
|
|
break;
|
|
default:
|
|
close();
|
|
break;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream closeRemoteSide() {
|
|
switch (state) {
|
|
case OPEN:
|
|
state = HALF_CLOSED_REMOTE;
|
|
notifyHalfClosed(this);
|
|
break;
|
|
case HALF_CLOSED_REMOTE:
|
|
break;
|
|
default:
|
|
close();
|
|
break;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
DefaultEndpoint<? extends Http2FlowController> createdBy() {
|
|
return localEndpoint.isValidStreamId(id) ? localEndpoint : remoteEndpoint;
|
|
}
|
|
|
|
final boolean isLocal() {
|
|
return localEndpoint.isValidStreamId(id);
|
|
}
|
|
|
|
/**
|
|
* Provides the lazy initialization for the {@link DefaultStream} data map.
|
|
*/
|
|
private class PropertyMap {
|
|
Object[] values = EmptyArrays.EMPTY_OBJECTS;
|
|
|
|
<V> V add(DefaultPropertyKey key, V value) {
|
|
resizeIfNecessary(key.index);
|
|
@SuppressWarnings("unchecked")
|
|
V prevValue = (V) values[key.index];
|
|
values[key.index] = value;
|
|
return prevValue;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
<V> V get(DefaultPropertyKey key) {
|
|
if (key.index >= values.length) {
|
|
return null;
|
|
}
|
|
return (V) values[key.index];
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
<V> V remove(DefaultPropertyKey key) {
|
|
V prevValue = null;
|
|
if (key.index < values.length) {
|
|
prevValue = (V) values[key.index];
|
|
values[key.index] = null;
|
|
}
|
|
return prevValue;
|
|
}
|
|
|
|
void resizeIfNecessary(int index) {
|
|
if (index >= values.length) {
|
|
values = Arrays.copyOf(values, propertyKeyRegistry.size());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stream class representing the connection, itself.
|
|
*/
|
|
private final class ConnectionStream extends DefaultStream {
|
|
ConnectionStream() {
|
|
super(CONNECTION_STREAM_ID, IDLE);
|
|
}
|
|
|
|
@Override
|
|
public boolean isResetSent() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
DefaultEndpoint<? extends Http2FlowController> createdBy() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream resetSent() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream open(boolean halfClosed) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream close() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream closeLocalSide() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream closeRemoteSide() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream headersSent(boolean isInformational) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public boolean isHeadersSent() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public Http2Stream pushPromiseSent() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public boolean isPushPromiseSent() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple endpoint implementation.
|
|
*/
|
|
private final class DefaultEndpoint<F extends Http2FlowController> implements Endpoint<F> {
|
|
private final boolean server;
|
|
/**
|
|
* The minimum stream ID allowed when creating the next stream. This only applies at the time the stream is
|
|
* created. If the ID of the stream being created is less than this value, stream creation will fail. Upon
|
|
* successful creation of a stream, this value is incremented to the next valid stream ID.
|
|
*/
|
|
private int nextStreamIdToCreate;
|
|
/**
|
|
* Used for reservation of stream IDs. Stream IDs can be reserved in advance by applications before the streams
|
|
* are actually created. For example, applications may choose to buffer stream creation attempts as a way of
|
|
* working around {@code SETTINGS_MAX_CONCURRENT_STREAMS}, in which case they will reserve stream IDs for each
|
|
* buffered stream.
|
|
*/
|
|
private int nextReservationStreamId;
|
|
private int lastStreamKnownByPeer = -1;
|
|
private boolean pushToAllowed = true;
|
|
private F flowController;
|
|
private int maxStreams;
|
|
private int maxActiveStreams;
|
|
private final int maxReservedStreams;
|
|
// Fields accessed by inner classes
|
|
int numActiveStreams;
|
|
int numStreams;
|
|
|
|
DefaultEndpoint(boolean server, int maxReservedStreams) {
|
|
this.server = server;
|
|
|
|
// Determine the starting stream ID for this endpoint. Client-initiated streams
|
|
// are odd and server-initiated streams are even. Zero is reserved for the
|
|
// connection. Stream 1 is reserved client-initiated stream for responding to an
|
|
// upgrade from HTTP 1.1.
|
|
if (server) {
|
|
nextStreamIdToCreate = 2;
|
|
nextReservationStreamId = 0;
|
|
} else {
|
|
nextStreamIdToCreate = 1;
|
|
// For manually created client-side streams, 1 is reserved for HTTP upgrade, so start at 3.
|
|
nextReservationStreamId = 1;
|
|
}
|
|
|
|
// Push is disallowed by default for servers and allowed for clients.
|
|
pushToAllowed = !server;
|
|
maxActiveStreams = MAX_VALUE;
|
|
this.maxReservedStreams = checkPositiveOrZero(maxReservedStreams, "maxReservedStreams");
|
|
updateMaxStreams();
|
|
}
|
|
|
|
@Override
|
|
public int incrementAndGetNextStreamId() {
|
|
return nextReservationStreamId >= 0 ? nextReservationStreamId += 2 : nextReservationStreamId;
|
|
}
|
|
|
|
private void incrementExpectedStreamId(int streamId) {
|
|
if (streamId > nextReservationStreamId && nextReservationStreamId >= 0) {
|
|
nextReservationStreamId = streamId;
|
|
}
|
|
nextStreamIdToCreate = streamId + 2;
|
|
++numStreams;
|
|
}
|
|
|
|
@Override
|
|
public boolean isValidStreamId(int streamId) {
|
|
return streamId > 0 && server == ((streamId & 1) == 0);
|
|
}
|
|
|
|
@Override
|
|
public boolean mayHaveCreatedStream(int streamId) {
|
|
return isValidStreamId(streamId) && streamId <= lastStreamCreated();
|
|
}
|
|
|
|
@Override
|
|
public boolean canOpenStream() {
|
|
return numActiveStreams < maxActiveStreams;
|
|
}
|
|
|
|
@Override
|
|
public DefaultStream createStream(int streamId, boolean halfClosed) throws Http2Exception {
|
|
State state = activeState(streamId, IDLE, isLocal(), halfClosed);
|
|
|
|
checkNewStreamAllowed(streamId, state);
|
|
|
|
// Create and initialize the stream.
|
|
DefaultStream stream = new DefaultStream(streamId, state);
|
|
|
|
incrementExpectedStreamId(streamId);
|
|
|
|
addStream(stream);
|
|
|
|
stream.activate();
|
|
return stream;
|
|
}
|
|
|
|
@Override
|
|
public boolean created(Http2Stream stream) {
|
|
return stream instanceof DefaultStream && ((DefaultStream) stream).createdBy() == this;
|
|
}
|
|
|
|
@Override
|
|
public boolean isServer() {
|
|
return server;
|
|
}
|
|
|
|
@Override
|
|
public DefaultStream reservePushStream(int streamId, Http2Stream parent) throws Http2Exception {
|
|
if (parent == null) {
|
|
throw connectionError(PROTOCOL_ERROR, "Parent stream missing");
|
|
}
|
|
if (isLocal() ? !parent.state().localSideOpen() : !parent.state().remoteSideOpen()) {
|
|
throw connectionError(PROTOCOL_ERROR, "Stream %d is not open for sending push promise", parent.id());
|
|
}
|
|
if (!opposite().allowPushTo()) {
|
|
throw connectionError(PROTOCOL_ERROR, "Server push not allowed to opposite endpoint");
|
|
}
|
|
State state = isLocal() ? RESERVED_LOCAL : RESERVED_REMOTE;
|
|
checkNewStreamAllowed(streamId, state);
|
|
|
|
// Create and initialize the stream.
|
|
DefaultStream stream = new DefaultStream(streamId, state);
|
|
|
|
incrementExpectedStreamId(streamId);
|
|
|
|
// Register the stream.
|
|
addStream(stream);
|
|
return stream;
|
|
}
|
|
|
|
private void addStream(DefaultStream stream) {
|
|
// Add the stream to the map and priority tree.
|
|
streamMap.put(stream.id(), stream);
|
|
|
|
// Notify the listeners of the event.
|
|
for (int i = 0; i < listeners.size(); i++) {
|
|
try {
|
|
listeners.get(i).onStreamAdded(stream);
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable from listener onStreamAdded.", cause);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void allowPushTo(boolean allow) {
|
|
if (allow && server) {
|
|
throw new IllegalArgumentException("Servers do not allow push");
|
|
}
|
|
pushToAllowed = allow;
|
|
}
|
|
|
|
@Override
|
|
public boolean allowPushTo() {
|
|
return pushToAllowed;
|
|
}
|
|
|
|
@Override
|
|
public int numActiveStreams() {
|
|
return numActiveStreams;
|
|
}
|
|
|
|
@Override
|
|
public int maxActiveStreams() {
|
|
return maxActiveStreams;
|
|
}
|
|
|
|
@Override
|
|
public void maxActiveStreams(int maxActiveStreams) {
|
|
this.maxActiveStreams = maxActiveStreams;
|
|
updateMaxStreams();
|
|
}
|
|
|
|
@Override
|
|
public int lastStreamCreated() {
|
|
return nextStreamIdToCreate > 1 ? nextStreamIdToCreate - 2 : 0;
|
|
}
|
|
|
|
@Override
|
|
public int lastStreamKnownByPeer() {
|
|
return lastStreamKnownByPeer;
|
|
}
|
|
|
|
private void lastStreamKnownByPeer(int lastKnownStream) {
|
|
this.lastStreamKnownByPeer = lastKnownStream;
|
|
}
|
|
|
|
@Override
|
|
public F flowController() {
|
|
return flowController;
|
|
}
|
|
|
|
@Override
|
|
public void flowController(F flowController) {
|
|
this.flowController = checkNotNull(flowController, "flowController");
|
|
}
|
|
|
|
@Override
|
|
public Endpoint<? extends Http2FlowController> opposite() {
|
|
return isLocal() ? remoteEndpoint : localEndpoint;
|
|
}
|
|
|
|
private void updateMaxStreams() {
|
|
maxStreams = (int) Math.min(MAX_VALUE, (long) maxActiveStreams + maxReservedStreams);
|
|
}
|
|
|
|
private void checkNewStreamAllowed(int streamId, State state) throws Http2Exception {
|
|
assert state != IDLE;
|
|
if (lastStreamKnownByPeer >= 0 && streamId > lastStreamKnownByPeer) {
|
|
throw streamError(streamId, REFUSED_STREAM,
|
|
"Cannot create stream %d greater than Last-Stream-ID %d from GOAWAY.",
|
|
streamId, lastStreamKnownByPeer);
|
|
}
|
|
if (!isValidStreamId(streamId)) {
|
|
if (streamId < 0) {
|
|
throw new Http2NoMoreStreamIdsException();
|
|
}
|
|
throw connectionError(PROTOCOL_ERROR, "Request stream %d is not correct for %s connection", streamId,
|
|
server ? "server" : "client");
|
|
}
|
|
// This check must be after all id validated checks, but before the max streams check because it may be
|
|
// recoverable to some degree for handling frames which can be sent on closed streams.
|
|
if (streamId < nextStreamIdToCreate) {
|
|
throw closedStreamError(PROTOCOL_ERROR, "Request stream %d is behind the next expected stream %d",
|
|
streamId, nextStreamIdToCreate);
|
|
}
|
|
if (nextStreamIdToCreate <= 0) {
|
|
throw connectionError(REFUSED_STREAM, "Stream IDs are exhausted for this endpoint.");
|
|
}
|
|
boolean isReserved = state == RESERVED_LOCAL || state == RESERVED_REMOTE;
|
|
if (!isReserved && !canOpenStream() || isReserved && numStreams >= maxStreams) {
|
|
throw streamError(streamId, REFUSED_STREAM, "Maximum active streams violated for this endpoint.");
|
|
}
|
|
if (isClosed()) {
|
|
throw connectionError(INTERNAL_ERROR, "Attempted to create stream id %d after connection was closed",
|
|
streamId);
|
|
}
|
|
}
|
|
|
|
private boolean isLocal() {
|
|
return this == localEndpoint;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allows events which would modify the collection of active streams to be queued while iterating via {@link
|
|
* #forEachActiveStream(Http2StreamVisitor)}.
|
|
*/
|
|
interface Event {
|
|
/**
|
|
* Trigger the original intention of this event. Expect to modify the active streams list.
|
|
* <p/>
|
|
* If a {@link RuntimeException} object is thrown it will be logged and <strong>not propagated</strong>.
|
|
* Throwing from this method is not supported and is considered a programming error.
|
|
*/
|
|
void process();
|
|
}
|
|
|
|
/**
|
|
* Manages the list of currently active streams. Queues any {@link Event}s that would modify the list of
|
|
* active streams in order to prevent modification while iterating.
|
|
*/
|
|
private final class ActiveStreams {
|
|
private final List<Listener> listeners;
|
|
private final Queue<Event> pendingEvents = new ArrayDeque<Event>(4);
|
|
private final Set<Http2Stream> streams = new LinkedHashSet<Http2Stream>();
|
|
private int pendingIterations;
|
|
|
|
ActiveStreams(List<Listener> listeners) {
|
|
this.listeners = listeners;
|
|
}
|
|
|
|
public int size() {
|
|
return streams.size();
|
|
}
|
|
|
|
public void activate(final DefaultStream stream) {
|
|
if (allowModifications()) {
|
|
addToActiveStreams(stream);
|
|
} else {
|
|
pendingEvents.add(new Event() {
|
|
@Override
|
|
public void process() {
|
|
addToActiveStreams(stream);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public void deactivate(final DefaultStream stream, final Iterator<?> itr) {
|
|
if (allowModifications() || itr != null) {
|
|
removeFromActiveStreams(stream, itr);
|
|
} else {
|
|
pendingEvents.add(new Event() {
|
|
@Override
|
|
public void process() {
|
|
removeFromActiveStreams(stream, itr);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public Http2Stream forEachActiveStream(Http2StreamVisitor visitor) throws Http2Exception {
|
|
incrementPendingIterations();
|
|
try {
|
|
for (Http2Stream stream : streams) {
|
|
if (!visitor.visit(stream)) {
|
|
return stream;
|
|
}
|
|
}
|
|
return null;
|
|
} finally {
|
|
decrementPendingIterations();
|
|
}
|
|
}
|
|
|
|
void addToActiveStreams(DefaultStream stream) {
|
|
if (streams.add(stream)) {
|
|
// Update the number of active streams initiated by the endpoint.
|
|
stream.createdBy().numActiveStreams++;
|
|
|
|
for (int i = 0; i < listeners.size(); i++) {
|
|
try {
|
|
listeners.get(i).onStreamActive(stream);
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable from listener onStreamActive.", cause);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void removeFromActiveStreams(DefaultStream stream, Iterator<?> itr) {
|
|
if (streams.remove(stream)) {
|
|
// Update the number of active streams initiated by the endpoint.
|
|
stream.createdBy().numActiveStreams--;
|
|
notifyClosed(stream);
|
|
}
|
|
removeStream(stream, itr);
|
|
}
|
|
|
|
boolean allowModifications() {
|
|
return pendingIterations == 0;
|
|
}
|
|
|
|
void incrementPendingIterations() {
|
|
++pendingIterations;
|
|
}
|
|
|
|
void decrementPendingIterations() {
|
|
--pendingIterations;
|
|
if (allowModifications()) {
|
|
for (;;) {
|
|
Event event = pendingEvents.poll();
|
|
if (event == null) {
|
|
break;
|
|
}
|
|
try {
|
|
event.process();
|
|
} catch (Throwable cause) {
|
|
logger.error("Caught Throwable while processing pending ActiveStreams$Event.", cause);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implementation of {@link PropertyKey} that specifies the index position of the property.
|
|
*/
|
|
final class DefaultPropertyKey implements PropertyKey {
|
|
final int index;
|
|
|
|
DefaultPropertyKey(int index) {
|
|
this.index = index;
|
|
}
|
|
|
|
DefaultPropertyKey verifyConnection(Http2Connection connection) {
|
|
if (connection != DefaultHttp2Connection.this) {
|
|
throw new IllegalArgumentException("Using a key that was not created by this connection");
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A registry of all stream property keys known by this connection.
|
|
*/
|
|
private final class PropertyKeyRegistry {
|
|
/**
|
|
* Initial size of 4 because the default configuration currently has 3 listeners
|
|
* (local/remote flow controller and {@link StreamByteDistributor}) and we leave room for 1 extra.
|
|
* We could be more aggressive but the ArrayList resize will double the size if we are too small.
|
|
*/
|
|
final List<DefaultPropertyKey> keys = new ArrayList<DefaultPropertyKey>(4);
|
|
|
|
/**
|
|
* Registers a new property key.
|
|
*/
|
|
DefaultPropertyKey newKey() {
|
|
DefaultPropertyKey key = new DefaultPropertyKey(keys.size());
|
|
keys.add(key);
|
|
return key;
|
|
}
|
|
|
|
int size() {
|
|
return keys.size();
|
|
}
|
|
}
|
|
}
|