I started learning Rust today and for my first project created a simple webserver.
I don't have a clear vision about Rust programming and I tried to write it with my previous knowledge in Python and PHP.
use std::io::{Read, Write,BufReader, BufRead};
use std::net::{TcpListener, TcpStream};
use std::fs::File;
use std::path::Path;
fn main(){
loop{
let listener = TcpListener::bind("localhost:8000").unwrap();
let stream = listener.accept().unwrap().0;
read_request(stream);
}
}
fn read_request(stream: TcpStream){
let mut lines = String::new();
let mut reader = BufReader::new(stream);
reader.read_line(&mut lines);
let mut vec_line = lines.split_whitespace();
let mut requested_page = vec_line.nth(1).unwrap().to_string();
requested_page = requested_page.replace("/","");
if requested_page == "" {
requested_page = String::from("index.html");
}
let mut response = String::new();
let path = Path::new(&requested_page);
println!("{}",requested_page);
let mut status = 200;
if !path.exists(){
response = String::from("Not Found!");
status = 404;
} else {
let mut file = File::open(&requested_page).expect("Unable to open file");
file.read_to_string(&mut response);
}
send_response(reader.into_inner(), &response.to_string(), status);
}
fn send_response(mut stream: TcpStream, res :&str, status :u32){
let response = format!("{}{}{}{}","HTTP/1.1 ",status," OK\n\n",res);
stream.write_all(response.as_bytes()).unwrap();
}
1 Answer 1
Learn to love rustfmt:
Spaces go between comma-separated items:
-use std::io::{Read, Write,BufReader, BufRead}; +use std::io::{Read, Write, BufReader, BufRead};
-let response = format!("{}{}{}{}","HTTP/1.1 ",status," OK\n\n",res); +let response = format!("{}{}{}{}", "HTTP/1.1 ", status, " OK\n\n", res);
Spaces go before braces:
-fn main(){ +fn main() {
-loop{ +loop {
Colons attach on the left side of type definitions:
-fn send_response(mut stream: TcpStream, res :&str, status :u32){ +fn send_response(mut stream: TcpStream, res: &str, status: u32) {
Pay attention to compiler warnings. One of the entire purposes of using a compiled language is to have the compiler watch your back. If you ignore it, then what good is it?
warning: unused result which must be used, #[warn(unused_must_use)] on by default --> src/main.rs:19:5 | 19 | reader.read_line(&mut lines); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: unused result which must be used, #[warn(unused_must_use)] on by default --> src/main.rs:35:9 | 35 | file.read_to_string(&mut response); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Introduce more functions to break up the distinct parts of
read_request
. From my inspection, there's a part that parses the request, a part that handles the request by reading a file, and a part that responds (which already exists). Note that doing this contrains the number of lines of code a variable is mutable and avoids the need to recover the innerTcpStream
from theBufReader
.vec_line
is poorly named; it's not aVec
. I am happy to see your use ofnth
; many people wouldcollect
into aVec
, which is inefficient.There's no reason to convert the result of
nth
into aString
, you can callreplace
on a&str
equally as well.Avoid using
mut
just to be able to reassign a variable (e.g. the result ofreplace
). Instead, just bind the variable a second time. This reduces the amount and scope of mutability.requested_page == ""
should berequested_page.is_empty()
Why would you convert a
String
to aString
usingto_string
? Just take a reference to the existing string.There's no need to create a string to write to the stream; just
write!
directly to the stream.The code always prints "OK", regardless of the status.
Checking if a file exists before reading it introduces a race where the file can be deleted between the check and the read. It's better to open the file and check any resulting error.
Instead of creating mutable variables (
response
,status
) with defaults, consider only setting them in the appropriate branches. Again, this reduces the number of mutable variables and the scopes of mutability.Once
response
andstatus
are moved to a function, you can just return them directly.read_request
does more than just read, so it should be renamed.The current code doesn't require a
TcpStream
except in the outermost loop. It only needs anything that implementsRead
andWrite
. Using generics will allow you to more easily test those functions in isolation.
use std::io::{self, Read, Write, BufReader, BufRead};
use std::net::TcpListener;
use std::fs::File;
fn main() {
loop {
let listener = TcpListener::bind("localhost:8000").unwrap();
let stream = listener.accept().unwrap().0;
handle_request(stream);
}
}
fn handle_request<S>(mut stream: S)
where S: Read + Write
{
let requested_page = parse_request(&mut stream);
let (response, status) = read_file(&requested_page);
send_response(stream, &response, status);
}
fn parse_request<R>(stream: R) -> String
where R: Read
{
let mut line = String::new();
let mut reader = BufReader::new(stream);
reader.read_line(&mut line).unwrap();
let mut words = line.split_whitespace();
let requested_page = words.nth(1).unwrap();
let requested_page = requested_page.replace("/", "");
if requested_page.is_empty() {
String::from("index.html")
} else {
requested_page
}
}
fn read_file(requested_page: &str) -> (String, u32) {
match File::open(&requested_page) {
Ok(mut file) => {
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
(s, 200)
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => (String::from("Not Found!"), 404),
Err(e) => panic!("Unable to open file: {}", e),
}
}
fn send_response<W>(mut stream: W, res: &str, status: u32)
where W: Write
{
write!(&mut stream, "{}{}{}{}", "HTTP/1.1 ", status, " OK\n\n", res).unwrap();
}
There are way too many uses of unwrap
. At the very least, use expect
so you have some clue about what caused the program to terminate. Even better, use Result
for error handling.
As a basic step, returning a Box<Error>
is extremely simple and allows you to consolidate your error handling:
use std::io::{self, Read, Write, BufReader, BufRead};
use std::net::TcpListener;
use std::fs::File;
type Error = Box<std::error::Error>;
type Result<T> = std::result::Result<T, Error>;
fn main() {
loop {
if let Err(e) = handle_one("localhost:8000") {
panic!("An error occurred, I'm exiting! {}", e);
}
}
}
fn handle_one(address: &str) -> Result<()> {
let listener = TcpListener::bind(address)?;
let stream = listener.accept()?.0;
handle_request(stream)
}
fn handle_request<S>(mut stream: S) -> Result<()>
where S: Read + Write
{
let requested_page = parse_request(&mut stream)?;
let (response, status) = read_file(&requested_page)?;
send_response(stream, &response, status)
}
fn parse_request<R>(stream: R) -> Result<String>
where R: Read,
{
let mut line = String::new();
let mut reader = BufReader::new(stream);
reader.read_line(&mut line)?;
let mut words = line.split_whitespace();
let requested_page = words.nth(1).ok_or("request missing path")?;
let requested_page = requested_page.replace("/", "");
if requested_page.is_empty() {
Ok(String::from("index.html"))
} else {
Ok(requested_page)
}
}
fn read_file(requested_page: &str) -> Result<(String, u32)> {
match File::open(&requested_page) {
Ok(mut file) => {
let mut s = String::new();
file.read_to_string(&mut s)?;
Ok((s, 200))
},
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
Ok((String::from("Not Found!"), 404))
}
Err(e) => Err(e.into()),
}
}
fn send_response<W>(mut stream: W, res: &str, status: u32) -> Result<()>
where W: Write,
{
write!(&mut stream, "{}{}{}{}", "HTTP/1.1 ", status, " OK\n\n", res)?;
Ok(())
}