eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

eBook – HTTP Client – NPI (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

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 HEADERS, DATA 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 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.

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.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LS – NPI (cat=HTTP Client-Side)
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Course – LS – NPI (cat=Java)
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)