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},
};
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);
}
}
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(())
}
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
};
...
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")
};
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(());
}
};
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}");
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(())
}
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(())
}
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>
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>
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/
ππ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)