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.
This commit is contained in:
Tristan Perry 2020-03-09 07:59:31 +00:00 committed by Norman Maurer
parent f07115c2a6
commit ab14e0b583
4 changed files with 250 additions and 2 deletions

View File

@ -51,9 +51,13 @@ 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;
/** /**
* An HTTP2 client that allows you to send HTTP2 frames to a server. Inbound and outbound frames are * An HTTP2 client that allows you to send HTTP2 frames to a server using HTTP1-style approaches
* logged. When run from the command-line, sends a single HEADERS frame to the server and gets back * (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. * 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 { public final class Http2Client {

View File

@ -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<Channel> {
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<Object>() {
@Override
protected void messageReceived(ChannelHandlerContext ctx, Object msg) {
// NOOP (this is the handler for 'inbound' streams, which is not relevant in this example)
}
}));
}
}

View File

@ -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<Http2StreamFrame> {
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;
}
}
}

View File

@ -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();
}
}
}