পাঠ ২১.১

Single-Threaded Web Server বানানো

Building a Single-Threaded Web Server

এই chapter-এর final project — একটা ছোট web server, পুরোটা std-library দিয়ে। আজকের পাঠে single-threaded version বানাব: TcpListener দিয়ে port-এ listen, incoming HTTP request পড়া, response পাঠানো, এবং URL-এর ভিত্তিতে আলাদা page serve। পরের পাঠে এটাকে multi-threaded করব।

প্রোটোকল — TCP আর HTTP

  • TCP (Transmission Control Protocol) — lower-level; দু'টা machine-এর মাঝে byte-এর reliable stream পাঠানোর protocol।
  • HTTP (Hypertext Transfer Protocol) — TCP-এর উপরে চলে; request আর response কেমন format-এ যাবে সেটা define করে।

দু'টোই request-response — client request পাঠায়, server response দেয়।

TCP connection-এ listen

src/main.rsrust
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!");
    }
}
  • TcpListener::bind("127.0.0.1:7878") — localhost-এর port 7878-এ listen।
  • Port 7878 কেন? টেলিফোন keypad-এ "rust" — দ্রবণ choice; HTTP-এর standard port না।
  • bind Result return করে — port already in use হলে fail।
  • incoming()TcpStream-এর iterator দেয়। প্রতিটা stream একটা open connection।
  • Connection মানে full request-response cycle; stream মানে সেই communication channel।

Browser থেকে 127.0.0.1:7878 visit করলে terminal-এ "Connection established!" multiple বার দেখা যায় — কারণ browser favicon, retry ইত্যাদি কারণে কয়েকটা connection খোলে।

Request পড়া

Stream থেকে বাইরে আসা byte পড়তে BufReader ব্যবহার করব — line-by-line read facilitate করে:

src/main.rsrust
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, 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 buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    println!("Request: {http_request:#?}");
}
  • lines()Result<String, io::Error>-এর iterator।
  • HTTP request দু'টা newline (blank line) দিয়ে শেষ। তাই take_while(|line| !line.is_empty()) দিয়ে blank line এলে থামি।
  • {:#?} — pretty-print debug format।

HTTP request format

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

একটা real request এমন দেখায়:

GET / HTTP/1.1
Host: 127.0.0.1:7878
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
DNT: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1

Response লেখা

HTTP response format:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

সবচেয়ে minimal — শুধু status line, body নেই:

HTTP/1.1 200 OK\r\n\r\n
src/main.rsrust
fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let response = "HTTP/1.1 200 OK\r\n\r\n";

    stream.write_all(response.as_bytes()).unwrap();
}
  • 200 OK — সফল response-এর standard status code।
  • \r\n — CRLF (carriage return + line feed); HTTP-এর line separator।
  • as_bytes() — string-কে byte slice-এ; তারপর write_all connection-এ পাঠায়।

আসল HTML পাঠানো

Project root-এ hello.html বানাও:

hello.htmltext
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>
src/main.rsrust
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, 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 buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let status_line = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();

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

    stream.write_all(response.as_bytes()).unwrap();
}

Content-Length header — body-র byte size; browser এর সাহায্যে জানে কতটুকু read করতে হবে।

Request validate করে selectively respond

এতক্ষণ যেকোনো request-এ একই page পাঠাচ্ছিল। এবার শুধু GET /-এ hello, বাকিতে অন্য কিছু:

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

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

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        // some other request
    }
}

lines().next() first line দেয় — দু'টা unwrap কারণ: next() Option দেয়, ভিতরে Result

404 page

404.htmltext
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>
fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

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

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        let status_line = "HTTP/1.1 404 NOT FOUND";
        let contents = fs::read_to_string("404.html").unwrap();
        let length = contents.len();

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

        stream.write_all(response.as_bytes()).unwrap();
    }
}

Refactor — duplicate code কমানো

if/else-এর দু'টো branch প্রায় একই। Rust-এ if expression — তাই dual destructure দিয়ে cleanup:

src/main.rsrust
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, 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 buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

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

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

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

    stream.write_all(response.as_bytes()).unwrap();
}

৪০-এর মতো line code, single file — minimal HTTP server প্রস্তুত। Test: cargo run তারপর browser-এ 127.0.0.1:7878 (hello), 127.0.0.1:7878/anything (404)। Ctrl-C দিয়ে stop, code changed করলে restart।

এই পাঠ থেকে যা শিখলে

  • TcpListener::bind + incoming() দিয়ে connection accept।
  • BufReader + lines() দিয়ে HTTP request পড়া; blank line-এ stop।
  • HTTP response = status line + headers + blank line + body; CRLF (\r\n) separator।
  • Content-Length header body size জানায়।
  • Request line (যেমন GET / HTTP/1.1) match করে selectively respond; 404 fallback।
  • Single-threaded — তাই একটা request slow হলে বাকিরা সব wait করে। পরের পাঠে এটাই solve করব।