ساخت یک وب سرور Single-Threaded
ما با راهاندازی یک وب سرور Single-Threaded شروع خواهیم کرد. پیش از شروع، بیایید یک مرور سریع بر پروتکلهای مرتبط با ساخت وب سرورها داشته باشیم. جزئیات این پروتکلها خارج از محدوده این کتاب است، اما یک نمای کلی اطلاعات لازم را به شما میدهد.
دو پروتکل اصلی که در وب سرورها درگیر هستند، Hypertext Transfer Protocol (HTTP) و Transmission Control Protocol (TCP) هستند. هر دو پروتکل request-response هستند، به این معنی که یک client درخواستها را آغاز میکند و یک server به درخواستها گوش میدهد و پاسخی به client ارائه میدهد. محتوای این درخواستها و پاسخها توسط پروتکلها تعریف شده است.
TCP یک پروتکل سطح پایینتر است که جزئیات چگونگی انتقال اطلاعات از یک سرور به سرور دیگر را توصیف میکند اما مشخص نمیکند که آن اطلاعات چیست. HTTP بر روی TCP ساخته شده است و محتوای درخواستها و پاسخها را تعریف میکند. از لحاظ فنی امکان استفاده از HTTP با سایر پروتکلها وجود دارد، اما در اکثر موارد، HTTP دادههای خود را بر روی TCP ارسال میکند. ما با بایتهای خام درخواستها و پاسخهای TCP و HTTP کار خواهیم کرد.
گوش دادن به اتصال TCP
وب سرور ما نیاز دارد به اتصال TCP گوش دهد، بنابراین این اولین بخشی است که روی آن کار میکنیم. کتابخانه استاندارد یک ماژول std::net
ارائه میدهد که به ما این امکان را میدهد این کار را انجام دهیم. بیایید یک پروژه جدید به روش معمول ایجاد کنیم:
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
حالا کد لیست ۲۱-۱ را در فایل src/main.rs وارد کنید تا شروع کنیم. این کد به آدرس محلی 127.0.0.1:7878
برای جریانهای ورودی TCP گوش میدهد. وقتی یک جریان ورودی دریافت میکند، پیام Connection established!
را چاپ میکند.
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
، میتوانیم به اتصالات TCP در آدرس 127.0.0.1:7878
گوش دهیم. در این آدرس، بخش قبل از دونقطه یک آدرس IP است که نمایانگر کامپیوتر شما است (این آدرس روی همه کامپیوترها یکسان است و نمایانگر کامپیوتر نویسندگان نیست) و 7878
پورت است. این پورت را به دو دلیل انتخاب کردهایم: HTTP معمولاً روی این پورت پذیرفته نمیشود، بنابراین احتمالاً سرور ما با هیچ وب سرور دیگری که ممکن است روی دستگاه شما اجرا شود تداخل نخواهد داشت، و 7878 روی تلفن به صورت rust تایپ میشود.
تابع bind
در این سناریو مانند تابع new
عمل میکند به این صورت که یک نمونه جدید از TcpListener
بازمیگرداند. این تابع bind
نامیده میشود زیرا در شبکه، اتصال به یک پورت برای گوش دادن به آن به عنوان “binding to a port” شناخته میشود.
تابع bind
مقداری از نوع Result<T, E>
برمیگرداند، که نشان میدهد امکان شکست در عملیات bind وجود دارد. برای مثال، اگر دو نمونه از برنامهی ما بهطور همزمان اجرا شوند و هر دو بخواهند به یک پورت گوش دهند، این عملیات ممکن است شکست بخورد. از آنجا که ما در حال نوشتن یک سرور ساده فقط برای اهداف آموزشی هستیم، نگران مدیریت این نوع خطاها نخواهیم بود؛ در عوض، از تابع unwrap
استفاده میکنیم تا در صورت بروز خطا، برنامه متوقف شود.
متد incoming
روی TcpListener
یک iterator بازمیگرداند که به ما دنبالهای از جریانها (streams) میدهد (به طور خاص، جریانهایی از نوع TcpStream
). یک stream نشاندهنده یک اتصال باز بین client و server است. یک connection به فرآیند کامل درخواست و پاسخ گفته میشود که در آن یک client به سرور متصل میشود، سرور یک پاسخ تولید میکند، و سپس اتصال توسط سرور بسته میشود. بنابراین، ما از TcpStream
برای خواندن آنچه client ارسال کرده استفاده میکنیم و سپس پاسخ خود را به جریان مینویسیم تا دادهها را به client بازگردانیم. به طور کلی، این حلقه for
هر اتصال را به نوبت پردازش کرده و یک سری جریانها را برای مدیریت به ما میدهد.
فعلاً، مدیریت ما روی جریان به فراخوانی unwrap
محدود میشود تا در صورتی که جریان دارای خطا باشد، برنامه متوقف شود. اگر خطایی وجود نداشته باشد، برنامه یک پیام چاپ میکند. در لیست بعدی، عملکرد بیشتری برای حالت موفقیت اضافه خواهیم کرد. دلیل اینکه ممکن است از متد incoming
هنگام اتصال یک client به سرور خطا دریافت کنیم این است که ما در واقع روی اتصالات تکرار نمیکنیم، بلکه روی تلاشهای اتصال تکرار میکنیم. اتصال ممکن است به دلایل مختلف موفقیتآمیز نباشد که بسیاری از آنها مربوط به سیستمعامل هستند. برای مثال، بسیاری از سیستمعاملها محدودیتی برای تعداد اتصالات همزمان باز دارند؛ تلاشهای اتصال جدیدی که بیش از این تعداد باشند، تا زمانی که برخی از اتصالات باز بسته نشوند، خطا تولید خواهند کرد.
بیایید اجرای این کد را امتحان کنیم! دستور cargo run
را در ترمینال اجرا کنید و سپس آدرس 127.0.0.1:7878 را در یک مرورگر وب بارگذاری کنید. مرورگر باید پیامی مانند «Connection reset» نمایش دهد، چون سرور در حال حاضر هیچ دادهای ارسال نمیکند. اما وقتی به ترمینال خود نگاه کنید، باید چندین پیام ببینید که هنگام اتصال مرورگر به سرور چاپ شدهاند!
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
گاهی ممکن است برای یک درخواست مرورگر، چندین پیام چاپشده ببینید؛ دلیل این موضوع میتواند این باشد که مرورگر، علاوه بر درخواست صفحه، برای منابع دیگری مانند آیکون favicon.ico که در تب مرورگر نمایش داده میشود نیز درخواست ارسال میکند.
همچنین ممکن است مرورگر چندین بار تلاش کند تا به سرور متصل شود، چون سرور هیچ دادهای پاسخ نمیدهد. وقتی stream
از محدوده خارج شده و در انتهای حلقه drop
میشود، اتصال به عنوان بخشی از پیادهسازی drop
بسته میشود. مرورگرها گاهی با اتصالهای بستهشده با تلاش مجدد برخورد میکنند، چون ممکن است مشکل موقتی باشد.
مرورگرها همچنین گاهی بدون ارسال هیچ درخواستی، چندین اتصال به سرور باز میکنند تا اگر بعداً بخواهند درخواستی ارسال کنند، آن درخواستها سریعتر انجام شوند. وقتی این اتفاق میافتد، سرور ما هر اتصال را مشاهده میکند، صرفنظر از اینکه آیا درخواستی از طریق آن اتصال وجود دارد یا نه. بسیاری از نسخههای مرورگرهای مبتنی بر Chrome این کار را انجام میدهند؛ میتوانید این بهینهسازی را با استفاده از حالت مرور خصوصی (private browsing) یا استفاده از مرورگر متفاوت غیرفعال کنید.
نکتهی مهم این است که ما موفق شدهایم به یک اتصال TCP دسترسی پیدا کنیم!
به یاد داشته باشید که پس از اجرای یک نسخه خاص از کد، برنامه را با فشردن ctrl+C متوقف کنید. سپس، پس از ایجاد هر مجموعه از تغییرات در کد، با اجرای دستور cargo run
برنامه را دوباره اجرا کنید تا مطمئن شوید جدیدترین نسخهی کد را اجرا میکنید.
خواندن درخواست
بیایید عملکرد خواندن درخواست از مرورگر را پیادهسازی کنیم! برای جدا کردن نگرانیها از اتصال اولیه و سپس انجام برخی اقدامات با اتصال، یک تابع جدید برای پردازش اتصالات ایجاد میکنیم. در این تابع جدید handle_connection
، دادهها را از جریان TCP میخوانیم و آنها را چاپ میکنیم تا بتوانیم دادههایی که از مرورگر ارسال میشوند را ببینیم. کد را تغییر دهید تا شبیه لیست ۲۱-۲ شود.
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:#?}"); }
TcpStream
و چاپ دادههاما std::io::prelude
و std::io::BufReader
را وارد دامنه میکنیم تا به ویژگیها و نوعهایی که به ما اجازه خواندن و نوشتن از جریان را میدهند دسترسی داشته باشیم. در حلقه for
در تابع main
، به جای چاپ یک پیام که میگوید یک اتصال برقرار کردیم، حالا تابع جدید handle_connection
را فراخوانی میکنیم و stream
را به آن ارسال میکنیم.
در تابع handle_connection
، یک نمونهی جدید از BufReader
ایجاد میکنیم که یک رفرنس به stream
را درون خود نگه میدارد. BufReader
با مدیریت فراخوانیهای متدهای trait مربوط به std::io::Read
، به ما امکان میدهد تا بهصورت بهینهتر از بافر استفاده کنیم.
ما یک متغیر به نام http_request
ایجاد میکنیم تا خطوط درخواست ارسالی مرورگر به سرور را جمعآوری کنیم. مشخص میکنیم که میخواهیم این خطوط را در یک بردار جمعآوری کنیم با اضافه کردن نوع Vec<_>
به عنوان حاشیهنویسی.
BufReader
ویژگی std::io::BufRead
را پیادهسازی میکند که متد lines
را ارائه میدهد. متد lines
یک iterator از نوع Result<String, std::io::Error>
بازمیگرداند، که با هر بار مشاهده یک بایت newline جریان داده را تقسیم میکند. برای دریافت هر String
، هر Result
را map و unwrap
میکنیم. اگر دادهها UTF-8 معتبری نباشند یا اگر مشکلی در خواندن از جریان وجود داشته باشد، ممکن است Result
خطایی باشد. باز هم، یک برنامه تولیدی باید این خطاها را به صورت کارآمدتری مدیریت کند، اما برای سادگی، ما انتخاب میکنیم که در حالت خطا برنامه متوقف شود.
مرورگر پایان یک درخواست HTTP را با ارسال دو کاراکتر newline متوالی نشان میدهد، بنابراین برای دریافت یک درخواست از جریان، خطوط را میگیریم تا زمانی که به یک خط خالی برسیم. پس از جمعآوری خطوط در بردار، آنها را با استفاده از فرمت دیباگ زیبا چاپ میکنیم تا بتوانیم دستورالعملهایی که مرورگر وب به سرور ما ارسال میکند را ببینیم.
بیایید این کد را امتحان کنیم! برنامه را اجرا کنید و دوباره یک درخواست در مرورگر وب ارسال کنید. توجه داشته باشید که همچنان در مرورگر یک صفحه خطا خواهیم دید، اما خروجی برنامه در ترمینال اکنون مشابه این خواهد بود:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
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",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
بسته به مرورگری که استفاده میکنید، ممکن است خروجی کمی متفاوت دریافت کنید. اکنون که دادههای درخواست را چاپ میکنیم، میتوانیم دلیل دریافت چندین اتصال از یک درخواست مرورگر را با نگاه کردن به مسیر بعد از GET
در خط اول درخواست متوجه شویم. اگر اتصالات تکراری همه / را درخواست کنند، میدانیم که مرورگر سعی دارد / را بارها و بارها درخواست کند زیرا پاسخی از برنامه ما دریافت نمیکند.
بیایید این دادههای درخواست را تجزیه کنیم تا متوجه شویم مرورگر از برنامه ما چه چیزی میخواهد.
نگاهی دقیقتر به یک درخواست HTTP
HTTP یک پروتکل مبتنی بر متن است و یک درخواست فرمت زیر را دارد:
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
خط اول خط درخواست است که اطلاعاتی درباره آنچه client درخواست میکند را نگه میدارد. قسمت اول خط درخواست، method استفادهشده را نشان میدهد، مانند GET
یا POST
، که توضیح میدهد client چگونه این درخواست را انجام میدهد. client ما از یک درخواست GET
استفاده کرده است، به این معنی که در حال درخواست اطلاعات است.
بخش بعدی از خط درخواست، / است که نشاندهندهی شناسهی یکنواخت منبع یا (uniform resource identifier) است که کلاینت آن را درخواست کرده است؛ یک URI تقریباً، اما نه کاملاً، همانند نشانی یکنواخت منبع یا (uniform resource locator) یا همان URL است. تفاوت بین URI و URL برای اهداف ما در این فصل اهمیت خاصی ندارد، اما مشخصات HTTP از اصطلاح URI استفاده میکند، بنابراین میتوانیم در ذهن خود بهجای URI از URL استفاده کنیم.
آخرین بخش نسخهی HTTP است که کلاینت استفاده میکند، و سپس خط درخواست با یک دنبالهی CRLF به پایان میرسد. (CRLF مخفف carriage return و line feed است، که اصطلاحاتی مربوط به دوران ماشین تحریر هستند!) دنبالهی CRLF همچنین بهصورت \r\n
نیز نوشته میشود، جایی که \r
به معنای carriage return و \n
به معنای line feed است. دنبالهی CRLF خط درخواست را از باقی دادههای درخواست جدا میکند. توجه داشته باشید که هنگام چاپ CRLF، ما شروع یک خط جدید را میبینیم، نه \r\n
.
با نگاه کردن به دادههای خط درخواست که تاکنون از اجرای برنامه خود دریافت کردهایم، میبینیم که GET
متد است، / شناسه URI درخواستشده است، و HTTP/1.1
نسخه است.
بعد از خط درخواست، خطوط باقیمانده از Host:
به بعد هدرها هستند. درخواستهای GET
بدنه ندارند.
سعی کنید یک درخواست از مرورگری دیگر یا آدرسی متفاوت، مانند 127.0.0.1:7878/test، ارسال کنید تا ببینید دادههای درخواست چگونه تغییر میکنند.
اکنون که میدانیم مرورگر چه چیزی میخواهد، بیایید دادهای را به آن بازگردانیم!
نوشتن یک پاسخ
ما قصد داریم ارسال داده در پاسخ به یک درخواست client را پیادهسازی کنیم. پاسخها فرمت زیر را دارند:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
خط اول یک خط وضعیت است که شامل نسخه HTTP استفادهشده در پاسخ، یک کد وضعیت عددی که نتیجه درخواست را خلاصه میکند، و یک عبارت دلیل که توضیح متنی برای کد وضعیت ارائه میدهد. پس از دنباله CRLF، هر هدر و سپس یک دنباله CRLF دیگر و بدنه پاسخ قرار میگیرد.
در اینجا یک مثال پاسخ آورده شده است که از نسخه HTTP 1.1 استفاده میکند، کد وضعیت 200 دارد، عبارت دلیل “OK” است، هیچ هدر و بدنهای ندارد:
HTTP/1.1 200 OK\r\n\r\n
کد وضعیت 200 پاسخ استاندارد موفقیت است. این متن یک پاسخ HTTP کوچک و موفقیتآمیز است. بیایید این را به عنوان پاسخ خود به یک درخواست موفق به جریان بنویسیم! از تابع handle_connection
، println!
که دادههای درخواست را چاپ میکرد، حذف کنید و آن را با کد موجود در لیست ۲۱-۳ جایگزین کنید.
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(); let response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write_all(response.as_bytes()).unwrap(); }
خط جدید اول متغیر response
را تعریف میکند که داده پیام موفقیت را در خود نگه میدارد. سپس با فراخوانی as_bytes
روی response
داده رشتهای را به بایتها تبدیل میکنیم. متد write_all
روی stream
یک &[u8]
میگیرد و آن بایتها را مستقیماً به اتصال ارسال میکند. از آنجا که عملیات write_all
ممکن است شکست بخورد، مانند قبل، روی هر نتیجه خطا از unwrap
استفاده میکنیم. باز هم، در یک برنامه واقعی باید اینجا مدیریت خطا اضافه کنید.
با این تغییرات، بیایید کد خود را اجرا کنیم و یک درخواست ارسال کنیم. دیگر هیچ دادهای به ترمینال چاپ نمیکنیم، بنابراین هیچ خروجیای به غیر از خروجی Cargo نخواهید دید. وقتی آدرس 127.0.0.1:7878 را در یک مرورگر وب بارگذاری میکنید، باید یک صفحه خالی به جای یک خطا دریافت کنید. شما اکنون دریافت یک درخواست HTTP و ارسال یک پاسخ را به صورت دستی کدنویسی کردهاید!
بازگرداندن HTML واقعی (Returning Real HTML)
بیایید عملکرد بازگرداندن چیزی بیش از یک صفحه خالی را پیادهسازی کنیم. فایل جدیدی به نام hello.html در ریشه دایرکتوری پروژه خود ایجاد کنید، نه در دایرکتوری src. میتوانید هر HTML که میخواهید وارد کنید؛ لیست ۲۱-۴ یک نمونه را نشان میدهد.
<!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>
این یک سند HTML5 حداقلی با یک عنوان و مقداری متن است. برای بازگرداندن این فایل از سرور هنگام دریافت یک درخواست، کد handle_connection
را همانطور که در لیست ۲۱-۵ نشان داده شده است تغییر میدهیم تا فایل HTML را بخواند، آن را به عنوان بدنه پاسخ اضافه کند و ارسال کند.
use std::{ fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, }; // --snip-- 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(); }
ما fs
را به دستور use
اضافه کردهایم تا ماژول سیستم فایل کتابخانه استاندارد را وارد دامنه کنیم. کدی که محتوای یک فایل را به یک رشته میخواند باید آشنا به نظر برسد؛ ما در فصل ۱۲ زمانی که محتوای یک فایل را برای پروژه ورودی/خروجی خود خواندیم از آن استفاده کردیم (لیست ۱۲-۴).
سپس، از format!
برای اضافه کردن محتوای فایل به عنوان بدنه پاسخ موفقیت استفاده میکنیم. برای اطمینان از یک پاسخ HTTP معتبر، هدر Content-Length
را اضافه میکنیم که به اندازه بدنه پاسخ ما تنظیم شده است، که در این مورد اندازه فایل hello.html
است.
این کد را با دستور cargo run
اجرا کنید و آدرس 127.0.0.1:7878 را در مرورگر خود بارگذاری کنید؛ باید HTML خود را که به درستی رندر شده است ببینید!
در حال حاضر، ما دادههای درخواست در http_request
را نادیده میگیریم و فقط محتوای فایل HTML را بدون شرط بازمیگردانیم. این بدان معناست که اگر در مرورگر خود آدرس 127.0.0.1:7878/something-else را درخواست کنید، همچنان همین پاسخ HTML را دریافت خواهید کرد. در این لحظه، سرور ما بسیار محدود است و کارهایی که اکثر وب سرورها انجام میدهند را انجام نمیدهد. ما میخواهیم پاسخهای خود را بر اساس درخواست سفارشی کنیم و فقط فایل HTML را برای یک درخواست خوشساخت به / بازگردانیم.
اعتبارسنجی درخواست و پاسخدهی انتخابی
در حال حاضر، وب سرور ما محتوای فایل HTML را بدون توجه به درخواست client بازمیگرداند. بیایید عملکردی اضافه کنیم تا بررسی کند که مرورگر / را درخواست کرده باشد، سپس فایل HTML را بازگرداند و اگر مرورگر چیز دیگری درخواست کرد، یک خطا بازگرداند. برای این کار باید تابع handle_connection
را همانطور که در لیست ۲۱-۶ نشان داده شده است تغییر دهیم. این کد جدید محتوای درخواست دریافتی را با آنچه که میدانیم یک درخواست برای / باید به نظر برسد مقایسه میکند و بلوکهای if
و else
را اضافه میکند تا درخواستها به صورت متفاوتی مدیریت شوند.
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); } } // --snip-- 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 } }
ما فقط به خط اول درخواست HTTP نگاه خواهیم کرد، بنابراین به جای خواندن کل درخواست در یک بردار، از next
استفاده میکنیم تا اولین آیتم از iterator را بگیریم. اولین unwrap
مقدار Option
را مدیریت میکند و اگر iterator هیچ آیتمی نداشته باشد برنامه را متوقف میکند. دومین unwrap
مقدار Result
را مدیریت میکند و همان اثری را دارد که unwrap
در map
اضافهشده در لیست ۲۱-۲ داشت.
سپس، مقدار request_line
را بررسی میکنیم تا ببینیم آیا برابر با خط درخواست یک درخواست GET به مسیر / است یا خیر. اگر اینطور باشد، بلوک if
محتوای فایل HTML ما را بازمیگرداند.
اگر مقدار request_line
برابر با درخواست GET به مسیر / نباشد، به این معنی است که یک درخواست دیگر دریافت کردهایم. در بلوک else
کدی اضافه خواهیم کرد تا به سایر درخواستها پاسخ دهد.
این کد را اجرا کنید و آدرس 127.0.0.1:7878 را درخواست کنید؛ باید HTML موجود در فایل hello.html را دریافت کنید. اگر درخواست دیگری مانند 127.0.0.1:7878/something-else ارسال کنید، یک خطای اتصال مشابه آنچه در اجرای کدهای لیست ۲۱-۱ و ۲۱-۲ دیدید دریافت خواهید کرد.
حالا کد موجود در لیست ۲۱-۷ را به بلوک else
اضافه کنید تا پاسخی با کد وضعیت 404 بازگرداند، که نشان میدهد محتوای درخواستشده پیدا نشد. همچنین مقداری HTML برای یک صفحه خطا بازمیگردانیم تا در مرورگر به کاربر نهایی نمایش داده شود.
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(); 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(); // --snip-- } 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(); } }
در اینجا، پاسخ ما یک خط وضعیت با کد وضعیت 404 و عبارت دلیل NOT FOUND
دارد. بدنه پاسخ HTML موجود در فایل 404.html خواهد بود. باید فایل 404.html را در کنار فایل hello.html برای صفحه خطا ایجاد کنید؛ باز هم، میتوانید هر HTML که میخواهید استفاده کنید یا از HTML نمونه موجود در لیست ۲۱-۸ استفاده کنید.
<!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>
با این تغییرات، سرور خود را دوباره اجرا کنید. درخواست آدرس 127.0.0.1:7878 باید محتوای فایل hello.html را بازگرداند، و هر درخواست دیگری مانند 127.0.0.1:7878/foo باید HTML خطا از فایل 404.html را بازگرداند.
کمی بازسازی (Refactoring)
در حال حاضر، بلوکهای if
و else
مقدار زیادی تکرار دارند: هر دو فایلها را میخوانند و محتوای فایلها را به جریان مینویسند. تنها تفاوتها خط وضعیت و نام فایل هستند. بیایید کد را مختصرتر کنیم و این تفاوتها را به خطوط جداگانه if
و else
انتقال دهیم که مقادیر خط وضعیت و نام فایل را به متغیرها اختصاص دهند؛ سپس میتوانیم از این متغیرها به طور شرطی برای خواندن فایل و نوشتن پاسخ استفاده کنیم. لیست ۲۱-۹ کد نتیجهشده پس از جایگزینی بلوکهای بزرگ if
و else
را نشان میدهد.
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); } } // --snip-- fn handle_connection(mut stream: TcpStream) { // --snip-- 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(); }
if
و else
برای شامل شدن تنها کدی که بین دو حالت متفاوت استاکنون بلوکهای if
و else
تنها مقادیر مناسب برای خط وضعیت و نام فایل را در یک تاپل بازمیگردانند؛ سپس با استفاده از یک الگو در دستور let
، این دو مقدار به status_line
و filename
تخصیص داده میشوند، همانطور که در فصل ۱۹ بحث شد.
کدی که قبلاً تکراری بود اکنون خارج از بلوکهای if
و else
قرار دارد و از متغیرهای status_line
و filename
استفاده میکند. این کار تشخیص تفاوت بین دو حالت را آسانتر میکند و به این معنی است که اگر بخواهیم نحوه خواندن فایل و نوشتن پاسخ را تغییر دهیم، تنها یک مکان برای بهروزرسانی کد داریم. رفتار کد در لیست ۲۱-۹ با لیست ۲۱-۷ یکسان خواهد بود.
عالی! اکنون یک وب سرور ساده با تقریباً ۴۰ خط کد Rust داریم که به یک درخواست با یک صفحه محتوا پاسخ میدهد و به تمام درخواستهای دیگر یک پاسخ 404 میدهد.
در حال حاضر، سرور ما در یک Thread اجرا میشود، به این معنی که تنها میتواند یک درخواست را در یک زمان سرویس دهد. بیایید بررسی کنیم که چگونه این موضوع میتواند مشکلساز شود، با شبیهسازی برخی درخواستهای کند. سپس سرور را طوری بهبود میدهیم که بتواند چندین درخواست را همزمان مدیریت کند.