triston-notes/Cards/dev/Rust http server.md
2023-10-21 18:52:54 -05:00

7.5 KiB

banner
https://images.unsplash.com/photo-1451187580459-43490279c0fa?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2372&q=80

up:: Rust X::Boilerplate Code tags:: #boilerplate

Rust HTTP Server

http/1.1

  • L7 Protocol
  • Sent Over TCP
  • Message based (client REQUEST / server RESPONSE)

REQUEST GET /search?name=abc HTTP/1.1 Accept: text/html RESPONSE HTTP/1.1 200 OK Content-Type: text/html <html></html>


Architecture

http_server.excalidraw


Implimentation

in the main function we want to create a server and run it

fn main() {
	let server = Server::new("localhost:8080");
	server.run();
}

the only problem is, we dont yet have a Server struct, so lets create one

struct Server{
	addr: String
}

we add addr property - but now we need to add some functionality

impl Server {
	fn new(addr: String) -> Server {
		Server { // this is implicit return Server
			addr: addr // or just put addr since names are same
		}
	}
}

// can also use word Self for Server
fn new(addr: String) -> Self{
	Self { }
}
// does the same thing

cool, now we have our constructor function - but now we need to impliment the run method

impl Server {
	fn run(self) { // lower case self is just this instance
		println!("Server listening on: {}", &self.addr[10..]); // &self is because we just want to borrow the intance
		loop {
			println!("Looping");
			std::thread::sleep(std::time::Duration::from_secs(1));
			// we want to create an infinite loop and sleep the iterations so as to not hammer the cpu
		}
	}
}

So now we need to create the Request concept. We will use another struct for this

struct Request {
	path: String,
	query_string: String,
	method: Method // <-- whats this? 
}

// method is enum, which looks like this
enum Method {
GET, PUT, POST, DELETE
}

But how do we use the ENUM?

let get = Method::GET;

Cool, but what if we dont have a query_string passed in the request? Theres a solution for that:

query_string = Option<String>;
// Option will make it possible to have a NONE value because its an optional generic
// This is what option looks like
pub enum Option<T> {
	None,
	Some(T),
}
// this is great for typing a none wihout the fear of Null pointer exceptions

Modules

Our code is now getting pretty long, so we will split our code into 'modules'. Modules control the visibility of code. Modules can be public or private. We can create an inline module with the mod keyword Modules look similar to Namespaces in other languages

mod server {
	stuct Server {...}
	impl Server {...}
}

But now, server is a private module, therefore the Server struct isn't usable from the outside i.e. our Main function.. We can fix that by making the parts we want public to be public by adding the pub keyword to them, like so:

mod server {
	pub stuct Server {...}
	impl Server {
		pub fn new() {...}
		pub fn run() {...}
		fn destroy() {...}
	}
}

Keep in mind, if you have a module within another module, and need to access it publically, you can prepend pub to that submodule as well

Modules act just like files as far as imports go. You can use the use keyword to import other modules into a module even within the same file


Break up into seperate files

create a new server.rs file and copy the contents of your server module into the server.rs file WAIT! So we dont need to copy the mod stuff? NOPE - every file is treated like a module WOOT WOOT easy peezy!

when using a module in a file from a module thats in another file, you still need to define the module in the file like so:

user server::Server;

mod server;
Folder modules

Folders can be used at modules for nesting sub modules. But if you do it that way, you must include a mod.rs file so the compiler knows this is a moudle folder. Similar with the __init__.py file in python modules. Then you expose the interfaces you want exposed in the mod.rs file like so:

// mod.rs
pub mod request;
pub mod method;

// but this still requires you to import them like this:
use http::method::Method;
// we want to just do http::Method
// to do that, use the modules as well
// 👇🏻👇🏻👇🏻👇🏻👇🏻
pub use request::Request;
pub use method::Method;
pub mod request;
pub mod method;

TCP Communication

NET Module

use std::net::{TcpListener, TcpStream};
let listener = TcpListener::bind(&self.addr).unwrap(); // unwrap will terminate application if errors

We want to do something on the new listener, so lets create an infinite loop to continuosly listen for events

Note: accept() returns a result.. But we dont want to kill the program if it errors, so we handle that

let listener = ...
loop{
	let res = listener.accept();
	if res.is_err(){
		continue;
	}
	let stream = res.unwrap(); // we can now safely unwrap since we error checked already
}

stream is actually a Tuple, so lets destructure that:

let (stream, addr) = res.unwrap();

A Better way to handle results in rust is to use the match keyword:

loop{
	match listener.accept(){
		Ok((stream, _addr)) => stream,
		Err(e) => println!('error: {}', e)
	}
}

Result Enum

This is the shape of the generic Result Enum - The result enum is auto included into every rust file.

pub enum Result<T, E> {
	Ok(T),
	Err(E),
}

Loop

In rust, to create a generic infinite loop, you can just use the loop keyword, which is equivalent to a while (true).

loop{
	// do something over and over
}

We can also label the loops, in case, say, you have a nested loop:

'outer: loop{
	'inner: loop{
		break 'outer;
		// continue 'outer;
	}
}

Tuple

Similar like tuples in python - except they have a fixed length, cannot grow or shrink in size

let tup = ('a', 5, listener) // has fixed length 3

Match

Match is essentially a switch statement:

match 'abcd'{
	'abcd' => println!('abcd'),
	'abc' => {},
	_ => {} // default 
}

Array

Arrays are also, like tuples, fixed in size and types

let arr: [u8; 3] = [1,2,3]

If you want to use an array as parameters, you should always pass a reference to an array: a slice:

fn arr(a: &[u8]) {} // now we dont need fixed length

// with fixed length
fn arr(a: [u8; 5]) {}

Type Conversion

STD Convert Rust standard library has a builtin type conversion library

use std::convert;

Options unwrapping

There are different ways to do the same thing in rust. For example, if you get an Option returned from a function, you can do a match on it, you can call methods on it, or some other weird thing. Take a look:

// this is a match, for explicitly checking the some and none variants of the option return type
match path.find('?') {
	Some(i) => {},
	None => {}
}

// Since option has a method called is_some, we can check if this option has None'd out or not, and if not, do some stuff
let q = path.find('?');
if q.is_some(){
	query_string = Some(&path[i+1..]); 
	path = &path[..i];
}

// This is wierder syntax here, we are conditionally setting a variable if there is SOME-thing, otherwise this condition will None out and not execute
if let Some(i) = path.find('?'){
	query_string = Some(&path[i+1..]); 
	path = &path[..i];
}