Skip navigation

Netty/Incubator/Codec/Http3 0.0.1.Final released

After a few months of work I am happy to announce the first incubator release of our own HTTP/3 codec. This codec is a pure Java implementation (written from scratch) that is using our own QUIC abstraction under the hood.

While it is still early days this implementation is mostly "feature-complete" and as far as we are aware of full-functional. Please test it out and provide feedback and/or PRs :)

Incubator, what does this mean ?

To be able to work on new exciting features without "affecting" the core of netty we decided to start development of such features in the "incubator". This means these features will be developed in a separate repository and only be merged back into the core netty repository once we feel that we can guarantee we not need to break the API anymore and consider these production ready. This hopefully helps to clarify exceptions and stability of the code and so people should be fully aware that APIs may change. Beside this it also allows to use different release schedules.

How can I use the HTTP/3 codec ?

You we will need to include the right dependency of the HTTP/3 codec and QUIC codec. At the moment we support Linux (x86_64), MacOS (x86_64) and Windows (x86_64) for the QUIC codec.

An example for maven / linux would be:

<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-quic</artifactId>
    <version>0.0.7.Final</version>
    <classifier>linux-x86_64</classifier>
</dependency>
<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-http3</artifactId>
    <version>0.0.1.Final</version>
</dependency>

Or if you use macOS:

<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-quic</artifactId>
    <version>0.0.7.Final</version>
    <classifier>osx-x86_64</classifier>
</dependency>
<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-http3</artifactId>
    <version>0.0.1.Final</version>
</dependency>

Or if you use windows:

<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-quic</artifactId>
    <version>0.0.7.Final</version>
    <classifier>windows-x86_64</classifier>
</dependency>
<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-http3</artifactId>
    <version>0.0.1.Final</version>
</dependency>

You can also simplify things by using the os-maven-plugin.

How would I setup a HTTP/3 server / HTTP/3 client?

Bootstrapping a HTTP/3 server looks like:

public final class Http3ServerExample {
    private static final byte[] CONTENT = "Hello World!\r\n".getBytes(CharsetUtil.US_ASCII);
    private Http3ServerExample() { }

    public static void main(String... args) throws Exception {
        class Http3ServerHandler extends Http3RequestStreamInboundHandler {
            @Override
            protected void channelRead(
                    ChannelHandlerContext ctx, Http3HeadersFrame frame, boolean isLast) {
                if (isLast) {
                    writeResponse(ctx);
                }
                ReferenceCountUtil.release(frame);
            }

            @Override
            protected void channelRead(
                    ChannelHandlerContext ctx, Http3DataFrame frame, boolean isLast) {
                if (isLast) {
                    writeResponse(ctx);
                }
                ReferenceCountUtil.release(frame);
            }

            private void writeResponse(ChannelHandlerContext ctx) {
                Http3HeadersFrame headersFrame = new DefaultHttp3HeadersFrame();
                headersFrame.headers().status("200");
                headersFrame.headers().set("server", "netty");
                ctx.write(headersFrame);
                ctx.writeAndFlush(new DefaultHttp3DataFrame(Unpooled.wrappedBuffer(CONTENT)))
                            .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT);
            }
        }

        SelfSignedCertificate cert = new SelfSignedCertificate();
        QuicSslContext sslContext = QuicSslContextBuilder.forServer(cert.key(), null, cert.cert())
                .applicationProtocols(Http3.supportedApplicationProtocols()).build();
        ChannelHandler codec = Http3.newQuicServerCodecBuilder()
                .sslContext(sslContext)
                .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
                .initialMaxData(10000000)
                .initialMaxStreamDataBidirectionalLocal(1000000)
                .initialMaxStreamDataBidirectionalRemote(1000000)
                .initialMaxStreamsBidirectional(100)
                .tokenHandler(InsecureQuicTokenHandler.INSTANCE)
                .handler(new ChannelInitializer<QuicChannel>() {
                    @Override
                    protected void initChannel(QuicChannel ch) {
                        // Called for each connection
                        ch.pipeline().addLast(new Http3ServerConnectionHandler(
                                new ChannelInitializer<QuicStreamChannel>() {
                                    // Called for each request-stream,
                                    @Override
                                    protected void initChannel(QuicStreamChannel ch) {
                                        ch.pipeline().addLast(new Http3ServerHandler());
                                    }
                                });
                    }
                }).build();

        NioEventLoopGroup group = new NioEventLoopGroup(1);
        try {
            Bootstrap bs = new Bootstrap();
            Channel channel = bs.group(group)
                    .channel(NioDatagramChannel.class)
                    .handler(codec)
                    .bind(new InetSocketAddress(9999)).sync().channel();
            channel.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

And the corresponding HTTP/3 client like that:

public final class Http3ClientExample {
    private Http3ClientExample() { }

    public static void main(String... args) throws Exception {
        class Http3ClientHandler extends Http3RequestStreamInboundHandler {
            @Override
            protected void channelRead(
                    ChannelHandlerContext ctx, Http3HeadersFrame frame, boolean isLast) {
                releaseFrameAndCloseIfLast(ctx, frame, isLast);
            }

            @Override
            protected void channelRead(
                    ChannelHandlerContext ctx, Http3DataFrame frame, boolean isLast) {
                System.out.print(frame.content().toString(CharsetUtil.US_ASCII));
                releaseFrameAndCloseIfLast(ctx, frame, isLast);
            }

            private void releaseFrameAndCloseIfLast(
                    ChannelHandlerContext ctx, Http3RequestStreamFrame frame, boolean isLast) {
                ReferenceCountUtil.release(frame);
                if (isLast) {
                    ctx.close();
                }
            }
        }

        QuicSslContext context = QuicSslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .applicationProtocols(Http3.supportedApplicationProtocols()).build();
        ChannelHandler codec = Http3.newQuicClientCodecBuilder()
                .sslContext(context)
                .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
                .initialMaxData(10000000)
                .initialMaxStreamDataBidirectionalLocal(1000000)
                .build();
        NioEventLoopGroup group = new NioEventLoopGroup(1);

        try {
            Bootstrap bs = new Bootstrap();
            Channel channel = bs.group(group)
                    .channel(NioDatagramChannel.class)
                    .handler(codec)
                    .bind(0).sync().channel();

            QuicChannel quicChannel = QuicChannel.newBootstrap(channel)
                    .handler(new Http3ClientConnectionHandler())
                    .remoteAddress(new InetSocketAddress(NetUtil.LOCALHOST4, 9999))
                    .connect()
                    .get();

            QuicStreamChannel streamChannel = 
                    Http3.newRequestStream(quicChannel, new Http3ClientHandler()).sync().getNow();

            // Write the Header frame and send the FIN to mark the end of the request.
            // After this its not possible anymore to write any more data.
            Http3HeadersFrame frame = new DefaultHttp3HeadersFrame();
            frame.headers().method("GET").path("/");
            streamChannel.writeAndFlush(frame)
                    .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT).sync();

            // Wait for the stream channel and quic channel to be closed (this will happen
            // after we received the FIN).
            // After this is done we will close the underlying datagram channel.
            streamChannel.closeFuture().sync();

            // After we received the response lets also close the underlying QUIC channel
            // and datagram channel.
            quicChannel.close().sync();
            channel.close().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

Converting layer

To make it easier for people we also include a "converting" layer which allows to just act on the usual HttpResponse / HttpRequest and HttpContent objects (like you are used too from the HTTP/1 codec). To make use of it just add it to the ChannelPipeline as show here:

class Http1RequestHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof LastHttpContent) {
             FullHttpResponse response = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, OK, CONTENT.retainedDuplicate());
             response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, CONTENT.readableBytes());
             ctx.writeAndFlush(response);
        }
    }
}

SelfSignedCertificate cert = new SelfSignedCertificate();
QuicSslContext sslContext = QuicSslContextBuilder.forServer(cert.key(), null, cert.cert())
           .applicationProtocols(Http3.supportedApplicationProtocols()).earlyData(true).build();
ChannelHandler codec = Http3.newQuicServerCodecBuilder()
        .option(QuicChannelOption.UDP_SEGMENTS, 10)
        .sslContext(sslContext)
        .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
        .maxSendUdpPayloadSize(1500)
        .maxRecvUdpPayloadSize(1500)
        .initialMaxData(10000000)
        .initialMaxStreamDataBidirectionalLocal(1000000)
        .initialMaxStreamDataBidirectionalRemote(1000000)
        .initialMaxStreamsBidirectional(100000)
        .tokenHandler(InsecureQuicTokenHandler.INSTANCE)
        .handler(new ChannelInitializer<QuicChannel>() {
                    @Override
                    protected void initChannel(QuicChannel ch) {
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                                System.err.println("INACTIVE");
                                ((QuicChannel) ctx.channel()).collectStats().addListener(f -> {
                                    System.err.println(f.getNow());
                                });
                            }
                        });
                        ch.pipeline().addLast(new Http3ServerConnectionHandler(
                                new ChannelInitializer<QuicStreamChannel>() {
                                    // Called for each request-stream,
                                    @Override
                                    protected void initChannel(QuicStreamChannel ch) {
                                        ch.pipeline().addLast(new Http3FrameToHttpObjectCodec(true));
                                        ch.pipeline().addLast(new Http1RequestHandler());
                                    }
                                }));
                    }
        }).build();

...

Also note that this adapter handler supports server and client.

Important notes

For maximal performance you should use the native epoll (EpollDatagramChannel / EpollEventLoopGroup) transport that is included by netty as it can use sendmmsg / recvmmsg which will help to improve the throughput.

Where can I find the project ?

The project itself lives on GitHub.