Probe

اتّصال TCP: خادم وعميل

الشبكات 1يوم السبتالجلسة 3

استخدم الأسهم أو اسحب للتنقّل

مقدّمة

برنامجان يتكلّمان عبر TCP

حتّى الآن تكلّمنا عن العناوين. اليوم نكتب برنامجين بـ Python: خادم (server) ينتظر، وعميل (client) يتّصل به ويرسل رسالة ويستلم ردّاً.

نستعمل بروتوكول TCP: يفتح اتّصالاً أوّلاً، ثمّ يضمن أنّ البايتات تصل كاملة وبالترتيب. سنقرأ الكود سطراً سطراً، ثمّ نشغّل البرنامجين معاً خطوةً خطوة ونرى ما يمرّ على الشبكة.

Socket

عنوان IP + منفذ

الـ socket نقطة نهاية للاتّصال، يُعرَّف بعنوان IP ورقم منفذ (port). العنوان يوصل للجهاز، والمنفذ يوصل للبرنامج داخله.

الخادم هنا على 127.0.0.1 (الجهاز نفسه، Loopback) والمنفذ 3450. والعميل يأخذ منفذاً عشوائيّاً يختاره النظام.

socket.socket(AF_INET, SOCK_STREAM): ‏AF_INET عناوين IPv4، وSOCK_STREAM يعني TCP.

الخادم

127.0.0.1:3450
ثابت ومعروف، ينتظر.

العميل

127.0.0.1:51544
منفذ مؤقّت، يبدأ الاتّصال.

server.py

الخادم: bind ← listen ← accept

يربط نفسه بعنوان ومنفذ، ثمّ يستمع، ثمّ ينتظر عميلاً. لكلّ عميل حلقة داخليّة تقرأ ما يصل وتردّ عليه، حتّى يُغلق العميل.

اضغط على أيّ سطر في الكود لترى شرحه.
server.pyاضغط على سطر
السطر 2 ينشئ socket: ‏AF_INET لعناوين IPv4، وSOCK_STREAM لـ TCP.
client.py

العميل: connect ← send ← recv

العميل أبسط: يتّصل بعنوان الخادم ومنفذه، يرسل رسالتين، يقرأ الردّ مرّة واحدة، ثمّ يغلق ويطبع.

العميل لا يحتاج bind: النظام يعطيه منفذاً مؤقّتاً عند connect.
client.pyاضغط على سطر
السطر 2 socket من النوع نفسه: IPv4 وTCP.
المحاكي

شغّل الخادم والعميل خطوةً خطوة

اضغط «شغّل الخادم»: كلّ ضغطة تنفّذ سطراً واحداً من البرنامجين بالترتيب الذي يحدث فعلاً.
server.pyلم يبدأ
1import socket
2serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3serv.bind(('127.0.0.1', 3450))
4serv.listen(5)
5while True:
6 conn, addr = serv.accept()
7 from_client = ''
8 while True:
9 data = conn.recv(4096)
10 if not data: break
11 from_client += data.decode('utf8')
12 print (f'From client: {from_client}')
13 conn.send("I am SERVER\n".encode())
14 conn.close()
15print ('client disconnected and shutdown')
طرفيّة الخادم
…
على الشبكة
    client.pyلم يبدأ
    1import socket
    2client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    3client.connect(('127.0.0.1', 3450))
    4a = input("enter message: ")
    5client.send("I am CLIENT\n".encode())
    6client.send(a.encode())
    7from_server = client.recv(4096)
    8client.close()
    9print (from_server.decode())
    طرفيّة العميل
    …
    إضافة: Stream

    TCP تيّار بايتات، لا رسائل

    العميل يستدعي send مرّتين، لكنّ TCP لا يحفظ أين تنتهي كلّ رسالة: قد تصل البايتات في دفعتين أو دفعة واحدة، حسب سرعة الشبكة والتوقيت.

    لهذا يجمع الخادم في from_client ولا يعتمد على أنّ كلّ recv رسالة كاملة. والبرامج الحقيقيّة تضع فاصلاً (مثل \n) أو طول الرسالة أوّلها.

    recv #1"I am CLIENT\n"
    recv #2"hello"
    طرفيّة الخادم
    From client: I am CLIENT
    From client: I am CLIENT
    hello

    الخادم يردّ 2 مرّتين بـ I am SERVER (ردّ لكلّ recv)، فيطبع العميل سطرين (إن وصل الردّان قبل أن يقرأ).

    إضافة: ما تلاحظه حين تشغّله

    أشياء ستصادفها في المخبر

    • شغّل الخادم أوّلاً. إن شغّلت العميل قبله: ConnectionRefusedError، لأنّ لا أحد يستمع على المنفذ 3450.
    • الخادم يخدم عميلاً واحداً في كلّ مرّة: ما دام في الحلقة الداخليّة لا يعود إلى accept. العميل الثاني ينتظر في طابور listen(5).
    • أعدت تشغيل الخادم فظهر OSError: Address already in use؟ المنفذ ما زال محجوزاً لحظات بعد الإغلاق. انتظر قليلاً أو أضف قبل bind: serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    • السطر الأخير في server.py لا يُطبع أبداً، والخادم يتوقّف بـ Ctrl + C.
    • العميل يقرأ مرّة واحدة: إن تأخّر الردّ الثاني فقد يطبع I am SERVER مرّة واحدة فقط.
    التشغيل الحقيقيّ

    طرفيّتان على جهاز واحد

    1. افتح طرفيّة في مجلّد الجلسة وشغّل الخادم: python server.py. لن يطبع شيئاً: هو ينتظر عند accept.
    2. افتح طرفيّة ثانية وشغّل العميل: python client.py، واكتب رسالة.
    3. ستطبع طرفيّة الخادم ما وصل، وطرفيّة العميل الردّ. شغّل العميل مرّة أخرى: الخادم ما زال يعمل.
    4. تحدٍّ: غيّر المنفذ في أحد الملفّين فقط. ماذا يحدث؟ ولماذا؟
    اختبار الجلسة