From ab14e0b583ae3cc1bd1adbae110c59e3c0835b5b Mon Sep 17 00:00:00 2001 From: Tristan Perry Date: Mon, 9 Mar 2020 07:59:31 +0000 Subject: [PATCH] Add a HTTP/2 client example using the newer frames approach the current HTTP/2 client example uses the older 'HTTP/1 <--> HTTP/2' translation approach. (#10081) **Motivation:** When I was previously working on a project using Netty's HTTP/2 support, I used the newer frames approach but I struggled to find any good examples or documentation online. I did, however, see a few people ask the same (or similar) questions as me on StackOverflow and a couple of older Netty Github issues. Reading issue [9733](https://github.com/netty/netty/issues/9733) therefore prompted me to pull together a few bits of code into this HTTP/2 frame client example. **Modification:** Populated the previously-empty `example/src/main/java/io/netty/example/http2/helloworld/frame/client/` folder with a HTTP/2 frame client example. **Result:** Gives a clear example of how the newer HTTP/2 support can be used for Netty clients. --- .../http2/helloworld/client/Http2Client.java | 8 +- .../client/Http2ClientFrameInitializer.java | 58 ++++++++ ...Http2ClientStreamFrameResponseHandler.java | 61 +++++++++ .../frame/client/Http2FrameClient.java | 125 ++++++++++++++++++ 4 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java diff --git a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java index 9a67ff2f5b..a0977ebd99 100644 --- a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java +++ b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java @@ -51,9 +51,13 @@ import static io.netty.handler.codec.http.HttpMethod.POST; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; /** - * An HTTP2 client that allows you to send HTTP2 frames to a server. Inbound and outbound frames are - * logged. When run from the command-line, sends a single HEADERS frame to the server and gets back + * An HTTP2 client that allows you to send HTTP2 frames to a server using HTTP1-style approaches + * (via {@link io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter}). Inbound and outbound + * frames are logged. + * When run from the command-line, sends a single HEADERS frame to the server and gets back * a "Hello World" response. + * See the ./http2/helloworld/frame/client/ example for a HTTP2 client example which does not use + * HTTP1-style objects and patterns. */ public final class Http2Client { diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java new file mode 100644 index 0000000000..114ff4346f --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 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.example.http2.helloworld.frame.client; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2FrameCodecBuilder; +import io.netty.handler.codec.http2.Http2MultiplexHandler; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.ssl.SslContext; + +/** + * Configures client pipeline to support HTTP/2 frames via {@link Http2FrameCodec} and {@link Http2MultiplexHandler}. + */ +public final class Http2ClientFrameInitializer extends ChannelInitializer { + + private final SslContext sslCtx; + + public Http2ClientFrameInitializer(SslContext sslCtx) { + this.sslCtx = sslCtx; + } + + @Override + protected void initChannel(Channel ch) throws Exception { + // ensure that our 'trust all' SSL handler is the first in the pipeline if SSL is enabled. + if (sslCtx != null) { + ch.pipeline().addFirst(sslCtx.newHandler(ch.alloc())); + } + + final Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient() + .initialSettings(Http2Settings.defaultSettings()) // this is the default, but shows it can be changed. + .build(); + ch.pipeline().addLast(http2FrameCodec); + ch.pipeline().addLast(new Http2MultiplexHandler(new SimpleChannelInboundHandler() { + + @Override + protected void messageReceived(ChannelHandlerContext ctx, Object msg) { + // NOOP (this is the handler for 'inbound' streams, which is not relevant in this example) + } + })); + } + +} diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java new file mode 100644 index 0000000000..7e5a417f9e --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 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.example.http2.helloworld.frame.client; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2DataFrame; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2StreamFrame; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Handles HTTP/2 stream frame responses. This is a useful approach if you specifically want to check + * the main HTTP/2 response DATA/HEADERs, but in this example it's used purely to see whether + * our request (for a specific stream id) has had a final response (for that same stream id). + */ +public final class Http2ClientStreamFrameResponseHandler extends SimpleChannelInboundHandler { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + protected void messageReceived(ChannelHandlerContext ctx, Http2StreamFrame msg) { + System.out.println("Received HTTP/2 'stream' frame: " + msg); + + // isEndStream() is not from a common interface, so we currently must check both + if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) { + latch.countDown(); + } else if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) { + latch.countDown(); + } + } + + /** + * Waits for the latch to be decremented (i.e. for an end of stream message to be received), or for + * the latch to expire after 5 seconds. + * @return true if a successful HTTP/2 end of stream message was received. + */ + public boolean responseSuccessfullyCompleted() { + try { + return latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + System.err.println("Latch exception: " + ie.getMessage()); + return false; + } + } + +} diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java new file mode 100644 index 0000000000..0fa64465a9 --- /dev/null +++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020 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.example.http2.helloworld.frame.client; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.MultithreadEventLoopGroup; +import io.netty.channel.nio.NioHandler; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2HeadersFrame; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +/** + * An HTTP2 client that allows you to send HTTP2 frames to a server using the newer HTTP2 + * approach (via {@link io.netty.handler.codec.http2.Http2FrameCodec}). + * When run from the command-line, sends a single HEADERS frame (with prior knowledge) to + * the server configured at host:port/path. + * You should include {@link io.netty.handler.codec.http2.Http2ClientUpgradeCodec} if the + * HTTP/2 server you are hitting doesn't support h2c/prior knowledge. + */ +public final class Http2FrameClient { + + static final boolean SSL = System.getProperty("ssl") != null; + static final String HOST = System.getProperty("host", "127.0.0.1"); + static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080")); + static final String PATH = System.getProperty("path", "/"); + + private Http2FrameClient() { + } + + public static void main(String[] args) throws Exception { + final EventLoopGroup clientWorkerGroup = new MultithreadEventLoopGroup(NioHandler.newFactory()); + + // Configure SSL. + final SslContext sslCtx; + if (SSL) { + final SslProvider provider = + SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK; + sslCtx = SslContextBuilder.forClient() + .sslProvider(provider) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + // you probably won't want to use this in production, but it is fine for this example: + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + Protocol.ALPN, + SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1)) + .build(); + } else { + sslCtx = null; + } + + try { + final Bootstrap b = new Bootstrap(); + b.group(clientWorkerGroup); + b.channel(NioSocketChannel.class); + b.option(ChannelOption.SO_KEEPALIVE, true); + b.remoteAddress(HOST, PORT); + b.handler(new Http2ClientFrameInitializer(sslCtx)); + + // Start the client. + final Channel channel = b.connect().syncUninterruptibly().channel(); + System.out.println("Connected to [" + HOST + ':' + PORT + ']'); + + final Http2ClientStreamFrameResponseHandler streamFrameResponseHandler = + new Http2ClientStreamFrameResponseHandler(); + + final Http2StreamChannelBootstrap streamChannelBootstrap = new Http2StreamChannelBootstrap(channel); + final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow(); + streamChannel.pipeline().addLast(streamFrameResponseHandler); + + // Send request (a HTTP/2 HEADERS frame - with ':method = GET' in this case) + final DefaultHttp2Headers headers = new DefaultHttp2Headers(); + headers.method("GET"); + headers.path(PATH); + headers.scheme(SSL? "https" : "http"); + final Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers); + streamChannel.writeAndFlush(headersFrame); + System.out.println("Sent HTTP/2 GET request to " + PATH); + + // Wait for the responses (or for the latch to expire), then clean up the connections + if (!streamFrameResponseHandler.responseSuccessfullyCompleted()) { + System.err.println("Did not get HTTP/2 response in expected time."); + } + + System.out.println("Finished HTTP/2 request, will close the connection."); + + // Wait until the connection is closed. + channel.close().syncUninterruptibly(); + } finally { + clientWorkerGroup.shutdownGracefully(); + } + } + +}