1. Overview
1.概述
Netty is an NIO-based client-server framework that gives Java developers the power to operate on the network layers. Using this framework, developers can build their own implementation of any known protocol, or even custom protocols.
Netty是一个基于NIO的客户-服务器框架,为Java开发人员提供了在网络层上操作的能力。使用这个框架,开发人员可以建立他们自己的任何已知协议的实现,甚至是自定义协议。
For a basic understanding of the framework, introduction to Netty is a good start.
对于框架的基本了解,Netty介绍是一个好的开始。
In this tutorial, we’ll see how to implement an HTTP/2 server and client in Netty.
在本教程中,我们将看到如何在Netty中实现一个HTTP/2服务器和客户端。
2. What Is HTTP/2?
2.什么是HTTP/2?
As the name suggests, HTTP version 2 or simply HTTP/2, is a newer version of the Hypertext Transfer Protocol.
顾名思义,HTTP第二版或简称HTTP/2,是超文本传输协议的一个较新版本。
Around the year 1989, when the internet was born, HTTP/1.0 came into being. In 1997, it was upgraded to version 1.1. However, it wasn’t until 2015 that it saw a major upgrade, version 2.
1989年左右,当互联网诞生时,HTTP/1.0应运而生。1997年,它被升级到1.1版本。然而,直到2015年,它才迎来了一次重大升级,即第二版。
As of writing this, HTTP/3 is also available, though not yet supported by default by all browsers.
截至目前,HTTP/3也是可用的,尽管尚未被所有浏览器默认支持。
HTTP/2 is still the latest version of the protocol that is widely accepted and implemented. It differs significantly from the previous versions with its multiplexing and server push features, among other things.
HTTP/2仍然是被广泛接受和实施的最新版本的协议。它与以前的版本有很大的不同,它的多路复用和服务器推送功能,以及其他方面。
Communication in HTTP/2 happens via a group of bytes called frames, and multiple frames form a stream.
HTTP/2中的通信是通过一组称为帧的字节发生的,多个帧构成一个流。
In our code samples, we’ll see how Netty handles the exchange of HEADERS, DATA and SETTINGS frames.
在我们的代码示例中,我们将看到Netty如何处理HEADERS、DATA和SETTINGS帧的交流。
3. The Server
3.服务器
Now let’s see how we can create an HTTP/2 server in Netty.
现在我们来看看如何在Netty中创建一个HTTP/2服务器。
3.1. SslContext
3.1. SslContext
Netty supports APN negotiation for HTTP/2 over TLS. So, the first thing we need to create a server is an SslContext:
Netty支持APN协商,用于HTTP/2 over TLS。因此,我们创建服务器首先需要的是一个SslContext。
SelfSignedCertificate ssc = new SelfSignedCertificate();
SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.sslProvider(SslProvider.JDK)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(
new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2))
.build();
Here, we created a context for the server with a JDK SSL provider, added a couple of ciphers, and configured the Application-Layer Protocol Negotiation for HTTP/2.
在这里,我们用JDK SSL提供商为服务器创建了一个上下文,添加了几个密码,并为HTTP/2配置了应用层协议协商。
This means that our server will only support HTTP/2 and its underlying protocol identifier h2.
这意味着我们的服务器将只支持HTTP/2及其基础协议标识符h2。
3.2. Bootstrapping the Server with a ChannelInitializer
3.2.用ChannelInitializer启动服务器
Next, we need a ChannelInitializer for our multiplexing child channel, so as to set up a Netty pipeline.
接下来,我们需要一个ChannelInitializer给我们的复用子通道,以便建立一个Netty管道。
We’ll use the earlier sslContext in this channel to initiate the pipeline, and then bootstrap the server:
我们将在这个通道中使用先前的sslContext来启动管道,然后启动服务器。
public final class Http2Server {
static final int PORT = 8443;
public static void main(String[] args) throws Exception {
SslContext sslCtx = // create sslContext as described above
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.group(group)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
if (sslCtx != null) {
ch.pipeline()
.addLast(sslCtx.newHandler(ch.alloc()), Http2Util.getServerAPNHandler());
}
}
});
Channel ch = b.bind(PORT).sync().channel();
logger.info("HTTP/2 Server is listening on https://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
As part of this channel’s initialization, we’re adding an APN handler to the pipeline in a utility method getServerAPNHandler() that we’ve defined in our own utility class Http2Util:
作为该通道初始化的一部分,我们将在我们自己的实用程序类getServerAPNHandler()中定义的实用程序方法中添加一个APN处理器到管道。
public static ApplicationProtocolNegotiationHandler getServerAPNHandler() {
ApplicationProtocolNegotiationHandler serverAPNHandler =
new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
ctx.pipeline().addLast(
Http2FrameCodecBuilder.forServer().build(), new Http2ServerResponseHandler());
return;
}
throw new IllegalStateException("Protocol: " + protocol + " not supported");
}
};
return serverAPNHandler;
}
This handler is, in turn, adding a Netty provided Http2FrameCodec using its builder and a custom handler called Http2ServerResponseHandler.
这个处理程序又是使用Netty提供的Http2FrameCodec构建器和一个名为Http2ServerResponseHandler的自定义处理程序来添加。
Our custom handler extends Netty’s ChannelDuplexHandler and acts as both an inbound as well as an outbound handler for the server. Primarily, it prepares the response to be sent to the client.
我们的自定义处理程序扩展了Netty的ChannelDuplexHandler,并同时作为服务器的入站和出站处理程序。主要是,它准备将响应发送到客户端。
For the purpose of this tutorial, we’ll define a static Hello World response in an io.netty.buffer.ByteBuf – the preferred object to read and write bytes in Netty:
在本教程中,我们将在io.netty.buffer.ByteBuf中定义一个静态的Hello World响应,这是Netty中读写字节的首选对象。
static final ByteBuf RESPONSE_BYTES = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));
This buffer will be set as a DATA frame in our handler’s channelRead method and written to the ChannelHandlerContext:
这个缓冲区将在我们处理程序的channelRead方法中被设置为DATA帧,并写入ChannelHandlerContext。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Http2HeadersFrame) {
Http2HeadersFrame msgHeader = (Http2HeadersFrame) msg;
if (msgHeader.isEndStream()) {
ByteBuf content = ctx.alloc().buffer();
content.writeBytes(RESPONSE_BYTES.duplicate());
Http2Headers headers = new DefaultHttp2Headers().status(HttpResponseStatus.OK.codeAsText());
ctx.write(new DefaultHttp2HeadersFrame(headers).stream(msgHeader.stream()));
ctx.write(new DefaultHttp2DataFrame(content, true).stream(msgHeader.stream()));
}
} else {
super.channelRead(ctx, msg);
}
}
And that’s it, our server is ready to dish out Hello World.
就这样,我们的服务器已经准备好了,可以为您提供Hello World.。
For a quick test, start the server and fire a curl command with the –http2 option:
为了进行快速测试,启动服务器并使用-http2选项发射一条curl命令。
curl -k -v --http2 https://127.0.0.1:8443
Which will give a response similar to:
这将给出一个类似于的回应。
> GET / HTTP/2
> Host: 127.0.0.1:8443
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 4294967295)!
< HTTP/2 200
<
* Connection #0 to host 127.0.0.1 left intact
Hello World* Closing connection 0
4. The Client
4.客户
Next, let’s have a look at the client. Of course, its purpose is to send a request and then handle the response obtained from the server.
接下来,让我们看一下客户端。当然,它的目的是发送一个请求,然后处理从服务器上获得的响应。
Our client code will comprise of a couple of handlers, an initializer class to set them up in a pipeline, and finally a JUnit test to bootstrap the client and bring everything together.
我们的客户端代码将包括几个处理程序,一个初始化类以在管道中设置它们,最后是一个JUnit测试来引导客户端并将所有东西整合在一起。
4.1. SslContext
4.1. SslContext
But again, at first, let’s see how the client’s SslContext is set up. We’ll write this as part of setting up of our client JUnit:
但还是那句话,首先,让我们看看客户端的SslContext是如何设置的。我们将把它作为设置客户端JUnit的一部分来写。
@Before
public void setup() throws Exception {
SslContext sslCtx = SslContextBuilder.forClient()
.sslProvider(SslProvider.JDK)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocolConfig(
new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2))
.build();
}
As we can see, it’s pretty much similar to the server’s SslContext, just that we are not providing any SelfSignedCertificate here. Another difference is that we are adding an InsecureTrustManagerFactory to trust any certificate without any verification.
我们可以看到,它与服务器的SslContext非常相似,只是我们没有在这里提供任何SelfSignedCertificate。另一个区别是,我们添加了一个InsecureTrustManagerFactory来信任任何证书,而无需任何验证。
Importantly, this trust manager is purely for demo purposes and should not be used in production. To use trusted certificates instead, Netty’s SslContextBuilder offers many alternatives.
重要的是,这个信任管理器纯粹是为了演示,不应该在生产中使用。要使用受信任的证书,Netty的SslContextBuilder提供了许多选择。
We’ll come back to this JUnit at the end to bootstrap the client.
我们将在最后回到这个JUnit来引导客户端。
4.2. Handlers
4.2 处理者
For now, let’s take a look at the handlers.
现在,让我们看一下处理程序。
First, we’ll need a handler we’ll call Http2SettingsHandler, to deal with HTTP/2’s SETTINGS frame. It extends Netty’s SimpleChannelInboundHandler:
首先,我们需要一个处理器,我们称之为Http2SettingsHandler,以处理HTTP/2的SETTINGS框架。 它扩展了Netty的SimpleChannelInboundHandler。
public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
private final ChannelPromise promise;
// constructor
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
promise.setSuccess();
ctx.pipeline().remove(this);
}
}
The class is simply initializing a ChannelPromise and flagging it as successful.
该类只是初始化一个ChannelPromise,并将其标记为成功。
It also has a utility method awaitSettings that our client will use in order to wait for the initial handshake completion:
它还有一个实用方法awaitSettings,我们的客户端将使用该方法来等待初始握手完成。
public void awaitSettings(long timeout, TimeUnit unit) throws Exception {
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for settings");
}
}
If the channel read does not happen in the stipulated timeout period, then an IllegalStateException is thrown.
如果通道读取没有在规定的超时时间内发生,那么会抛出一个IllegalStateException。
Second, we’ll need a handler to deal with the response obtained from the server, we’ll name it Http2ClientResponseHandler:
其次,我们需要一个处理程序来处理从服务器获得的响应,我们将它命名为Http2ClientResponseHandler。
public class Http2ClientResponseHandler extends SimpleChannelInboundHandler {
private final Map<Integer, MapValues> streamidMap;
// constructor
}
This class also extends SimpleChannelInboundHandler and declares a streamidMap of MapValues, an inner class of our Http2ClientResponseHandler:
这个类还扩展了SimpleChannelInboundHandler,并声明了MapValues的streamidMap,这是我们的Http2ClientResponseHandler的一个内类。
public static class MapValues {
ChannelFuture writeFuture;
ChannelPromise promise;
// constructor and getters
}
We added this class to be able to store two values for a given Integer key.
我们添加这个类是为了能够为一个给定的Integer键存储两个值。
The handler also has a utility method put, of course, to put values in the streamidMap:
处理程序也有一个实用的方法put,当然是为了把值放到streamidMap中。
public MapValues put(int streamId, ChannelFuture writeFuture, ChannelPromise promise) {
return streamidMap.put(streamId, new MapValues(writeFuture, promise));
}
Next, let’s see what this handler does when the channel is read in the pipeline.
接下来,让我们看看当管道中的通道被读取时,这个处理程序做什么。
Basically, this is the place where we get the DATA frame or ByteBuf content from the server as a FullHttpResponse and can manipulate it in the way we want.
基本上,这是我们从服务器获得DATA帧或ByteBuf内容的地方,作为一个FullHttpResponse,并可以以我们想要的方式操作它。
In this example, we’ll just log it:
在这个例子中,我们只记录它。
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
if (streamId == null) {
logger.error("HttpResponseHandler unexpected message received: " + msg);
return;
}
MapValues value = streamidMap.get(streamId);
if (value == null) {
logger.error("Message received for unknown stream id " + streamId);
} else {
ByteBuf content = msg.content();
if (content.isReadable()) {
int contentLength = content.readableBytes();
byte[] arr = new byte[contentLength];
content.readBytes(arr);
logger.info(new String(arr, 0, contentLength, CharsetUtil.UTF_8));
}
value.getPromise().setSuccess();
}
}
At the end of the method, we flag the ChannelPromise as successful to indicate proper completion.
在方法结束时,我们将ChannelPromise标记为成功,以表示正确完成。
As the first handler we described, this class also contains a utility method for our client’s use. The method makes our event loop wait until the ChannelPromise is successful. Or, in other words, it waits till the response processing is complete:
和我们描述的第一个处理程序一样,这个类也包含了一个供我们客户使用的实用方法。该方法使我们的事件循环等待,直到ChannelPromise成功。或者,换句话说,它一直等到响应处理完成。
public String awaitResponses(long timeout, TimeUnit unit) {
Iterator<Entry<Integer, MapValues>> itr = streamidMap.entrySet().iterator();
String response = null;
while (itr.hasNext()) {
Entry<Integer, MapValues> entry = itr.next();
ChannelFuture writeFuture = entry.getValue().getWriteFuture();
if (!writeFuture.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting to write for stream id " + entry.getKey());
}
if (!writeFuture.isSuccess()) {
throw new RuntimeException(writeFuture.cause());
}
ChannelPromise promise = entry.getValue().getPromise();
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for response on stream id "
+ entry.getKey());
}
if (!promise.isSuccess()) {
throw new RuntimeException(promise.cause());
}
logger.info("---Stream id: " + entry.getKey() + " received---");
response = entry.getValue().getResponse();
itr.remove();
}
return response;
}
4.3. Http2ClientInitializer
4.3.Http2ClientInitializer
As we saw in the case of our server, the purpose of a ChannelInitializer is to set up a pipeline:
正如我们在服务器的案例中所看到的,ChannelInitializer的目的是建立一个管道。
public class Http2ClientInitializer extends ChannelInitializer {
private final SslContext sslCtx;
private final int maxContentLength;
private Http2SettingsHandler settingsHandler;
private Http2ClientResponseHandler responseHandler;
private String host;
private int port;
// constructor
@Override
public void initChannel(SocketChannel ch) throws Exception {
settingsHandler = new Http2SettingsHandler(ch.newPromise());
responseHandler = new Http2ClientResponseHandler();
if (sslCtx != null) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port));
pipeline.addLast(Http2Util.getClientAPNHandler(maxContentLength,
settingsHandler, responseHandler));
}
}
// getters
}
In this case, we are initiating the pipeline with a new SslHandler to add the TLS SNI Extension at the start of the handshaking process.
在这种情况下,我们用一个新的SslHandler来启动管道,以便在握手过程的开始添加TLS SNI扩展。
Then, it’s the responsibility of the ApplicationProtocolNegotiationHandler to line up a connection handler and our custom handlers in the pipeline:
然后,ApplicationProtocolNegotiationHandler的责任是在管道中排定一个连接处理程序和我们的自定义处理程序。
public static ApplicationProtocolNegotiationHandler getClientAPNHandler(
int maxContentLength, Http2SettingsHandler settingsHandler, Http2ClientResponseHandler responseHandler) {
final Http2FrameLogger logger = new Http2FrameLogger(INFO, Http2ClientInitializer.class);
final Http2Connection connection = new DefaultHttp2Connection(false);
HttpToHttp2ConnectionHandler connectionHandler =
new HttpToHttp2ConnectionHandlerBuilder().frameListener(
new DelegatingDecompressorFrameListener(connection,
new InboundHttp2ToHttpAdapterBuilder(connection)
.maxContentLength(maxContentLength)
.propagateSettings(true)
.build()))
.frameLogger(logger)
.connection(connection)
.build();
ApplicationProtocolNegotiationHandler clientAPNHandler =
new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
ChannelPipeline p = ctx.pipeline();
p.addLast(connectionHandler);
p.addLast(settingsHandler, responseHandler);
return;
}
ctx.close();
throw new IllegalStateException("Protocol: " + protocol + " not supported");
}
};
return clientAPNHandler;
}
Now all that is left to do is to bootstrap the client and send across a request.
现在所要做的就是启动客户端并发送一个请求。
4.4. Bootstrapping the Client
4.4.引导客户端
Bootstrapping of the client is similar to that of the server up to a point. After that, we need to add a little bit more functionality to handle sending the request and receiving the response.
客户端的引导与服务器的引导在一定程度上是类似的。在这之后,我们需要增加一点功能来处理发送请求和接收响应。
As mentioned previously, we’ll write this as a JUnit test:
如前所述,我们将把它写成一个JUnit测试。
@Test
public void whenRequestSent_thenHelloWorldReceived() throws Exception {
EventLoopGroup workerGroup = new NioEventLoopGroup();
Http2ClientInitializer initializer = new Http2ClientInitializer(sslCtx, Integer.MAX_VALUE, HOST, PORT);
try {
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.remoteAddress(HOST, PORT);
b.handler(initializer);
channel = b.connect().syncUninterruptibly().channel();
logger.info("Connected to [" + HOST + ':' + PORT + ']');
Http2SettingsHandler http2SettingsHandler = initializer.getSettingsHandler();
http2SettingsHandler.awaitSettings(60, TimeUnit.SECONDS);
logger.info("Sending request(s)...");
FullHttpRequest request = Http2Util.createGetRequest(HOST, PORT);
Http2ClientResponseHandler responseHandler = initializer.getResponseHandler();
int streamId = 3;
responseHandler.put(streamId, channel.write(request), channel.newPromise());
channel.flush();
String response = responseHandler.awaitResponses(60, TimeUnit.SECONDS);
assertEquals("Hello World", response);
logger.info("Finished HTTP/2 request(s)");
} finally {
workerGroup.shutdownGracefully();
}
}
Notably, these are the extra steps we took with respect to the server bootstrap:
值得注意的是,这些是我们在服务器引导方面采取的额外步骤。
- First, we waited for the initial handshake, making use of Http2SettingsHandler‘s awaitSettings method
- Second, we created the request as a FullHttpRequest
- Third, we put the streamId in our Http2ClientResponseHandler‘s streamIdMap, and called its awaitResponses method
- And at last, we verified that Hello World is indeed obtained in the response
In a nutshell, here’s what happened – the client sent a HEADERS frame, initial SSL handshake took place, and the server sent the response in a HEADERS and a DATA frame.
简而言之,事情是这样的–客户发送了一个HEADERS帧,发生了最初的SSL握手,而服务器在一个HEADERS和一个DATA帧中发送了响应。
5. Conclusion
5.总结
In this tutorial, we saw how to implement an HTTP/2 server and client in Netty using code samples to get a Hello World response using HTTP/2 frames.
在本教程中,我们看到了如何使用代码示例在Netty中实现HTTP/2服务器和客户端,以使用HTTP/2框架获得Hello World响应。
We hope to see a lot more improvements in Netty API for handling HTTP/2 frames in the future, as it is still being worked upon.
我们希望在未来看到Netty API在处理HTTP/2框架方面有更多的改进,因为它仍在努力之中。
As always, source code is available over on GitHub.
一如既往,源代码可在GitHub上获取。