Skip to content

Instantly share code, notes, and snippets.

@alistairjevans
Last active Feb 2, 2022
Embed
What would you like to do?
Rust TCP Echo Server, for C# Devs
///
/// # TCP Echo Server in Rust, for C# Devs.
///
/// I normally write .NET code, but am picking up some Rust
/// and wanted to explain it to other devs in my org.
/// So I've taken a very basic TCP echo server example
/// (sends back everything it receives) written in Rust, and added
/// a load of comments that hopefully explain each line in a way
/// C# devs would understand.
///
// 'use' is more like imports in typescript than using statements
// in C#, because often you will pull in specific types into scope,
// rather than a whole rust `module` (similar to a `namespace` in C#).
//
// Here we are bringing the TcpListener type into scope from the 'tokio::net' module.
// tokio provides (among other things) some good async wrappers around OS sockets, and is available
// as a 'crate'. Crates are somewhat like nuget packages in C# world, but are distributed as 'source'
// packages, rather than pre-built. You can browse https://crates.io for available crates.
use tokio::net::TcpListener;
// The below 'use' pulls in two rust 'Traits', which are similar to interfaces in C#.
// By pulling in these Traits, we get access to some async helpers for streams.
//
// For these Traits specifically, it's *sort of* like having C# extension methods
// that read like below, then having to `using` the extension class' namespace
// to access them.
//
// ```
// public static class AsyncWriteExt
// {
// public static ValueTask<int> ReadAsync(this IAsyncWriteExt stream, Span<byte> buffer)
// {
// // ...
// }
// }
// ```
//
// > This is a big oversimplification of what Traits are in Rust, they work quite differently to interfaces
// > in a number of ways.
use tokio::io::{AsyncWriteExt, AsyncReadExt};
// The #[....] syntax is an attribute.
// This attribute says we are using tokio as our 'async runtime'.
//
// Unlike C#/.NET, which includes a built-in async runtime, the Rust language itself
// provides the async/await keywords and the equivalent of C#'s `Task`, but nothing
// to actually run those tasks. That functionality is provided by external packages
// (called crates). The one we're using is tokio, which also provides async sockets.
//
// The 'Result' type is a generic built-in type, that can either succeed,
// in this case with the empty type, referred to as a 'unit', `()`, or fail with
// the `Box<dyn std::error::Error>>` type.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// let is equivalent to var in C#, but defaults to immutable, so I can't change this variable after I declare it.
let bind_addr = "0.0.0.0:9999";
// Bind the socket, and 'await' the bind completing.
// `.await` in rust is a keyword, very much like C#, just the await comes
// after the expression, rather than before it.
//
// The question-mark (?) at the end of the line means:
//
// "if bind failed, return immediately, propagating the error to the caller."
//
// This may sound like exceptions in C#, but it's best not to think of them
// like that (and it doesn't have the same stack-unwinding and CPU overhead),
// because it just returns from the method.
let listener = TcpListener::bind(bind_addr).await?;
// println in rust is a 'macro', hence the '!'.
// The {} is a format placeholder, this will feel familiar to String.Format in C#.
// Unlike C#, the numeric values in the placeholder indicating parameter position are optional.
println!("Listening on {}", bind_addr);
loop {
// TCP socket accept (async). Again, if the accept errors, propagate the
// result out.
// The accept method returns a 'tuple' of socket stream and source address.
let (mut stream, source_addr) = listener.accept().await?;
// This is equivalent to `Task.Run`, but with some rust magic.
// The `async move` here says "any variables I reference in the
// provided closure/lambda, now belong to that closure, so no-one else can use them."
tokio::spawn(async move {
// Note here I have 'captured' the source_addr variable, so this closure now owns it
// because of `async move`.
println!("Accepted connection from {}:{}", source_addr.ip(), source_addr.port());
// Declare a Vector of 1024 bytes (u8 means unsigned 8-bit, equivalent to C#'s `byte`), pre-filled with 0,
// using the short-hand macro `vec!`.
// Vector<u8> is sort of like a List<byte> in C#, stored on the heap.
// Normal 'arrays' are typically declared on the stack, and are fixed length, so less suitable for buffers.
// Note that I'm declaring the 'buffer' as 'mut', because otherwise I can't
// change any values. Rust variables are immutable by default.
let mut buffer: Vec<u8> = vec![0; 1024];
loop {
// Ok, so read some bytes into our buffer, with the `read` method on the `stream` variable.
// I have to put `&mut` here when passing the buffer, to be clear that
// I'm letting the `read` method temporarily 'borrow' a reference
// to my buffer that allows the `read` method to modify the contents
// of the buffer.
let read_result = stream.read(&mut buffer).await;
// `read_result` here is of type `Result<usize, Error>`.
// Result is a commonly used `enum` that can be either `Ok`, or `Err`,
// Enums in Rust are more powerful than C#, because they
// can hold values, and are properly type-safe (unlike C# enums which can technically hold any numerical value).
//
// There are powerful ways to handle the 'variants' of each enum.
// Here I'm handling the read result with an `if let`.
// The below line is somewhat similar to `if (read_result is Ok okResult)` in C#, but with the added benefit
// of extracting a value (`num_bytes`) from the variant at the same time.
if let Ok(num_bytes) = read_result {
// 0 bytes means the socket is closed. Convention is not to have outer parentheses on the if statement.
if num_bytes == 0
{
println!("Connection from {} closed", source_addr);
break;
}
// Here I'm getting an immutable 'slice' of my buffer, from 0 to the number of bytes I
// read. This is quite similar to getting a `ReadOnlySpan<byte>` from a byte array in C# using ranges.
let write_slice = &buffer[0..num_bytes];
// This is another way of handling results; the `match` statement.
// It's a little bit like `switch expressions` in C#, but failing to handle all states is an error in rust,
// not a warning.
// We are required to handle all cases, or use the 'discard' case.
match stream.write_all(write_slice).await {
// Handle the error case...
Err(e) => {
eprintln!("failed to write {} bytes to {} - {}", num_bytes, source_addr, e);
break;
},
// The `_` means anything else, just like the discard pattern in C# switch expressions.
// The `()` is a `unit` in rust, which basically means return 'nothing'.
_ => ()
};
}
else
{
// Print to stderr.
eprintln!("Failed to read from socket for {}", source_addr);
// This break will break out of the loop we're currently in.
break;
}
}
});
// A final note on Rust's ownership rules.
// The stream write line below would cause a compilation error. Why?
//
// Because I 'moved' `stream` into my task with `async move`, so
// this scope (outside the background closure) no longer has access to it.
//
// This prevents memory race conditions, because, for the most part, only one thing
// can 'own' a single allocated variable.
//
// stream.write_u8(0xff);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment