Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/kachayev/quiche4j
QUIC transport protocol and HTTP/3 for Java
https://github.com/kachayev/quiche4j
http3 java network-programming protocol quic rust
Last synced: 4 months ago
JSON representation
QUIC transport protocol and HTTP/3 for Java
- Host: GitHub
- URL: https://github.com/kachayev/quiche4j
- Owner: kachayev
- License: bsd-2-clause
- Created: 2020-09-05T18:49:34.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2022-08-18T08:54:23.000Z (over 2 years ago)
- Last Synced: 2023-03-12T03:06:21.883Z (almost 2 years ago)
- Topics: http3, java, network-programming, protocol, quic, rust
- Language: Java
- Homepage: https://kachayev.github.io/quiche4j/
- Size: 148 KB
- Stars: 72
- Watchers: 5
- Forks: 12
- Open Issues: 8
-
Metadata Files:
- Readme: README.md
- License: COPYING
Awesome Lists containing this project
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](https://github.com/cloudflare/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:
```xml
io.quiche4j
quiche4j-core
0.2.5
io.quiche4j
quiche4j-jni
linux_x64_86
0.2.5
```
Note that `quiche4j-jni` contains native library and should be installed with proper classifier. [`os-maven-plugin`](https://github.com/trustin/os-maven-plugin) could be used to simplify classifier detection
```xml
kr.motd.maven
os-maven-plugin
1.6.1
io.quiche4j
quiche4j-jni
${os.detected.classifier}
0.2.5
```
## Building
`Quiche4j` requires `cargo` and Rust 1.39+ to build. The latest stable Rust release can be installed using [rustup](https://rustup.rs/). Once the Rust build environment is setup,
```bash
$ git clone https://github.com/kachayev/quiche4j
$ mvn clean install
```## Run Examples
Run HTTP3 client example:
```bash
$ ./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:
```bash
$ ./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,
```bash
$ 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](https://doc.rust-lang.org/cargo/commands/cargo-build.html).
## API
### Connection
Before establishing a QUIC connection, you need to create a configuration object:
```java
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:
```java
// 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:
```java
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:
```java
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:
```java
// 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:
```java
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:
```java
if(conn.isEstablished()) {
final byte[] buf = new byte[1350];
for(long streamId: conn.readable()) {
// stream 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"](#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:
```java
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:
```java
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:
```java
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:
```java
import io.quiche4j.http3.Http3Header;List 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:
```java
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
```java
import io.quiche4j.http3.Http3EventListener;
import io.quiche4j.http3.Http3Header;final long streamId = h3Conn.poll(new Http3EventListener() {
public void onHeaders(long streamId, List 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 returns immediately. If there's not enough data to fire any of the events, `poll` immediately returns `Quiche.ErrorCode.DONE`. The application is responsible for handling incoming packets from the network and feeding packets data into connection before executing next `poll`.
### Examples
Have a look at the [quiche4j-examples](quiche4j-examples/src/main/java/io/quiche4j/examples/) folder for more complete examples on how to use the Quiche4j API to work with HTTP/3 protocol.
Examples package has [`Http3NettyClient`](quiche4j-examples/src/main/java/io/quiche4j/examples/Http3NettyClient.java) with a toy implementation of HTTP/3 client to show case the idea of how `quiche4j` connection state management could be integrated with [Netty](https://netty.io/) I/O primitives.
### Errors Hanlding
Native JNI code propagates errors using return codes (typically the return code < 0 means either DONE or failed). For example, [`quiche::Error`](https://github.com/cloudflare/quiche/blob/204d693bb543e12a605073181ae605eacb743039/src/lib.rs#L320-L365) enum. `Quiche4j` follows the same convention instead of throwing Java exceptions to ensure good perfomance and compatibility with async runtimes (catching exception in async environemnt might be somewhat problematic). See [`Quiche.ErrorCode`](src/main/java/io/quiche4j/Quiche.java) and [`Http3.ErrorCode`](src/main/java/io/quiche4j/http3/Http3.java) for more details.
Unlike other methods, `Quiche.connect` and `Quiche.accept` throw `ConnectionFailureException` if JNI code failed before `quiche::Connection` struct had been allocated. In this case there's no pointer to carry around, thus Java code does not create `Connection` object.
## Debug
Use `QUICHEJ4_JNI_LOG` environment variable to tweak JNI log level. Setting variable to `trace` gives good visibility into the processing. Example
```bash
$ QUICHE4J_JNI_LOG=trace ./http3-client.sh https://quic.tech:8443
...
[2020-09-27T20:49:39Z TRACE quiche] 3457285232348874d2bda1ed5add4a0c894dc9f2 rx pkt Handshake version=ff00001d dcid=3457285232348874d2bda1ed5add4a0c894dc9f2 scid=1b48925e8fcf6281be7f5ca472dd44b71a2f2fc1 len=731 pn=2
[2020-09-27T20:49:39Z TRACE quiche] 3457285232348874d2bda1ed5add4a0c894dc9f2 rx frm CRYPTO off=2252 len=709
[2020-09-27T20:49:39Z TRACE quiche::tls] 3457285232348874d2bda1ed5add4a0c894dc9f2 write message lvl=Handshake len=36
[2020-09-27T20:49:39Z TRACE quiche::tls] 3457285232348874d2bda1ed5add4a0c894dc9f2 set write secret lvl=OneRTT
[2020-09-27T20:49:39Z TRACE quiche::tls] 3457285232348874d2bda1ed5add4a0c894dc9f2 set read secret lvl=OneRTT
[2020-09-27T20:49:39Z TRACE quiche] 3457285232348874d2bda1ed5add4a0c894dc9f2 connection established: proto=Ok("h3-29") cipher=Some(AES128_GCM) curve=Some("X25519") sigalg=Some("rsa_pss_rsae_sha256") resumed=false TransportParams { original_destination_connection_id: Some([121, 203, 4, 8, 44, 253, 150, 111, 224, 200, 201, 105, 201, 162, 250, 160]), max_idle_timeout: 30000, stateless_reset_token: None, max_udp_payload_size: 1350, initial_max_data: 10000000, initial_max_stream_data_bidi_local: 1000000, initial_max_stream_data_bidi_remote: 1000000, initial_max_stream_data_uni: 1000000, initial_max_streams_bidi: 100, initial_max_streams_uni: 100, ack_delay_exponent: 3, max_ack_delay: 25, disable_active_migration: true, active_conn_id_limit: 2, initial_source_connection_id: Some([27, 72, 146, 94, 143, 207, 98, 129, 190, 127, 92, 164, 114, 221, 68, 183, 26, 47, 47, 193]), retry_source_connection_id: None }
...
```## Implementation Details
* Modules [Native.java](src/main/java/io/quiche4j/Native.java) and [Http3Native.java](src/main/java/io/quiche4j/http3/Http3Native.java) contains definition of all native calls, structurally close to `quiche`'s [`src/ffi.rs`](https://github.com/cloudflare/quiche/blob/master/src/ffi.rs) and [`src/h3/ffi.rs`](https://github.com/cloudflare/quiche/blob/master/src/h3/ffi.rs) respectively.
* JNI calls are implmeneted in Rust (see [quiche4j-jni](quiche4j-jni/) for more details) using [`rust-jni`](https://docs.rs/jni/0.17.0/jni/) library. The goal was to stick to primitive types as much as possible and avoid Java objects manipulations in native code. There are still a few exceptions from this rule, e.g. operations with connection `Stats`, management of `Http3Header` lists, etc.
* Proxy Java objects maintain a handle (pointer) to the corresponding Rust struct to maximise compatability with all `quiche` features. A single instance of a `Cleaner` is statically defined in `io.quiche4j.Native` class and is used to register all deallocation callback (conventionally called `free` for each class that maintains a native pointer).
## Contribute
* Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug (also, check out "TODO" section of this document).
* Fork the repository on Github & fork master to `feature-*` branch to start making your changes.
* Write a test which shows that the bug was fixed or that the feature works as expected.or simply...
* Use it.
* Enjoy it.
* Spread the word.## TODO
There are still a few `xxx` comments in the code. Both for Java and for Rust. Plus, there are a few methods that are not exposed to Java layer. Notably, operations with stream priorities and HTTP/3 connection configuration (some of those would require to extend `quiche` library as well).
Other ideas to work on:
- [ ] Propagate Rust panics into Java exceptions (when necessary)
- [ ] Setup integration testing suite against different QUIC implementations out there
- [ ] Qlog support
- [ ] Experiment with in-memory serialization (Arrow?) to deal with (presumably) high overhead of manipulating objects in native code## Copyright
Copyright (C) 2020, Oleksii Kachaiev.
See [COPYING](/COPYING) for the license.
See [cloudflare/quiche/copying](https://github.com/cloudflare/quiche/blob/master/COPYING) for Quiche license.