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

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

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

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

شکست ارتباطات (Communication Breakdown)

بیاید یه مقدار دقیقتر به چگونگیه ارتباط کلاینت و سرور با یدکیگر بپردازیم:

زمانی که از واسط Loopback (لوکال هاست) استفاده می کنید (IPv4 address 127.0.0.1 or IPv6 address ::1)، داده هرگز hostرا ترک نکرده و به شبکه های خارجی دسترسی ندارد.

اپلیکیشن ها از واسط loopbackبه جهت ارتباط با دیگرفرآیند هایی که روی هاست در حال اجرا هستند استفاده می کنند، این کار را بدلیل امنیتی و ایزوله سازی از شبکه های خارجی انجام می دهند.

زمانی که شما از آدرس آی پی غیر از 127.0.0.1  یا ::1 در اپلیکیشن خود استفاده می کنید، احتمالا وارد محدود واسط Ethernet شده اید که به شبکه خارجی متصل است. این دروازه ی شما به Host های دیگری خارج از پادشاهی «لوکال هاست» می باشد :

مدیریت کردن چند ارتباط

کد echo-sever که نوشته بودیم محدودیت های خودش رو داره که بزرگترینش اینه که تنها یک کلااینت را سرویس می دهد و سپس خارج می شود. echo clientنیز مانند echo server همین محدودیت را دارد. یک مسئله دیگری ام هست، زمانی که کلاینت فراخوانی زیر را انجام می دهد، ممکن است که s.recv() تنها یک بایت b’H’ از b’Hello, world’ را بازگرداند:

data = s.recv(1024)

آرگومان bufsize گه در بالا ۱۰۲۴ برایش استفاده شده است، بیشترین مقدار برای داده است که در یک زمان دریافت داشته باشد. به این معنی نیست که recv() نیز ۱۰۲۴ بایت برخواهد گرداند.

برای send() نیز همین مسئله است. send() تعداد بایت های ارسال شده را برمیگرداند، که ممکن است کمتر از مقدار داده ای که به آن داده شده، باشد. شما باید مسئولیت بررسی اش را داشته باشید و send() را هر چند بار که نیاز است فراخوانی کنید تا کل داده ارسال شود.

برای جلوگیری از مسئله فوق می توان از sendall() استفاده کرد که ارسال داده را تا زمانی که تمام داده ارسال شود ادامه می دهد.

ما در اینجا دو مشکل داریم :

  • چگونه چند connection را به صورت یکجا (concurrently) و همزمان مدیریت کنیم؟
  • ما نیاز به فراخوانی send() و recv() تا زانی که تمام داده ها ارسال یا دریافت شوند داریم.

چه باید بکنیم؟ چندین رویکرد برای همزمانی (concurrency) وجود دارد. یکی از رویکرد های اخیر و رایج استفاده از Asynchronous I/O می باشد. asyncio در کتابخانه استاندارد پایتون ۳.۴ معرفی شده است. انتخاب سنتی و قدیمی نیز استفاده از thread ها می باشد.

مشکلی که با همزمانی داریم اینه که سخته که بشه به درستی باهاش کار کرد. این رو نمیگیم که به سمت یادگیری concurrent programming نروید. اگر اپلیکیشن شما نیاز به هم زمانی دارد باید آن را فرابگیرید. در این آموزش ما از روش سنتی تر از Thread و آسانتر از آن استفاده خواهیم کرد. ما از پدربزرگ فراخوانی های سیستم، select() استفاده خواهیم کرد.

select() به شما این اجازه را می دهد تا تکمیل شدن I/O در بیش از یک سوکت را بررسی کنید. در نتیجه شما می توانید select() را جهت دیدن اینکه کدام سوکت ها آمادگی I/O برای خواندن یا نوشتن دارند، فراخوانی کنید.

برای پایتون ماژول ها و کتابخانه های متفاوت زیادی وجود دارد که بنا به اپلیکیشن شما هر کدام کیتواند مفید تر باشد، ما در این آموزش فعلا Select را در نظر گرفته ایم.

کلاینت و سرور Multi-Connection

در دو قسمت بعدی ما سرور و کلاینیت را خواهیم ساخت که با استفاده از Objectی از selector که از ماژول selectors ساخته شده است ، چندین اتصال را مدیریت خواهند بود. (Multi-Connection)

Multi-Connection Server

خب در ابتدا سرور چند ارتباطی را داریم، multiconn-server.py ، در ادامه اولین قسمت که listening socket را تنظیم می کند داریم:

import selectors
sel = selectors.DefaultSelector()
# ...
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)

بزرگترین تفاوت بین این سرور و echo-server فراخوانی lsock.setblocking(False) جهت کانفیگ سوکت در حالت (mode) غیر-بلوکه (non-blocking) می باشد.

فراخوانی ها روی این سوکت بلوکه نخواهند شد.زمانی که همراه با sel.select() استفاده شود، همانطور که درادامه خواهید دید، ما میتوانیم برای رویداد ها (event) روی یک یا بیشتر از یک سوکت، منتظر بمانیم و زمانی که آماده بود به خواندن و نوشتن داده بپردازیم.

sel.register() به ثبت سوکت جهت مانیتور شدن توسط sel.select() برای رویداد هایی که شما به آنها علاقه دارید می پردازد. برای listening socket ، ما میخواهیم که رویداد ها را بخوانیم: selectors.EVENT_READ .

از data جهت ذخیره هرگونه اطلاعات دلخواه مورد نظر به همراه سوکت استفاده می شود. data هنگامی که select() بازگردانده شود ، return می شود. از data برای پیگیری آنچه در سوکت فرستاده یا دریافت شده است استفاده می کنیم.

در ادامه event loop را مشاهده می کنید:

import selectors
sel = selectors.DefaultSelector()

# ...

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        if key.data is None:
            accept_wrapper(key.fileobj)
        else:
            service_conne

sel.select(timeout=None) تا زمانی که سوکتی آماده ی I/O داریم، بلوکه می کند و لیستی از چندگانه های (key,event) را یکی به ازای هر سوکت باز می گرداند.

key در اینجا namedtuple از SelectorKey می باشد که دارای یک attribute (ویژگی) fileobj هست .

key.fileobj شیئ سوکت می باشد، و mask، ماسک رویداد از عملیاتهاییست که آماده اند، می باشد.

اگر key.data در اینجا Noneباشد، سپس ما این را ای Listening socket متوجه می شویم و ما نیاز است که اتصال را accept() کنیم. ما فانکشن بسته بندی accept() خودمان (accept_wrapper()) را برای گرفتن شیء سوکت جدید و رجیستر کردن آن همراه با selector ، فراخوانی می کنیم. یکم جلوتر به آن نگاهی خواهیم انداخت.

اگر key.data در اینجا None نباشد، پس ما می دانیم که سوکت کلاینیت داریم که accept هم شده است و ما نیاز است تا به آن خدمت-service دهیم. سپس service_connection() فراخوانی شده و key و mask را عبور داده (pass) که هر چیزی که ما نیاز است روی سوکت انجام دهیم را شامل است.

پس بیاید به accept_wrapper() بپردازیم :

def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print('accepted connection from', addr)
    conn.setblocking(False)
    data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)

بدلیل اینکه سوکتی که در حال شنیدن است (listening socket) برای رویداد selectors.EVENT_READ رجیستر شده است، باید برای خواندن آماده باشد. ما sock.accept() را فراخوانی می کنیم و سپس سریعاً conn.setblocking(False) را بجهت قرار دادن سوکت در مُد (حالت) non-blocking فراخوانی می کنیم.

به یاد داشته باشید که در این ورژن از سرور این خدف و مقصود اصلی است د رنتیجه ما نمیخواهیم که بلاک شود. اگر بلاک شود، سپس تمامی سرور تا زمان بازگشت متوقف می شود. به این معنی که سوکت های دیگر باید منتظر بمانند. به این وضعیت مخوف «هنگ شدن» می گویند که شما هیچ وقت تمایل ندارید که سرورتان هنگ کند 🙂

در ادامه ما آبجکتی (شیئ) جهت نگهداری داده هایی که میخواهیم همراه با سوکت باشد با استفاده از کلاس types.SimpleNamespace می سازیم. بدلیل اینکه ما میخواهیم بدانیم چه زمانی ارتباط کلاینت برای خواندن و نوشتن آماده است، هر دوی آن رویداد ها را به صورت زیر تنظیم می کنیم:

events = selectors.EVENT_READ | selectors.EVENT_WRITE

و اینکه events ، سوکت و آبجکت های داده به sel.register() منتقل (pass) می شوند.

حال بیاید نگاهی به service_connection() بیندازیم و ببینیم که ارتباط کلاینت زمانی که آماده باشد چگونه مدیریت می شود:

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            data.outb += recv_data
        else:
            print('closing connection to', data.addr)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            print('echoing', repr(data.outb), 'to', data.addr)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]

رسیدیم به قلب سرور چند ارتباطه ساده ( simple multi-connection server). در اینجا key یک namedtuple می باشد که از select() برگشت داده می شود و شامل شئ سوکت (fileobj)  و شئ داده می باشد . mask نیز شامل رویداد هایی است که آماده هستند.

اگر سوکت آماده ی خواندن باشد، سپس mask و selectors.EVENT_READ مقدار True خواهند داشت و sock.recv() فراخوانی می شود. هر داده ای که خوانده شود به data.outb افزوده می شود که میتواند بعداً ارسال شود.

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

if recv_data:
    data.outb += recv_data
else:
    print('closing connection to', data.addr)
    sel.unregister(sock)
    sock.close()

اینجا به این معنی است که کلاینت سوکت خودش را بسته است، در نتیجه سرور نیز خواهد بست. اما فراموش نکنید که ابتدا sel.unregister() فراخوانی وشد درنتیجه دیگر با select() کنترل نمی شود.

زمانی که سوکت آماده ی نوشتن باشد، که همواره باید سوکت سالم در این فرم باشد، هر داده دریافت شده در data.outb ذخیره می شود با استفاده از sock.send() به کلاینت ارسال می شود. بایت های ارسال شده از بافر واک می شوند.

data.outb = data.outb[sent:]

خب رسیدیم به پایان قسمت دوم (تو پرانتز بگم که به دلیل مشغله ای که داشتم این قسمت را مرور مجدد برای ارسال نکردم و نیاز به یک ویرایش داره ولی چون حدود یه ماهی از آخرین پست ارسالیم می گذشت و این مطلب رو قول ادامه اش را داده بودم، بدون ویرایش فعلا به اشتراک می گذارم و بعدا حتما اصلاح میشه 🙂 )