SkillAgentSearch skills...

Quiche4j

QUIC transport protocol and HTTP/3 for Java

Install / Use

/learn @kachayev/Quiche4j
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Quiche4j

Java implementation of the QUIC transport protocol and HTTP/3.

The library provides thin Java API layer on top of JNI calls to quiche. Quiche4j provides a low level API for processing QUIC packets and handling connection state. The application is responsible for providing I/O (e.g. sockets handling) as well as timers. The library itself does not make any assumptions on how I/O layer is organized, making it's pluggle into different architectures.

The main goal of the JNI bindings is to ensure high-performance and flexibility for the application developers while maintaining full access to quiche library features. Specifically, the bindings layer tries to ensure zero-copy data trasfer between runtimes where possible and perform minimum allocations on Java side.

Usage

Maven:

<dependencies>
    <dependency>
        <groupId>io.quiche4j</groupId>
        <artifactId>quiche4j-core</artifactId>
        <version>0.2.5</version>
    </dependency>
    <dependency>
        <groupId>io.quiche4j</groupId>
        <artifactId>quiche4j-jni</artifactId>
        <classifier>linux_x64_86</classifier>
        <version>0.2.5</version>
    </dependency>
</dependencies>

Note that quiche4j-jni contains native library and should be installed with proper classifier. os-maven-plugin could be used to simplify classifier detection

<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.6.1</version>
        </extension>
    </extensions>
</build>
<dependencies>
    <dependency>
        <groupId>io.quiche4j</groupId>
        <artifactId>quiche4j-jni</artifactId>
        <classifier>${os.detected.classifier}</classifier>
        <version>0.2.5</version>
    </dependency>
</dependencies>

Building

Quiche4j requires cargo and Rust 1.39+ to build. The latest stable Rust release can be installed using rustup. Once the Rust build environment is setup,

$ git clone https://github.com/kachayev/quiche4j
$ mvn clean install

Run Examples

Run HTTP3 client example:

$ ./http3-client.sh https://quic.tech:8443
> sending request to https://quic.tech:8443
> handshake size: 1200
> socket.recieve 167 bytes
> conn.recv 167 bytes
...
! conn is closed recv=10 sent=12 lost=0 rtt=95 cwnd=14520 delivery_rate=1436

Run HTTP3 server example:

$ ./http3-server.sh :4433
! listening on localhost:4433

Compile Manually

Maven project is setup to automatically compile JNI library and include the result of the compilation into the quiche4j-jni JAR. Even thought this method is convenient for distribution, it might lack flexibility. To compile JNI manually follow the next steps,

$ git clone https://github.com/kachayev/quiche4j
$ cargo build --release --manifest-path quiche4j-jni/Cargo.toml
$ mvn clean install
$ java \
    -Djava.library.path=quiche4j-jni/target/release/ \
    -cp quiche4j-examples/target/quiche4j-examples-*.jar \
    io.quiche4j.examples.Http3Server

The code would try to load native libraries from java.library.path first, using built-in artifact as a fallback only.

For cross-compilation options, see cargo build documentation.

API

Connection

Before establishing a QUIC connection, you need to create a configuration object:

import io.quiche4j.Config;
import io.quiche4j.ConfigBuilder;

final Config config = new ConfigBuilder(Quiche.PROTOCOL_VERSION).build();

On the client-side the Quiche.connect utility function can be used to create a new connection, while Quiche.accept is for servers:

// client
final byte[] connId = Quiche.newConnectionId();
// note, that "quic.tech" here is not used for establishing network
// connection. it's used only for peer verification (thus, optional)
final Connection conn = Quiche.connect("quic.tech", connId, config);

// server
final Connection conn = Quiche.accept(sourceConnId, originalDestinationId, config);

Incoming packets

Using the connection's recv method the application can process incoming packets that belong to that connection from the network:

final byte[] buf = new byte[1350];
while(true) {
    DatagramPacket packet = new DatagramPacket(buf, buf.length);
    try {
        // read from the socket
        socket.receive(packet);
        final byte[] buffer = Arrays.copyOfRange(packet.getData(), packet.getOffset(), packet.getLength());
        // update the connection state
        final int read = conn.recv(buffer);
        if(read <= 0) break;
    } catch (SocketTimeoutException e) {
        conn.onTimeout();
        break;
    }
}

Outgoing packets

Outgoing packet are generated using the connection's send method instead:

final byte[] buf = new byte[1350];
while(true) {
    // get data that's need to be sent based on the connection state
    final int len = conn.send(buf);
    if (len <= 0) break;
    final DatagramPacket packet = new DatagramPacket(buf, len, address, port);
    // send it to the network
    socket.send(packet);
}

Timers

The application is responsible for maintaining a timer to react to time-based connection events. When a timer expires, the connection's onTimeout method should be called, after which additional packets might need to be sent on the network:

// handle timer
conn.onTimeout();

// sending corresponding packets
final byte[] buf = new byte[1350];
while(true) {
    final int len = conn.send(buf);
    if (len <= 0) break;
    final DatagramPacket packet = new DatagramPacket(buf, len, address, port);
    socket.send(packet);
}

Streams Data

After some back and forth, the connection will complete its handshake and will be ready for sending or receiving application data.

Data can be sent on a stream by using the streamSend method:

if(conn.isEstablished()) {
    // handshake completed, send some data on stream 0
    conn.streamSend(0, "hello".getBytes(), true);
}

The application can check whether there are any readable streams by using the connection's readable method, which returns an iterator over all the streams that have outstanding data to read.

The streamRecv method can then be used to retrieve the application data from the readable stream:

if(conn.isEstablished()) {
    final byte[] buf = new byte[1350]; 
    for(long streamId: conn.readable()) {
        // stream <streamId> is readable, read until there's no more data
        while(true) {
            final int len = conn.streamRecv(streamId, buf);
            if(len <= 0) break;
        }
    }
}

HTTP/3

The library provides a high level API for sending and receiving HTTP/3 requests and responses on top of the QUIC transport protocol.

Connection

HTTP/3 connections require a QUIC transport-layer connection, see "Connection" for a full description of the setup process. To use HTTP/3, the QUIC connection must be configured with a suitable ALPN Protocol ID:

import io.quiche4j.Config;
import io.quiche4j.ConfigBuilder;
import io.quiche4j.http3.Http3Connection;

final Config config = new ConfigBuilder(Quiche.PROTOCOL_VERSION)
    .withApplicationProtos(Http3.APPLICATION_PROTOCOL)
    .build();

The QUIC handshake is driven by sending and receiving QUIC packets. Once the handshake has completed, the first step in establishing an HTTP/3 connection is creating its configuration object:

import io.quiche4j.http3.Http3Config;
import io.quiche4j.http3.Http3ConfigBuilder;

final Http3Config h3Config = new Http3ConfigBuilder().build();

HTTP/3 client and server connections are both created using the Http3Connection.withTransport function:

import io.quiche4j.http3.Http3Connection;

final Http3Connection h3Conn = Http3Connection.withTransport(conn, h3Config);

Sending Request

An HTTP/3 client can send a request by using the connection's sendRequest method to queue request headers; sending QUIC packets causes the requests to get sent to the peer:

import io.quiche4j.http3.Http3Header;

List<Http3Header> req = new ArrayList<>();
req.add(new Http3Header(":method", "GET"));
req.add(new Http3Header(":scheme", "https"));
req.add(new Http3Header(":authority", "quic.tech"));
req.add(new Http3Header(":path", "/"));
req.add(new Http3Header("user-agent", "Quiche4j"));
h3Conn.sendRequest(req, true);

An HTTP/3 client can send a request with additional body data by using the connection's sendBody method:

final long streamId = h3Conn.sendRequest(req, false);
h3Conn.sendBody(streamId, "Hello there!".getBytes(), true);

Handling Responses

After receiving QUIC packets, HTTP/3 data is processed using the connection's poll method.

An HTTP/3 server uses poll to read requests and responds to them, an HTTP/3 client uses poll to read responses. poll method accepts object that implements Http3EventListener interface defining callbacks for different type of events

import io.quiche4j.http3.Http3EventListener;
import io.quiche4j.http3.Http3Header;

final long streamId = h3Conn.poll(new Http3EventListener() {
    public void onHeaders(long streamId, List<Http3Header> headers) {
        // got headers
    }

    public void onData(long streamId) {
        // got body
        final byte[] body = new byte[MAX_DATAGRAM_SIZE];
        final int len = h3Conn.recvBody(streamId, body);
    }

    public void onFinished(long streamId) {
        // done with this stream
        conn.close(true, 0x00, "Bye! :)".getBytes()));
    }
});

if(Quiche.ErrorCode.DONE == streamId) {
    // this means no event was emitted
    // it would take more packets to proceed with new events
}

Note that poll would either execute callbacks and return

Related Skills

View on GitHub
GitHub Stars107
CategoryDevelopment
Updated2d ago
Forks16

Languages

Java

Security Score

100/100

Audited on Mar 30, 2026

No findings