9.12. مقابس UDP

يُرسَل ويُستقبَل حركة UDP في Python بطريقتين على مقبس مخطط بيانات (datagram): 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() هو الحد الأقصى لعدد البايتات التي تُقرأ في المخزن المؤقت المُعاد. وستُقتطع مخططات بيانات UDP التي تتجاوز هذا الحجم؛ فاختر القيمة لتطابق أكبر مخطط بيانات يتوقعه التطبيق.

recvfrom() يعيد (data, src): البايتات المستقبَلة، وعنوان المرسِل. وعنوان المرسِل هو ما يُرد عليه، مما يسهّل كتابة بروتوكول صغير من نوع طلب/استجابة:

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

بشكل افتراضي، يحجب recvfrom() التنفيذ حتى يصل مخطط بيانات. وأنماط جعله لا يحجب -- المهل الزمنية، والمقابس غير الحاجبة، و asyncio -- موجودة في المقابس مع 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 كيلوبايت، لكن الحد العملي أصغر بكثير. فكل وصلة في المسار بين المرسِل والمستقبِل لها وحدة نقل قصوى (MTU) -- أكبر كتلة واحدة من البايتات يمكن لتلك الوصلة حملها في إطار واحد. ويحدّ كل من Ethernet و Wi-Fi هذا عند نحو 1500 بايت، وتعود جميع مسارات الإنترنت تقريباً إلى ذلك الحد في مكان ما.

عندما يتجاوز مخطط بيانات وحدة النقل القصوى لوصلة عليه أن يعبرها، تقسمه طبقة الشبكة إلى أجزاء (fragments) أصغر وتعيد تجميعها عند الوجهة. ولا يرى UDP نفسه التقسيم أبداً، لكن للأجزاء عدة خصائص غير مريحة:

  • إذا فُقد أي جزء واحد، يُسقَط مخطط البيانات بأكمله عند المستقبِل -- إذ لا توجد إعادة إرسال لكل جزء على حدة. وتنمو احتمالية الفقد مع عدد الأجزاء.

  • تُسقِط بعض الشبكات والجدران النارية الحزم المجزأة بالكامل، معاملةً إياها على أنها مشبوهة.

  • تكلّف إعادة التجميع ذاكرة عند المستقبِل، وهي على المتحكم الدقيق نادرة التوفر.

القاعدة العملية على الكاميرا: أبقِ رسائل UDP أقل بكثير من 1500 بايت. فنحو 1400 بايت يترك مجالاً لترويسات IP و UDP، ولأي حِمل أنفاق (tunneling) يضيفه المسار، وللتغيرات الصغيرة في وحدة النقل القصوى بين وصلات Ethernet و Wi-Fi و VPN. وعلى التطبيقات التي تحتاج إلى إرسال أكثر من ذلك إما تقسيم البيانات على طبقة التطبيق أو التحول إلى TCP، الذي يتولى التقسيم وإعادة التجميع تلقائياً.

9.12.5. المزالق الشائعة

  • نسيان أن UDP يمكن أن يفقد الحزم. الشيفرة التي تعمل بشكل مثالي على شبكة محلية هادئة قد تفشل أحياناً بطرق خفية على شبكة أكثر ازدحاماً أو أوسع. صمّم دائماً مع احتمال أن الرسالة لم تصل.

  • عدم ربط المستقبِل قبل أن يرسل المرسِل. مخطط البيانات المُرسَل إلى منفذ لا يستمع عليه أحد يُسقَط بصمت. ابدأ المستقبِل أولاً.

  • إرسال مخطط بيانات أكبر من وحدة النقل القصوى للمسار. انظر القسم السابق -- أبقِ الرسائل أقل من ~1400 بايت.

تغطي الأنماط أعلاه تقريباً كل استخدام لـ UDP تلجأ إليه الكاميرا. وتفعل الصفحة التالية المكافئ لـ TCP.