مقدمه
به فصل پایانی این دوره خوش آمدید! در این فصل، تمام مفاهیمی را که از ابتدا تاکنون یاد گرفتهایم،
از مالکیت و traitها گرفته تا همزمانی و مدیریت خطا، در یک پروژه بزرگ و عملی به کار خواهیم بست:
ساخت یک وب سرور ساده با استفاده از کتابخانه استاندارد Rust. این پروژه به ما کمک میکند تا درک
عمیقتری از نحوه کار پروتکلهای شبکه مانند TCP و HTTP پیدا کنیم.
در این درس، ما نسخه اولیه و تک-ریسمانی (single-threaded) سرور خود را خواهیم ساخت.
گوش دادن به اتصالات TCP
قلب یک وب سرور، توانایی آن در گوش دادن به اتصالات TCP روی یک پورت شبکه است. کتابخانه استاندارد
Rust در ماژول std::net ابزارهای لازم برای این کار را فراهم میکند. ما از TcpListener برای
اتصال (bind) به یک آدرس IP و پورت و منتظر ماندن برای درخواستهای ورودی استفاده خواهیم کرد.
src/main.rs
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
println!("Server listening on port 7878");
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("Connection established!");
}
}
این کد یک شنونده TCP روی آدرس لوکال و پورت ۷۸۷۸ ایجاد میکند. متد .incoming() یک تکرارگر بر روی اتصالات ورودی برمیگرداند. حلقه for
اجرای برنامه را تا زمانی که یک اتصال جدید برقرار شود، مسدود میکند. پس از اجرای این کد با cargo
run و باز کردن آدرس 127.0.0.1:7878 در مرورگر، پیام "Connection established!" را در ترمینال
مشاهده خواهید کرد.
خواندن درخواست و ارسال پاسخ
هر اتصال ورودی (TcpStream)، trait مربوط به Read و Write را پیادهسازی میکند که به ما
اجازه میدهد دادهها را از آن خوانده و در آن بنویسیم. یک درخواست HTTP ساده، یک متن با فرمت مشخص
است. ما این درخواست را میخوانیم و یک پاسخ HTTP ساده را به کلاینت (مرورگر) برمیگردانیم.
src/main.rs
use std::{
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};
fn main() {
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 واقعی را به عنوان پاسخ
ارسال کند.
src/main.rs
let contents = fs::read_to_string("hello.html").unwrap();
ما میتوانیم یک منطق شرطی نیز اضافه کنیم تا بسته به خط اول درخواست HTTP پاسخهای
متفاوتی را برگردانیم. برای مثال، اگر درخواست به مسیر `/` باشد، فایل hello.html را سرویس
دهیم و برای سایر درخواستها، یک پاسخ ۴۰۴ برگردانیم.
این سرور تک-ریسمانی است. این یعنی به هر درخواست به صورت ترتیبی پاسخ میدهد و تا زمانی که پردازش
یک درخواست تمام نشود، نمیتواند به درخواست بعدی رسیدگی کند. اگر ما یک درخواست زمانبر را
شبیهسازی کنیم (مثلاً با افزودن یک thread::sleep)، خواهیم دید که تمام درخواستهای بعدی باید
منتظر بمانند.
در این درس، ما با موفقیت یک وب سرور ساده و تک-ریسمانی را با استفاده از کتابخانه استاندارد Rust
ساختیم. یاد گرفتیم که چگونه به اتصالات TCP گوش دهیم، درخواستهای HTTP را بخوانیم و پاسخهای
معتبر ارسال کنیم. در درس بعدی، با «ساخت یک وبسرور Multi-threaded»، سرور خود را با استفاده از یک
thread pool بهبود خواهیم داد تا بتواند چندین درخواست را به صورت همزمان پردازش کند و عملکرد آن
را به شکل چشمگیری افزایش دهیم.