בונים שרת צ'אט בפייתון עם סוקטים - כך נראה האינטרנט מתחת למכסה המנוע¶
כל אפליקציה שאי פעם שלחה הודעה, טענה עמוד אינטרנט, או סנכרנה נתונים עם שרת, עושה את זה בסוף על גבי אותו יסוד בדיוק - סוקטים (sockets). כשאתם משתמשים בפריימוורק כמו Flask או בספריית WebSocket מוכנה, כל הפרטים האלה מוסתרים מכם בכוונה כדי להקל על החיים. הפרויקט הזה עושה בדיוק ההפך - הוא חושף אתכם ליסוד עצמו, ומראה לכם איך שני מחשבים בכלל מדברים אחד עם השני.
בונים כאן שרת צ'אט אמיתי - כמה אנשים מתחברים, כותבים הודעות, וכל ההודעות מגיעות לכולם. זה נשמע פשוט, אבל זה נוגע ביסודות שהרבה מפתחים משתמשים בהם כל יום בלי להבין באמת מה קורה מתחתם.
שלב ראשון - להבין מה זה בכלל סוקט¶
סוקט הוא בעצם "קצה חוט" של חיבור בין שני מחשבים ברשת. הוא מזוהה על ידי כתובת IP ומספר פורט. ה-TCP, הפרוטוקול שנשתמש בו, מבטיח שהנתונים יגיעו בסדר הנכון ובשלמותם - בניגוד לפרוטוקולים אחרים שמוותרים על האחריות הזאת בשביל מהירות.
המודול socket בפייתון מובנה, ואין צורך בשום התקנה. שרת סוקטים בסיסי עושה בעצם ארבעה דברים - יוצר סוקט, מקשר אותו לכתובת ופורט, מאזין לחיבורים נכנסים, ומקבל אותם:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 5000))
server.listen()
print("השרת מאזין על פורט 5000")
while True:
client, address = server.accept()
print(f"התחבר: {address}")
server.accept() הוא שורה מעניינת - התוכנית פשוט נעצרת שם ומחכה, ולא ממשיכה הלאה עד שמישהו מתחבר. זו הפעם הראשונה שרוב המתחילים חווים "המתנה חוסמת" (blocking) בפועל.
שלב שני - הבעיה של לקוח יחיד¶
הקוד למעלה עובד מצוין - כל עוד יש רק לקוח אחד. ברגע שלקוח שני מנסה להתחבר, הוא פשוט תקוע בתור, כי השרת עסוק בלטפל בלקוח הראשון. זו בדיוק הבעיה שהופכת "שרת שעובד" ל"שרת אמיתי" - צריך לטפל בכמה חיבורים בו זמנית.
הפתרון הנפוץ ביותר ללומדים הוא threading - כל חיבור לקוח מקבל תהליכון (thread) משלו, שרץ במקביל לשאר:
import threading
def handle_client(client, address):
while True:
message = client.recv(1024).decode()
if not message:
break
broadcast(message, client)
client, address = server.accept()
threading.Thread(target=handle_client, args=(client, address)).start()
זה המקום ללמוד למה תכנות מקבילי שונה כל כך מתכנות רגיל - כמה תהליכונים יכולים לרוץ "באותו זמן", ואם לא זהירים, שניים מהם עלולים לנסות לגעת באותו משתנה בו זמנית וליצור באגים שכמעט בלתי אפשר לשחזר. לפרויקט הזה זה עדיין לא בעיה דחופה, אבל שווה לדעת שהיא קיימת.
שלב שלישי - broadcast, הפיצוץ להודעה לכולם¶
הרעיון המרכזי של צ'אט הוא ששרת שמקבל הודעה מלקוח אחד צריך לשלוח אותה לכל שאר הלקוחות המחוברים. לשם כך שומרים רשימה של כל הסוקטים המחוברים כרגע, ובכל הודעה חדשה עוברים על הרשימה ושולחים אליה:
clients = []
def broadcast(message, sender):
for client in clients:
if client != sender:
client.send(message.encode())
זה שלב טוב להבין הבדל חשוב - השרת לא "יודע" כלום על העיצוב או הממשק. כל מה שהוא עושה זה להעביר בייטים ממקום למקום. כל השאר - איך זה נראה על המסך, איך נכתבת הודעה - זה תפקיד הלקוח.
שלב רביעי - כתיבת הלקוח¶
הלקוח הרבה יותר פשוט - הוא פשוט מתחבר לשרת, ואז מריץ שני תהליכים במקביל, אחד ששולח הודעות שהמשתמש מקליד, ואחד שמאזין להודעות שמגיעות מהשרת ומדפיס אותן. זה דוגמה נהדרת לכך שאותו רעיון - threading - שימושי גם בצד הלקוח, לא רק בצד השרת, כי בלעדיו הלקוח לא יכול גם לשלוח וגם לקבל בו זמנית.
מלכודות נפוצות¶
הבאג הכי שכיח הוא לשכוח שנתונים שמגיעים דרך recv הם בייטים, לא מחרוזת - צריך decode() כדי לקרוא אותם ו-encode() כדי לשלוח בחזרה. מלכודת נוספת היא לשכוח שהודעה יכולה להגיע "פרוסה" לכמה קריאות recv שונות, במיוחד אם היא ארוכה - בפרויקט לימודי אפשר להתעלם מזה, אבל שווה לדעת שזו בעיה אמיתית בפרוטוקולים אמיתיים ברשת.
למה זה שווה את המאמץ¶
אחרי הפרויקט הזה, כשתשתמשו ב-API, ב-WebSocket, או בכל ספריית רשת מוכנה, לא תראו בהם "קסם" - תדעו בדיוק אילו בעיות הן פותרות בשבילכם מתחת למכסה המנוע. זו הבנה שמבדילה בין מי שרק מרכיב ספריות אחת על השנייה לבין מי שבאמת מבין איך התוכנה שלו עובדת.
כדי להגיע לרמה שבה קוד עם threading וסוקטים מרגיש נגיש, צריך קודם בסיס טוב מאוד בפייתון - פונקציות, לולאות, וטיפול נכון בשגיאות. את זה בונים בקורס תכנות בסיסי החינמי שלנו, שלב אחר שלב, לפני שמגיעים לנושאים ברמת רשתות.
אם רשתות ותקשורת בין תוכניות נשמעות לכם עדיין כמו קופסה שחורה, קורס תכנות בסיסי ייתן לכם את היסודות הנדרשים כדי להבין באמת מה קורה שם, בחינם ובעברית.