Trigger user event when H2 conn preface & SETTINGS frame are sent

Motivation:
Previously client Http2ConnectionHandler trigger a user event
immediately when the HTTP/2 connection preface is sent. Any attempt to
immediately send a new request could cause the server to terminate the
connection, as it might not have received the SETTINGS frame from the
client. Per RFC7540 Section 3.5, the preface "MUST be followed by a
SETTINGS frame (Section 6.5), which MAY be empty."
(https://tools.ietf.org/html/rfc7540#section-3.5)

This event could be made more meaningful if it also indicates that the
initial client SETTINGS frame has been sent to signal that the channel
is ready to send new requests.

Modification:
- Renamed event to Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.
- Modified Http2ConnectionHandler to trigger the user event only if it
  is a client and it has sent both the preface and SETTINGS frame.

Result:
It is now safe to use the event as an indicator that the HTTP/2
connection is ready to send new requests.
This commit is contained in:
Lionel Li 2017-10-23 10:30:25 -07:00 committed by Norman Maurer
parent 55b501d0d4
commit baf273aea8
8 changed files with 48 additions and 15 deletions

View File

@ -364,15 +364,20 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
prefaceSent = true; prefaceSent = true;
if (!connection().isServer()) { final boolean isClient = !connection().isServer();
if (isClient) {
// Clients must send the preface string as the first bytes on the connection. // Clients must send the preface string as the first bytes on the connection.
ctx.write(connectionPrefaceBuf()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); ctx.write(connectionPrefaceBuf()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
ctx.fireUserEventTriggered(Http2ConnectionPrefaceWrittenEvent.INSTANCE);
} }
// Both client and server must send their initial settings. // Both client and server must send their initial settings.
encoder.writeSettings(ctx, initialSettings, ctx.newPromise()).addListener( encoder.writeSettings(ctx, initialSettings, ctx.newPromise()).addListener(
ChannelFutureListener.CLOSE_ON_FAILURE); ChannelFutureListener.CLOSE_ON_FAILURE);
if (isClient) {
ctx.fireUserEventTriggered(
Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE);
}
} }
} }

View File

@ -17,14 +17,15 @@ package io.netty.handler.codec.http2;
import io.netty.util.internal.UnstableApi; import io.netty.util.internal.UnstableApi;
/** /**
* Signifies that the <a href="https://tools.ietf.org/html/rfc7540#section-3.5">connection preface</a> has been sent. * Signifies that the <a href="https://tools.ietf.org/html/rfc7540#section-3.5">connection preface</a> and
* The client sends the preface, and the server receives the preface. The client shouldn't write any data until this * the initial SETTINGS frame have been sent. The client sends the preface, and the server receives the preface.
* event has been processed. * The client shouldn't write any data until this event has been processed.
*/ */
@UnstableApi @UnstableApi
public final class Http2ConnectionPrefaceWrittenEvent { public final class Http2ConnectionPrefaceAndSettingsFrameWrittenEvent {
static final Http2ConnectionPrefaceWrittenEvent INSTANCE = new Http2ConnectionPrefaceWrittenEvent(); static final Http2ConnectionPrefaceAndSettingsFrameWrittenEvent INSTANCE =
new Http2ConnectionPrefaceAndSettingsFrameWrittenEvent();
private Http2ConnectionPrefaceWrittenEvent() { private Http2ConnectionPrefaceAndSettingsFrameWrittenEvent() {
} }
} }

View File

@ -231,7 +231,7 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
*/ */
@Override @Override
public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) { if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
// The user event implies that we are on the client. // The user event implies that we are on the client.
tryExpandConnectionFlowControlWindow(connection()); tryExpandConnectionFlowControlWindow(connection());
} else if (evt instanceof UpgradeEvent) { } else if (evt instanceof UpgradeEvent) {

View File

@ -347,7 +347,7 @@ public class DataCompressionHttp2Test {
p.addLast(clientHandler); p.addLast(clientHandler);
p.addLast(new ChannelInboundHandlerAdapter() { p.addLast(new ChannelInboundHandlerAdapter() {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) { if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
prefaceWrittenLatch.countDown(); prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this); ctx.pipeline().remove(this);
} }

View File

@ -44,6 +44,7 @@ import org.mockito.stubbing.Answer;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static io.netty.buffer.Unpooled.copiedBuffer; import static io.netty.buffer.Unpooled.copiedBuffer;
import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf; import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf;
@ -240,6 +241,34 @@ public class Http2ConnectionHandlerTest {
} }
} }
@Test
public void clientShouldveSentPrefaceAndSettingsFrameWhenUserEventIsTriggered() throws Exception {
when(connection.isServer()).thenReturn(false);
when(channel.isActive()).thenReturn(false);
handler = newHandler();
when(channel.isActive()).thenReturn(true);
final Http2ConnectionPrefaceAndSettingsFrameWrittenEvent evt =
Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE;
final AtomicBoolean verified = new AtomicBoolean(false);
final Answer verifier = new Answer() {
@Override
public Object answer(final InvocationOnMock in) throws Throwable {
assertTrue(in.getArgument(0).equals(evt)); // sanity check...
verify(ctx).write(eq(connectionPrefaceBuf()));
verify(encoder).writeSettings(eq(ctx), any(Http2Settings.class), any(ChannelPromise.class));
verified.set(true);
return null;
}
};
doAnswer(verifier).when(ctx).fireUserEventTriggered(evt);
handler.channelActive(ctx);
assertTrue(verified.get());
}
@Test @Test
public void clientShouldSendClientPrefaceStringWhenActive() throws Exception { public void clientShouldSendClientPrefaceStringWhenActive() throws Exception {
when(connection.isServer()).thenReturn(false); when(connection.isServer()).thenReturn(false);

View File

@ -933,7 +933,7 @@ public class Http2ConnectionRoundtripTest {
p.addLast(new ChannelInboundHandlerAdapter() { p.addLast(new ChannelInboundHandlerAdapter() {
@Override @Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) { if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
prefaceWrittenLatch.countDown(); prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this); ctx.pipeline().remove(this);
} }

View File

@ -61,7 +61,6 @@ import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.handler.codec.http2.Http2TestUtil.of; import static io.netty.handler.codec.http2.Http2TestUtil.of;
import static io.netty.util.CharsetUtil.UTF_8; import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@ -541,7 +540,7 @@ public class HttpToHttp2ConnectionHandlerTest {
p.addLast(new ChannelInboundHandlerAdapter() { p.addLast(new ChannelInboundHandlerAdapter() {
@Override @Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) { if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
prefaceWrittenLatch.countDown(); prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this); ctx.pipeline().remove(this);
} }

View File

@ -60,7 +60,6 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.getEmbeddedHttp2Except
import static io.netty.handler.codec.http2.Http2Exception.isStreamError; import static io.netty.handler.codec.http2.Http2Exception.isStreamError;
import static io.netty.handler.codec.http2.Http2TestUtil.of; import static io.netty.handler.codec.http2.Http2TestUtil.of;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel; import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -725,7 +724,7 @@ public class InboundHttp2ToHttpAdapterTest {
}); });
p.addLast(new ChannelInboundHandlerAdapter() { p.addLast(new ChannelInboundHandlerAdapter() {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) { if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
prefaceWrittenLatch.countDown(); prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this); ctx.pipeline().remove(this);
} }