পাইথনের অ্যাসিঙ্কিও নিম্ন-স্তরের নেটওয়ার্কিং-এ দক্ষতা অর্জন করুন। এই গভীর আলোচনা ট্রান্সপোর্ট এবং প্রোটোকলগুলি কভার করে, যা উচ্চ-কার্যকারিতা, কাস্টম নেটওয়ার্ক অ্যাপ্লিকেশন তৈরির জন্য বাস্তব উদাহরণসহ উপস্থাপন করা হয়েছে।
পাইথনের অ্যাসিঙ্কিও ট্রান্সপোর্টকে সরলীকরণ: নিম্ন-স্তরের নেটওয়ার্কিং-এর গভীরে
আধুনিক পাইথনের বিশ্বে, asyncio
উচ্চ-কার্যকারিতা নেটওয়ার্ক প্রোগ্রামিংয়ের ভিত্তি হয়ে দাঁড়িয়েছে। ডেভেলপাররা প্রায়শই এর সুন্দর উচ্চ-স্তরের API দিয়ে শুরু করেন, async
এবং await
ব্যবহার করে aiohttp
বা FastAPI
-এর মতো লাইব্রেরিগুলির সাথে অসাধারণ সহজে প্রতিক্রিয়াশীল অ্যাপ্লিকেশন তৈরি করতে পারেন। StreamReader
এবং StreamWriter
অবজেক্ট, যা asyncio.open_connection()
-এর মতো ফাংশন দ্বারা সরবরাহ করা হয়, নেটওয়ার্ক I/O হ্যান্ডেল করার জন্য একটি চমৎকার সরল, ক্রমানুসারে উপায় সরবরাহ করে। কিন্তু বিমূর্ততা যখন যথেষ্ট না হয় তখন কী ঘটে? যদি আপনাকে একটি জটিল, স্টেটফুল, বা অ-মানক নেটওয়ার্ক প্রোটোকল প্রয়োগ করতে হয়? যদি আপনি অন্তর্নিহিত সংযোগটিকে সরাসরি নিয়ন্ত্রণ করে কর্মক্ষমতার প্রতিটি শেষ বিন্দু বের করতে চান? এখানেই asyncio-এর নেটওয়ার্কিং ক্ষমতার আসল ভিত্তি রয়েছে: নিম্ন-স্তরের ট্রান্সপোর্ট এবং প্রোটোকল API। প্রথমে এটিকে ভীতিকর মনে হতে পারে, তবে এই শক্তিশালী জুটির ধারণা একটি নতুন স্তরের নিয়ন্ত্রণ এবং নমনীয়তা উন্মুক্ত করে, যা আপনাকে কার্যত কল্পনযোগ্য যে কোনও নেটওয়ার্ক অ্যাপ্লিকেশন তৈরি করতে সক্ষম করে। এই বিস্তৃত গাইড বিমূর্ততার স্তরগুলিকে সরিয়ে দেবে, ট্রান্সপোর্ট এবং প্রোটোকলগুলির মধ্যে মিথোজীবী সম্পর্ক অন্বেষণ করবে এবং আপনাকে পাইথনে নিম্ন-স্তরের অ্যাসিঙ্ক্রোনাস নেটওয়ার্কিং আয়ত্ত করতে ক্ষমতায়িত করার জন্য ব্যবহারিক উদাহরণগুলির মাধ্যমে পরিচালিত করবে।
Asyncio নেটওয়ার্কিংয়ের দুটি মুখ: উচ্চ-স্তর বনাম নিম্ন-স্তর
নিম্ন-স্তরের API-গুলিতে গভীরভাবে ডুব দেওয়ার আগে, asyncio ইকোসিস্টেমে তাদের স্থান বোঝা গুরুত্বপূর্ণ। Asyncio বুদ্ধিমানের সাথে নেটওয়ার্ক যোগাযোগের জন্য দুটি স্বতন্ত্র স্তর সরবরাহ করে, প্রতিটি বিভিন্ন ব্যবহারের ক্ষেত্রের জন্য তৈরি।
উচ্চ-স্তরের API: স্ট্রিম
উচ্চ-স্তরের API, সাধারণভাবে "স্ট্রিম" হিসাবে উল্লেখ করা হয়, যা বেশিরভাগ বিকাশকারী প্রথমে সম্মুখীন হন। আপনি যখন asyncio.open_connection()
বা asyncio.start_server()
ব্যবহার করেন, তখন আপনি StreamReader
এবং StreamWriter
অবজেক্ট পান। এই API সরলতা এবং ব্যবহারের সহজতার জন্য ডিজাইন করা হয়েছে।
- বাধ্যতামূলক শৈলী: এটি আপনাকে এমন কোড লিখতে দেয় যা ক্রমানুসারে দেখায়। আপনি 100 বাইট পেতে
await reader.read(100)
ব্যবহার করেন, তারপর একটি প্রতিক্রিয়া পাঠানোর জন্যwriter.write(data)
ব্যবহার করেন। এইasync/await
প্যাটার্নটি স্বজ্ঞাত এবং বুঝতে সহজ। - সুবিধাজনক সহায়ক: এটি
readuntil(separator)
এবংreadexactly(n)
-এর মতো পদ্ধতি সরবরাহ করে যা সাধারণ ফ্রেমিংয়ের কাজগুলি পরিচালনা করে, আপনাকে ম্যানুয়ালি বাফার পরিচালনা থেকে বাঁচায়। - আদর্শ ব্যবহারের ক্ষেত্র: সাধারণ অনুরোধ-প্রতিক্রিয়া প্রোটোকলের জন্য উপযুক্ত (যেমন একটি বেসিক HTTP ক্লায়েন্ট), লাইন-ভিত্তিক প্রোটোকল (যেমন Redis বা SMTP), বা এমন কোনও পরিস্থিতি যেখানে যোগাযোগ একটি অনুমানযোগ্য, লিনিয়ার প্রবাহ অনুসরণ করে।
তবে, এই সরলতা একটি আপস সঙ্গে আসে. স্ট্রিম-ভিত্তিক পদ্ধতিটি অত্যন্ত সমসাময়িক, ইভেন্ট-চালিত প্রোটোকলের জন্য কম দক্ষ হতে পারে যেখানে অযাচিত বার্তা যে কোনও সময় আসতে পারে। ক্রমানুসারে await
মডেল যুগপত পঠন এবং লিখন পরিচালনা বা জটিল সংযোগ স্টেটগুলি পরিচালনা করা কষ্টকর করে তুলতে পারে।
নিম্ন-স্তরের API: ট্রান্সপোর্ট এবং প্রোটোকল
এটি ভিত্তিগত স্তর যার উপরে উচ্চ-স্তরের স্ট্রিম API আসলে নির্মিত। নিম্ন-স্তরের API দুটি স্বতন্ত্র উপাদানের উপর ভিত্তি করে একটি ডিজাইন প্যাটার্ন ব্যবহার করে: ট্রান্সপোর্ট এবং প্রোটোকল।
- ইভেন্ট-চালিত শৈলী: ডেটা পেতে আপনি একটি ফাংশন কল করার পরিবর্তে, যখন ইভেন্ট ঘটে তখন asyncio আপনার অবজেক্টের পদ্ধতি কল করে (যেমন, একটি সংযোগ তৈরি হয়েছে, ডেটা প্রাপ্ত হয়েছে)। এটি একটি কলব্যাক-ভিত্তিক পদ্ধতি।
- উদ্বেগ এর পৃথকীকরণ: এটি পরিষ্কারভাবে "কী" কে "কীভাবে" থেকে পৃথক করে। প্রোটোকল ডেটা দিয়ে কী করতে হবে তা সংজ্ঞায়িত করে (আপনার অ্যাপ্লিকেশন লজিক), যেখানে ট্রান্সপোর্ট নেটওয়ার্কের মাধ্যমে ডেটা কীভাবে পাঠানো এবং গ্রহণ করা হয় তা পরিচালনা করে (I/O মেকানিজম)।
- সর্বোচ্চ নিয়ন্ত্রণ: এই API আপনাকে বাফারিং, ফ্লো কন্ট্রোল (ব্যাকপ্রেসার) এবং সংযোগ লাইফসাইকেলের উপর সূক্ষ্ম-দানাযুক্ত নিয়ন্ত্রণ দেয়।
- আদর্শ ব্যবহারের ক্ষেত্র: কাস্টম বাইনারি বা টেক্সট প্রোটোকল প্রয়োগ করা, হাজার হাজার স্থায়ী সংযোগ পরিচালনা করে এমন উচ্চ-কার্যকারিতা সার্ভার তৈরি করা বা নেটওয়ার্ক ফ্রেমওয়ার্ক এবং লাইব্রেরি বিকাশের জন্য প্রয়োজনীয়।
বিষয়টি এভাবে ভাবুন: স্ট্রিম API একটি খাবার কিট পরিষেবা অর্ডার করার মতো। আপনি পূর্ব-বিভাজিত উপকরণ এবং অনুসরণ করার জন্য একটি সহজ রেসিপি পান। ট্রান্সপোর্ট এবং প্রোটোকল API একটি পেশাদার রান্নাঘরের একজন শেফের মতো যিনি কাঁচামাল এবং প্রক্রিয়ার প্রতিটি ধাপের উপর সম্পূর্ণ নিয়ন্ত্রণ রাখেন। উভয়ই একটি দুর্দান্ত খাবার তৈরি করতে পারে, তবে পরেরটি সীমাহীন সৃজনশীলতা এবং নিয়ন্ত্রণ সরবরাহ করে।
মূল উপাদান: ট্রান্সপোর্ট এবং প্রোটোকলগুলির একটি ক্লোজার লুক
নিম্ন-স্তরের API-এর শক্তি প্রোটোকল এবং ট্রান্সপোর্টের মধ্যে মার্জিত মিথস্ক্রিয়া থেকে আসে। এগুলি স্বতন্ত্র তবে কোনও নিম্ন-স্তরের অ্যাসিঙ্কিও নেটওয়ার্ক অ্যাপ্লিকেশনে অবিচ্ছেদ্য অংশীদার।
প্রোটোকল: আপনার অ্যাপ্লিকেশনের মস্তিষ্ক
প্রোটোকল হল একটি ক্লাস যা আপনি লিখেন। এটি asyncio.Protocol
(বা এর একটি রূপ) থেকে উত্তরাধিকারসূত্রে প্রাপ্ত এবং একটি একক নেটওয়ার্ক সংযোগ পরিচালনার জন্য স্টেট এবং লজিক ধারণ করে। আপনি নিজে এই ক্লাসটির উদাহরণ তৈরি করেন না; আপনি এটি asyncio-কে সরবরাহ করেন (যেমন, loop.create_server
-এ), এবং asyncio প্রতিটি নতুন ক্লায়েন্ট সংযোগের জন্য আপনার প্রোটোকলের একটি নতুন উদাহরণ তৈরি করে।
আপনার প্রোটোকল ক্লাসটি ইভেন্ট হ্যান্ডলার পদ্ধতির একটি সেট দ্বারা সংজ্ঞায়িত করা হয় যা ইভেন্ট লুপ সংযোগের লাইফসাইকেলের বিভিন্ন সময়ে কল করে। সবচেয়ে গুরুত্বপূর্ণগুলি হল:
connection_made(self, transport)
একটি নতুন সংযোগ সফলভাবে প্রতিষ্ঠিত হলে ঠিক একবার কল করা হয়। এটি আপনার এন্ট্রি পয়েন্ট। এখানেই আপনি transport
অবজেক্টটি পান, যা সংযোগের প্রতিনিধিত্ব করে। আপনার সর্বদা এটির একটি রেফারেন্স সংরক্ষণ করা উচিত, সাধারণত self.transport
হিসাবে। এটি প্রতিটি সংযোগের জন্য কোনও প্রাথমিকীকরণ সম্পাদন করার আদর্শ স্থান, যেমন বাফার সেট আপ করা বা পিয়ারের ঠিকানা লগ করা।
data_received(self, data)
আপনার প্রোটোকলের হৃদয়। সংযোগের অন্য প্রান্ত থেকে যখনই নতুন ডেটা পাওয়া যায় তখনই এই পদ্ধতিটি কল করা হয়। data
আর্গুমেন্টটি একটি bytes
অবজেক্ট। এটি মনে রাখা গুরুত্বপূর্ণ যে TCP একটি স্ট্রিম প্রোটোকল, কোনও বার্তা প্রোটোকল নয়। আপনার অ্যাপ্লিকেশন থেকে একটি একক লজিক্যাল বার্তা একাধিক data_received
কলের মধ্যে বিভক্ত হতে পারে বা একাধিক ছোট বার্তা একটি একক কলে একত্রিত হতে পারে। আপনার কোডকে এই বাফারিং এবং পার্সিং পরিচালনা করতে হবে।
connection_lost(self, exc)
সংযোগ বন্ধ হয়ে গেলে কল করা হয়। এটি বিভিন্ন কারণে ঘটতে পারে। যদি সংযোগটি পরিষ্কারভাবে বন্ধ হয়ে যায় (যেমন, অন্য দিকটি এটি বন্ধ করে দেয়, বা আপনি transport.close()
কল করেন), exc
হবে None
। যদি কোনও ত্রুটির কারণে সংযোগটি বন্ধ হয়ে যায় (যেমন, নেটওয়ার্ক ব্যর্থতা, রিসেট), exc
ত্রুটির বিশদ বিবরণ সহ একটি ব্যতিক্রম অবজেক্ট হবে। এটি আপনার পরিষ্কার করার, সংযোগ বিচ্ছিন্ন লগ করার বা আপনি যদি কোনও ক্লায়েন্ট তৈরি করেন তবে পুনরায় সংযোগ করার চেষ্টা করার সুযোগ।
eof_received(self)
এটি একটি আরও সূক্ষ্ম কলব্যাক। যখন অন্য প্রান্তটি ইঙ্গিত দেয় যে এটি আর কোনও ডেটা প্রেরণ করবে না (যেমন, POSIX সিস্টেমে shutdown(SHUT_WR)
কল করে), তবে আপনার ডেটা প্রেরণের জন্য সংযোগটি এখনও খোলা থাকতে পারে। আপনি যদি এই পদ্ধতি থেকে True
ফেরান, তবে ট্রান্সপোর্টটি বন্ধ হয়ে যাবে। আপনি যদি False
ফেরান (ডিফল্ট), তবে আপনি নিজেই পরে ট্রান্সপোর্টটি বন্ধ করার জন্য দায়বদ্ধ।
ট্রান্সপোর্ট: যোগাযোগ চ্যানেল
ট্রান্সপোর্ট হল একটি অবজেক্ট যা asyncio দ্বারা সরবরাহ করা হয়েছে। আপনি এটি তৈরি করেন না; আপনি আপনার প্রোটোকলের connection_made
পদ্ধতিতে এটি পান। এটি অন্তর্নিহিত নেটওয়ার্ক সকেট এবং ইভেন্ট লুপের I/O সময়সূচীর উপর একটি উচ্চ-স্তরের বিমূর্ততা হিসাবে কাজ করে। এর প্রাথমিক কাজ হল ডেটা প্রেরণ এবং সংযোগের নিয়ন্ত্রণ পরিচালনা করা।
আপনি এর পদ্ধতির মাধ্যমে ট্রান্সপোর্টের সাথে যোগাযোগ করেন:
transport.write(data)
ডেটা প্রেরণের প্রাথমিক পদ্ধতি। data
অবশ্যই একটি bytes
অবজেক্ট হতে হবে। এই পদ্ধতিটি নন-ব্লকিং। এটি অবিলম্বে ডেটা প্রেরণ করে না। পরিবর্তে, এটি ডেটাটিকে একটি অভ্যন্তরীণ লেখার বাফারে রাখে এবং ইভেন্ট লুপ পটভূমিতে যতটা সম্ভব দক্ষতার সাথে নেটওয়ার্কের মাধ্যমে এটি প্রেরণ করে।
transport.writelines(list_of_data)
একবারে বাফারে bytes
অবজেক্টগুলির একটি ক্রম লেখার আরও কার্যকর উপায়, সম্ভাব্যভাবে সিস্টেম কলগুলির সংখ্যা হ্রাস করে।
transport.close()
এটি একটি সুন্দর শাটডাউন শুরু করে। ট্রান্সপোর্ট প্রথমে তার লেখার বাফারে থাকা যে কোনও ডেটা ফ্লাশ করবে এবং তারপরে সংযোগটি বন্ধ করবে। close()
কল করার পরে আর কোনও ডেটা লেখা যাবে না।
transport.abort()
এটি একটি হার্ড শাটডাউন সম্পাদন করে। সংযোগটি অবিলম্বে বন্ধ হয়ে যায় এবং লেখার বাফারে থাকা যে কোনও ডেটা বাতিল করা হয়। এটি ব্যতিক্রমী পরিস্থিতিতে ব্যবহার করা উচিত।
transport.get_extra_info(name, default=None)
অনুসন্ধানের জন্য একটি খুব দরকারী পদ্ধতি। আপনি সংযোগ সম্পর্কে তথ্য পেতে পারেন, যেমন পিয়ারের ঠিকানা ('peername'
), অন্তর্নিহিত সকেট অবজেক্ট ('socket'
), বা SSL/TLS শংসাপত্রের তথ্য ('ssl_object'
)।
মিথোজীবী সম্পর্ক
এই ডিজাইনের সৌন্দর্য হল তথ্যের পরিষ্কার, চক্রাকার প্রবাহ:
- সেটআপ: ইভেন্ট লুপ একটি নতুন সংযোগ গ্রহণ করে।
- ইনস্ট্যান্টিয়েশন: লুপ আপনার
Protocol
ক্লাসের একটি উদাহরণ এবং সংযোগের প্রতিনিধিত্বকারী একটিTransport
অবজেক্ট তৈরি করে। - লিঙ্কেজ: লুপ
your_protocol.connection_made(transport)
কল করে, দুটি অবজেক্টকে একসাথে লিঙ্ক করে। আপনার প্রোটোকলের এখন ডেটা প্রেরণের একটি উপায় রয়েছে। - ডেটা গ্রহণ করা: যখন নেটওয়ার্ক সকেটে ডেটা আসে, তখন ইভেন্ট লুপ জেগে ওঠে, ডেটা পড়ে এবং
your_protocol.data_received(data)
কল করে। - প্রক্রিয়াকরণ: আপনার প্রোটোকলের লজিক প্রাপ্ত ডেটা প্রক্রিয়া করে।
- ডেটা প্রেরণ করা: এর লজিকের উপর ভিত্তি করে, আপনার প্রোটোকল একটি উত্তর পাঠানোর জন্য
self.transport.write(response_data)
কল করে। ডেটা বাফার করা হয়। - ব্যাকগ্রাউন্ড I/O: ইভেন্ট লুপ ট্রান্সপোর্টের মাধ্যমে বাফার করা ডেটা নন-ব্লকিং প্রেরণ পরিচালনা করে।
- টিয়ারডাউন: যখন সংযোগ শেষ হয়, তখন ইভেন্ট লুপ চূড়ান্ত পরিষ্কারের জন্য
your_protocol.connection_lost(exc)
কল করে।
একটি ব্যবহারিক উদাহরণ তৈরি করা: একটি ইকো সার্ভার এবং ক্লায়েন্ট
তত্ত্বটি দুর্দান্ত, তবে ট্রান্সপোর্ট এবং প্রোটোকলগুলি বোঝার সেরা উপায় হল কিছু তৈরি করা। আসুন একটি ক্লাসিক ইকো সার্ভার এবং একটি সংশ্লিষ্ট ক্লায়েন্ট তৈরি করি। সার্ভার সংযোগ গ্রহণ করবে এবং এটি প্রাপ্ত যে কোনও ডেটা কেবল ফেরত পাঠাবে।
ইকো সার্ভার বাস্তবায়ন
প্রথমে, আমরা আমাদের সার্ভার-সাইড প্রোটোকল সংজ্ঞায়িত করব। এটি উল্লেখযোগ্যভাবে সহজ, মূল ইভেন্ট হ্যান্ডলারগুলি প্রদর্শন করে।
import asyncio
class EchoServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
# একটি নতুন সংযোগ প্রতিষ্ঠিত হয়েছে।
# লগিংয়ের জন্য দূরবর্তী ঠিকানা পান।
peername = transport.get_extra_info('peername')
print(f"Connection from: {peername}")
# পরবর্তী ব্যবহারের জন্য ট্রান্সপোর্ট সংরক্ষণ করুন।
self.transport = transport
def data_received(self, data):
# ক্লায়েন্ট থেকে ডেটা পাওয়া যায়।
message = data.decode()
print(f"Data received: {message.strip()}")
# ক্লায়েন্টের কাছে ডেটা প্রতিধ্বনি করুন।
print(f"Echoing back: {message.strip()}")
self.transport.write(data)
def connection_lost(self, exc):
# সংযোগ বন্ধ হয়ে গেছে।
print("Connection closed.")
# ট্রান্সপোর্ট স্বয়ংক্রিয়ভাবে বন্ধ হয়ে যায়, এখানে 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'Serving on {addrs}')
# সার্ভারটি ব্যাকগ্রাউন্ডে চলে। মূল কোরুটিনকে জীবিত রাখতে,
# আমরা এমন কিছু অপেক্ষা করতে পারি যা কখনই সম্পূর্ণ হয় না, যেমন একটি নতুন ফিউচার।
# এই উদাহরণের জন্য, আমরা কেবল এটি "চিরকালের জন্য" চালাব।
async with server:
await server.serve_forever()
if __name__ == "__main__":
try:
# সার্ভারটি চালাতে:
asyncio.run(main_server())
except KeyboardInterrupt:
print("Server shut down.")
এই সার্ভার কোডে, 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"Sending: {self.message}")
self.transport.write(self.message.encode())
def data_received(self, data):
print(f"Received echo: {data.decode().strip()}")
def connection_lost(self, exc):
print("The server closed the connection")
# সংকেত দিন যে সংযোগটি হারিয়ে গেছে এবং কাজটি সম্পূর্ণ।
self.on_con_lost.set_result(True)
def eof_received(self):
# সার্ভার বন্ধ করার আগে যদি একটি EOF পাঠায় তবে এটি কল করা যেতে পারে।
print("Received EOF from server.")
async def main_client():
loop = asyncio.get_running_loop()
# ক্লায়েন্টের কাজের সমাপ্তি সংকেত দিতে on_con_lost ফিউচার ব্যবহৃত হয়।
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("Connection refused. Is the server running?")
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
ফিউচারের ব্যবহার একটি গুরুত্বপূর্ণ প্যাটার্ন। main_client
কোরুটিন এই ফিউচারের জন্য await
করে, কার্যকরভাবে তার নিজের এক্সিকিউশন থামিয়ে দেয় যতক্ষণ না প্রোটোকল সংকেত দেয় যে এর কাজটি connection_lost
-এর মধ্যে থেকে on_con_lost.set_result(True)
কল করে সম্পন্ন হয়েছে।
উন্নত ধারণা এবং বাস্তব-বিশ্বের পরিস্থিতি
ইকো উদাহরণটি বেসিকগুলি কভার করে, তবে বাস্তব-বিশ্বের প্রোটোকলগুলি খুব কমই এত সহজ। আসুন কিছু আরও উন্নত বিষয় অন্বেষণ করি যা আপনি অনিবার্যভাবে সম্মুখীন হবেন।
বার্তা ফ্রেমিং এবং বাফারিং পরিচালনা করা
বেসিকগুলির পরে বোঝার জন্য সবচেয়ে গুরুত্বপূর্ণ ধারণা হল TCP হল বাইটের একটি স্ট্রিম। কোনও অন্তর্নিহিত "বার্তা" সীমানা নেই। যদি কোনও ক্লায়েন্ট "Hello" এবং তারপরে "World" পাঠায়, তবে আপনার সার্ভারের data_received
b'HelloWorld'
দিয়ে একবার, b'Hello'
এবং b'World'
দিয়ে দুবার বা এমনকি আংশিক ডেটা সহ একাধিকবার কল করা যেতে পারে।
আপনার প্রোটোকলটি "ফ্রেমিং" এর জন্য দায়ী — এই বাইট স্ট্রিমগুলিকে অর্থবহ বার্তাগুলিতে পুনরায় একত্রিত করা। একটি সাধারণ কৌশল হল একটি ডেলিমিটার ব্যবহার করা, যেমন একটি নিউলাইন অক্ষর (\n
)।
এখানে একটি পরিবর্তিত প্রোটোকল রয়েছে যা নিউলাইন না পাওয়া পর্যন্ত ডেটা বাফার করে, একবারে একটি লাইন প্রক্রিয়া করে।
class LineBasedProtocol(asyncio.Protocol):
def __init__(self):
self._buffer = b''
self.transport = None
def connection_made(self, transport):
self.transport = transport
print("Connection established.")
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"Processing complete message: {line}")
response = f"Processed: {line}\n"
self.transport.write(response.encode())
def connection_lost(self, exc):
print("Connection lost.")
ফ্লো কন্ট্রোল পরিচালনা করা (ব্যাকপ্রেসার)
কী হবে যদি আপনার অ্যাপ্লিকেশন নেটওয়ার্ক বা দূরবর্তী পিয়ার পরিচালনা করতে পারে তার চেয়ে দ্রুত ট্রান্সপোর্টে ডেটা লিখছে? ডেটা ট্রান্সপোর্টের অভ্যন্তরীণ বাফারে স্তূপ হয়। যদি এটি অপরিবর্তিত থাকে তবে বাফারটি অনির্দিষ্টকালের জন্য বাড়তে পারে, উপলব্ধ সমস্ত মেমরি গ্রাস করে। এই সমস্যাটি "ব্যাকপ্রেসার" এর অভাব হিসাবে পরিচিত।
Asyncio এটি পরিচালনা করার জন্য একটি প্রক্রিয়া সরবরাহ করে। ট্রান্সপোর্ট তার নিজস্ব বাফারের আকার পর্যবেক্ষণ করে। যখন বাফারটি একটি নির্দিষ্ট উচ্চ-জলের চিহ্ন ছাড়িয়ে যায়, তখন ইভেন্ট লুপ আপনার প্রোটোকলের pause_writing()
পদ্ধতি কল করে। এটি আপনার অ্যাপ্লিকেশনকে ডেটা প্রেরণ বন্ধ করার একটি সংকেত। যখন বাফারটি নিম্ন-জলের চিহ্নের নীচে নিষ্কাশন করা হয়েছে, তখন লুপ 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("Pausing writing.")
self._paused = True
def resume_writing(self):
# ট্রান্সপোর্ট বাফার নিষ্কাশন হয়েছে।
print("Resuming writing.")
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
দেয় এবং আপনিdatagram_received(data, addr)
এবংerror_received(exc)
-এর মতো পদ্ধতি সহ একটিasyncio.DatagramProtocol
প্রয়োগ করবেন। - SSL/TLS: এনক্রিপশন যুক্ত করা অবিশ্বাস্যভাবে সরল। আপনি
loop.create_server()
বাloop.create_connection()
-এ একটিssl.SSLContext
অবজেক্ট পাস করেন। Asyncio স্বয়ংক্রিয়ভাবে TLS হ্যান্ডশেক পরিচালনা করে এবং আপনি একটি সুরক্ষিত ট্রান্সপোর্ট পান। আপনার প্রোটোকল কোড পরিবর্তন করার দরকার নেই। - সাবপ্রসেস: তাদের স্ট্যান্ডার্ড I/O পাইপের মাধ্যমে চাইল্ড প্রসেসগুলির সাথে যোগাযোগের জন্য, একটি
asyncio.SubprocessProtocol
সহloop.subprocess_exec()
এবংloop.subprocess_shell()
ব্যবহার করা যেতে পারে। এটি আপনাকে সম্পূর্ণরূপে অ্যাসিঙ্ক্রোনাস, নন-ব্লকিং উপায়ে চাইল্ড প্রসেসগুলি পরিচালনা করতে দেয়।
কৌশলগত সিদ্ধান্ত: কখন ট্রান্সপোর্ট বনাম স্ট্রিম ব্যবহার করবেন
আপনার হাতে দুটি শক্তিশালী API থাকায়, একটি মূল স্থাপত্যগত সিদ্ধান্ত হল কাজের জন্য সঠিকটি বেছে নেওয়া। আপনাকে সিদ্ধান্ত নিতে সাহায্য করার জন্য এখানে একটি গাইড রয়েছে।
স্ট্রিমগুলি (StreamReader
/StreamWriter
) কখন চয়ন করবেন...
- আপনার প্রোটোকলটি সহজ এবং অনুরোধ-প্রতিক্রিয়া ভিত্তিক। যদি লজিকটি "একটি অনুরোধ পড়ুন, এটি প্রক্রিয়া করুন, একটি প্রতিক্রিয়া লিখুন" হয় তবে স্ট্রিমগুলি নিখুঁত।
- আপনি একটি সুপরিচিত, লাইন-ভিত্তিক বা নির্দিষ্ট-দৈর্ঘ্যের বার্তা প্রোটোকলের জন্য একটি ক্লায়েন্ট তৈরি করছেন। উদাহরণস্বরূপ, একটি Redis সার্ভার বা একটি সাধারণ FTP সার্ভারের সাথে ইন্টারঅ্যাক্ট করা।
- আপনি কোড পাঠযোগ্যতা এবং একটি লিনিয়ার, বাধ্যতামূলক শৈলীকে অগ্রাধিকার দেন। স্ট্রিমগুলির সাথে
async/await
সিনট্যাক্স প্রায়শই অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের নতুন ডেভেলপারদের জন্য বোঝা সহজ। - দ্রুত প্রোটোটাইপিং মূল বিষয়। আপনি কোডের কয়েকটি লাইনে স্ট্রিমগুলির সাথে একটি সাধারণ ক্লায়েন্ট বা সার্ভার চালু এবং চালাতে পারেন।
ট্রান্সপোর্ট এবং প্রোটোকলগুলি কখন চয়ন করবেন...
- আপনি স্ক্র্যাচ থেকে একটি জটিল বা কাস্টম নেটওয়ার্ক প্রোটোকল প্রয়োগ করছেন। এটি প্রাথমিক ব্যবহারের ক্ষেত্র। গেমিং, আর্থিক ডেটা ফিড, IoT ডিভাইস বা পিয়ার-টু-পিয়ার অ্যাপ্লিকেশনগুলির জন্য প্রোটোকলগুলির কথা ভাবুন।
- আপনার প্রোটোকলটি অত্যন্ত ইভেন্ট-চালিত এবং সম্পূর্ণরূপে অনুরোধ-প্রতিক্রিয়া নয়। যদি সার্ভার যে কোনও সময় ক্লায়েন্টের কাছে অযাচিত বার্তা প্রেরণ করতে পারে তবে প্রোটোকলগুলির কলব্যাক-ভিত্তিক প্রকৃতি আরও স্বাভাবিকভাবে উপযুক্ত।
- আপনার সর্বাধিক কর্মক্ষমতা এবং সর্বনিম্ন ওভারহেড প্রয়োজন। প্রোটোকলগুলি আপনাকে স্ট্রিম API এর সাথে যুক্ত কিছু ওভারহেডকে বাইপাস করে ইভেন্ট লুপের আরও সরাসরি পথ দেয়।
- আপনার সংযোগের উপর সূক্ষ্ম-দানাযুক্ত নিয়ন্ত্রণের প্রয়োজন। এর মধ্যে ম্যানুয়াল বাফার ম্যানেজমেন্ট, স্পষ্ট ফ্লো কন্ট্রোল (
pause/resume_writing
) এবং সংযোগ লাইফসাইকেলের বিশদ পরিচালনা অন্তর্ভুক্ত রয়েছে। - আপনি একটি নেটওয়ার্ক ফ্রেমওয়ার্ক বা লাইব্রেরি তৈরি করছেন। আপনি যদি অন্যান্য ডেভেলপারদের জন্য একটি সরঞ্জাম সরবরাহ করেন তবে প্রোটোকল/ট্রান্সপোর্ট API এর শক্তিশালী এবং নমনীয় প্রকৃতি প্রায়শই সঠিক ভিত্তি।
উপসংহার: Asyncio এর ভিত্তি গ্রহণ করা
পাইথনের asyncio
লাইব্রেরিটি স্তরযুক্ত ডিজাইনের একটি মাস্টারপিস। যদিও উচ্চ-স্তরের স্ট্রিম API একটি অ্যাক্সেসযোগ্য এবং উত্পাদনশীল এন্ট্রি পয়েন্ট সরবরাহ করে, তবে এটি নিম্ন-স্তরের ট্রান্সপোর্ট এবং প্রোটোকল API যা asyncio এর নেটওয়ার্কিং ক্ষমতার সত্য, শক্তিশালী ভিত্তির প্রতিনিধিত্ব করে। I/O মেকানিজম (ট্রান্সপোর্ট) কে অ্যাপ্লিকেশন লজিক (প্রোটোকল) থেকে পৃথক করে, এটি অত্যাধুনিক নেটওয়ার্ক অ্যাপ্লিকেশন তৈরির জন্য একটি শক্তিশালী, স্কেলেবল এবং অবিশ্বাস্যভাবে নমনীয় মডেল সরবরাহ করে।
এই নিম্ন-স্তরের বিমূর্ততা বোঝা কেবল একটি একাডেমিক অনুশীলন নয়; এটি একটি ব্যবহারিক দক্ষতা যা আপনাকে সাধারণ ক্লায়েন্ট এবং সার্ভারগুলির বাইরে যেতে সক্ষম করে। এটি আপনাকে যে কোনও নেটওয়ার্ক প্রোটোকল মোকাবেলার আত্মবিশ্বাস, চাপের মধ্যে কর্মক্ষমতা অপ্টিমাইজ করার নিয়ন্ত্রণ এবং পাইথনে উচ্চ-কার্যকারিতা, অ্যাসিঙ্ক্রোনাস পরিষেবাদির পরবর্তী প্রজন্ম তৈরি করার ক্ষমতা দেয়। পরবর্তীকালে আপনি যখন কোনও চ্যালেঞ্জিং নেটওয়ার্কিং সমস্যার মুখোমুখি হন, তখন পৃষ্ঠের ঠিক নীচে থাকা শক্তির কথা মনে রাখবেন এবং ট্রান্সপোর্ট এবং প্রোটোকলগুলির মার্জিত জুটির জন্য পৌঁছাতে দ্বিধা করবেন না।