9.12. שקעי UDP

תעבורת UDP ב-Python נשלחת ומתקבלת באמצעות שתי מתודות על שקע דאטגרם: sendto() כדי לשגר דאטגרם אל יעד נבחר, ו-recvfrom() כדי לקבל דאטגרם ולגלות מהיכן הגיע. כל קריאה מעבירה הודעה אחת עצמאית; אין מצב חיבור.

9.12.1. שליחת דאטגרם

שליחת ה-UDP הפשוטה ביותר היא שורת Python אחת מעל בנאי שקע:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()

זה שולח b"hello" אל פורט 9000 בכתובת 192.168.1.20 והולך הלאה. MicroPython בוחר פורט מקור ארעי; הסקריפט אינו חייב לקשר דבר.

שליחת אותו מטען ליעדים רבים היא פשוט לולאה – השקע ניתן לשימוש חוזר בין שליחות, ואין חיבור להקים:

targets = [
    ("192.168.1.20", 9000),
    ("192.168.1.21", 9000),
    ("192.168.1.22", 9000),
]

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    for addr in targets:
        s.sendto(b"hello", addr)

9.12.2. קבלת דאטגרם

כדי לקבל דאטגרמים, על השקע לתפוס פורט ידוע שבו השולחים ישתמשו כיעד שלהם. זוהי הקריאה bind()

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    data, src = s.recvfrom(1024)
    print("from", src, "got", data)

הכתובת "0.0.0.0" משמעה ”כל ממשק IPv4 במצלמה“ – בכל ממשק Wi-Fi או Ethernet שמכניס מנות, פורט 9000 שייך לשקע זה.

הארגומנט 1024 של recvfrom() הוא המספר המרבי של בייטים לקריאה אל החוצץ (buffer) המוחזר. דאטגרמים של UDP מעל גודל זה יקוצצו; בחר את הערך כך שיתאים לדאטגרם הגדול ביותר שהאפליקציה מצפה לו.

recvfrom() מחזירה (data, src): הבייטים שהתקבלו, וכתובת השולח. כתובת השולח היא זו שאליה יש להשיב, מה שמקל על כתיבת פרוטוקול קטן של בקשה/תשובה:

while True:
    request, src = s.recvfrom(1024)
    if request == b"ping":
        s.sendto(b"pong", src)

כברירת מחדל recvfrom() חוסמת עד שמגיע דאטגרם. הדפוסים להפיכתה ללא-חוסמת – פסקי זמן, שקעים לא-חוסמים, asyncio – מופיעים ב-Sockets עם asyncio.

9.12.3. בקשה ותשובה

שני סקריפטים קצרים: אחד שולח בקשה, אחד מקבל ומשיב.

המקבל:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    req, src = s.recvfrom(64)
    print("got", req, "from", src)
    s.sendto(b"ack: " + req, src)

השולח:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    s.settimeout(2.0)                              # 2 s reply window
    s.sendto(b"ping", ("192.168.1.20", 9000))
    try:
        reply, _ = s.recvfrom(64)
        print("reply:", reply)
    except OSError:
        print("no reply in 2 s -- packet lost?")

כמה דברים שכדאי לשים לב אליהם בשולח:

  • אין bind() ואין connect(). לקוחות UDP פשוט שולחים.

  • settimeout() מציבה מועד אחרון על קריאת הקבלה. אם לא מגיעה תשובה בתוך שתי שניות, הקריאה מעלה OSError במקום לחסום לנצח – דרך טבעית לזהות מנה שאבדה.

  • בלוק ה-with סוגר את השקע אוטומטית.

9.12.4. מגבלות גודל דאטגרם

דאטגרמים של UDP יכולים להגיע עד כ-64 KB תיאורטית, אך המגבלה המעשית קטנה בהרבה. לכל קישור בנתיב שבין השולח למקבל יש יחידת שידור מרבית (MTU) – הבלוק הבודד הגדול ביותר של בייטים שאותו קישור יכול לשאת בפריים אחד. גם Ethernet וגם Wi-Fi מגבילים זאת לכ-1500 בייטים, וכמעט כל נתיב באינטרנט מתחקה בסופו של דבר אחר מגבלה זו במקום כלשהו.

כאשר דאטגרם חורג מה-MTU של קישור שעליו לחצות, שכבת הרשת מפצלת אותו לפרגמנטים קטנים יותר ומרכיבה אותם מחדש ביעד. UDP עצמו לעולם אינו רואה את הפיצול, אך לפרגמנטים יש כמה תכונות לא נוחות:

  • אם פרגמנט בודד אובד, כל הדאטגרם נזרק במקבל – אין שידור חוזר לכל פרגמנט. הסתברות האובדן גדלה עם מספר הפרגמנטים.

  • כמה רשתות וחומות אש זורקות מנות מפוצלות לחלוטין, ומתייחסות אליהן כחשודות.

  • ההרכבה מחדש עולה בזיכרון אצל המקבל, שעל מיקרו-בקר נמצא במחסור.

הכלל המעשי במצלמה: שמור הודעות UDP הרבה מתחת ל-1500 בייטים. כ-1400 בייטים משאירים מקום לכותרות ה-IP וה-UDP, לכל תקורת מנהור שהנתיב מוסיף, ולשינויים קטנים ב-MTU בין קישורי Ethernet, Wi-Fi ו-VPN. אפליקציות שצריכות לשלוח יותר מכך צריכות או לחלק את הנתונים בשכבת האפליקציה או לעבור ל-TCP, שמטפל בפיצול ובהרכבה מחדש אוטומטית.

9.12.5. מלכודות נפוצות

  • שכחה ש-UDP יכול לאבד מנות. קוד שעובד באופן מושלם ברשת מקומית שקטה לעיתים נכשל בדרכים עדינות ברשת עמוסה או רחבה יותר. תמיד תכנן עבור האפשרות שההודעה לא הגיעה.

  • המקבל אינו מקושר לפני שהשולח שולח. דאטגרם שנשלח לפורט שאיש אינו מאזין לו נזרק בשקט. הפעל תחילה את המקבל.

  • שליחת דאטגרם גדול מה-MTU של הנתיב. ראה את הסעיף הקודם – שמור הודעות מתחת ל-~1400 בייטים.

הדפוסים שלמעלה מכסים כמעט כל שימוש UDP שהמצלמה נזקקת לו. העמוד הבא עושה את המקבילה עבור TCP.