#minecraft #games #io #async #futures

craftio-rs

A crate to read & write packets defined in mcproto-rs to I/O sinks/sources

1 unstable release

0.1.0 Jan 9, 2021

#147 in Games

Apache-2.0

69KB
1.5K SLoC

craftio-rs

Version 0.1.0, by Twister915!

craftio-rs is a library which let's you read & write packets defined in mcproto-rs to real Minecraft servers/clients.

You can use this library to implement anything from a simple server status ping client, BungeeCord-like proxy, a bot to join your favorite server, or an entire Minecraft server or client implementation.

The protocol definition is managed in a separate crate, mentioned above, called mcproto-rs which defines a set of traits to support custom protocol implementations, and also defines all packets for a few of the versions of Minecraft.

This crate optionally implements the following features:

  • compression (using the flate2 crate)
  • encryption (using the aes crate) with a fast implementation of CFB-8
  • futures-io enables reading/writing to implementors of the AsyncRead/AsyncWrite traits from the futures crate
  • tokio-io enables reading/writing to implementors of the AsyncRead/AsyncWrite traits from the tokio crate

Usage

[dependencies]
craftio-rs = "0.1"

This library can be used to connect to servers or host client connections. It implements all features of the Minecraft protocol, and these features can be disabled for simpler use-cases (such as hitting servers to gather status information).

You can also use an async based I/O implementation, or a blocking I/O implementation.

Connecting to a Server

To connect to a Minecraft server, you can write something like this:

let mut conn = CraftTokioConnection::connect_server_tokio("localhost:25565").await?;
conn.write_packet_async(Packet578::Handshake(HandshakeSpec { ... })).await?;
conn.set_state(State::Login);
...

This CraftTokioConnection struct is actually a type alias for the more general CraftConnection<R, W> type which wraps any R (reader) and W (writer) type supported by CraftReader and CraftWriter. More detail on these types below.

You can also connect using a blocking socket from std::net like this:

let mut conn = CraftTcpConnection::connect_server_std("localhost:25565")?;
conn.write_packet(Packet578::Handshake(HandshakeSpec { ... }))?;
conn.set_state(State::Login);
...

Serving Clients

You can use CraftConnection::from_std_with_state(your_client, PacketDirection::ServerBound, State::Handshaking) to wrap a blocking TcpStream, and you can use CraftConnection::from_async_with_state((client_read_half, client_write_half), PacketDirection::ServerBound, State::Handshaking) to wrap an async TcpStream. In the async case you must split your connection into reader/writer halves before passing it to the CraftConnection.

In all cases it is recommended to first wrap the reader in a buffering reader implementation of your choice. This is because this crate typically reads the packet length (first 5 bytes) as one call, then the entire packet body as another call. If you choose to not use a buffering implementation, these two calls could have an undesirable overhead, because both may actually require an operating system call.

Types

There are two structs which implement the behavior of this crate: CraftReader<R> and CraftWriter<W>.

They are defined to implement the CraftAsyncReader/CraftSyncReader and CraftAsyncWriter/CraftSyncWriter traits when wrapping R/W types which implement the craftio_rs::AsyncReadExact/std::io::Read and craftio_rs::AsyncWriteExact/std::io::Write traits respectively.

This crate provides implementations of craftio_rs::AsyncReadExact and craftio_rs::AsyncWriteExact for implementors of the tokio::io::AsyncRead/tokio::io::AsyncWrite and futures::AsyncRead/futures::AsyncWrite traits when you enable the tokio-io and futures-io features respectively.

Performance

A CraftReader<R> and CraftWriter<W> hold some buffers, both of which are lazily allocated Vec<u8>s:

  • raw_buf which is a buffer for packet bytes
  • compress_buf/decompress_buf. When compression is enabled (both as a crate-feature called compression and after a call to .set_compression_threshold with a Some(> 0) value) this buffer is used to store a compressed packet (in the case of a writer) or the decompressed packet (in the case of a reader).

These buffers can be eagerly allocated using calls to .ensure_buf_capacity(usize) and .ensure_compression_buf_capacity(usize), but they cannot yet be provided by the user.

Motivation

This library was designed when I was working on these three projects: a replacement for BungeeCord, a bot client that can join servers for me, and a tool to ping a list of servers quickly and print their status. This crate tries to avoid dynamic allocation, but does have some buffers to make serialization/deserialization fast. These allocations are done lazily by default, but can be done eagerly (described below) if desired.

When implementing something like a game server, or a proxy like BungeeCord, you are dealing with tens to hundreds of joins per second in the maximum case, so the dynamic allocation is not going to dramatically impact performance. Therefore, lazily allocation and growing of the buffers aren't going to impact your flame-graph.

However, in the case of trying to ping servers, I really wanted to ensure we only allocate once per connection half. To that end, you can eagerly allocate a large-enough buffer and also limit the max packet size to prevent it from growing any further (call .set_max_packet_size and .ensure_buf_capacity).

A great feature would be allowing the user to provide a &mut Vec<u8> which can be used by the wrapper types until the connection is closed. This way, in a many-worker model (like a ping tool), you can simply allocate a buffer for each worker, which you re-use for each subsequent connection. This does not exist yet.

Adapting to different I/O implementations

To add your favorite I/O library, you can either implement the std I/O traits (std::io::Read and std::io::Write) or for an async implementation you can implement the traits provided by this crate (AsyncReadExact and AsyncWriteExact).

Todo

  • Allow user to provide buffers which they already allocated for raw_buf
  • See if we can stop managing the Vec<u8> ourselves and just use BufReader traits that already exist?
  • Extract the offset tracking from CraftReader struct.

Dependencies

~1.7–4MB
~82K SLoC