Masai Mahapa

30 days of Rust - Day Twenty One - Single Thread Web Server

30 days of rust - day Twenty One As a web developer, I wanted to know if it's possible to build a web server using Rust. Can I have Travcar.co.za run on Rust, allowing us to serve our users with 10 times the speed?πŸš€ Well there is only one way to find out. Lets build it.

Day 21 - Single Thread Web Server

In order for us to have a web server, we need to understand the two protocols used by web servers. The first is http which stands for Hypertext Transfer Protocol. You see this when you enter a URL such as http://www.travcar.co.za. The next is TCP which stands for Transmission Control Protocol.

So both these protocols need to first be initiated by a request from a client (e.g your cell phone browser) and only then does a server give a response.

Listening to the TCP Connection

Rust provides a way for us to listen for TCP connections. This functionality is in the standard library.

use std::net::TcpListener;
 
fn main() {
   let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
 
   for stream in listener.incoming() {
       let stream = stream.unwrap();
 
       println!("Connection established!");
   }
}

The code above creates a listener on the IP address 127.0.0.1 also known as localhost. This is the developer's computer which can be your or mine. Following the colon is 7878 which is the port at which the site is accessed.

If we run the program and open the browser at 127.0.0.1:7878, we should see the following output in our command line.

Running `target/debug/travcar`
Connection established!
Connection established!
Connection established!

The browser has successfully found our application. It's just that there is no response thus the browser page is blank.

Reading the Request

So now that our browser is sending a request successfully to our server, we need to read what information is inside the request. Let's just print it out;

use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
 
fn main() {
   let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
 
   for stream in listener.incoming() {
       let stream = stream.unwrap();
 
       handle_connection(stream);
   }
}
 
fn handle_connection(mut stream: TcpStream) {
   let mut buffer = [0; 1024];
 
   stream.read(&mut buffer).unwrap();
 
   println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
}
 

Restating the app and reloading our browser, we see the following info inside our command line. Note that this will probably be slightly different because your computer and browser might be different from mine.

Request : GET / HTTP/1.1
Host: localhost:7878
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: ".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: _ga=GA1.1.1690147691.1655823506; _ga_ZXXBWHV6QK=GS1.1.1655825992.2.1.1655826743.0; _ga_C31H63Q6JG=GS1.1.1657617281.37.0.1657617281.0

The only line we shall use here is the first one which is GET / HTTP/1.1.

  • This tells the server that it is a GET request, which is asking to retrieve information from the server. Check out other request methods we could use here.
  • The second part / basically means the base url. It's the location of the resource being requested. If we try to access another page like make-a-request, then this would be /make-a-request.
  • The third part HTTP/1.1 is the protocol we are using.

Let's respond to the request

Let's create an HTML page that shall be sent back when the home page is requested. This the page our users will see when they visit TravcarπŸš—. Let's save it inside a travcar folder in the base directory, not inside src. I'll name it landing-page.html.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Travcar</title>
</head>
<body>
   <h1>Welcome to Travcar</h1>
   <p>The future of long distance Ride share πŸš—.</p>
</body>
</html>

To return the landing page above together with the request;

use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
 
fn main() {
   let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
 
   for stream in listener.incoming() {
       let stream = stream.unwrap();
 
       handle_connection(stream);
   }
}
 
// --snip--
 
fn handle_connection(mut stream: TcpStream) {
   let mut buffer = [0; 1024];
   stream.read(&mut buffer).unwrap();
 
   let get = b"GET / HTTP/1.1\r\n";
 
   if buffer.starts_with(get) {
       let status_line = "HTTP/1.1 200 OK";
       let contents = fs::read_to_string("travcar/landing-page.html").unwrap();
 
       let response = format!(
           "{}\r\nContent-Length: {}\r\n\r\n{}",
           status_line,
           contents.len(),
           contents
       );
 
       stream.write(response.as_bytes()).unwrap();
       stream.flush().unwrap();
   } else {
       // This would be other requests such
       // Other pages, Other request types such as POST
   }
}

If we restart our application and head back to our browser, we should see the following output. travcar rust server

Conclusion

We've successfully created a web server for Travcar using Rust. I am so excited to actually compare how it would perform in comparison to node if we were to build some of our services with Rust.

This was just a simple application obviously, just for the sake of learning about how to read TCP requests as well as respond to them. So stick to node and Django until further notice.

Overall, this was a fun and insightful project for me and I hope you find it cool too. Next up, I need to figure out how to have multiple threads so we can handle many requests from our millions of users 😎. So let's continue building this tomorrow.

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

Share