nio_uring
is an I/O library for Java that uses io_uring under the hood, which aims to be:
- A simple and flexible API
- Super fast and efficient
- Truly zero-copy (the kernel addresses direct
ByteBuffer
s for I/O operations) - Slightly opinionated
Feedback, suggestions, and contributions are most welcome!
- Linux >= 5.1
- Java >= 8
For both of these, the higher the version the better - free performance!
<dependency>
<groupId>sh.blake.niouring</groupId>
<artifactId>nio_uring</artifactId>
<version>0.1.3</version>
</dependency>
Here's a basic HTTP server from sh.blake.niouring.examples.HttpHelloWorldServer
. There are a few other examples in the same package.
public static void main(String[] args) {
String response = "HTTP/1.1 200 OK\r\n\r\nHello, world!";
ByteBuffer responseBuffer = ByteBufferUtil.wrapDirect(response);
IoUringServerSocket serverSocket = new IoUringServerSocket(8080);
serverSocket.onAccept((ring, socket) -> {
// queue another accept request for the next client
ring.queueAccept(serverSocket);
// set up the read handler and queue a read operation
socket.onRead(in -> ring.queueWrite(socket, responseBuffer.slice()));
ring.queueRead(socket, ByteBuffer.allocateDirect(1024));
// HTTP spec says the server should close when done
socket.onWrite(out -> socket.close());
// and some really basic error handling
socket.onException(ex -> {
ex.printStackTrace();
socket.close();
});
});
new IoUring()
.onException(Exception::printStackTrace)
.queueAccept(serverSocket) // queue an accept request, onAccept will be called when a socket connects
.loop(); // process I/O events until interrupted
}
A barebones cat
implementation from sh.blake.niouring.examples.CatExample
:
public static void main(String[] args) {
IoUringFile file = new IoUringFile(args[0]);
file.onRead(in -> {
System.out.println(StandardCharsets.UTF_8.decode(in));
file.close();
});
new IoUring()
.queueRead(file, ByteBuffer.allocateDirect(8192))
.execute(); // process at least one I/O event (blocking until complete)
}
There's also an example HTTP server that will respond with this README in the examples package!
Pretty much all performance tuning is done with one knob - the ringSize
argument to the IoUring
constructor, which has a default value of 512 if not provided. This value controls the number of outstanding I/O events (accepts, reads, and writes) at any given time. It is constrained by memlock
limits (ulimit -l
) which can be increased as necessary. Don't forget about file descriptor limits (ulimit -n
) too!
Beyond this, you will have to run multiple rings across multiple threads. See sh.blake.niouring.examples.ParallelHttpEchoServer
for a simple starter using java.util.concurrent
APIs.
As of now, you should only call read/write/close operations from an IoUring
handler (onAccept
, onRead
, onWrite
, etc). This is because nio_uring
uses liburing
under the hood and its internal submission/completion system is shared and not thread safe. We understand this is an important feature and are working on an efficient way to wake up the main ring loop and have it wait for external threads to perform modifications before resuming.
nio_uring
holds the opinion that checked exceptions, as a concept, were probably a mistake. Therefore, all exceptions produced by nio_uring
will extend java.lang.RuntimeException
and will always be forwarded to the appropriate exception handler if generated intentionally (see socket.onException
).
All ByteBuffer
instances used with this library must be direct (e.g. allocated with allocateDirect
). This is a hard requirement and is what enables zero-copy functionality.
Queuing multiple operations is fine, but try not to queue a read/write operation for the same buffer to the same socket more than once per ring execution, because the internal mapping system is not designed to handle this. The data will be read/written, but your handler will only be called for the first operation, and an java.lang.IllegalStateException
exception with message "Buffer already removed" will be sent to the exception handler for the second.
Set the LIBURING_PATH
environment variable to the root of a fully compiled liburing directory.
Then ./gradlew build
and you're off!
MIT. Have fun and make cool things!