From a3927e752c55dbc70d135ff97cf809eb356d60a4 Mon Sep 17 00:00:00 2001 From: Joey Sacchini Date: Sat, 9 Jan 2021 14:36:46 -0500 Subject: add a readme and cleanup some of the wrapper methods in CraftConnection --- README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/reader.rs | 31 --------------- src/tcp.rs | 78 +++++++++++++++++++++++++++---------- 3 files changed, 178 insertions(+), 52 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d1c280 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# craftio-rs + +Version 0.1.0, by [Twister915](https://github.com/Twister915)! + +craftio-rs is a library which let's you read & write packets defined in [mcproto-rs](https://github.com/Twister915/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](https://github.com/Twister915/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](https://crates.io/crates/flate2) crate) +* `encryption` (using the [aes](https://crates.io/crates/aes) crate) with a fast implementation of CFB-8 +* `futures-io` enables reading/writing to implementors of the `AsyncRead`/`AsyncWrite` traits from the + [futures](https://crates.io/crates/futures) crate +* `tokio-io` enables reading/writing to implementors of the `AsyncRead`/`AsyncWrite` traits from the + [tokio](https://crates.io/crates/tokio) crate + +# Usage + +```toml +[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: + +```rust +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` 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: + +```rust +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` and `CraftWriter`. + +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` and `CraftWriter` hold some buffers, both of which are lazily allocated `Vec`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` 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` ourselves and just use `BufReader` traits that already exist? +* Extract the offset tracking from `CraftReader` struct. \ No newline at end of file diff --git a/src/reader.rs b/src/reader.rs index b3bb2dd..1439a17 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -400,37 +400,6 @@ where } } -#[cfg(any(feature = "futures-io", feature = "tokio-io"))] -pub trait IntoBufferedAsyncRead { - type Target: AsyncReadExact; - - fn into_buffered(self, capacity: usize) -> Self::Target; -} - -#[cfg(all(feature = "futures-io", not(feature = "tokio-io")))] -impl IntoBufferedAsyncRead for R -where - R: futures::io::AsyncRead + Send + Sync + Unpin, -{ - type Target = futures::io::BufReader; - - fn into_buffered(self, capacity: usize) -> Self::Target { - futures::io::BufReader::with_capacity(capacity, self) - } -} - -#[cfg(feature = "tokio-io")] -impl IntoBufferedAsyncRead for R -where - R: tokio::io::AsyncRead + Send + Sync + Unpin, -{ - type Target = tokio::io::BufReader; - - fn into_buffered(self, capacity: usize) -> Self::Target { - tokio::io::BufReader::with_capacity(capacity, self) - } -} - #[cfg(any(feature = "futures-io", feature = "tokio-io"))] #[async_trait] pub trait AsyncReadExact: Unpin + Sync + Send { diff --git a/src/tcp.rs b/src/tcp.rs index bd88885..f16a3c3 100644 --- a/src/tcp.rs +++ b/src/tcp.rs @@ -6,14 +6,30 @@ use std::io::BufReader as StdBufReader; use std::net::TcpStream; #[cfg(any(feature = "futures-io", feature = "tokio-io"))] -use crate::{CraftAsyncReader, CraftAsyncWriter, IntoBufferedAsyncRead}; +use crate::{CraftAsyncReader, CraftAsyncWriter}; + +#[cfg(feature = "tokio-io")] +use tokio::{ + net::{ + TcpStream as TokioTcpStream, + tcp::{ + OwnedReadHalf as TokioReadHalf, + OwnedWriteHalf as TokioWriteHalf, + }, + ToSocketAddrs as TokioToSocketAddrs, + }, + io::{ + BufReader as TokioBufReader, + Error as TokioIoError, + }, +}; pub const BUF_SIZE: usize = 8192; pub type CraftTcpConnection = CraftConnection, TcpStream>; -impl CraftConnection, TcpStream> { - pub fn connect_server_std(to: String) -> Result { +impl CraftTcpConnection { + pub fn connect_server_std(to: A) -> Result where A: std::net::ToSocketAddrs { Self::from_std(TcpStream::connect(to)?, PacketDirection::ClientBound) } @@ -43,32 +59,52 @@ impl CraftConnection, TcpStream> { } } -#[cfg(any(feature = "futures-io", feature = "tokio-io"))] -impl CraftConnection -where - CraftReader: CraftAsyncReader, - CraftWriter: CraftAsyncWriter, -{ - pub fn from_unbuffered_async(tuple: (U, W), read_direction: PacketDirection) -> Self +#[cfg(feature = "tokio-io")] +pub type CraftTokioConnection = CraftConnection, TokioWriteHalf>; + +#[cfg(feature = "tokio-io")] +impl CraftTokioConnection { + pub async fn connect_server_tokio( + to: A + ) -> Result where - U: IntoBufferedAsyncRead, + A: TokioToSocketAddrs { - Self::from_unbuffered_async_with_state(tuple, read_direction, State::Handshaking) + let conn = TokioTcpStream::connect(to).await?; + conn.set_nodelay(true)?; + let (reader, writer) = conn.into_split(); + let reader = TokioBufReader::with_capacity(BUF_SIZE, reader); + Ok(Self::from_async((reader, writer), PacketDirection::ClientBound)) } +} - pub fn from_unbuffered_async_with_state( - tuple: (U, W), - read_direction: PacketDirection, - state: State, - ) -> Self +#[cfg(feature = "tokio-io")] +pub type CraftUnbufferedTokioConnection = CraftConnection; + +#[cfg(feature = "tokio-io")] +impl CraftUnbufferedTokioConnection { + pub async fn connect_server_tokio_unbuffered( + to: A + ) -> Result where - U: IntoBufferedAsyncRead, + A: TokioToSocketAddrs { - let (ru, writer) = tuple; - let reader = ru.into_buffered(BUF_SIZE); - Self::from_async_with_state((reader, writer), read_direction, state) + let conn = TokioTcpStream::connect(to).await?; + conn.set_nodelay(true)?; + + Ok(Self::from_async( + conn.into_split(), + PacketDirection::ClientBound, + )) } +} +#[cfg(any(feature = "futures-io", feature = "tokio-io"))] +impl CraftConnection +where + CraftReader: CraftAsyncReader, + CraftWriter: CraftAsyncWriter, +{ pub fn from_async(tuple: (R, W), read_direction: PacketDirection) -> Self { Self::from_async_with_state(tuple, read_direction, State::Handshaking) } -- cgit