Rust TCP Echo Server, for C# Devs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// | |
/// # 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