HTTP/2 in Netty

标签: | 发表时间:2021-06-15 14:25 | 作者:
出处:https://www.baeldung.com

1. Overview

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.

For a basic understanding of the framework,  introduction to Netty is a good start.

In this tutorial, we'll see how to implement an HTTP/2 server and client in Netty.

2. What Is  HTTP/2?

As the name suggests,  HTTP version 2 or simply HTTP/2, is a newer version of the Hypertext Transfer Protocol.

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.

As of writing this,  HTTP/3 is also available, though not yet supported by default by all browsers.

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.

Communication in HTTP/2 happens via a group of bytes called frames, and multiple frames form a stream.

In our code samples, we'll see how Netty handles the exchange of  HEADERSDATA and  SETTINGS frames.

3. The Server

Now let's see how we can create an HTTP/2 server in Netty.

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:

  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.

This means that our server will only support HTTP/2 and its underlying  protocol identifier h2.

3.2. Bootstrapping the Server with a  ChannelInitializer

Next, we need a  ChannelInitializer for our multiplexing child channel, so as to set up a Netty pipeline.

We'll use the earlier  sslContext in this channel to initiate the pipeline, and then bootstrap the server:

  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:

  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.

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.

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:

  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:

  @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.

For a quick test, start the server and fire a curl command with the  –http2 option:

  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

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.

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:

  @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 S slContext, 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.

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.

We'll come back to this JUnit at the end to bootstrap the client.

4.2. Handlers

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:

  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.

It also has a utility method  awaitSettings that our client will use in order to wait for the initial handshake completion:

  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.

Second, we'll need a handler to deal with the response obtained from the server, we'll name it  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:

  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.

The handler also has a utility method  put, of course, to put values in the  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.

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.

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:

  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

As we saw in the case of our server, the purpose of a  ChannelInitializer is to set up a pipeline:

  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.

Then, it's the responsibility of the  ApplicationProtocolNegotiationHandler to line up a connection handler and our custom handlers in the pipeline:

  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

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:

  @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.

5. Conclusion

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.

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.

As always, source code is available  over on GitHub.

相关 [http in netty] 推荐:

HTTP/2 in Netty

- -
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..

Netty系列之Netty高性能之道

- - CSDN博客推荐文章
最近一个圈内朋友通过私信告诉我,通过使用Netty4 + Thrift压缩二进制编解码技术,他们实现了10W TPS(1K的复杂POJO对象)的跨节点远程服务调用. 相比于传统基于Java序列化+BIO(同步阻塞IO)的通信框架,性能提升了8倍多. 事实上,我对这个数据并不感到惊讶,根据我5年多的NIO编程经验,通过选择合适的NIO框架,加上高性能的压缩二进制编解码技术,精心的设计Reactor线程模型,达到上述性能指标是完全有可能的.

Netty代码分析

- LightingMan - 淘宝JAVA中间件团队博客
Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序[官方定义],整体来看其包含了以下内容:1.提供了丰富的协议编解码支持,2.实现自有的buffer系统,减少复制所带来的消耗,3.整套channel的实现,4.基于事件的过程流转以及完整的网络事件响应与扩展,5.丰富的example.

Netty 5用户指南

- - 并发编程网 - ifeve.com
原文地址: http://netty.io/wiki/user-guide-for-5.x.html     译者:光辉勇士       校对:郭蕾. 现如今我们使用通用的应用程序或者类库来实现系统之间地互相访问,比如我们经常使用一个HTTP客户端来从web服务器上获取信息,或者通过web service来执行一个远程的调用.

Netty 用户指南4.x

- - CSDN博客推荐文章
现在我们经常使用程序或者库和其他人交流信息.例如,我们经常使用http程序库去从一个web server接收信息,或者调用一个远程的web服务.然而,一个通用的传输协议或者实现有的时候不能适应我们自己的场景.例如,我们不会用http server来传输一些大的文件,Email和一些实时性的信息,例如金融方面或者有些游戏数据方面的信息.这些需要一个高度优化的协议,为了使用某一种特定的应用场景.

Netty实现网络通信

- - 互联网 - ITeye博客
原文同步至  http://www.waylau.com/netty-chat/. Netty 是一个 Java NIO 客户端服务器框架,使用它可以快速简单地开发网络应用程序,比如服务器和客户端的协议. Netty 大大简化了网络程序的开发过程比如 TCP 和 UDP 的 socket 服务的开发.

Netty Client重连实现

- - 鸟窝
当我们用Netty实现一个TCP client时,我们当然希望当连接断掉的时候Netty能够自动重连. Netty Client有两种情况下需要重连:. Netty Client启动的时候需要重连. 在程序运行中连接断掉需要重连. 对于第一种情况,Netty的作者在stackoverflow上给出了 解决方案,.

Netty 长连接服务

- - 非技术 - ITeye博客
还记得一年半前,做的一个项目需要用到  Android 推送服务. 和  iOS 不同,Android 生态中没有统一的推送服务. Google 虽然有  Google Cloud Messaging ,但是连国外都没统一,更别说国内了,直接被墙. 所以之前在 Android 上做推送大部分只能靠轮询.

Netty 实现原理浅析

- - Java - 编程语言 - ITeye博客
关注微信号:javalearns   随时随地学Java. Netty是JBoss出品的高效的Java NIO开发框架,关于其使用,可参考我的另一篇文章 netty使用初步. 本文将主要分析Netty实现方面的东西,由于精力有限,本人并没有对其源码做了极细致的研 究. 如果下面的内容有错误或不严谨的地方,也请指正和谅解.

Netty SSL性能调优

- - ImportNew
嗯,这篇不长的文章,是一个晚上工作到三点的血泪加班史总结而成. 《 TLS协议分析 与 现代加密通信协议设计》 首先要感谢这篇文章,如果没有它,我可能还要花更多的时间才能完成. 文章有点长,能看多少是多少,每句都是收获. 目前(2015年)已经不安全,必须禁用. 互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版.