مقدمه

به فصل پایانی این دوره خوش آمدید! در این فصل، تمام مفاهیمی را که از ابتدا تاکنون یاد گرفته‌ایم، از مالکیت و traitها گرفته تا همزمانی و مدیریت خطا، در یک پروژه بزرگ و عملی به کار خواهیم بست: ساخت یک وب سرور ساده با استفاده از کتابخانه استاندارد Rust. این پروژه به ما کمک می‌کند تا درک عمیق‌تری از نحوه کار پروتکل‌های شبکه مانند TCP و HTTP پیدا کنیم.

در این درس، ما نسخه اولیه و تک-ریسمانی (single-threaded) سرور خود را خواهیم ساخت.

گوش دادن به اتصالات TCP

قلب یک وب سرور، توانایی آن در گوش دادن به اتصالات TCP روی یک پورت شبکه است. کتابخانه استاندارد Rust در ماژول std::net ابزارهای لازم برای این کار را فراهم می‌کند. ما از TcpListener برای اتصال (bind) به یک آدرس IP و پورت و منتظر ماندن برای درخواست‌های ورودی استفاده خواهیم کرد.

Copy Icon src/main.rs
use std::net::TcpListener;

fn main() {
    // Bind the listener to a local address and port
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    println!("Server listening on port 7878");

    // The `incoming` method returns an iterator over the connections
    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("Connection established!");
    }
}

این کد یک شنونده TCP روی آدرس لوکال و پورت ۷۸۷۸ ایجاد می‌کند. متد .incoming() یک تکرارگر بر روی اتصالات ورودی برمی‌گرداند. حلقه for اجرای برنامه را تا زمانی که یک اتصال جدید برقرار شود، مسدود می‌کند. پس از اجرای این کد با cargo run و باز کردن آدرس 127.0.0.1:7878 در مرورگر، پیام "Connection established!" را در ترمینال مشاهده خواهید کرد.

خواندن درخواست و ارسال پاسخ

هر اتصال ورودی (TcpStreamtrait مربوط به Read و Write را پیاده‌سازی می‌کند که به ما اجازه می‌دهد داده‌ها را از آن خوانده و در آن بنویسیم. یک درخواست HTTP ساده، یک متن با فرمت مشخص است. ما این درخواست را می‌خوانیم و یک پاسخ HTTP ساده را به کلاینت (مرورگر) برمی‌گردانیم.

Copy Icon src/main.rs
use std::{
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    // ... listener setup ...
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut 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 = "<h1>Hello from Rust!</h1>";
    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();
}

در تابع handle_connection، ما درخواست HTTP را می‌خوانیم (در اینجا ما فقط آن را مصرف می‌کنیم و کاری با آن انجام نمی‌دهیم) و سپس یک پاسخ HTTP معتبر با کد وضعیت ۲۰۰، هدر Content-Length و یک بدنه HTML ساده می‌سازیم و آن را با write_all() به کلاینت ارسال می‌کنیم. اکنون با اجرای مجدد برنامه، باید صفحه HTML را در مرورگر خود مشاهده کنید.

سرویس دادن به یک فایل HTML واقعی

بیایید کد را کمی بهبود دهیم تا به جای یک رشته ثابت، محتوای یک فایل HTML واقعی را به عنوان پاسخ ارسال کند.

Copy Icon src/main.rs
// In handle_connection function
let contents = fs::read_to_string("hello.html").unwrap();
// ... rest of the response logic ...

ما می‌توانیم یک منطق شرطی نیز اضافه کنیم تا بسته به خط اول درخواست HTTP پاسخ‌های متفاوتی را برگردانیم. برای مثال، اگر درخواست به مسیر `/` باشد، فایل hello.html را سرویس دهیم و برای سایر درخواست‌ها، یک پاسخ ۴۰۴ برگردانیم.

این سرور تک-ریسمانی است. این یعنی به هر درخواست به صورت ترتیبی پاسخ می‌دهد و تا زمانی که پردازش یک درخواست تمام نشود، نمی‌تواند به درخواست بعدی رسیدگی کند. اگر ما یک درخواست زمان‌بر را شبیه‌سازی کنیم (مثلاً با افزودن یک thread::sleep)، خواهیم دید که تمام درخواست‌های بعدی باید منتظر بمانند.

در این درس، ما با موفقیت یک وب سرور ساده و تک-ریسمانی را با استفاده از کتابخانه استاندارد Rust ساختیم. یاد گرفتیم که چگونه به اتصالات TCP گوش دهیم، درخواست‌های HTTP را بخوانیم و پاسخ‌های معتبر ارسال کنیم. در درس بعدی، با «ساخت یک وب‌سرور Multi-threaded»، سرور خود را با استفاده از یک thread pool بهبود خواهیم داد تا بتواند چندین درخواست را به صورت همزمان پردازش کند و عملکرد آن را به شکل چشمگیری افزایش دهیم.