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
f07115c2a6
commit
ab14e0b583
@ -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 {
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user