Masai Mahapa

30 days of Rust - Day Twenty Two - Multi Threaded Web Server

30 days of rust - day Twenty Two Yesterday, I built a web server for TravCar. Check out the post here. The problem with that server is it can only handle one request at a time. If there is a request that takes a long time to process, other requests need to wait for it to first be completed. Our users would not love that.

Day 21 - Multi Threaded Web Server

In order to understand the importance of having a multi-threaded server, I shall create a simulation of a request that takes a while to process. Using the sleep method which just makes the thread wait for a set amount of time. The request will be on the /sleep url.

// other code explained previously
let sleep = b"GET /sleep HTTP/1.1\r\n";
 
   let (status_line, filename) = if buffer.starts_with(get){
       ("HTTP/1.1 200 OK", "travcar/landing-page.html")
   } else if buffer.starts_with(sleep){
       thread::sleep(Duration::from_secs(10));
       ("HTTP/1.1 200 OK", "travcar/landing-page.html")
   }
// other code

The sleep is set for 10 seconds. So basically a user will wait 10 seconds in order to get a response. If we had 6 requests, the last one would have to wait for a minute at most (6*10 seconds). So in order for these requests to happen at the same time we shall use multi-threading as we have gone through on day eighteen.

Go to your browser and open 3 tabs at the following url; http://127.0.0.1:7878/sleep. The first one will take 10 seconds to show, the second one will take 20 seconds at most and the last one will take 30 seconds at most.

Not good.

Let's improve our server

I will use a quick fix for this. Ideally we would create a thread pool to solve this. Which is a finite number of threads that can be dispatched to work on tasks. Each thread picks a task, completes it and returns to the pool.

For our use case, I shall just create new threads without a pool. The disadvantage is that someone can just make a million requests causing a Denial Of Service Attack, making our server unable to do other tasks.

fn main() {
   let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
 
   for stream in listener.incoming() {
       let stream = stream.unwrap();
 
       thread::spawn(|| {
           handle_connection(stream);
       });
   }
}

The above code basically spawns a new thread for every incoming request with the thread::spawn method, passing it a closure with no arguments and calling the stream from inside.

Now we can make many requests to sleep and other requests such as / will get completed almost instantaneously without having to wait for the other tasks queued in the main thread to finish running.

Conclusion

This was a nice project to get a feel of how concurrent programming might come in handy. The server is obviously not good for production environments but is a good learning curve for anyone learning Rust. I saw crates like Rocket which might be better suited for creating web apps than building it manually like we just did.

Please let me know what you think of this series and what you are currently trying to learn now.

Thanks for spending some time learning Rust with me. Until next time. Peace ✌🏼 .

Share