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:
parent
b892722efb
commit
59c8ce317a
@ -50,9 +50,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 {
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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() {
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
|
||||
// NOOP (this is the handler for 'inbound' streams, which is not relevant in this example)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
@ -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 channelRead0(ChannelHandlerContext ctx, Http2StreamFrame msg) throws Exception {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.nio.NioEventLoopGroup;
|
||||
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 NioEventLoopGroup();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user