برنامه نویسی سوکت و شبکه در پایتون (Socket Programming) قسمت اول

برنامه نویسی سوکت در پایتون

با سلام و درود، در این مطلب سعی می کنم یه مقدمه ای در مورد برنامه نویسی شبکه و سوکت در پایتون در خدمتتون داشته باشم.

مراجع این مطلب از قرار زیر می باشد:

سوکت ها و Socket API جهت ارسال پیام ها در طول شبکه استفاده می شوند. آنها فرمی از  inter-process communication (IPC) را فراهم می کنند.

شبکه و سوکت ها مباحث بزرگی هستند و درموردشان تحت اللفظی در کامپیوتر زیاد حرف زده شده است. اگر زمینه سوکت پروگرمینگ و شبکه برایتان تازه است این کاملا معمولی است که در ارتباط با واژگان و مفاهیم یه مقدار احساس دستپاچگی بکنید.

خب پس مسئله اصلی ارتباط بین کامپیوتر ها می باشد:

دو مسئله اصلی مطرح می شود:

  • آدرس دهی (Addressing)
    • تعیین کامپیوتر ریموت و سرویس ها
  • انتقال داده ها

آدرس دهی از طریق آدرس های IP انجام می گیرد. برنامه ها و سرویس ها نیز هر کدام شماره پورتی مخصوص به خود دارند:

یک سری شماره پورت برای سرویس های رایج داریم که از قبل انتخاب شده اند:

ا استفاده از دستور ss می تونید تمامی شبکه های فعال خود را مشاهده کنید، برای مشاهده لیستی اتصالات TCP کافیست از دستور زیر استفاده کنید:

برای آشنایی بیشتر با دستور SS به اینجا یه نگاهی بیاندازید.

مفهموم کلاینت/سرور Client/Server

رایج ترین نوع از اپلیکیشن های سوکت، اپلیکیشن های کلاینت-سرور می باشد،و در آنها یک طرف سرور را داریم که منتظر اتصال از سمت کلاینت ها هست.

اکثر برنامه های شبکه از مدل request/response برای پیام هایشان استفاده می کنند:

کلاینت پیام request را ارسال می کند (برای مثال Http):

GET /index.html HTTP/1.0

سرور پیام response را در جوابش می فرستد:

HTTP/1.0 200 OK
Content-type: text/html
Content-length: 48823
<HTML>
...

انتقال داده

دو نوع پایه برای ارتباطات و انتقال وجود دارد:

  • جریان ها (TCP – Streams) : کامپیوتر ها ارتباطی را با یکدیگر برقرار کرده و در جریانی متصل از بایت ها (همانند یک فایل) داده های خود را read/write می کنند. این رایج ترین نوع می باشد.
  • دیتاگرام (UDP) : کامپیوتر ها بسته (پیام) های جدا از هم را به یکدیگر ارسال می کنند. هر بسته شاملی تعدادی بایت می باشد، اما هر بسته جدا، مستقل و کامل می باشد.

سوکت : نقطه ی پایانی یک اتصال را سوکت می گویند. توسط ماژول کتابخانه سوکت پشتیبانی می شود. و در حقیقت اجازه ی برقراری اتصال و انتقال داده ها را می دهد.

مروری بر Socket API

ماژول سوکت پایتون واسطی (interface) از  Berkeley sockets API فراهم کرده است.در ادامه از این ماژول تاستفاده خواهیم کرد و در موردش صحبت نیز می کنیم.

توابع و متُد های اصلی Socket API از قرار زیر می باشند:

  • socket()
  • bind()
  • listen()
  • accept()
  • connect()
  • connect_ex()
  • send()
  • recv()
  • close()

به عنوان بخشی از کتابخانه استاندارد، پایتون همچنین کلاس هایی را برای انجام ساده تر توابع سطح پایین دارد. البته در این آموزش پوشش داده نشده است. همچنین ماژول هایی برای پیاده سازی پروتکل های اینترنت سطح بالا همچون Http نیز وجود دارد .

سوکت های TCP

همین طور که جلوتر خواهید دید، ما یک object از سوکت را با استفاده از socket.socket() خواهیم ساخت و نوع سوکت را از طریق socket.SOCK_STREAM مشخص می کنیم. زمانی که شما این کار را انجام دهید، پروتوکل پیش فرضی که استفاده می کنید پروتکل TCP (جریانی- stream) خواهد بود. (Transmission Control Protocol (TCP)) این پیشفرضی خوبی خواهد بود و شما هم در آینده تاییدش خواهید کرد 🙂

چرا باید از TCP استفاده کنید؟

چون که پروتکل کنرل انتقال (TCP):

  • قابل اطمینان: بسته های افتاده شده در شبکه قابل تشخیص هستند و توسط ارسال کننده انتقال داده می شود.
  • دارای تحویل داده به ترتیب است: داده ها د راپلیکیشن شما به تریتب ارسالی که توسط ارسال کننده نوشته شده است، خوانده می شوند.

در مقابل، سوکت های پروتکل دیتاگرام کاربر ( User Datagram Protocol – UDP) توسط socket.SOCK_DGRAM ساخته شده و قابل اطمینان نیست و داده ی خوانده شده توسط گیرنده ممکن خارج از ترتیب ارسال توسط ارسال کننده باشد.

در تصویر زیر توالی فراخوانی و جرییان داده در TCP را می بینید:

ستون سمت چپ نشان دهنده ی سرور است و ستون سمت راست کلاینت.

از قسمت بالا ستون سمت چپ شروع می کنیم. توجه داشته باشید API سرور را فراخوانی می کند تا آماده ی “listening” سوکت باشد:

  • socket()
  • bind()
  • listen()
  • accept()

شندین سوکت شبیه معنای خودش می باشد. شروع به شنیدن یک اتصال از سمت کلاینت می کند. زمانی که کلاینت متصل شد، سرور accept() را جهت پذیرفتن فراخوانی می کند یا اتصال را تکمیل می کند.

کلاینت نیز connect() را بجهت ساخت اتصال جدید به سرور و آغاز three-way handshake فراخوانی می کند. مزحله Handshakeیا دست تکام دادن مهم است چرا که تضمین می کند هر دو سمت اتصال داخل شبکه در دسترس هستند، به عبارت دیگر کلاینت به سرور دسترسی دارد و برعکس.

در وسط قسمت رفت و برگشت (round-trip) می باشد، جایی که داده بین کلاینت و سرور با استفاده از فراخوانی های send() و recv() ردوبدل می شوند (به تصویر بالا نگاه بیاندازید).

در انتها و قسمت پایین، کلاینت و سرور سوکت های قابل احترامشان را با استفاده از close() می بندند.

Echo Client and Server

حالا که یه مقدمه ای در مورد Socket API داشتیم و در مورد طریقه ارتباط کلاینت و سرور اطلاعات کسب شد، بیاید تا اولین کلاینت و سرورمان را بسازیم. در ابتدا با پیاده سازی ساده ای پیش می رویم. سرور به شادگی هر چه را که از سمت کلاینت دریافت می کند به سمت خود کلاینت پژواک – echo میکند.

Echo Server

یک سری نکات از پیاده سازی سرور گفته میشه و در ادامه وارد عمل می شیم :

  • شرور ها در شبکه یه مقدار مهارت بیشتری نیاز دارند
  • باید شنود جهت اتصال ورودی بر روی شماره پورتی که می شناسند داشته باشند
  • به صورت معمول برای همیشه روی Server-loop در حال اجرا هستند.
  • ممکن به چند تا کلاینت سرویس دهند.

در ادامه کد سرور را مشاهده می کنید ( echo-server.py ) :

#!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

توجه : در مورد فهم هر چیزی که در بالا امده در حال حاضر نگران نباشید. اینجا نقطه ی شروعی برای این است که یک سرور ساده را در عمل ببینید.

خب بیاید به صورت دقیق ببینیم با فراخوانی های API چه اتفاقاتی می افتد.

socket.socket() یک Object از Socket می سازد کهcontext manager type را پشتیبانی می کند، بنابراین می توانید با عبارت with از آن استفاده کنید. نیازی به استفاده از s.close() نمی باشد.

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    pass  # Use the socket object without calling s.close().

آرگومان های داده شده به socket() نوع سوکت و address family را مشخص می کند. ( Address family : IPv4 | IPv6)

  • AF_INET برای Ipv4 می باشد.
  • SOCK_STREAM برای نوع سوکت تی سی پی (TcP) می باشد.

از bind() به جهت پیوند دادن سوکت با واسط شبکه (network interface) و شماره پورت استفاده می شود:

HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)

# ...

s.bind((HOST, PORT))

مقادیر داده شده به bind() وابسته به  address family  سوکت هستند. در این مثال از socket.AF_INET (IPv4) استفاده شده است.در نتیجه انتظار دو آرگومان (هاست و پورت) را داریم

host می تواند نام هاست، آدرس آی پی یا یک String خالی باشد. اگر آدرس IP استفاده شده است، host باید آدرسی با فرمت IPv4 باشد. آدرس IP بشرح 127.0.0.1 آدرس استاندارد IPv4 برای لوکال هاست (واسط loopback) می باشد، در نتیجه فقط فرآیند های روی هاست می توانند به سرور متصل شوند. اگر String خالی به آن دهدی، سرور تمامی اتصالات از آدرس های IPv4 را می پذیرد.

port باید عدد اینتیجر در بازه 165535 باشد (عدد 0 رزرو شده است).

نکته: اگر در مورد هاست از hostname استفاده می کنید نکاتی وجود دارد و ممکن است برنامه رفتار غیر قطعی داشته باشد. بهتره به جای نام از IP استفاده کنید.

پس تپجه داشته باشید زمانی که از hostname استفاده می کنید ممکن بر اساس فرایند تجزیه نام، هر دفعه آدرس متفاوتی را مشاهده کنید.

این میتونه هر چیزی باشه، ممکن دفعه اول که اپلیکیشن را اجرا می کنید آدرس 10.1.2.3 باشد، دفعه بعدی آدرس 192.168.0.1 و دفعه سوم 172.16.7.8 و همین طور هر دفعه آدرس متفاوت باشد.

در ادامه مثال سرور، listen() سرور را برای accept() کردن اتصالات فعال می کند.

s.listen()
conn, addr = s.accept()

listen() دارای پارامتری به نام backlog است که تعداد اتصالات غیر قابل قبول، که سیستم قبل از امتناع اتصال جدید اجازه می دهد را مشخص می کند. در پایتون ۳.۵ این مقدار آپشنال است و اگر مقداری بهش ندهید مقدار پیش فرض انتخاب می شود.

اگر سرور شما اتصالات مجازی زیادی ا دریافت می کند، زیاد کردن مقدار backlog ممکن است به طول بیشینه صف برای اتصالات در انتظار کمک کند. مقدار بیشینه به وابسته به سیستم است. برای مثال دی لینوکس به این آدرس یه نگاهی بیاندازید :
/proc/sys/net/core/somaxconn

accept() بلو که می کند و منتظر یه اتصال ورودی می شود. زمانی که کلاینت متصل می شود، یک شیء سوکت جدید که نشان دهنده ی connection و tuple ای که آدرس کلاینت را نگهداری می کند، باز می گرداند (return می کند). این tuple شامل (host, port)  برای اتصالات IPv4 یا (host, port, flowinfo, scopeid) برای IPv6 می باشد.

چیزی که دانستن آن ضروری است اینه که ما در حال حاضر یک شیء سوکت جدید از accept() داریم. این مهمه چرا که این سوکتی هست که شما برای ارتباط با کلاینت از آن استفاده می کنید. این سوکت متمایز از Listening socket می باشد که سرور برای قبول اتصالات جدید از آن استفاده می کند:

conn, addr = s.accept()
with conn:
    print('Connected by', addr)
    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.sendall(data)

بعد از گرفتن شیء سوکت کلاینت (conn) از accept()، یک حلقه لووپ بینهایت برای لوپ کردن روی فراخوانی های بلاک کردن برای conn.resrv() استفاده می شود. این تمامی داده های که کلاینت ارسال کرده است را می خواند و با conn.sendall() برگشت می دهد.

توجه داشته باشید که از سوکت کلاینت برای انتقال داده استفاده شده است. سوکت سرور تنها برای قبول کردن اتصالات جدی استفاده می شود.

اگر conn.recv() شیء بایت خالی را برگرداند (b”) سپس کلاینت اتصال را می بندد و لووپ نیز خاتمه میابد. عبارت with با conn استفاده شده تا خودکار سوکت را در انتهای بلاک ببندد.

می توانستیم قسمت While را به شکل زیر بنویسیم و با دستور close اتصال کلاینت را ببندیم، به کد زیر توجه کنید:

بستن اتصال در TCP server

Echo Client

حال بیاید به کلاینت نگاهی بیاندزایم (echo-client.py):

#!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # The server's hostname or IP address
PORT = 65432        # The port used by the server

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)

print('Received', repr(data))

در مقایسه با سرور، کلاینت خیلی ساده است. د رکلاینت یک شیء سوکت ساخته شده، به سرور متصل شده و s.sendall()  جهت ارسال پیام فراخوانی می شود. در انتها s.recv() برای خواندن پیام پاسخ سرور و سپس پرینت آن، فراخوانی می شود.

خلاصه ای از مبانی سوکت

اجرای کلاینت و سرور

خب نوبت به اجرای کلاینت و سرور رسید تا هم روش اجرا را ببینیم و هم این که رفتارشون چطوریه و چه اتفاقی می افتد.

تا اینجای کار ما دو فایل با نام های echo-server.py و echo-client.py داریم. برا ی اجرایشان کافیست به محلی که فایل ها در آن قرار دارند برویم، برای اجرای سرور کافیست دستور زیر را در ترمینال بزنید:

$ ./echo-server.py

در تصویر زیر تلاش من برای اجرای فایل سرور را مشاهده میکنید :

با اجرای فایل سرور، ترمینالتان هنگ خواهد کرد، چرا که سرور در فراخوانی دستور زیر بلوکه می شود:

conn, addr = s.accept()

منتظر اتصالی از سمت کلاینت می باشد. حال ترمینال دیگری باز کنید و کلاینت را اجرا کنید:

$ ./echo-client.py 

تصویر زیر حاصل اجرای کلاینت می باشد، هم ترمینال کلاینت(سمت راست) و هم ترمینال سرور(سمت چپ) نمایش داده شده است

در ترمینال سرور، چندگانه ی (tuple) مربوط به addr که از سمت s.accept() برگردانده شده است را چاپ کرده است. این چندگانه آدرس IP کلاینت و شماره پورت می باشد.