7.5 KiB
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
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
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];
}