HTTP/2 Connection Preface User Event

Motivation:
If an HTTP/2 client writes data before the connection preface the peer will shutdown the socket. Depending on what is in the pipeline (SslHandler) may require different evaluation criteria to infer when the codec-http2 has written the connection preface on behalf of the client. This can lead to unnecessarily complexity and error prone/racy application code.

Modifications:
- Introduce a user event that is fired up the pipeline when codec-http2 writes the connection preface

Result:
Reliable mechanism for applications to use to know when connection preface has been written (related to https://github.com/netty/netty/issues/6272).
This commit is contained in:
Scott Mitchell 2017-01-30 14:50:55 -08:00
parent 66b1731041
commit 6e5b25733f
6 changed files with 78 additions and 2 deletions

View File

@ -337,6 +337,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
if (!connection().isServer()) {
// Clients must send the preface string as the first bytes on the connection.
ctx.write(connectionPrefaceBuf()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
ctx.fireUserEventTriggered(Http2ConnectionPrefaceWrittenEvent.INSTANCE);
}
// Both client and server must send their initial settings.

View File

@ -0,0 +1,30 @@
/*
* Copyright 2017 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.util.internal.UnstableApi;
/**
* Signifies that the <a href="https://tools.ietf.org/html/rfc7540#section-3.5">connection preface</a> has been sent.
* The client sends the preface, and the server receives the preface. The client shouldn't write any data until this
* event has been processed.
*/
@UnstableApi
public final class Http2ConnectionPrefaceWrittenEvent {
static final Http2ConnectionPrefaceWrittenEvent INSTANCE = new Http2ConnectionPrefaceWrittenEvent();
private Http2ConnectionPrefaceWrittenEvent() {
}
}

View File

@ -21,6 +21,7 @@ import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
@ -256,6 +257,7 @@ public class DataCompressionHttp2Test {
}
private void bootstrapEnv(int serverOutSize) throws Exception {
final CountDownLatch prefaceWrittenLatch = new CountDownLatch(1);
serverOut = new ByteArrayOutputStream(serverOutSize);
serverLatch = new CountDownLatch(1);
sb = new ServerBootstrap();
@ -336,6 +338,14 @@ public class DataCompressionHttp2Test {
.gracefulShutdownTimeoutMillis(0)
.codec(decoder, clientEncoder).build();
p.addLast(clientHandler);
p.addLast(new ChannelInboundHandlerAdapter() {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this);
}
}
});
}
});
@ -345,6 +355,7 @@ public class DataCompressionHttp2Test {
ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port));
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
assertTrue(prefaceWrittenLatch.await(5, SECONDS));
assertTrue(serverChannelLatch.await(5, SECONDS));
}

View File

@ -25,10 +25,12 @@ import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalServerChannel;
@ -178,7 +180,7 @@ public class Http2ConnectionRoundtripTest {
}
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertTrue(latch.await(5, SECONDS));
}
@Test
@ -660,6 +662,7 @@ public class Http2ConnectionRoundtripTest {
private void bootstrapEnv(int dataCountDown, int settingsAckCount,
int requestCountDown, int trailersCountDown, int goAwayCountDown) throws Exception {
final CountDownLatch prefaceWrittenLatch = new CountDownLatch(1);
requestLatch = new CountDownLatch(requestCountDown);
serverSettingsAckLatch = new CountDownLatch(settingsAckCount);
dataLatch = new CountDownLatch(dataCountDown);
@ -702,6 +705,14 @@ public class Http2ConnectionRoundtripTest {
.validateHeaders(false)
.gracefulShutdownTimeoutMillis(0)
.build());
p.addLast(new ChannelInboundHandlerAdapter() {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this);
}
}
});
}
});
@ -710,8 +721,9 @@ public class Http2ConnectionRoundtripTest {
ChannelFuture ccf = cb.connect(serverChannel.localAddress());
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
assertTrue(prefaceWrittenLatch.await(5, SECONDS));
http2Client = clientChannel.pipeline().get(Http2ConnectionHandler.class);
assertTrue(serverInitLatch.await(5, TimeUnit.SECONDS));
assertTrue(serverInitLatch.await(5, SECONDS));
http2Server = serverHandlerRef.get();
}

View File

@ -21,6 +21,7 @@ import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
@ -499,6 +500,7 @@ public class HttpToHttp2ConnectionHandlerTest {
}
private void bootstrapEnv(int requestCountDown, int serverSettingsAckCount, int trailersCount) throws Exception {
final CountDownLatch prefaceWrittenLatch = new CountDownLatch(1);
final CountDownLatch serverChannelLatch = new CountDownLatch(1);
requestLatch = new CountDownLatch(requestCountDown);
serverSettingsAckLatch = new CountDownLatch(serverSettingsAckCount);
@ -536,6 +538,14 @@ public class HttpToHttp2ConnectionHandlerTest {
.gracefulShutdownTimeoutMillis(0)
.build();
p.addLast(handler);
p.addLast(new ChannelInboundHandlerAdapter() {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this);
}
}
});
}
});
@ -544,6 +554,7 @@ public class HttpToHttp2ConnectionHandlerTest {
ChannelFuture ccf = cb.connect(serverChannel.localAddress());
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
assertTrue(prefaceWrittenLatch.await(5, SECONDS));
assertTrue(serverChannelLatch.await(WAIT_TIME_SECONDS, TimeUnit.SECONDS));
}

View File

@ -22,6 +22,7 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
@ -745,6 +746,7 @@ public class InboundHttp2ToHttpAdapterTest {
private void boostrapEnv(int clientLatchCount, int clientLatchCount2, int serverLatchCount, int serverLatchCount2,
int settingsLatchCount) throws InterruptedException {
final CountDownLatch prefaceWrittenLatch = new CountDownLatch(1);
clientDelegator = null;
serverDelegator = null;
serverConnectedChannel = null;
@ -818,6 +820,14 @@ public class InboundHttp2ToHttpAdapterTest {
}
}
});
p.addLast(new ChannelInboundHandlerAdapter() {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
prefaceWrittenLatch.countDown();
ctx.pipeline().remove(this);
}
}
});
}
});
@ -826,6 +836,7 @@ public class InboundHttp2ToHttpAdapterTest {
ChannelFuture ccf = cb.connect(serverChannel.localAddress());
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
assertTrue(prefaceWrittenLatch.await(5, SECONDS));
assertTrue(serverChannelLatch.await(5, SECONDS));
}