From ac3ba074d24c0e0ef62b60d4c95a8cc219bd4f48 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Sat, 3 Sep 2022 13:45:08 -0700 Subject: [PATCH] HTTP/2 set initial settings Motivaiton: There is no way to define initial values for [http/2 settings](https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2). Modifications: - Enhance H2ProtocolConfigBuilder to accept Map to set the initial settings values. - Set the default value of settings with bound max header list size and 1mb default initial flow control window. - Increase flow control quantum to 16kb because the flow control window is larger. This allows streams to write an entire frame per write operation and increases goodput on the connection. --- .../servicetalk/http/api/Http2Settings.java | 57 +++++++++ .../gradle/spotbugs/main-exclusions.xml | 10 ++ .../H2ClientParentChannelInitializer.java | 24 +++- .../http/netty/H2ProtocolConfig.java | 16 +++ .../http/netty/H2ProtocolConfigBuilder.java | 77 ++++++++++-- .../H2ServerParentChannelInitializer.java | 42 ++++++- .../http/netty/Http2SettingsBuilder.java | 118 ++++++++++++++++++ .../http/netty/HttpProtocolConfigs.java | 13 ++ .../OptimizedHttp2FrameCodecBuilder.java | 13 +- 9 files changed, 350 insertions(+), 20 deletions(-) create mode 100644 servicetalk-http-api/src/main/java/io/servicetalk/http/api/Http2Settings.java create mode 100644 servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/Http2SettingsBuilder.java diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/Http2Settings.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/Http2Settings.java new file mode 100644 index 0000000000..aee604779b --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/Http2Settings.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * + * Licensed 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.servicetalk.http.api; + +/** + * Utilities for HTTP/2 Setting. + */ +public final class Http2Settings { + /** + * Identifier SETTINGS_HEADER_TABLE_SIZE. + */ + public static final char HEADER_TABLE_SIZE = 0x1; + + /** + * Identifier SETTINGS_ENABLE_PUSH. + */ + public static final char ENABLE_PUSH = 0x2; + + /** + * Identifier + * SETTINGS_MAX_CONCURRENT_STREAMS. + */ + public static final char MAX_CONCURRENT_STREAMS = 0x3; + + /** + * Identifier + * SETTINGS_INITIAL_WINDOW_SIZE. + */ + public static final char INITIAL_WINDOW_SIZE = 0x4; + + /** + * Identifier SETTINGS_MAX_FRAME_SIZE. + */ + public static final char MAX_FRAME_SIZE = 0x5; + + /** + * Identifier + * SETTINGS_MAX_HEADER_LIST_SIZE. + */ + public static final char MAX_HEADER_LIST_SIZE = 0x6; + + private Http2Settings() { + } +} diff --git a/servicetalk-http-netty/gradle/spotbugs/main-exclusions.xml b/servicetalk-http-netty/gradle/spotbugs/main-exclusions.xml index f13d3c57a5..70403eb956 100644 --- a/servicetalk-http-netty/gradle/spotbugs/main-exclusions.xml +++ b/servicetalk-http-netty/gradle/spotbugs/main-exclusions.xml @@ -131,4 +131,14 @@ + + + + + + + + + + diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ClientParentChannelInitializer.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ClientParentChannelInitializer.java index 935e49e2ed..a8818d4c1d 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ClientParentChannelInitializer.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ClientParentChannelInitializer.java @@ -24,14 +24,18 @@ import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2Settings; +import java.util.Map; import java.util.function.BiPredicate; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.servicetalk.http.netty.H2ServerParentChannelInitializer.initFrameLogger; +import static io.servicetalk.http.netty.H2ServerParentChannelInitializer.toNettySettings; final class H2ClientParentChannelInitializer implements ChannelInitializer { - + private static final Http2Settings DEFAULT_NETTY_SETTINGS = + applyClientDefaultSettings(Http2Settings.defaultSettings()); private final H2ProtocolConfig config; H2ClientParentChannelInitializer(final H2ProtocolConfig config) { @@ -40,7 +44,8 @@ final class H2ClientParentChannelInitializer implements ChannelInitializer { @Override public void init(final Channel channel) { - final Http2FrameCodecBuilder multiplexCodecBuilder = new OptimizedHttp2FrameCodecBuilder(false) + final Http2FrameCodecBuilder multiplexCodecBuilder = + new OptimizedHttp2FrameCodecBuilder(false, config.flowControlQuantum()) // We do not want close to trigger graceful closure (go away), instead when user triggers a graceful // close, we do the appropriate go away handling. .decoupleCloseAndGoAway(true) @@ -53,8 +58,12 @@ public void init(final Channel channel) { // the user to apply their own timeout at the call site. .gracefulShutdownTimeoutMillis(-1); - // Notify server that this client does not support server push and request it to be disabled. - multiplexCodecBuilder.initialSettings().pushEnabled(false).maxConcurrentStreams(0L); + final Map h2Settings = config.initialSettings(); + if (h2Settings.isEmpty()) { + multiplexCodecBuilder.initialSettings(DEFAULT_NETTY_SETTINGS); + } else { + multiplexCodecBuilder.initialSettings(applyClientDefaultSettings(toNettySettings(h2Settings))); + } final BiPredicate headersSensitivityDetector = config.headersSensitivityDetector(); @@ -62,7 +71,7 @@ public void init(final Channel channel) { initFrameLogger(multiplexCodecBuilder, config.frameLoggerConfig()); - // TODO(scott): more configuration. header validation, settings stream, etc... + // TODO(scott): more configuration. header validation, etc... channel.pipeline().addLast(multiplexCodecBuilder.build(), new Http2MultiplexHandler(H2PushStreamHandler.INSTANCE)); @@ -84,4 +93,9 @@ public void channelRegistered(final ChannelHandlerContext ctx) { // Http2ConnectionHandler.processGoAwayWriteResult will close the connection after GO_AWAY is flushed } } + + private static Http2Settings applyClientDefaultSettings(Http2Settings settings) { + // Notify server that this client does not support server push and request it to be disabled. + return settings.pushEnabled(false).maxConcurrentStreams(0L); + } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfig.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfig.java index 4be58f45b9..417c239e67 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfig.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfig.java @@ -19,6 +19,7 @@ import io.servicetalk.logging.api.UserDataLoggerConfig; import java.time.Duration; +import java.util.Map; import java.util.function.BiPredicate; import javax.annotation.Nullable; @@ -60,6 +61,21 @@ default String alpnId() { @Nullable KeepAlivePolicy keepAlivePolicy(); + /** + * Get a {@link Map} which provides a hint for the initial settings for any h2 connection. Note that some settings + * may be ignored if not supported (e.g. push promise). + * @return a {@link Map} which provides a hint for the initial settings for any h2 connection. Note that some + * settings may be ignored if not supported (e.g. push promise). + */ + Map initialSettings(); + + /** + * Provide a hint on the number of bytes that the flow controller will attempt to give to a stream for each + * allocation (assuming the stream has this much eligible data). + * @return number of bytes. + */ + int flowControlQuantum(); + /** * A policy for sending PING frames to the peer. */ diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfigBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfigBuilder.java index d9b4b2f45f..356f0d11e5 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfigBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ProtocolConfigBuilder.java @@ -22,10 +22,12 @@ import io.servicetalk.logging.api.UserDataLoggerConfig; import io.servicetalk.logging.slf4j.internal.DefaultUserDataLoggerConfig; +import java.util.Map; import java.util.function.BiPredicate; import java.util.function.BooleanSupplier; import javax.annotation.Nullable; +import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE; import static io.servicetalk.http.netty.H2KeepAlivePolicies.DISABLE_KEEP_ALIVE; import static java.util.Objects.requireNonNull; @@ -35,15 +37,15 @@ * @see HttpProtocolConfigs#h2() */ public final class H2ProtocolConfigBuilder { - private static final BiPredicate DEFAULT_SENSITIVITY_DETECTOR = (name, value) -> false; - + private Map h2Settings = newDefaultSettingsBuilder().build(); private HttpHeadersFactory headersFactory = H2HeadersFactory.INSTANCE; private BiPredicate headersSensitivityDetector = DEFAULT_SENSITIVITY_DETECTOR; @Nullable private UserDataLoggerConfig frameLoggerConfig; @Nullable private KeepAlivePolicy keepAlivePolicy; + private int flowControlQuantum = defaultFlowControlQuantum(); H2ProtocolConfigBuilder() { } @@ -103,33 +105,79 @@ public H2ProtocolConfigBuilder keepAlivePolicy(final KeepAlivePolicy policy) { return this; } + /** + * Sets the initial HTTP/2 Setting to for + * each h2 connection. + * @param settings the initial settings to for each h2 connection. + * @return {@code this} + * @see Http2SettingsBuilder + */ + public H2ProtocolConfigBuilder initialSettings(Map settings) { + this.h2Settings = requireNonNull(settings); + return this; + } + + /** + * Provide a hint on the number of bytes that the flow controller will attempt to give to a stream for each + * allocation (assuming the stream has this much eligible data). + * @param flowControlQuantum a hint on the number of bytes that the flow controller will attempt to give to a + * stream for each allocation (assuming the stream has this much eligible data). + * @return {@code this} + */ + public H2ProtocolConfigBuilder flowControlQuantum(int flowControlQuantum) { + if (flowControlQuantum <= 0) { + throw new IllegalArgumentException("flowControlQuantum " + flowControlQuantum + " (expected >0)"); + } + this.flowControlQuantum = flowControlQuantum; + return this; + } + /** * Builds {@link H2ProtocolConfig}. * * @return {@link H2ProtocolConfig} */ public H2ProtocolConfig build() { - return new DefaultH2ProtocolConfig(headersFactory, headersSensitivityDetector, frameLoggerConfig, - keepAlivePolicy); + return new DefaultH2ProtocolConfig(h2Settings, headersFactory, headersSensitivityDetector, frameLoggerConfig, + keepAlivePolicy, flowControlQuantum); } - private static final class DefaultH2ProtocolConfig implements H2ProtocolConfig { + static Http2SettingsBuilder newDefaultSettingsBuilder() { + return new Http2SettingsBuilder() + .initialWindowSize(1048576) // 1mb default window size + .maxHeaderListSize((int) DEFAULT_HEADER_LIST_SIZE); + } + + /** + * Default allocation quantum to use for the remote flow controller. + * @return Default allocation quantum to use for the remote flow controller. + */ + private static int defaultFlowControlQuantum() { + return 1024 * 16; + } + private static final class DefaultH2ProtocolConfig implements H2ProtocolConfig { + private final Map h2Settings; private final HttpHeadersFactory headersFactory; private final BiPredicate headersSensitivityDetector; @Nullable private final UserDataLoggerConfig frameLoggerConfig; @Nullable private final KeepAlivePolicy keepAlivePolicy; + private final int flowControlQuantum; - DefaultH2ProtocolConfig(final HttpHeadersFactory headersFactory, + DefaultH2ProtocolConfig(final Map h2Settings, + final HttpHeadersFactory headersFactory, final BiPredicate headersSensitivityDetector, @Nullable final UserDataLoggerConfig frameLoggerConfig, - @Nullable final KeepAlivePolicy keepAlivePolicy) { + @Nullable final KeepAlivePolicy keepAlivePolicy, + final int flowControlQuantum) { + this.h2Settings = h2Settings; this.headersFactory = headersFactory; this.headersSensitivityDetector = headersSensitivityDetector; this.frameLoggerConfig = frameLoggerConfig; this.keepAlivePolicy = keepAlivePolicy; + this.flowControlQuantum = flowControlQuantum; } @Override @@ -154,16 +202,27 @@ public KeepAlivePolicy keepAlivePolicy() { return keepAlivePolicy; } + @Override + public Map initialSettings() { + return h2Settings; + } + + @Override + public int flowControlQuantum() { + return flowControlQuantum; + } + @Override public String toString() { return getClass().getSimpleName() + "{alpnId=" + alpnId() + ", headersFactory=" + headersFactory + ", headersSensitivityDetector=" + (headersSensitivityDetector == DEFAULT_SENSITIVITY_DETECTOR ? - "DEFAULT_SENSITIVITY_DETECTOR" : headersSensitivityDetector.toString()) + + "DEFAULT_SENSITIVITY_DETECTOR" : headersSensitivityDetector.toString()) + ", frameLoggerConfig=" + frameLoggerConfig + ", keepAlivePolicy=" + keepAlivePolicy + - '}'; + ", flowControlQuantum=" + flowControlQuantum + + ", h2Settings=" + h2Settings + '}'; } } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ServerParentChannelInitializer.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ServerParentChannelInitializer.java index 61abd5dd5a..f313ccdfd3 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ServerParentChannelInitializer.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ServerParentChannelInitializer.java @@ -15,6 +15,7 @@ */ package io.servicetalk.http.netty; +import io.servicetalk.http.api.Http2Settings; import io.servicetalk.logging.api.UserDataLoggerConfig; import io.servicetalk.transport.netty.internal.ChannelInitializer; @@ -23,6 +24,7 @@ import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.codec.http2.Http2StreamChannel; +import java.util.Map; import java.util.function.BiPredicate; import javax.annotation.Nullable; @@ -41,7 +43,8 @@ final class H2ServerParentChannelInitializer implements ChannelInitializer { @Override public void init(final Channel channel) { - final Http2FrameCodecBuilder multiplexCodecBuilder = new OptimizedHttp2FrameCodecBuilder(true) + final Http2FrameCodecBuilder multiplexCodecBuilder = + new OptimizedHttp2FrameCodecBuilder(true, config.flowControlQuantum()) // We do not want close to trigger graceful closure (go away), instead when user triggers a graceful // close, we do the appropriate go away handling. .decoupleCloseAndGoAway(true) @@ -55,9 +58,14 @@ public void init(final Channel channel) { config.headersSensitivityDetector(); multiplexCodecBuilder.headerSensitivityDetector(headersSensitivityDetector::test); + final Map h2Settings = config.initialSettings(); + if (!h2Settings.isEmpty()) { + multiplexCodecBuilder.initialSettings(toNettySettings(h2Settings)); + } + initFrameLogger(multiplexCodecBuilder, config.frameLoggerConfig()); - // TODO(scott): more configuration. header validation, settings stream, etc... + // TODO(scott): more configuration. header validation, etc... channel.pipeline().addLast(multiplexCodecBuilder.build(), new Http2MultiplexHandler(streamChannelInitializer)); } @@ -70,4 +78,34 @@ static void initFrameLogger(final Http2FrameCodecBuilder multiplexCodecBuilder, frameLoggerConfig.logLevel()), frameLoggerConfig.logUserData())); } } + + static io.netty.handler.codec.http2.Http2Settings toNettySettings(Map h2Settings) { + io.netty.handler.codec.http2.Http2Settings nettySettings = new io.netty.handler.codec.http2.Http2Settings(); + h2Settings.forEach((identifier, value) -> { + switch (identifier) { + case Http2Settings.HEADER_TABLE_SIZE: + nettySettings.headerTableSize(value); + break; + case Http2Settings.ENABLE_PUSH: + nettySettings.pushEnabled(value != 0); + break; + case Http2Settings.MAX_CONCURRENT_STREAMS: + nettySettings.maxConcurrentStreams(value); + break; + case Http2Settings.INITIAL_WINDOW_SIZE: + nettySettings.initialWindowSize(value); + break; + case Http2Settings.MAX_FRAME_SIZE: + nettySettings.maxFrameSize(value); + break; + case Http2Settings.MAX_HEADER_LIST_SIZE: + nettySettings.maxHeaderListSize(value); + break; + default: + nettySettings.put(identifier, Long.valueOf(value)); + break; + } + }); + return nettySettings; + } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/Http2SettingsBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/Http2SettingsBuilder.java new file mode 100644 index 0000000000..cbfa3b4693 --- /dev/null +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/Http2SettingsBuilder.java @@ -0,0 +1,118 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project authors + * + * Licensed 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.servicetalk.http.netty; + +import java.util.HashMap; +import java.util.Map; + +import static io.servicetalk.http.api.Http2Settings.HEADER_TABLE_SIZE; +import static io.servicetalk.http.api.Http2Settings.INITIAL_WINDOW_SIZE; +import static io.servicetalk.http.api.Http2Settings.MAX_CONCURRENT_STREAMS; +import static io.servicetalk.http.api.Http2Settings.MAX_FRAME_SIZE; +import static io.servicetalk.http.api.Http2Settings.MAX_HEADER_LIST_SIZE; + +/** + * Builder to help create a {@link Map} for + * HTTP/2 Setting. + */ +public final class Http2SettingsBuilder { + private final Map settings; + + /** + * Create a new instance. + */ + public Http2SettingsBuilder() { + this(8); + } + + /** + * Create a new instance. + * @param initialSize The initial size of the map. + */ + Http2SettingsBuilder(int initialSize) { + settings = new HashMap<>(initialSize); + } + + /** + * Set the value for + * SETTINGS_HEADER_TABLE_SIZE. + * + * @param value The value. + * @return {@code this}. + */ + public Http2SettingsBuilder headerTableSize(int value) { + settings.put(HEADER_TABLE_SIZE, value); + return this; + } + + /** + * Set the value for + * SETTINGS_MAX_CONCURRENT_STREAMS. + * + * @param value The value. + * @return {@code this}. + */ + public Http2SettingsBuilder maxConcurrentStreams(int value) { + settings.put(MAX_CONCURRENT_STREAMS, value); + return this; + } + + /** + * Set the value for + * SETTINGS_INITIAL_WINDOW_SIZE. + * + * @param value The value. + * @return {@code this}. + */ + public Http2SettingsBuilder initialWindowSize(int value) { + settings.put(INITIAL_WINDOW_SIZE, value); + return this; + } + + /** + * Set the value for + * SETTINGS_MAX_FRAME_SIZE. + * + * @param value The value. + * @return {@code this}. + */ + public Http2SettingsBuilder maxFrameSize(int value) { + settings.put(MAX_FRAME_SIZE, value); + return this; + } + + /** + * Set the value for + * SETTINGS_MAX_HEADER_LIST_SIZE. + * + * @param value The value. + * @return {@code this}. + */ + public Http2SettingsBuilder maxHeaderListSize(int value) { + settings.put(MAX_HEADER_LIST_SIZE, value); + return this; + } + + /** + * Build the {@link Map} that represents + * HTTP/2 Setting. + * @return the {@link Map} that represents + * HTTP/2 Setting. + */ + public Map build() { + return settings; + } +} diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpProtocolConfigs.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpProtocolConfigs.java index 789dfc4bdd..0b8b237c7f 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpProtocolConfigs.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpProtocolConfigs.java @@ -17,6 +17,8 @@ import io.servicetalk.http.api.HttpProtocolConfig; +import java.util.Map; + /** * Factory methods for {@link HttpProtocolConfig}s and builders for their customization. */ @@ -52,6 +54,11 @@ public static H1ProtocolConfigBuilder h1() { /** * Returns {@link H2ProtocolConfig} with the default configuration for * HTTP/2. + *

+ * Note this doesn't necessarily provide {@link H2ProtocolConfig#initialSettings()} that corresponds to + * default values as described in + * HTTP/2 Settings. Some identifiers + * maybe overridden for safety or performance reasons and are subject to change. For more control use {@link #h2()}. * * @return {@link H2ProtocolConfig} with the default configuration for * HTTP/2 @@ -62,6 +69,12 @@ public static H2ProtocolConfig h2Default() { /** * Returns a builder for {@link H2ProtocolConfig}. + *

+ * Note this doesn't necessarily provide {@link H2ProtocolConfig#initialSettings()} that corresponds to + * default values as described in + * HTTP/2 Settings. Some identifiers + * maybe overridden for safety or performance reasons and are subject to change. For more control use + * {@link H2ProtocolConfigBuilder#initialSettings(Map)}. * * @return {@link H2ProtocolConfigBuilder} */ diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/OptimizedHttp2FrameCodecBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/OptimizedHttp2FrameCodecBuilder.java index 193c7ada6f..7c6786dcbf 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/OptimizedHttp2FrameCodecBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/OptimizedHttp2FrameCodecBuilder.java @@ -28,7 +28,7 @@ import java.lang.invoke.MethodHandles; import javax.annotation.Nullable; -import static io.servicetalk.utils.internal.PlatformDependent.throwException; +import static io.servicetalk.utils.internal.ThrowableUtils.throwException; import static java.lang.invoke.MethodType.methodType; /** @@ -61,14 +61,18 @@ final class OptimizedHttp2FrameCodecBuilder extends Http2FrameCodecBuilder { } private final boolean server; + private final int flowControlQuantum; /** * Creates a new instance. * * @param server {@code true} if for server, {@code false} otherwise + * @param flowControlQuantum a hint on the number of bytes that the flow controller will attempt to give to a + * stream for each allocation. */ - OptimizedHttp2FrameCodecBuilder(final boolean server) { + OptimizedHttp2FrameCodecBuilder(final boolean server, final int flowControlQuantum) { this.server = server; + this.flowControlQuantum = flowControlQuantum; disableFlushPreface(FLUSH_PREFACE, this); } @@ -80,8 +84,9 @@ public boolean isServer() { @Override public Http2FrameCodec build() { final DefaultHttp2Connection connection = new DefaultHttp2Connection(isServer(), maxReservedStreams()); - connection.remote().flowController(new DefaultHttp2RemoteFlowController(connection, - new UniformStreamByteDistributor(connection))); + UniformStreamByteDistributor distributor = new UniformStreamByteDistributor(connection); + distributor.minAllocationChunk(flowControlQuantum); + connection.remote().flowController(new DefaultHttp2RemoteFlowController(connection, distributor)); connection(connection); return super.build(); }