More efficient way then cloning a vector in a loop move

Hello

Just for learning and experimenting purposes I am implementing my own basic HTTP server. I retrieve the incoming HTTP-stream, interpret it, and then have multiple predefined routes to give a HTTP response.

type route = (&'static str, &'static str, fn(&Routes) -> BoxFuture<Result<HttpResponse, String>>);

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    
    let server = TcpListener::bind("127.0.0.1:8080").await.expect("Failed to bind to 127.0.0.1:8080");

    // List with all the routes
    let routes: std::vec::Vec<route> = vec![
        ("GET", "/", |r| Routes::index(r).boxed())
    ];

    loop {
        let cloned_routes = routes.clone();
        let (mut stream, _) = server.accept().await.unwrap();
        tokio::spawn(async move {
            if let Err(e) = process(&mut stream, cloned_routes).await {
                eprintln!("Error: {}", e);
            }
        });
    }
}

// Function to process stream and defer to matching route.
// E.G:
// GET / HTTP/1.1 will go to Route::index
async fn process(stream: &mut TcpStream, routes: std::vec::Vec<route>) -> Result<(), Box<dyn Error>> {
    let mut buffer = [0; 1024];

    stream.read(&mut buffer).await;
    
    let stream_string = String::from_utf8_lossy(&buffer[..]);

    let http = Http::from_str(&stream_string);
    

    // HERE I WILL HANDLE THE ROUTES
    // ...

    Ok(())
}

struct Http {
    method: String,
    path: String,
    host: String,
    cookies: std::collections::HashMap<String, String>,
}

impl Http {
    fn from_str(str: &str) -> Self {
        println!("{}", str);
        let lines : std::vec::Vec<&str> = str.lines().collect();
        let request_line : std::vec::Vec<&str> = lines[0].split_whitespace().collect();
        
        let method = request_line[0];
        let path = request_line[1];
        let host = lines[1].split_whitespace().collect::<std::vec::Vec<&str>>()[1];
        
        Http {
            method: method.into(),
            path: path.into(),
            host: host.into(),
            cookies: std::collections::HashMap::new(),
        }
    }
}

struct HttpResponse {
    status: u16,
    content_type: String,
    body: String,
}

impl std::fmt::Display for HttpResponse {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f, 
            "HTTP/1.1 {status}\nContent-Type: {content_type}\n\n{body}",
            status = self.status,
            content_type = self.content_type,
            body = self.body
        )
    }
}


struct Routes;
impl Routes {
    async fn index(&self) -> Result<HttpResponse, String> {
        Ok(HttpResponse {
            status: 200,
            content_type: "text/plain".into(),
            body: "Welcome".into(),
        })
    }
}

This code works. But I don't think it is efficient. I define the path and the routes like this:

// List with all the routes
    let routes: std::vec::Vec<route> = vec![
        ("GET", "/", |r| Routes::index(r).boxed())
    ];

My goal is to a) define the vector of routes only once and b) make it look as clean as possible for the programmer |r| Routes::index(r).boxed()) looks a bit messy IMHO.

What currently worries me is this piece of code:

 loop {
        let cloned_routes = routes.clone();
        let (mut stream, _) = server.accept().await.unwrap();
        tokio::spawn(async move {
            if let Err(e) = process(&mut stream, cloned_routes).await {
                eprintln!("Error: {}", e);
            }
        });
    }

I need to clone routes because it otherwise can't be used in the loop because of move-issues. But isn't it really inefficient to clone the vector of routes each time?

What would be a better way? Using lazy_static?

It's really easy to share immutable data between threads/tasks by wrapping it in Arc. You clone the Arc each time you move it into another task, which just increments a reference count.

2 Likes

You can just use Arc<Vec<route>> for this case of a shared, immutable vector… you might even use Arc<[route]>. I’ve just posted this video in another thread, so I’ll put it here, too :slight_smile: feel free to have a look.

(the channel has a few more interesting short Rust-related videos 1 2 3 4 5)

2 Likes

Alright! Thanks!

So to be sure: This is the way to do it? So Arc somehow manages the memory different then a normal clone without Arc?

    let routes: Arc<std::vec::Vec<route>> = Arc::new(vec![
        ("GET", "/", |r| Routes::index(r).boxed())
    ]);

    loop {
        let cloned_routes = routes.clone();
        let (mut stream, _) = server.accept().await.unwrap();
        tokio::spawn(async move {
            if let Err(e) = process(&mut stream, &cloned_routes).await {
                eprintln!("Error: {}", e);
            }
        });
    }

Yes, it uses reference counting so the data it wraps is not copied when it is shared. Calling clone increments the reference count as I mentioned.

1 Like