9.13. مقابس TCP

تأتي مقابس TCP بشكلين يبدوان مختلفين لكنهما يتشاركان النوع الأساسي نفسه: مقابس العميل التي تستخدم connect() للاتصال بخادم بعيد، ومقابس الخادم التي تستخدم bind() و listen() و accept() لقبول الاتصالات الواردة. يستخدم كلا الدورين الفئة socket نفسها التي قُدّمت في كائنات المقابس؛ تختلف فقط الطرق التي تُستدعى عليهما.

9.13.1. عميل TCP

أبسط عميل يفتح اتصالاً، ويرسل طلباً، ويقرأ الرد، ثم يغلق الاتصال:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))

s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)

s.close()

connect() ينفّذ المصافحة الثلاثية التي تناولها TCP -- تدفق موثوق للبايتات ويعود عندما يكون الاتصال مفتوحاً. send() يكتب البايتات إلى الاتصال؛ و recv() يقرأ حتى عدد معيّن من البايتات منه. وبمجرد انتهاء التطبيق، يغلق close() الاتصال.

البرنامج النصي نفسه مغلّف في صيغة عبارة with من كائنات المقابس، بحيث يُغلق المقبس حتى وإن أُثير خطأ ما:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("192.168.1.20", 9000))
    s.send(b"hello\n")
    print(s.recv(1024))

9.13.1.1. القراءة حتى الاكتمال

تعيد استدعاءة recv() الواحدة حتى العدد المطلوب من البايتات -- وقد تعيد عدداً أقل، لأن TCP عبارة عن دفق وليس تسلسلاً من الرسائل. على التطبيق أن يستمر في القراءة حتى يحصل على الرد الكامل:

chunks = []
while True:
    chunk = s.recv(1024)
    if not chunk:                  # empty bytes -> other side closed
        break
    chunks.append(chunk)
reply = b"".join(chunks)

تنتهي الحلقة عندما تعيد recv() كائن bytes فارغاً. يحدث ذلك عندما يغلق الطرف الآخر نصفه من الاتصال بشكل نظيف؛ ويقرأ التطبيق "نهاية الدفق" على أنها نفس "نهاية الرسالة" في هذا النمط من البروتوكولات.

9.13.1.2. الإرسال حتى الاكتمال

ينطبق التحذير المعاكس على send(): فقد يرسل عدداً أقل من البايتات المطلوبة، معيداً عدد البايتات التي كُتبت فعلياً. وبالنسبة للحمولات الكبيرة، أعد إرسال الجزء المتبقي الذي لم يُرسل:

payload = some_big_bytes
while payload:
    n = s.send(payload)
    payload = payload[n:]

sendall() ينفّذ الحلقة داخلياً، لذا يمكن لمعظم الشيفرة استدعاؤه وتجنب إعادة المحاولة اليدوية:

s.sendall(some_big_bytes)

9.13.2. خادم TCP

يتكون جانب الخادم من أربع خطوات: المطالبة بمنفذ، وتبديل المقبس إلى وضع الاستماع، وقبول الاتصالات واحداً تلو الآخر، والتواصل عبر كل مقبس مقبول. خادم صدى بسيط:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")

while True:
    conn, addr = server.accept()
    print("connection from", addr)

    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.send(data)            # echo back

    conn.close()

خطوة بخطوة:

  • bind() يطالب بمضيف ومنفذ على الكاميرا. تقبل "0.0.0.0" على أي واجهة؛ واستبدالها بعنوان IP محدد يقيّد المستمع على تلك الواجهة.

  • listen() يبدّل المقبس من مقبس عادي إلى مقبس مستمِع. والوسيط هو قائمة الانتظار (backlog) -- أي عدد الاتصالات المعلّقة التي ستضعها MicroPython في الطابور بينما يكون التطبيق مشغولاً. اختر عدداً صغيراً؛ والقيمة 1 مناسبة لمعظم الحالات.

  • accept() يحجب التنفيذ حتى يتصل عميل، ثم يعيد (conn, addr): مقبساً جديداً يمثّل هذا الاتصال الواحد، وعنوان العميل. ويبقى المقبس المستمع نفسه مفتوحاً لقبول المزيد.

  • تتدفق جميع بايتات المحادثة عبر conn، المقبس الجديد. وتستخدم القراءات والكتابات استدعاءات recv() / send() نفسها كما في جانب العميل.

  • عندما يغلق العميل، تعيد recv() القيمة b""؛ فتنتهي الحلقة الداخلية ويغلق الخادم طرفه باستخدام close().

تعود حلقة while True الخارجية إلى accept() لانتظار العميل التالي. يتعامل الخادم مع عميل واحد في كل مرة بهذا الشكل؛ ويتطلب تشغيل عملاء متعددين بالتوازي إما خيوطاً (threads) أو asyncio. والأخير هو موضوع الصفحة التالية.

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

  • اعتبار recv() ذا شكل رسالة. ليس كذلك. فاستدعاءان لـ send(b"hi") قد يصلان كـ recv(4) واحد بقيمة b"hihi"، أو كـ recv(2)ين. على التطبيق إضافة تأطير إن كانت حدود الرسائل مهمة -- سطر جديد، أو بادئة طول، أو أياً كان.

  • نسيان إعادة المحاولة عند الإرسال القصير. استخدم sendall() لأي شيء يتجاوز بضع مئات من البايتات.

  • نسيان إغلاق المقبس المقبول. كل conn هو مقبس منفصل؛ وإغلاق المقبس المستمع لا يغلق المقابس المقبولة. وكتل with على كليهما تجعل من الصعب الوقوع في الخطأ:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • إعادة الربط بمنفذ ما زال في حالة TIME_WAIT. عندما يُعاد تشغيل خادم خلال ثوانٍ قليلة من إغلاقه، قد يفشل bind() برسالة "العنوان قيد الاستخدام" لأن MicroPython ما زالت تحتفظ بالمنفذ للاتصال السابق. يؤدي server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) قبل bind() إلى إزالة ذلك.

9.13.4. ما التالي

الحجب على accept() يعني أن الخادم لا يمكنه خدمة سوى عميل واحد في كل مرة. والحجب على recv() يعني أن عميلاً بطيئاً واحداً يعلّق الحلقة بأكملها. والإجابة المعيارية على الكاميرا هي asyncio -- شغّل كل اتصال كمهمة خاصة به، ودع حلقة الأحداث توزّع بينها. تغطي الصفحة التالية إصدارات asyncio لكل ما في هذه الصفحة.