Quiche4j
QUIC transport protocol and HTTP/3 for Java
Install / Use
/learn @kachayev/Quiche4jREADME
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
himalaya
343.3kCLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language).
node-connect
343.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
92.1kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
coding-agent
343.3kDelegate coding tasks to Codex, Claude Code, or Pi agents via background process
