בפוסט הקודם דיברתי על TCP. הגיע הזמן למבט מעמיק על UDP. אי אפשר להבין את הפוסט הזה בלי לקרוא את הקודם אז ממליץ לכם לעבור לפחות על ההתחלה שלו ולחזור לפה. גם פה אני מדגים באמצעות Wireshark וכיוון שהפעם אין לי דפדפן או curl, אני אדגים עם קליינט ושרת בפייתון. לא צריך לדעת פייתון או אפילו תכנות והקוד הוא שולי להבנה.
אז מה זה UDP? מדובר בפרוטוקול שגם הוא יושב בשכבה 4 כמו tcp. ראשי התיבות שלו הם User Datagram Protocol ובניגוד ל-tcp שהוא מובנה יותר ויש לו זרימה של ״משלוח ואז קבלה״ ב-udp זה ברדק מאורגן – שולחים את ההודעה? הגיע? הגיע. לא הגיע? לא נורא. כתוצאה מכך זה פרוטוקול מהיר, שמאפשר משלוח של המון מידע בזמן קצר ומעולה לדברים מסוימים.
זה לא אומר ש-udp לא בנוי לאמינות. אפשר לבנות מעליו פרוטוקול אמין (למשל QUIC שאותו ניתחתי באופן מעמיק). אבל זה בשכבות למעלה. בשכבה 4 אנחנו מדברים על הצינור והצינור פה הוא כמו איזה צינור גומי בגינה שהמים נשפכים ממנו בלי הכרה ופחות כמו איזה מלצר במסעדה.
הכנת הסביבה
ב-tcp יש לנו דפדפן או כלים כמו curl. ב-udp קצות פחות – אני יכול להדגים עם QUIC או עם מימושים אחרים אבל אני רוצה פשטות ובשביל פשטות ב-udp אני אצטרך לממש גם קליינט וגם שרת. אני אעשה את זה בפייתון. גם אם אתם לא יודעים פייתון זה לא משנה. כי אני מסתכל ב-wireshark על התנועה. אני שם פה את הקוד רק לרפרנס וגם ככה השתמשתי ב-LLM לייצר אותו.
השרת
# udp_echo_server.py
import socket
HOST = "0.0.0.0"
PORT = 9999
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((HOST, PORT))
print(f"UDP echo server on {HOST}:{PORT} (Ctrl+C to stop)")
try:
while True:
data, addr = sock.recvfrom(65535) # קבלת Datagram
print(f"from {addr}: {data!r}")
sock.sendto(data, addr) # Echo חזרה
except KeyboardInterrupt:
pass
finally:
sock.close()
את השרת אני מריץ עם uv שהוא באמת ברירת המחדל שלי בפייתון. uv run udp_echo_server.py
הקליינט
# udp_client_sequence.py
import socket
import time
SERVER = ("127.0.0.1", 9999) # כתובת השרת שלך
COUNT = 10 # מספר ההודעות לשליחה
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1.0)
for i in range(1, COUNT + 1):
msg = f"message {i}".encode()
print(f"-> sending: {msg.decode()}")
sock.sendto(msg, SERVER)
try:
data, addr = sock.recvfrom(65535)
print(f"<- received: {data.decode()}")
except socket.timeout:
print("(timeout waiting for response)")
time.sleep(0.5) # שיהיה קל לראות ב-Wireshark
sock.close()
print("done.")
הנה הקליינט, אפילו השארתי את ההערות של ה-LLM. כאמור הפייתון לא מעניין וזו רק דרך עבורנו להריץ בקשות udp כדי לבחון אותן.
אז אחרי שהרצתי את השרת ואז הרצתי את הקליינט והקפדתי שה-wireshark יהיה פתוח, אני אשים לו את הפילטר:
udp.port == 9999
זו התוצאה שאני אראה – 20 פקטות. 10 שהקליינט שלח ו-10 שהשרת החזיר בתגובה. איך אני מבדיל בינהן? בגדול מדובר פה באותו IP אז ההבדל הוא בפורט. השרת נמצא בפורט 9999, הקליינט שולח מפורט זמני, במקרה שלי זה 58814.

נכנס לתוך הבקשה הראשונה

הדבר הראשון שבולט פה יחסית ל-tcp שאין לי כאן לחיצת יד, ליטרלי הכל נשלח. אין כאן דרמה וניסיון ליצור צינור כמו ב-tcp אלא פשוט נשלח. הצד השני לא מקבל? בעסה לו. כן מקבל? אחלה לו. אבל זה לא מעניין את הקליינט. הוא שולח ולא מחכה לכלום. החבילה הרבה יותר קטנה ורזה ויש בה כבר גם את המידע שנשלח. במקרה שלנו message 1.
6d 65 73 73 61 67 65 20 31 message 1
ב-UDP אין Handshake או ניהול מצב; כל הודעה נשלחת כיחידה עצמאית לחלוטין, ללא קשר להודעות שקדמו לה או שיגיעו אחריה. אם אחת מההודעות תאבד בדרך, הפרוטוקול לא ינסה לשלוח אותה מחדש, משום שאין בו שום מנגנון של אמינות, מספרי רצף או אישורי קבלה. גם אין בו זרימה דו-כיוונית “רשמית” כמו ב-TCP. כל צד פשוט שולח Datagram כשהוא רוצה. המשמעות היא שכל אחריות על אמינות התקשורת, שמירת סדר ההודעות, או טיפול באובדן נתונים נופלת על שכבות יותר גבוהות, כמו שכבה 7 שצריכה לממש את הלוגיקה הזו במפורש אם היא בכלל יש בה צורך.
בואו ונראה תשובה של השרת, היא מאד דומה לשליחה. כאמור, אין פה דרמה או מורכבות.

זו בצילום המסך רואים שזה UDP שהשרת שולח מהפורט 9999 (הפורט שעליו הוא מאזין) אל הפורט 58814, כאמור הפורט הזמני שהקליינט השתמש בו כששלח את הבקשה. זה כבר אומר לנו שהשרת ביצע את מה שציפינו ממנו: הוא החזיר תשובה לכתובת ולפורט שממנה הגיע ה-Datagram המקורי.
האורך הכולל של החבילה הוא 17 בתים – בדיוק כמו בבקשה, כלומר 8 בתים של כותרת UDP ועוד 9 בתים של נתונים ("message 1"). גם ה־Checksum זהה (0xfe24), מה שאומר שהתוכן לא השתנה כלל והחבילה שלמה וללא שגיאות. ה־timestamp מציין שהתגובה נשלחה 714 מיקרו-שניות אחרי הבקשה.
בחלק של ה־payload מופיעים 9 בתים, שהם בדיוק אותם הנתונים שהקליינט שלח. אין שדות נוספים, לא נוספה כותרת פרוטוקול עליונה, ולא הוחזר שום קוד סטטוס או אישור כמו ב־HTTP. זו פשוט החזרה ישירה של התוכן. במילים אחרות, זו המחשה מדויקת של Echo over UDP: השרת אינו “יודע” דבר על מצב החיבור או על ההיסטוריה של ההודעות, אלא רק משיב לכל Datagram שנכנס אליו.
לסיכום
זהו, אין דרמה. כלומר לא בשכבה הזו. בדרך כלל בשכבות אחרות אנחנו דואגים ללוגיקה. למשל לבדיקה שלא נפל מידע בדרך (את זה אפשר לראות ב-QUIC) או שמתעלמים ממשהו שנפל בדרך (די מקובל בסטרימינג). בניגוד ל-tcp שאפשר לסמוך עליו, ב-udp אין על מי לסמוך והעבודה היא על מי שמממש את הקליינט ואת השרת. זה אומר מצד אחד שיש לנו כאן כאב ראש אבל מהצד השני שאנחנו לא חייבים לממש את מה שאין לנו צורך בו. אפשר לראות ש-udp הוא רזה מאד יחסית ל-tcp וזה אומר חסכון עצום במשאבים כאשר אנחנו בסקייל גדול.
וזו הסיבה שכדאי להכיר את udp. אם אתם בוני אתרים או מפתחי ווב לסביבות קטנות – הכל בסדר. אבל ברגע שמטפסים גבוה בסקייל, יש מצב שאתם יכולים לחסוך המון כסף וזמן אם אתם מממשים דברים ב-udp. א-ב-ל – יש דברים שצריך להזהר מהם. כי יש למשל בעיות של אבטחה. ב-udp קל לזייף IP למשל (בניגוד ל-tcp) או לבצע התקפת DDOS. או בעיות של אמינות. אבל מהצד השני אפשר להשיג המון עם שימוש ב-udp.
מהצד שלי, זה תמיד מעניין לפתוח כלי ניטור כמו wireshark ולהציץ בקרביים של התקשורת. גם בשכבות יותר נמוכות. אני מתכנת, קל לי לראות דברים כשהם ממש בעיניים. אפשר לדבר תיאוריה על tcp vs udp אבל אין כמו ממש לראות את זה בכלי ולהבין דרך הידיים. אני ממליץ לכם לא להתעצל, להתקין wireshark ולבחון את התקשורת הזו ממש בידיים שלכם.






