Let’s make server-push enabled HTTP/2 server with Netty

Chanaka Balasooriya
3 min readMar 26, 2017

--

As a open-source software library, Apache Netty plays a huge role when implementing servers and clients in Java. Even though they have shown how to implement a simple HTTP/2 server, many programmers have to face some difficulties when they need to catch all HTTP/2 frames that client sends and to enable HTTP/2 specific features like server-push. So, I’m going to implement a simple HTTP/2 server that can send and receive any frame under HTTP/2 spectrum.

Here we go …

Start Maven project including netty-all as a dependency.

We will create 4 java classes to build up our server

  1. FrameListner.java — Listen to each frame data, wrap them with an appropriate object and expose for the rest of the program.
  2. ServerInitializer.java — Initilize the Connnection and register our listeners and handlers.
  3. RequestHandler.java — Handle the requests and send responses.
  4. Server.java — Our application main class.

Frame Listener

Frame listener inherits netty’s Http2EventAdapter class that exposes events for all HTTP/2 frame types. Since we only care about http/2 GET requests for this application I am going to override onHeadersRead method only. I will create a HTTP2HeadersFrame object with the event data as follows.

public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
Http2HeadersFrame frame=new DefaultHttp2HeadersFrame(headers,endStream,padding).setStreamId(streamId);
ctx.fireChannelRead(frame);
}
}

You can override onDataRead method to accept data frames for our server with the help of DefaultHttp2DataFrame object.

Server Initializer

Server Initializer inherits ChannelInitializer class and overrides its initChannel method as follows. We create a HTTP2Connection and register our Frame Listener and Request Handler as follows.

protected void initChannel(SocketChannel socketChannel) throws Exception {
Http2Connection connection = new DefaultHttp2Connection(true);
FrameListener frameListener = new FrameListener();
Http2ConnectionHandler connectionHandler = new Http2ConnectionHandlerBuilder().connection
(connection).
frameListener(frameListener).frameLogger(logger).build();
RequestHandler requestHandler=new RequestHandler(connection,connectionHandler.encoder());
socketChannel.pipeline().addLast(connectionHandler,
requestHandler);
}
}

Request Handler

We handle each client request by its request’s URL. we define two URLs to reply including server push and without server push. If client request with URL “/index.html”, server will send index.html data and if the URL is “/main.css”, it will send main.css file. If the URL is “/all” server will response index.html with a server push of main.css file.

Before sending a server push we need to send a push-promise frame to the client saying we are going to send main.css file as a serverpush as follows,

int prmiseId=connection.local().incrementAndGetNextStreamId(); //get next available id.
Http2Headers prmiseHeader=new DefaultHttp2Headers();
prmiseHeader.scheme(frame.headers().scheme()).
path("main.css").
add(HttpHeaderNames.CONTENT_TYPE,"text/css");
ChannelPromise promise=ctx.newPromise();
encoder.writePushPromise(ctx,frame.streamId(),prmiseId,prmiseHeader,0,promise);

Then we write actual data of main.css under above promiseId,

encoder.writeHeaders(ctx,prmiseId,prmiseHeader,0,false,promise);
encoder.writeData(ctx,prmiseId,promiseDATABuff,0,true,promise);

Finally we write requested data (index.html) and flush all bytes.

encoder.writeHeaders(ctx,frame.streamId(),responseHeader,0,false,promise);
encoder.writeData(ctx,frame.streamId(),htmlBuf,0,true,promise);
ctx.flush();

As you can see encoder object can be used to write any frame to the channel.

Most of the client applications that support HTTP/2, only support for HTTP/2 over TLS or HTTPS connections. In order to use this server with those clients it is better to implement server to establish connections as HTTP/2 over TLS.

You need to create a SSLContext and register it on the connection while overriding initChannel method in ServerInitilizer class as follows,

io.netty.handler.ssl.SslContext sslCtx=createSSLContext();
socketChannel.pipeline().addLast(sslCtx.newHandler(socketChannel.alloc()), connectionHandler,
reqeustHandler);

For this example I have created a self-signed-certificate with jetty ALPN library.

private SslContext createSSLContext(){
SslContext sslCtx;
try {
SslProvider provider = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK;
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.sslProvider(provider)
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN, ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();
}catch (Exception e){
sslCtx=null;
}
return sslCtx;
}

Build the project with maven.

To start the server you need to download jetty-alpn library compatible with your JDK version and give the xbootclasspath to that jar file.

java -Xbootclasspath/p:/home/{path-to-jar}/alpn-boot.jar -jar server-v0.1-1.0.0.jar

You can try HTTP/2 behavior by sending a request to https://0.0.0.0:8888 using an appropriate client. But most of the browsers don’t support server-push.

Complete Project will be here : https://github.com/chanakadinesh/Http2-server

Stay tuned…!

--

--