.ریچارد فاینمن میگه "When I cannot create it, I do not understand it" ... اگر نتونم چیزی رو بسازم پس نمیتونم بفهممش
مدتها به عنوان برنامه نویس وب کلمه http رو شنیدم و باهاش کار کردم اما به طور دقیق نمیدونستم چی کار میکنه چون همیشه از یه فریمورک یا لایبرری استفاده میکردم که این لایه رو برای من می پوشوند . چند وقت پیش شروع به خوندن کتاب Rust Programming Language کردم . پروژه آخر این کتاب نوشتن یک وب سرور اول به صورت Single Thread و بعد به صورت Multi Thread بود که باعث شد ایده ی این پست به ذهنم بخوره .
یه وب سرور یه طور خلاصه کاری که میکنه اینه که روی یه پورت خاص که ما بهش میگیم ( عموما 80 یا 81 ) گوش میده و منتظر یه کانکشن میمونه بعد از اینکه 3-step handshake انجام شد حالا کلاینت ما یه درخواست http میفرسته و منتظر جواب میمونه و از اینجاست که کار ما شروع میشه . خب برای درک بهتر شروع کنیم به نوشتن کد .
اول یه پروژه جدید با Rust ایجاد میکنیم ...
cargo new webserver --bin
اینجا با کمک cargo ( که در واقع ابزار مدیریت بسته و پروژه Rust هست ) یه پروژه به نام webserver درست کردیم .
خب اولین کاری که باید بکنیم اینه که روی port ی که بهمون میگن منتظر یه کانکشن tcp بمونیم . فعلا این port رو ثابت بگیریم تا بعدا به عنوان ورودی از کاربر بگیریمش .
برای اینکه روی یه port گوش بدیم از لایبرری استاندارد Rust و ماژول TcpListener استفاده میکنیم .
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
خب با این خط کد ما یه TcpListener درست کردیم که روی لوکال هاست و پورت 8080 گوش میده و منتظر یه کانکشن Tcp میمونه .
خب حالا فرض کنیم یه کانکشن اومد چی کارش کنیم ؟
for stream in listener.incoming() { let mut stream = stream.unwrap();
خب الان با اومدن هر ریکویست ما اون رو دریافت میکنیم و میتونیم باهاش هرکاری بخوایم بکنیم !
اول بیاین بفهمیم یه ریکویست http به صورت خام چه شکلیه ؟
let mut buffer = [0; 512]; stream.read(&mut buffer).unwrap(); let buffer = String::from_utf8_lossy(&buffer[..]) println!("{}", buffer)
خب در اینجا ما یه آرایه 512 تایی از اعداد درست میکنیم که در واقع ریکویست ما قبل از تبدیل شدن به رشته توی اون قرار بگیره و بعد ریکویست رو میخونیم و توی این آرایه قرار میدیم و در مرحله بعد اون رو تبدیل به یک String میکنیم .
خب حالا وقتی Println انجام میشه یه همچین خروجی خواهیم داشت ...
GET / HTTP/1.1
Host: localhost:7878
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en;q=0.9,en-US;q=0.8,fa;q=0.7,la;q=0.6,ar;q=0.5
Cookie: csrftoken=SLaqNbhHwKU
** یه نکته در مورد سینتکس اگر نا آشناس اینه که Rust از یک سیستم جدید در کامپایلرش استفاده میکنه و برای اینکه بتونه کد های مدیریت حافظه رو تولید کنه نیاز داره با بهش کمک کنیم به همین خاطر بعضی وقتا کدای Rust یه خرده عجیب به نظر میان **
خب حالا بیایم به ازای هر ریکویست یه جواب ثابت رو بفرستیم .
let response = "HTTP/1.1 200 OK\r\n\r\nHelloAgain"; stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap();
با اجرای کد و رفتن به صفحه localhost:7878 این صفحه رو میبینید
خب تونستیم که اولین صفحه وب رو بدون استفاده از هیچ لایبرری و صرفا Tcp نشون بدیم مرحله بعد چیه ؟
اینکه به جای متن ساده یه صفحه html رو نشون بدیم .
برای اینکار باید اول یک فایل html رو بخونیم
let contents = fs::read_to_string("hello.html").unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
برای اینکار کافیه کد های بالا رو تغییرات بدیم تا جواب ریکویست شامل متن کامل یک فایل مثلا hello.html باشه . حالا فرض کنیم شما html رو مثلا به این شکل مینویسید
<html lang="en"> <head> <meta charset="utf-8"> <title>Hello!</title> </head> <body> <h1>Hello</h1> <p>Salam man be to yare ghadimi</p> </body> </html>
این فایل رو در روت پروژه یعنی جایی که فایل های .toml وجود دارن قرار میدیم .
خب بازم اگر برنامه مون رو اجرا کنیم اینبار صفحه html بالا رو بهمون نشون میده
عالی شد نه ؟
البته این برنامه فقط برای این خوبه که بفهمیم http اونقدرا هم پیچیده نیست و چطوری داره کار میکنه وگرنه برنامه ما تا یه وب سرور مثل IIS یا Apache یا Nginx خیلی خیلی فاصله داره .
مثل همیشه نظراتتون خوش حالم میکنه .