יום 5 - מימוש שרת Echo ב Rust
אחרי כל הדברים היפים שמצאתי ב Rust, היום גיליתי את שני הדברים שעדיין חסרים - תמיכה מובנית במודל תכנות אסינכרוני ותמיכה ב Green Threads. בואו נראה את שתי הבעיות דרך בניית שרת Echo.
1. מה אנחנו בונים
נרצה לבנות שרת שמאזין לבקשות, כל פעם שמשתמש מתחבר (פשוט באמצעות TCP Socket) השרת יקרא שורה מהמשתמש, יכתוב אותה בחזרה לאותו Socket ואז ימתין לשורה הבאה. בשביל לדבר עם כמה לקוחות במקביל השרת ישתמש ב Threads, ונפתח Thread לכל לקוח חדש שמתחבר.
2. קוד התוכנית
הקוד לא ארוך וילמד אותנו איך לעבוד ברשת ובצורה מקבילית ב Rust. קודם הדבקה אחרי זה הסברים:
use std::io::{Write, BufReader, BufRead, BufWriter};
use std::net::TcpListener;
use std::thread;
fn main() {
let listener = TcpListener::bind("127.0.0.1:9123").unwrap();
println!("listening started, ready to accept");
for stream in listener.incoming() {
thread::spawn(|| {
let stream = match stream {
Ok(s) => s,
Err(_) => return,
};
let mut reader = BufReader::new(&stream);
let mut writer = BufWriter::new(&stream);
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => return,
Ok(l) => l,
Err(_) => return,
};
let output = format!("You Said: {}", &line);
if let Err(_) = writer.write(output.as_bytes()) {
return;
}
if let Err(_) = writer.flush() {
return;
}
}
});
}
}
3. שימו לב לשני הקווים האנכיים
הפקודה bind מתחילה להאזין ל Port ואפילו בלי לקרוא עליה אנחנו יודעים שהיא מחזירה Result, בגלל שהקוד מפעיל unwrap עליו כדי לקבל את מה שבפנים. אם לא הצלחנו לתפוס את הפורט זה בסדר לסיים את התוכנית ולא להפעיל את השרת. גם לולאת ה for בשורה הבאה לא צריכה להפתיע.
אבל מה שקורה בתוך הלולאה זה סיפור אחר:
thread::spawn(|| {
הפקודה thread::spawn
מתחילה Thread חדש. היא מקבלת פרמטר מסוג שעדיין לא נתקלנו בו ונקרא Closure. קלוז'ר הוא בעצם פונקציה שיכולה "לתפוס" את הסביבה שלה, ולגשת למשתנים שהוגדרו מעליה. זה מבנה שאנחנו מכירים משפות דינמיות רבות ובמיוחד JavaScript ו Python. הקלוז'ר של thread::spawn מקבלת זה הקוד שה Thread יריץ, כלומר הקוד שירוץ במקביל לתוכנית הנוכחית.
אפשר להגיד שכל איטרציה של הלולאה מתחילה פעולה ברקע, ולכן הלולאה מסתיימת ממש מהר. הפעולות ברקע יכולות להיות איטיות אבל הן לא מפריעות להתקדמות הרגילה של השרת, שמהר מאוד יחזור לחפש עוד חיבורים חדשים.
ה Thread ש thread::spawn מייצרת הוא תהליכון של מערכת ההפעלה ולא Green Thread. מצד אחד זה אומר שהקוד שלנו קרוב יותר לברזלים, אבל מצד שני אנחנו מפסידים ביצועים טובים יותר שהיינו יכולים לקבל מספריית ה Thread-ים של ראסט. מה שיותר צורם בקוד הזה הוא הבחירה לעבוד ב Thread-ים במקום במודל פיתוח אסינכרוני, שעדיין לא מובנה בשפת Rust. יש ספריית עזר בשם tokio שכן מאפשרת את המודל האסינכרוני, ואנחנו עוד נחזור לדבר עליה.
מבחינת הקוד שרץ בתוך ה Thread, שם לא רציתי לרסק את התהליכון בשום מצב ולכן הקפדתי להשתמש ב match כדי לזהות שגיאות ולא ב unwrap. מבנה כזה יכול לעזור אם חשוב לנו לסגור את החיבור בצורה מסודרת ולא להראות שגיאות בלוג של השרת, אבל ממשחקים שעשיתי גם unwrap עובד די טוב והשרת שורד בעיות תקשורת אצל לקוח אחד בלי לפגוע בשאר הלקוחות.
4. תרגילים להרחבה
חושבים שהבנתם איך שרת ראסט עובד? בואו נלך לבעוט בו קצת:
הפעילו את השרת שיאזין לפורט 9123. השתמשו ב telnet או nc כדי להתחבר אליו מחלון אחר ותראו שאתם מצליחים לשלוח הודעה ולקבל אותה חזרה.
הוסיפו מנגנון שסופר כמה לקוחות פעילים יש. כל פעם שלקוח מתחבר יש להדפיס במסך של השרת הודעה שלקוח חדש התחבר וכמה לקוחות כרגע יש. כל פעם שלקוח מתנתק יש להדפיס הודעה דומה במסך של השרת.
הפכו את שרת ה Echo לשרת Chat - שמרו וקטור של כל הלקוחות שכרגע מחוברים, וכל פעם שמישהו שולח הודעה שלחו את ההודעה הזאת לכל הלקוחות האחרים.
הפקודה
io::copy
של ראסט מקבלת Reader ו Writer, ומעתיקה את כל התוכן של ה Reader ל Writer. אפשר לקרוא עליה בתיעוד כאן https://doc.rust-lang.org/std/io/fn.copy.html. החליפו את קוד הקריאה והכתיבה בקריאה ל copy, כדי שהמערכת תעבוד גם על קלט שלא מכיל תווי ירידות שורה.
מוזמנים להדביק את הפיתרונות או שאלות אם יש לכם כאן בתגובות, ואנחנו נמשיך מחר לכתוב משחק קצר ב Rust.