Skip navigation

Netty/Incubator/Codec/Quic 0.0.1.Final released

After a bit more than 1 month of developement we are finally ready to release our first incubator version of Netty/Incubator/Codec/Quic.

This codec provides a QUIC implementation of draft 32 by wrapping quiche and expose QUIC via the Channel API.

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 seperate 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 expections 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 quic codec ?

You we will need to include the right dependency for your platform. At the moment we only support either macOS x86_64 or linux x86_64. That said, this may change in the future.

An example for maven / linux would be:

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

Or if you use macOS:

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

How would I setup a QUIC server / client ?

Bootstrapping a QUIC server that speaks HTTP/0.9 would look like that:

// We just want to support HTTP 0.9 as application protocol
byte[] proto = new byte[] {
        0x08, 'h', 't', 't', 'p', '/', '0', '.', '9'
};

NioEventLoopGroup group = new NioEventLoopGroup(1);
ChannelHandler codec = new QuicServerCodecBuilder()
        .certificateChain("./src/test/resources/cert.crt")
        .privateKey("./src/test/resources/cert.key")
        .applicationProtocols(proto)
        .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
        // Configure some limits for the maximal number of streams (and the data) that we want to handle.
        .initialMaxData(10000000)
        .initialMaxStreamDataBidirectionalLocal(1000000)
        .initialMaxStreamDataBidirectionalRemote(1000000)
        .initialMaxStreamsBidirectional(100)
        .initialMaxStreamsUnidirectional(100)

        // Setup a token handler. In a production system you would want to implement and provide your
        // custom one.
        .tokenHandler(InsecureQuicTokenHandler.INSTANCE)
        // ChannelHandler that is added into QuicChannel pipeline.
        .handler(new ChannelInboundHandlerAdapter() {
            @Override
            public void channelActive(ChannelHandlerContext ctx) {
                QuicChannel channel = (QuicChannel) ctx.channel();
                // Create streams etc..
            }

            public void channelInactive(ChannelHandlerContext ctx) {
                ((QuicChannel) ctx.channel()).collectStats().addListener(f -> {
                    if (f.isSuccess()) {
                        LOGGER.info("Connection closed: {}", f.getNow());
                    }
                });
            }

            @Override
            public boolean isSharable() {
                return true;
            }
        })
        .streamHandler(new ChannelInitializer<QuicStreamChannel>() {
            @Override
            protected void initChannel(QuicStreamChannel ch)  {
                // Add a LineBasedFrameDecoder here as we just want to do some simple HTTP 0.9 handling.
                ch.pipeline().addLast(new LineBasedFrameDecoder(1024))
                        .addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) {
                        ByteBuf byteBuf = (ByteBuf) msg;
                        try {
                            if (byteBuf.toString(CharsetUtil.US_ASCII).trim().equals("GET /")) {
                                ByteBuf buffer = ctx.alloc().directBuffer();
                                buffer.writeCharSequence("Hello World!\r\n", CharsetUtil.US_ASCII);
                                // Write the buffer and shutdown the output by writing a FIN.
                                ctx.writeAndFlush(buffer).addListener(QuicStreamChannel.SHUTDOWN_OUTPUT);
                            }
                        } finally {
                            byteBuf.release();
                        }
                    }
                });
            }
        }).build();
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 corespanding QUIC client like that:

// We just want to support HTTP 0.9 as application protocol
byte[] proto = new byte[] {
        0x08, 'h', 't', 't', 'p', '/', '0', '.', '9'
};

NioEventLoopGroup group = new NioEventLoopGroup(1);
try {
    ChannelHandler codec = new QuicClientCodecBuilder()
            .applicationProtocols(proto)
            .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
            .initialMaxData(10000000)
            // As we don't want to support remote initiated streams just setup the limit for
            // local initiated streams in this example.
            .initialMaxStreamDataBidirectionalLocal(1000000)
            .build();

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

    QuicChannel quicChannel = QuicChannel.newBootstrap(channel)
            .streamHandler(new ChannelInboundHandlerAdapter() {
                @Override
                public void channelActive(ChannelHandlerContext ctx) {
                    // As we did not allow any remote initiated streams we will never see
                    // this method called. That said just let us keep it here to demonstrate
                    // that this handle would be called for each remote initiated stream.
                    ctx.close();
                }
            })
            .remoteAddress(new InetSocketAddress(NetUtil.LOCALHOST4, 9999))
            .connect()
            .get();

    QuicStreamChannel streamChannel = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL,
            new ChannelInboundHandlerAdapter() {
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) {
                    ByteBuf byteBuf = (ByteBuf) msg;
                    System.err.println(byteBuf.toString(CharsetUtil.US_ASCII));
                    byteBuf.release();
                }

                @Override
                public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
                    if (evt == ChannelInputShutdownReadComplete.INSTANCE) {
                        // Close the connection once the remote peer did send the FIN
                        // for this stream.
                        ((QuicChannel) ctx.channel().parent()).close(true, 0,
                                ctx.alloc().directBuffer(16)
                                        .writeBytes(new byte[]{'k', 't', 'h', 'x', 'b', 'y', 'e'}));
                    }
                }
            }).sync().getNow();
    // Write the data and send the FIN. After this its not possible anymore to write any more data.
    streamChannel.writeAndFlush(Unpooled.copiedBuffer("GET /\r\n", CharsetUtil.US_ASCII))
            .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT);

    // 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();
    quicChannel.closeFuture().sync();
    channel.close().sync();
} finally {
    group.shutdownGracefully();
}

Where can I find the project ?

The project itself lives on GitHub.

What's next ?

While we are activly working on this project itself we also already started with an HTTP3 implementation on top of it.