2021-06-11

Learning Rust by building a server client chat

Overview

We will do this once or twice to test it out first before we commit to this.

Note: Most code snippets here are written as if they belong to a context or inside a scope / function.

This an exercise in learning Rust together on stream. We will do this for one hour on [date tbc], and announce this on the Discord server and in chat one hour in advance.

Goal: Multi threaded server / client chat

Prerequisites

Format

  1. Go over some functionality from the list.
  2. Implement functionality for the project
  3. Answer questions in the process.

Goal

To have a basic understanding of Rust and to be able to create something more than just hello world.

Topics to cover

  1. Variable bindings
  2. Mutability / references / &ref / *deref
  3. Functions and closures
  4. Importing modules
  5. Vectors, Arrays and Slices
  6. Strings
  7. Match
  8. Control flow and loops
  9. Structs and tuples
  10. Modules
  11. Drop
  12. Enums, Result and Option
  13. Traits
  14. Threads, Mutex and Arc
  15. Channels
  16. Error handling and type alias
  17. IO
  18. Iterators
  19. Using the api docs
  20. Lifetimes

Plan

* Goal 1: cover 1 - 9       A server that can accept connections
* Goal 2: cover 10 - 15     Sending / receiving messages
* Goal 3: cover 16 - 17     Error handling and proper termination of clients
* Goal 4: cover 18 - ...

Project creation and running

cargo new helloworld
cargo run
cargo build
./target/debug/helloworld
cargo new --lib common

Variable bindings

Talk about:

let x = 1;
let mut y = 2;

References and mutability

Talk about:

let mut byte = 0u8;

change(&mut byte);

fn change(b: &mut u8) {
    *b = 1; // deref `b` and give it a new value
}

Control flow

Talk about:

let cond = true;

if cond {
    // body
}

loop {
}

while cond {
}

for i in 0..100 {
}

Match

let value = 1u8;

match value {
    0 => {}
    1 => {}
    5 => {}
    _ => { /* Wildcard */ }
}

let value = true;

let data = match value {
    true => 1,
    false => 2,
}; // semicolon is important

Functions and closures

A function

Talk about:

fn do_thing() {
}

A function with one or more arguments

fn do_thing(arg1: u32, arg2: u32) {
}

A function with a return value

fn do_thing() -> u32 {
    return 123;
}

fn do_thing_implicit_return() -> u32 {
    123
}

A closure

let cl = |arg1: u32| {
    println!("{}", arg1);
};

let string = "Hello closure".to_string();
cl(string);

Importing from std lib

Talk about:

Importing a single path

use std::net::TcpStream;

Importing multiple paths from the same module

use std::net::{TcpStream, TcpListener};

Importing a path under another name

use std::net::TcpStream as Stream;

Vectors, arrays and slices

Talk about:

Creating vectors

// Create a new vector
let mut a_vec: Vec<usize> = Vec::new();

// Create a new vector with a capacity of ten.
let mut a_vec: Vec<usize> = Vec::with_capacity(10);

// Create a new vector with a capacity of two and two elements
let mut a_vec: Vec<usize> = vec![1, 2];

// Create a new vector with a capacity of 200, and it's full of ones
let mut a_vec: Vec<usize> = vec![1; 200];

Creating an array

// An array with three bytes in it
let array_of_bytes = [0u8, 1, 2, 3];

// An array with 200 bytes in it, where every byte is zero
let array_of_bytes = [0u8; 200];

Creating a slice

let data = vec![1u8, 2, 3, 4];
// We specify the type here to prevent
let slice = &data[1..2]; // a slice containing 2, 3

Looping over all the values in a vector. If we didn't use a reference (&) we would consume the vector

let v = vec![1, 2];

for value in &v {
    println!("{}", value);
}

Pushing and popping. Popping returns an Option<T>

let mut v = vec![];

v.push(1); // v = vec![1]
v.push(2); // v = vec![1, 2]
v.pop();   // v = vec![1]
v.pop();   // v = vec![]

String and &str (string slice)

Talk about:

let string = String::new();
let string_slice: &str = &string;
let string_slice = string.as_str();
let string_slice = &string[..];

Structs

A struct is a way to organise data

Talk about:

Note: Structs can't contain fields that are of the same type as the struct, as the compiler has to know the size of the struct at compile time. This would create an infinitely sized struct.

struct Thing {
    field_1: u8,
    field_2: u8,
}

We can add methods to structs

impl Thing {
    fn do_thing(&self) {
        let sum = self.field_1 + self.field_2;
        println!("sum: {}", sum);
    }
}

Tuple struct

struct Pos(u32, u32);

let pos = Pos(12, 33);
let (x, y) = (pos.0, pos.1);
println!("x: {}, y: {}", x, y);

New type pattern

struct UserId(u32);

let user_id = UserId(123);
let val: u32 = user_id.0;

Destructure a struct

struct Values {
    a: u32,
    b: u32,
    c: u32,
    d: u32,
}

let values = Values { a: 1, b: 2, c: 3, d: 4 };

let Values { a, c, .. } = values;

println!("a: {}, c: {}", a, c);

Enum

Talk about:

enum State {
    Stopped,
    Running(String),
    Paused { duration: u32, count: u32 }
    SubState(State)
}

Enums combined with match makes for happy times

enum Food {
    Hamburger,
    AvocadoToast,
}

let food = Food::Hamburger;
let mut burger_counter = 0;

match food {
    Food::Hamburger => {
        printlin!("I love hamburgers");
        burger_counter += 1;
    },
    Food::AvocadoToast => println!("Avocado toast sounds like fun"),
}

Traits

Talk about:

trait Describe {
    fn describe(&self) -> String;
}

Channels

Talk about:

use std::sync::mpsc;
use std::thread;

let (sender, receiver) = mpsc::channel();

thread::spawn(move || {
    loop {
        if let Ok(val) = receiver.recv() {
            println!("Received: {}", val);
        }
    }
});

let mut counter = 0usize;

loop {
    sender.send(counter);
    counter += 1;
    thread::sleep_ms(1000);
}

Threads

Talk about:

Creating a thread

use std::thread;

thread::spawn(|| {
    printline!("Hello from another thread");
});

Arc and Mutex combo: modify a string in two different threads.

use std::sync::{Mutex, Arc};
use std::thread;

let s = String::new();
let s = Arc::new(Mutex::new(s));

// Thread 1
let s_clone = Arc::clone(&s);
thread::spawn(move ||  s_clone.lock().map(|mut s| s.push_str("A")));

// Thread 2
let s_clone = Arc::clone(&s);
thread::spawn(move ||  s_clone.lock().map(|mut s| s.push_str("B")));

IO

Talk about:

Reading bytes from a file. A similar approach can be used to read bytes from a network socket (where reading zero bytes most likely means connection closed)

use std::io::Read;
use std::fs::File;

let mut buf = [0u8; 1024];
let file = File::open("/tmp/somefile.txt").expect("Failed to open the file");
let bytes_read = file.read(&mut buf).unwrap();

let file_content: &[u8] = &buf[..bytes_read];

Read an entire file as a string

use std::fs::read_to_string;

let file_content = read_to_string("/tmp/somefile.txt").unwrap();

Iterators

Talk about:

Iterate and produce a new Vec<T> that has all the values squared

let mut v = vec![1, 2, 3];
let new_vec = v
    .iter()
    .map(|val| val * val)
    .collect::<Vec<usize>>();

Modify all elements in a Vec<T>

let mut v = vec![1, 2, 3];
v.iter_mut().for_each(|val| val *= val);

Combining adaptors

let v = vec![1, 2, 3, 4];

let new_values = v
    .iter()
    .enumerate()
    .filter(|number| number > 2)
    .map(|number| number += 10)
    .collect::<Vec<_>>();

Flatten an iterator

let v = vec![
    vec![1, 2],
    vec![3, 4],
];

for val in v.into_iter().flatten() {
    eprintln!("{:?}", val);
}

Modules

Talk about:

// main.rs
mod parent {
    mod child {
    }
}

parent::child

File system layout for a module named "submod", which has its own sub module named "misc"

project
  \_ submod \
     \_ mod.rs
     \_ misc.rs
  \_ main.rs

Drop

Talk about:

struct A;

impl Drop for A {
    fn drop(&mut self) {
        println!("A was dropped");
    }
}

Error handling

Talk about:

Implementing your own error type that works with the ? operator

use std::io::Error as IoErr;

type Result<T> = std::result::Result<T, OurErr>;

pub enum OurErr {
    Io(IoErr),
    Misc(String),
}

impl From<IoErr> for OurErr {
    fn from(e: IoErr) -> Self {
        Self::Io(e)
    }
}

Using api docs

Talk about:

Open docs in default browser:

cargo doc --open

Local version of std lib api docs and "The Book":

Open docs

rustup doc

Go directly to std lib api

rustup doc --std

Lifetimes

Talk about:

struct Application<'a> {
    name: &'a str
}

struct Config<'a> {
    name: &'a str,
}

let app_name = "Blorp".to_string();

let app = Application { name: &app_name };
let config = Config { name: &app_name };