Rust
Async web server
Build a mini server that handles clients in parallel with tokio.
Final task: web server
The advanced level's first final task is a small HTTP server. A real project uses tokio::net::TcpListener and a tokio::spawn per connection; in the exercise we build the same structure with std threads. HTTP/1.1 is text-based: a request line, headers, an empty line; the response has the same form:
GET / HTTP/1.1\r\n
Host: localhost\r\n
\r\n
HTTP/1.1 200 OK\r\n
Content-Length: 7\r\n
\r\n
merhaba
The server reads the path from the request line, consumes the headers up to the empty line, and responds based on the path.
Request handler
The handler reads line by line and writes the response in one go:
fn istek_isle(akis: TcpStream) -> io::Result<()> {
let mut okuyucu = BufReader::new(akis.try_clone()?);
let mut ilk = String::new();
okuyucu.read_line(&mut ilk)?;
loop {
let mut s = String::new();
okuyucu.read_line(&mut s)?;
if s == "\r\n" || s.is_empty() { break; }
}
let (durum, govde) = match ilk.split_whitespace().nth(1) {
Some("/") => ("200 OK", "merhaba"),
_ => ("404 NOT FOUND", "bulunamadı"),
};
let mut akis = akis;
write!(akis, "HTTP/1.1 {durum}\r\nContent-Length: {}\r\n\r\n{govde}", govde.len())
}
Content-Length is in bytes: Turkish characters count as two bytes, so len() is used.
Exercise HTTP server
Write the istek_isle function above. Run the server on a thread with incoming().take(2). Let the main thread send requests for / and /yok in turn and read each response with read_to_string; print the first response's status line and body, and the second's status line, on separate lines.
Show example solution
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::thread;
fn istek_isle(akis: TcpStream) -> io::Result<()> {
let mut okuyucu = BufReader::new(akis.try_clone()?);
let mut ilk = String::new();
okuyucu.read_line(&mut ilk)?;
loop {
let mut s = String::new();
okuyucu.read_line(&mut s)?;
if s == "\r\n" || s.is_empty() {
break;
}
}
let (durum, govde) = match ilk.split_whitespace().nth(1) {
Some("/") => ("200 OK", "merhaba"),
_ => ("404 NOT FOUND", "bulunamadı"),
};
let mut akis = akis;
write!(akis, "HTTP/1.1 {durum}\r\nContent-Length: {}\r\n\r\n{govde}", govde.len())
}
fn iste(adres: SocketAddr, yol: &str) -> io::Result<(String, String)> {
let mut akis = TcpStream::connect(adres)?;
write!(akis, "GET {yol} HTTP/1.1\r\nHost: localhost\r\n\r\n")?;
let mut yanit = String::new();
akis.read_to_string(&mut yanit)?;
let (bas, govde) = yanit.split_once("\r\n\r\n").unwrap_or((&yanit, ""));
let durum = bas.lines().next().unwrap_or("").to_string();
Ok((durum, govde.to_string()))
}
fn main() -> io::Result<()> {
let dinleyici = TcpListener::bind("127.0.0.1:0")?;
let adres = dinleyici.local_addr()?;
thread::spawn(move || {
for akis in dinleyici.incoming().take(2) {
let akis = akis.unwrap();
thread::spawn(move || istek_isle(akis));
}
});
let (durum, govde) = iste(adres, "/")?;
println!("{}", durum);
println!("{}", govde);
let (durum2, _) = iste(adres, "/yok")?;
println!("{}", durum2);
Ok(())
}
Test yourself
-
What can happen if the server closes the connection without reading the headers?
- The response is sent twice
- Because of unread data, the OS may reset the connection (RST) and the client may not get the response
- Nothing
- Compile error
Show answer
Because of unread data, the OS may reset the connection (RST) and the client may not get the response — Consuming the request fully lets the connection close cleanly (with FIN).
-
Read the path
let yol = ilk.split_whitespace().____(1); // "GET / HTTP/1.1" → Some("/")
Show answer
let yol = ilk.split_whitespace().nth(1); // "GET / HTTP/1.1" → Some("/")
-
Request and response
- GET /
- 200 OK
- GET /yok
- 404 NOT FOUND
- Content-Length
- Byte count
- \r\n\r\n
- End of headers
Next lesson: ThreadPool