بر شبکه سطح پایین asyncio پایتون مسلط شوید. این راهنمای عمیق، ترنسپورتها و پروتکلها را با مثالهای عملی برای ساخت برنامههای شبکهی سفارشی و پربازده پوشش میدهد.
رمزگشایی از ترنسپورت Asyncio پایتون: نگاهی عمیق به شبکه سطح پایین
در دنیای پایتون مدرن، asyncio
به سنگ بنای برنامهنویسی شبکهی پربازده تبدیل شده است. توسعهدهندگان اغلب با APIهای سطح بالای زیبای آن شروع میکنند و از async
و await
با کتابخانههایی مانند aiohttp
یا FastAPI
برای ساخت برنامههای واکنشگرا با سهولت قابل توجهی استفاده میکنند. اشیاء StreamReader
و StreamWriter
که توسط توابعی مانند asyncio.open_connection()
ارائه میشوند، روشی فوقالعاده ساده و ترتیبی برای مدیریت ورودی/خروجی شبکه ارائه میدهند. اما چه اتفاقی میافتد وقتی این انتزاع کافی نباشد؟ چه میشود اگر نیاز به پیادهسازی یک پروتکل شبکهی پیچیده، حالتدار یا غیراستاندارد داشته باشید؟ چه میشود اگر نیاز داشته باشید با کنترل مستقیم اتصال زیربنایی، آخرین قطرهی عملکرد را بیرون بکشید؟ اینجاست که بنیان واقعی قابلیتهای شبکهی asyncio نهفته است: API سطح پایین ترنسپورت (Transport) و پروتکل (Protocol). اگرچه ممکن است در ابتدا ترسناک به نظر برسد، درک این زوج قدرتمند، سطح جدیدی از کنترل و انعطافپذیری را باز میکند و شما را قادر میسازد تا تقریباً هر برنامه شبکهی قابل تصوری را بسازید. این راهنمای جامع لایههای انتزاع را کنار میزند، رابطهی همزیستی بین ترنسپورتها و پروتکلها را بررسی میکند، و شما را از طریق مثالهای عملی راهنمایی میکند تا بر برنامهنویسی شبکهی ناهمگام سطح پایین در پایتون مسلط شوید.
دو روی شبکه Asyncio: سطح بالا در مقابل سطح پایین
قبل از اینکه عمیقاً وارد APIهای سطح پایین شویم، درک جایگاه آنها در اکوسیستم asyncio بسیار مهم است. Asyncio هوشمندانه دو لایهی مجزا برای ارتباطات شبکه فراهم میکند که هر کدام برای موارد استفادهی متفاوتی طراحی شدهاند.
API سطح بالا: استریمها (Streams)
API سطح بالا، که معمولاً به آن «استریمها» گفته میشود، چیزی است که اکثر توسعهدهندگان برای اولین بار با آن روبرو میشوند. وقتی از asyncio.open_connection()
یا asyncio.start_server()
استفاده میکنید، اشیاء StreamReader
و StreamWriter
دریافت میکنید. این API برای سادگی و سهولت استفاده طراحی شده است.
- سبک دستوری (Imperative): این امکان را به شما میدهد کدی بنویسید که به نظر ترتیبی میآید. شما
await reader.read(100)
را برای دریافت ۱۰۰ بایت و سپسwriter.write(data)
را برای ارسال پاسخ فراخوانی میکنید. این الگویasync/await
شهودی است و استدلال در مورد آن آسان است. - ابزارهای کمکی راحت: متدهایی مانند
readuntil(separator)
وreadexactly(n)
را فراهم میکند که وظایف رایج قاببندی (framing) را انجام میدهند و شما را از مدیریت دستی بافرها بینیاز میکنند. - موارد استفادهی ایدهآل: برای پروتکلهای سادهی درخواست-پاسخ (مانند یک کلاینت HTTP پایه)، پروتکلهای مبتنی بر خط (مانند Redis یا SMTP) یا هر موقعیتی که ارتباط از یک جریان قابل پیشبینی و خطی پیروی میکند، عالی است.
با این حال، این سادگی با یک بدهبستان همراه است. رویکرد مبتنی بر استریم میتواند برای پروتکلهای بسیار همزمان و رویدادمحور که پیامهای ناخواسته میتوانند در هر زمانی برسند، کارایی کمتری داشته باشد. مدل ترتیبی await
میتواند مدیریت خواندن و نوشتن همزمان یا مدیریت وضعیتهای پیچیدهی اتصال را دشوار کند.
API سطح پایین: ترنسپورتها و پروتکلها
این لایهی بنیادی است که API سطح بالای استریمها در واقع بر روی آن ساخته شده است. API سطح پایین از یک الگوی طراحی مبتنی بر دو مؤلفهی متمایز استفاده میکند: ترنسپورتها و پروتکلها.
- سبک رویدادمحور (Event-Driven): به جای اینکه شما تابعی را برای دریافت داده فراخوانی کنید، asyncio متدهایی را روی شیء شما هنگامی که رویدادها رخ میدهند (مثلاً یک اتصال برقرار میشود، داده دریافت میشود) فراخوانی میکند. این یک رویکرد مبتنی بر فراخوانی برگشتی (callback) است.
- جداسازی مسئولیتها (Separation of Concerns): این رویکرد به طور تمیز «چه کاری» را از «چگونه» جدا میکند. پروتکل تعریف میکند که چه کاری با دادهها انجام شود (منطق برنامهی شما)، در حالی که ترنسپورت مدیریت میکند که دادهها چگونه از طریق شبکه ارسال و دریافت شوند (مکانیسم ورودی/خروجی).
- حداکثر کنترل: این API به شما کنترل دقیق بر بافرینگ، کنترل جریان (فشار معکوس) و چرخهی عمر اتصال میدهد.
- موارد استفادهی ایدهآل: برای پیادهسازی پروتکلهای باینری یا متنی سفارشی، ساخت سرورهای پربازده که هزاران اتصال پایدار را مدیریت میکنند، یا توسعهی چارچوبها و کتابخانههای شبکه ضروری است.
به این صورت به آن فکر کنید: API استریمها مانند سفارش یک سرویس بستهی آشپزی است. شما مواد اولیهی از پیش تقسیمشده و یک دستور پخت ساده برای دنبال کردن دریافت میکنید. API ترنسپورت و پروتکل مانند یک سرآشپز در یک آشپزخانهی حرفهای با مواد اولیهی خام و کنترل کامل بر هر مرحله از فرآیند است. هر دو میتوانند یک وعدهی غذایی عالی تولید کنند، اما دومی خلاقیت و کنترل بی حد و حصری را ارائه میدهد.
مؤلفههای اصلی: نگاهی دقیقتر به ترنسپورتها و پروتکلها
قدرت API سطح پایین از تعامل زیبای بین پروتکل و ترنسپورت ناشی میشود. آنها شرکای متمایز اما جداییناپذیر در هر برنامهی شبکهی سطح پایین asyncio هستند.
پروتکل: مغز برنامهی شما
پروتکل کلاسی است که شما مینویسید. این کلاس از asyncio.Protocol
(یا یکی از انواع آن) ارث میبرد و شامل وضعیت و منطق برای مدیریت یک اتصال شبکهی واحد است. شما خودتان این کلاس را نمونهسازی نمیکنید؛ شما آن را به asyncio ارائه میدهید (مثلاً به loop.create_server
)، و asyncio برای هر اتصال کلاینت جدید، یک نمونهی جدید از پروتکل شما ایجاد میکند.
کلاس پروتکل شما با مجموعهای از متدهای کنترلکنندهی رویداد تعریف میشود که حلقهی رویداد در نقاط مختلف چرخهی عمر اتصال فراخوانی میکند. مهمترین آنها عبارتند از:
connection_made(self, transport)
دقیقاً یک بار زمانی که یک اتصال جدید با موفقیت برقرار میشود، فراخوانی میشود. این نقطهی ورود شماست. اینجاست که شما شیء transport
را دریافت میکنید که نمایندهی اتصال است. شما همیشه باید یک ارجاع به آن را ذخیره کنید، معمولاً به عنوان self.transport
. این مکان ایدهآلی برای انجام هرگونه مقداردهی اولیهی مربوط به هر اتصال، مانند تنظیم بافرها یا ثبت آدرس همتا (peer) است.
data_received(self, data)
قلب پروتکل شما. این متد هر زمان که دادهی جدیدی از طرف دیگر اتصال دریافت شود، فراخوانی میشود. آرگومان data
یک شیء bytes
است. به خاطر سپردن این نکته حیاتی است که TCP یک پروتکل جریانی (stream) است، نه یک پروتکل پیامی (message). یک پیام منطقی واحد از برنامهی شما ممکن است در چندین فراخوانی data_received
تقسیم شود، یا چندین پیام کوچک ممکن است در یک فراخوانی واحد جمع شوند. کد شما باید این بافرینگ و تجزیه را مدیریت کند.
connection_lost(self, exc)
هنگامی که اتصال بسته میشود، فراخوانی میشود. این میتواند به دلایل مختلفی رخ دهد. اگر اتصال به طور تمیز بسته شود (مثلاً طرف دیگر آن را ببندد، یا شما transport.close()
را فراخوانی کنید)، exc
برابر با None
خواهد بود. اگر اتصال به دلیل خطا بسته شود (مثلاً خرابی شبکه، بازنشانی)، exc
یک شیء استثنا خواهد بود که جزئیات خطا را شرح میدهد. این فرصت شما برای انجام پاکسازی، ثبت قطع اتصال، یا تلاش برای اتصال مجدد در صورتی که در حال ساخت یک کلاینت هستید، میباشد.
eof_received(self)
این یک فراخوانی برگشتی ظریفتر است. زمانی فراخوانی میشود که طرف دیگر اعلام کند که دیگر دادهای ارسال نخواهد کرد (مثلاً با فراخوانی shutdown(SHUT_WR)
در یک سیستم POSIX)، اما اتصال ممکن است هنوز برای ارسال داده از طرف شما باز باشد. اگر از این متد True
برگردانید، ترنسپورت بسته خواهد شد. اگر False
برگردانید (پیشفرض)، شما مسئول بستن ترنسپورت در آینده خواهید بود.
ترنسپورت: کانال ارتباطی
ترنسپورت یک شیء است که توسط asyncio ارائه میشود. شما آن را ایجاد نمیکنید؛ شما آن را در متد connection_made
پروتکل خود دریافت میکنید. این به عنوان یک انتزاع سطح بالا بر روی سوکت شبکهی زیربنایی و زمانبندی ورودی/خروجی حلقهی رویداد عمل میکند. وظیفهی اصلی آن مدیریت ارسال داده و کنترل اتصال است.
شما از طریق متدهای آن با ترنسپورت تعامل دارید:
transport.write(data)
متد اصلی برای ارسال داده. data
باید یک شیء bytes
باشد. این متد غیرمسدودکننده (non-blocking) است. داده را فوراً ارسال نمیکند. در عوض، داده را در یک بافر نوشتاری داخلی قرار میدهد، و حلقهی رویداد آن را در پسزمینه به کارآمدترین شکل ممکن از طریق شبکه ارسال میکند.
transport.writelines(list_of_data)
یک روش کارآمدتر برای نوشتن یک دنباله از اشیاء bytes
به بافر به صورت یکجا، که به طور بالقوه تعداد فراخوانیهای سیستمی را کاهش میدهد.
transport.close()
این متد یک خاموش کردن باوقار (graceful shutdown) را آغاز میکند. ترنسپورت ابتدا هر دادهی باقیمانده در بافر نوشتاری خود را تخلیه (flush) کرده و سپس اتصال را میبندد. پس از فراخوانی close()
دیگر نمیتوان دادهای نوشت.
transport.abort()
این یک خاموش کردن سخت (hard shutdown) را انجام میدهد. اتصال فوراً بسته میشود و هر دادهی در انتظار در بافر نوشتاری دور ریخته میشود. این باید در شرایط استثنایی استفاده شود.
transport.get_extra_info(name, default=None)
یک متد بسیار مفید برای بازرسی (introspection). شما میتوانید اطلاعاتی در مورد اتصال دریافت کنید، مانند آدرس همتا ('peername'
)، شیء سوکت زیربنایی ('socket'
)، یا اطلاعات گواهی SSL/TLS ('ssl_object'
).
رابطهی همزیستی
زیبایی این طراحی در جریان اطلاعات واضح و چرخهای است:
- راهاندازی: حلقهی رویداد یک اتصال جدید را میپذیرد.
- نمونهسازی: حلقه یک نمونه از کلاس
Protocol
شما و یک شیءTransport
که نمایندهی اتصال است را ایجاد میکند. - پیوند: حلقه
your_protocol.connection_made(transport)
را فراخوانی میکند و دو شیء را به هم پیوند میدهد. پروتکل شما اکنون راهی برای ارسال داده دارد. - دریافت داده: هنگامی که داده به سوکت شبکه میرسد، حلقهی رویداد بیدار میشود، داده را میخواند و
your_protocol.data_received(data)
را فراخوانی میکند. - پردازش: منطق پروتکل شما دادهی دریافت شده را پردازش میکند.
- ارسال داده: بر اساس منطق خود، پروتکل شما
self.transport.write(response_data)
را برای ارسال پاسخ فراخوانی میکند. داده بافر میشود. - ورودی/خروجی پسزمینه: حلقهی رویداد ارسال غیرمسدودکنندهی دادهی بافر شده را از طریق ترنسپورت مدیریت میکند.
- پایان کار: هنگامی که اتصال به پایان میرسد، حلقهی رویداد
your_protocol.connection_lost(exc)
را برای پاکسازی نهایی فراخوانی میکند.
ساخت یک مثال عملی: یک سرور و کلاینت اکو (Echo)
تئوری عالی است، اما بهترین راه برای درک ترنسپورتها و پروتکلها ساختن چیزی است. بیایید یک سرور اکوی کلاسیک و یک کلاینت متناظر آن را ایجاد کنیم. سرور اتصالات را میپذیرد و به سادگی هر دادهای را که دریافت میکند، بازمیگرداند.
پیادهسازی سرور اکو
ابتدا، پروتکل سمت سرور خود را تعریف خواهیم کرد. این پروتکل به طرز شگفتآوری ساده است و کنترلکنندههای رویداد اصلی را به نمایش میگذارد.
import asyncio
class EchoServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
# یک اتصال جدید برقرار شده است.
# آدرس ریموت را برای ثبت وقایع دریافت کنید.
peername = transport.get_extra_info('peername')
print(f"اتصال از: {peername}")
# ترنسپورت را برای استفادههای بعدی ذخیره کنید.
self.transport = transport
def data_received(self, data):
# داده از کلاینت دریافت شده است.
message = data.decode()
print(f"دادهی دریافتی: {message.strip()}")
# داده را به کلاینت بازگردانید (اکو کنید).
print(f"اکو کردن: {message.strip()}")
self.transport.write(data)
def connection_lost(self, exc):
# اتصال بسته شده است.
print("اتصال بسته شد.")
# ترنسپورت به طور خودکار بسته میشود، نیازی به فراخوانی self.transport.close() در اینجا نیست.
async def main_server():
# یک ارجاع به حلقهی رویداد دریافت کنید زیرا قصد داریم سرور را به طور نامحدود اجرا کنیم.
loop = asyncio.get_running_loop()
host = '127.0.0.1'
port = 8888
# کروتین `create_server` سرور را ایجاد و راهاندازی میکند.
# آرگومان اول protocol_factory است، یک فراخوانپذیر که یک نمونهی پروتکل جدید را برمیگرداند.
# در مورد ما، به سادگی پاس دادن کلاس `EchoServerProtocol` کار میکند.
server = await loop.create_server(
lambda: EchoServerProtocol(),
host,
port)
addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets)
print(f'در حال سرویسدهی روی {addrs}')
# سرور در پسزمینه اجرا میشود. برای زنده نگه داشتن کروتین اصلی،
# میتوانیم منتظر چیزی باشیم که هرگز کامل نمیشود، مانند یک Future جدید.
# برای این مثال، ما فقط آن را "برای همیشه" اجرا میکنیم.
async with server:
await server.serve_forever()
if __name__ == "__main__":
try:
# برای اجرای سرور:
asyncio.run(main_server())
except KeyboardInterrupt:
print("سرور خاموش شد.")
در این کد سرور، loop.create_server()
کلید کار است. این متد به میزبان و پورت مشخص شده متصل میشود و به حلقهی رویداد میگوید که شروع به گوش دادن برای اتصالات جدید کند. برای هر اتصال ورودی، protocol_factory
ما را (تابع lambda: EchoServerProtocol()
) فراخوانی میکند تا یک نمونهی پروتکل تازه و اختصاصی برای آن کلاینت خاص ایجاد کند.
پیادهسازی کلاینت اکو
پروتکل کلاینت کمی پیچیدهتر است زیرا باید وضعیت خود را مدیریت کند: چه پیامی ارسال کند و چه زمانی کار خود را «انجام شده» تلقی کند. یک الگوی رایج استفاده از asyncio.Future
یا asyncio.Event
برای اعلام اتمام کار به کروتین اصلی است که کلاینت را راهاندازی کرده است.
import asyncio
class EchoClientProtocol(asyncio.Protocol):
def __init__(self, message, on_con_lost):
self.message = message
self.on_con_lost = on_con_lost
self.transport = None
def connection_made(self, transport):
self.transport = transport
print(f"در حال ارسال: {self.message}")
self.transport.write(self.message.encode())
def data_received(self, data):
print(f"اکوی دریافتی: {data.decode().strip()}")
def connection_lost(self, exc):
print("سرور اتصال را بست")
# اعلام کنید که اتصال قطع شده و وظیفه کامل است.
self.on_con_lost.set_result(True)
def eof_received(self):
# این متد میتواند فراخوانی شود اگر سرور قبل از بستن، یک EOF ارسال کند.
print("EOF از سرور دریافت شد.")
async def main_client():
loop = asyncio.get_running_loop()
# on_con_lost future برای اعلام اتمام کار کلاینت استفاده میشود.
on_con_lost = loop.create_future()
message = "Hello World!"
host = '127.0.0.1'
port = 8888
# `create_connection` اتصال را برقرار کرده و پروتکل را پیوند میدهد.
try:
transport, protocol = await loop.create_connection(
lambda: EchoClientProtocol(message, on_con_lost),
host,
port)
except ConnectionRefusedError:
print("اتصال رد شد. آیا سرور در حال اجراست؟")
return
# منتظر بمانید تا پروتکل اعلام کند که اتصال قطع شده است.
try:
await on_con_lost
finally:
# ترنسپورت را باوقار ببندید.
transport.close()
if __name__ == "__main__":
# برای اجرای کلاینت:
# ابتدا، سرور را در یک ترمینال اجرا کنید.
# سپس، این اسکریپت را در ترمینال دیگری اجرا کنید.
asyncio.run(main_client())
در اینجا، loop.create_connection()
همتای سمت کلاینت برای create_server
است. این متد تلاش میکند به آدرس داده شده متصل شود. در صورت موفقیت، EchoClientProtocol
ما را نمونهسازی کرده و متد connection_made
آن را فراخوانی میکند. استفاده از on_con_lost
Future یک الگوی حیاتی است. کروتین main_client
منتظر (await
) این future میماند، که به طور مؤثر اجرای خود را متوقف میکند تا زمانی که پروتکل با فراخوانی on_con_lost.set_result(True)
از درون connection_lost
اعلام کند که کارش تمام شده است.
مفاهیم پیشرفته و سناریوهای دنیای واقعی
مثال اکو اصول اولیه را پوشش میدهد، اما پروتکلهای دنیای واقعی به ندرت به این سادگی هستند. بیایید برخی از موضوعات پیشرفتهتری را که به ناچار با آنها روبرو خواهید شد، بررسی کنیم.
مدیریت قاببندی پیام و بافرینگ
مهمترین مفهومی که پس از اصول اولیه باید درک کرد این است که TCP یک جریان از بایتها است. هیچ مرز «پیام» ذاتی وجود ندارد. اگر یک کلاینت «Hello» و سپس «World» را ارسال کند، data_received
سرور شما میتواند یک بار با b'HelloWorld'
، دو بار با b'Hello'
و b'World'
، یا حتی چندین بار با دادههای جزئی فراخوانی شود.
پروتکل شما مسئول «قاببندی» (framing) است — یعنی بازسازی این جریانهای بایت به پیامهای معنادار. یک استراتژی رایج استفاده از یک جداکننده، مانند کاراکتر خط جدید (\n
) است.
در اینجا یک پروتکل اصلاحشده وجود دارد که دادهها را تا زمانی که یک خط جدید پیدا کند، بافر میکند و هر بار یک خط را پردازش میکند.
class LineBasedProtocol(asyncio.Protocol):
def __init__(self):
self._buffer = b''
self.transport = None
def connection_made(self, transport):
self.transport = transport
print("اتصال برقرار شد.")
def data_received(self, data):
# دادهی جدید را به بافر داخلی اضافه کنید
self._buffer += data
# تا زمانی که خطوط کاملی در بافر داریم، آنها را پردازش کنید
while b'\n' in self._buffer:
line, self._buffer = self._buffer.split(b'\n', 1)
self.process_line(line.decode().strip())
def process_line(self, line):
# اینجا جایی است که منطق برنامهی شما برای یک پیام واحد قرار میگیرد
print(f"در حال پردازش پیام کامل: {line}")
response = f"پردازش شد: {line}\n"
self.transport.write(response.encode())
def connection_lost(self, exc):
print("اتصال قطع شد.")
مدیریت کنترل جریان (فشار معکوس)
چه اتفاقی میافتد اگر برنامهی شما دادهها را سریعتر از آنچه شبکه یا همتای راه دور میتواند مدیریت کند، به ترنسپورت بنویسد؟ دادهها در بافر داخلی ترنسپورت انباشته میشوند. اگر این روند بدون کنترل ادامه یابد، بافر میتواند به طور نامحدود رشد کرده و تمام حافظهی موجود را مصرف کند. این مشکل به عنوان فقدان «فشار معکوس» (backpressure) شناخته میشود.
Asyncio مکانیزمی برای مدیریت این موضوع فراهم میکند. ترنسپورت اندازهی بافر خود را نظارت میکند. هنگامی که بافر از یک آستانهی بالا (high-water mark) عبور میکند، حلقهی رویداد متد pause_writing()
پروتکل شما را فراخوانی میکند. این یک سیگنال به برنامهی شما برای توقف ارسال داده است. هنگامی که بافر به زیر یک آستانهی پایین (low-water mark) تخلیه شد، حلقه resume_writing()
را فراخوانی میکند، که نشان میدهد ارسال مجدد داده ایمن است.
class FlowControlledProtocol(asyncio.Protocol):
def __init__(self):
self._paused = False
self._data_source = some_data_generator() # یک منبع داده را تصور کنید
self.transport = None
def connection_made(self, transport):
self.transport = transport
self.resume_writing() # فرآیند نوشتن را شروع کنید
def pause_writing(self):
# بافر ترنسپورت پر است.
print("متوقف کردن نوشتن.")
self._paused = True
def resume_writing(self):
# بافر ترنسپورت تخلیه شده است.
print("از سرگیری نوشتن.")
self._paused = False
self._write_more_data()
def _write_more_data(self):
# این حلقهی نوشتن برنامهی ما است.
while not self._paused:
try:
data = next(self._data_source)
self.transport.write(data)
except StopIteration:
self.transport.close()
break # دادهی بیشتری برای ارسال وجود ندارد
# اندازهی بافر را بررسی کنید تا ببینیم آیا باید فوراً متوقف شویم
if self.transport.get_write_buffer_size() > 0:
self.pause_writing()
فراتر از TCP: ترنسپورتهای دیگر
در حالی که TCP رایجترین مورد استفاده است، الگوی ترنسپورت/پروتکل به آن محدود نمیشود. Asyncio انتزاعاتی برای انواع دیگر ارتباطات فراهم میکند:
- UDP: برای ارتباطات بدون اتصال، از
loop.create_datagram_endpoint()
استفاده میکنید. این به شما یکDatagramTransport
میدهد و شما یکasyncio.DatagramProtocol
با متدهایی مانندdatagram_received(data, addr)
وerror_received(exc)
پیادهسازی خواهید کرد. - SSL/TLS: افزودن رمزگذاری فوقالعاده ساده است. شما یک شیء
ssl.SSLContext
را بهloop.create_server()
یاloop.create_connection()
پاس میدهید. Asyncio به طور خودکار دستدهی (handshake) TLS را مدیریت میکند و شما یک ترنسپورت امن دریافت میکنید. کد پروتکل شما اصلاً نیازی به تغییر ندارد. - زیرفرایندها (Subprocesses): برای ارتباط با فرایندهای فرزند از طریق لولههای ورودی/خروجی استاندارد آنها،
loop.subprocess_exec()
وloop.subprocess_shell()
میتوانند با یکasyncio.SubprocessProtocol
استفاده شوند. این به شما امکان میدهد تا فرایندهای فرزند را به روشی کاملاً ناهمگام و غیرمسدودکننده مدیریت کنید.
تصمیم استراتژیک: چه زمانی از ترنسپورتها در مقابل استریمها استفاده کنیم
با دو API قدرتمند در اختیار شما، یک تصمیم معماری کلیدی، انتخاب گزینهی مناسب برای کار است. در اینجا راهنمایی برای کمک به تصمیمگیری شما آورده شده است.
استریمها (StreamReader
/StreamWriter
) را انتخاب کنید وقتی...
- پروتکل شما ساده و مبتنی بر درخواست-پاسخ است. اگر منطق «خواندن یک درخواست، پردازش آن، نوشتن یک پاسخ» باشد، استریمها عالی هستند.
- شما در حال ساخت یک کلاینت برای یک پروتکل پیام شناخته شده، مبتنی بر خط یا با طول ثابت هستید. به عنوان مثال، تعامل با یک سرور Redis یا یک سرور FTP ساده.
- شما خوانایی کد و یک سبک خطی و دستوری را در اولویت قرار میدهید. سینتکس
async/await
با استریمها اغلب برای توسعهدهندگانی که با برنامهنویسی ناهمگام تازه آشنا شدهاند، آسانتر است. - نمونهسازی سریع کلیدی است. شما میتوانید یک کلاینت یا سرور ساده را با استریمها تنها در چند خط کد راهاندازی کنید.
ترنسپورتها و پروتکلها را انتخاب کنید وقتی...
- شما در حال پیادهسازی یک پروتکل شبکهی پیچیده یا سفارشی از ابتدا هستید. این مورد استفادهی اصلی است. به پروتکلهایی برای بازی، فیدهای دادههای مالی، دستگاههای IoT یا برنامههای همتا به همتا فکر کنید.
- پروتکل شما بسیار رویدادمحور است و صرفاً درخواست-پاسخ نیست. اگر سرور بتواند در هر زمانی پیامهای ناخواسته را به کلاینت ارسال کند، ماهیت مبتنی بر فراخوانی برگشتی پروتکلها مناسبتر است.
- شما به حداکثر عملکرد و حداقل سربار نیاز دارید. پروتکلها به شما یک مسیر مستقیمتر به حلقهی رویداد میدهند و برخی از سربارهای مرتبط با API استریمها را دور میزنند.
- شما به کنترل دقیق بر روی اتصال نیاز دارید. این شامل مدیریت دستی بافر، کنترل جریان صریح (
pause/resume_writing
) و مدیریت دقیق چرخهی عمر اتصال است. - شما در حال ساخت یک چارچوب یا کتابخانهی شبکه هستید. اگر در حال ارائهی ابزاری برای سایر توسعهدهندگان هستید، ماهیت قوی و انعطافپذیر API پروتکل/ترنسپورت اغلب پایهی مناسبی است.
نتیجهگیری: پذیرش بنیان Asyncio
کتابخانهی asyncio
پایتون یک شاهکار طراحی لایهای است. در حالی که API سطح بالای استریمها یک نقطهی ورود قابل دسترس و پربازده را فراهم میکند، این API سطح پایین ترنسپورت و پروتکل است که بنیان واقعی و قدرتمند قابلیتهای شبکهی asyncio را نمایندگی میکند. با جدا کردن مکانیزم ورودی/خروجی (ترنسپورت) از منطق برنامه (پروتکل)، این API یک مدل قوی، مقیاسپذیر و فوقالعاده انعطافپذیر برای ساخت برنامههای شبکهی پیچیده فراهم میکند.
درک این انتزاع سطح پایین فقط یک تمرین آکادمیک نیست؛ این یک مهارت عملی است که شما را قادر میسازد تا فراتر از کلاینتها و سرورهای ساده حرکت کنید. این به شما اطمینان میدهد که با هر پروتکل شبکهای مقابله کنید، کنترل لازم برای بهینهسازی عملکرد تحت فشار را داشته باشید، و توانایی ساخت نسل بعدی سرویسهای پربازده و ناهمگام در پایتون را پیدا کنید. دفعهی بعد که با یک مشکل چالشبرانگیز شبکه روبرو شدید، قدرتی را که درست در زیر سطح نهفته است به یاد بیاورید و در استفاده از زوج زیبای ترنسپورتها و پروتکلها تردید نکنید.