DEV Community

Cover image for πŸš€ Day 32: Crafting a Single-Threaded Web Server in Rust! πŸ•Έ πŸ’»βœ¨
Aniket Botre
Aniket Botre

Posted on

πŸš€ Day 32: Crafting a Single-Threaded Web Server in Rust! πŸ•Έ πŸ’»βœ¨

Greetings, fellow Rustaceans! Today, I embarked on a journey to channel my inner web developer spirit by crafting a single-threaded web server using Rust. Because let's face it, what's more exciting than building a web server from scratch? So, grab your favorite beverage and let's dive into the delightful world of Rust web development! β˜•πŸ¦€

Web development in Rust? Absolutely! As someone with a fervent love for web development, I've decided to explore the untamed lands of Rust and see how it handles the mighty task of serving web pages. This is just the beginning; we're laying the foundation for future explorations into Rust web frameworks and more intricate projects. So, fasten your seatbelts, and let's roll with our single-threaded web server!


🏁🎬Setting the Stage🎬🏁

Before we dive into the code, let's set the stage. We're going to build a simple, single-threaded web server. It's like the "Hello, World!" of web servers - simple, but a great way to get your feet wet.πŸ˜‰

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};
Enter fullscreen mode Exit fullscreen mode

Here, we're just importing the necessary modules. fs for file handling, BufReader for buffered reading of our TcpStream, and TcpListener and TcpStream for network programming. It's like gathering our ingredients before we start cooking. 🍳


πŸƒβ€β™‚οΈπŸš¦Starting the ServerπŸš¦πŸƒβ€β™‚οΈ

fn main() {
    if let Err(err) = run_server() {
        eprintln!("Error: {}", err);
    }
}
Enter fullscreen mode Exit fullscreen mode

main function is the entry point of our server, in our main function, we're calling the run_server function. We're using the if let construct to handle any potential errors. If run_server encounters an error, it handles it by printing them to the standard error output.

fn run_server() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    println!("Server running on http://127.0.0.1:7878/");

    for stream in listener.incoming() {
        let stream = stream?;
        handle_connection(stream)?;
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

In run_server, we create a TcpListener that will listen for incoming TCP connections at the specified address and port (127.0.0.1:7878). This is like setting up a shop and putting out an open sign. Here, we're looping over the listener.incoming(), which creates an iterator over the incoming TcpStream. For each incoming stream (connection), we call handle_connection.


πŸ€πŸ”ŒHandling ConnectionsπŸ”ŒπŸ€

fn handle_connection(mut stream: TcpStream) -> std::io::Result<()> {
    let buf_reader = BufReader::new(&mut stream);
    let request_line = match buf_reader.lines().next() {
        Some(line) => line?,
        None => return Ok(()), // Ignore empty requests
    };
...

Enter fullscreen mode Exit fullscreen mode

In handle_connection, we wrap our stream in a BufReader, which gives us handy methods for reading. We then attempt to read the first line of the request.

let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "src/hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "src/404.html")
    };
Enter fullscreen mode Exit fullscreen mode

We check if the request line is asking for the root path ("/"). If it is, we prepare a 200 status and the hello.html file. If it's not, we prepare a 404 status and the 404.html file.

let contents = match fs::read_to_string(filename) {
        Ok(content) => content,
        Err(_) => {
            return Ok(());
        }
    };
Enter fullscreen mode Exit fullscreen mode

We attempt to read the contents of the chosen file. If we succeed, we store the contents. If we fail, we simply return from the function.

let length = contents.len();
    let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
Enter fullscreen mode Exit fullscreen mode

We calculate the length of the response and then format our response. This response includes the status line, the content length, and the contents of our file.

stream.write_all(response.as_bytes())?;
    println!("Connection established!");

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Finally, we write our response back to the stream, print a message to the console, and return Ok(()) to signify that everything went well.


Complete Code

Filename: main.rs

use std::{
    fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    if let Err(err) = run_server() {
        eprintln!("Error: {}", err);
    }
}

fn run_server() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    println!("Server running on http://127.0.0.1:7878/");

    for stream in listener.incoming() {
        let stream = stream?;
        handle_connection(stream)?;
    }

    Ok(())
}

fn handle_connection(mut stream: TcpStream) -> std::io::Result<()> {
    let buf_reader = BufReader::new(&mut stream);
    let request_line = match buf_reader.lines().next() {
        Some(line) => line?,
        None => return Ok(()), // Ignore empty requests
    };

    let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "src/hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "src/404.html")
    };

    let contents = match fs::read_to_string(filename) {
        Ok(content) => content,
        Err(_) => {
            return Ok(());
        }
    };

    let length = contents.len();
    let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes())?;
    println!("Connection established!");

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Filename: hello.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rust Web Server</title>
</head>
<body>
    <h1>Hello, Rustaceans!</h1>
    <p>This is my single threaded web server using Rust✨πŸ₯³</p>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Filename: 404.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Page Not Found</title>
</head>

<body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what are you asking for?</p>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Output

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/http_server`
Server running on http://127.0.0.1:7878/
Enter fullscreen mode Exit fullscreen mode

Client side output


πŸŽ‰πŸAnd... Cut!πŸπŸŽ‰

Today's performance was nothing short of thrilling! We've set up a basic, single-threaded web server in Rust, ready to conquer the world of web development. As we continue this Rust web odyssey, we'll explore frameworks, build projects, and bring more excitement to the stage. Stay tuned, fellow developers; the best is yet to come! πŸŒπŸš€

Top comments (0)