যোগাযোগের একটি গল্প
আপনি কখনো চিন্তা করেছেন কি ইন্টারনেট আসলে কীভাবে কথা বলে? একটি কম্পিউটার কীভাবে ইন্টারনেটের মাধ্যমে অন্য একটি কম্পিউটারের সাথে "কথা বলে"?
যখন মানুষ একে অপরের সাথে যোগাযোগ করে, আমরা শব্দগুলো ব্যবহার করি অর্থবহ বাক্য তৈরি করতে। বাক্যগুলো অর্থবহ হয় কারণ আমরা এই বাক্যগুলোর অর্থে একমত হয়েছি। আমরা একটি যোগাযোগের প্রোটোকল ডিফাইন করেছি, কথা বলতে।
বিকল্পে, কম্পিউটাররা ইন্টারনেটের ওপর একে অপরের সাথে সিমিলার ফ্যাশনে কথা বলে। কিন্তু, আমরা নিজেদের আগে ছাড়া যাচ্ছি। মানুষ মুখ ব্যবহার করে যোগাযোগ, চলুন প্রথমে কম্পিউটারের মুখ কী সেটা বের করি।
সকেট এন্ট্রি
সকেট কম্পিউটার সাইন্সের সবচেয়ে মৌলিক ধারণাগুলোর একট। সকেট ব্যবহার করে আপনি ইন্টার-কানেক্টেড ডিভাইসের পুরো নেটওয়ার্ক বানাতে পারেন।
কম্পিউটার সাইন্সের সব জিনিসের মতো, সকেট একটি খুব অ্যাবস্ট্রাক্ট ধারণা। তাই, সকেট কী সেটা ডিফাইন করার চেয়ে, সকেট কী করে সেটা ডিফাইন করা অনেক সহজ।
তাহলে, সকেট কী করে? এটি দুটি কম্পিউটারের একে অপরের সাথে যোগাযোগ করতে সাহায্য করে। এটি কীভাবে করে? এর দুটি মেথড ডিফাইন করা আছে, send() এবং recv() সেন্ড এবং রিসিভের জন্য যথাক্রমে।
ঠিক আছে, কিন্তু send() এবং recv() আসলে কী সেন্ড এবং রিসিভ করে? মানুষ মুখ হিলালে শব্দ বিনিময় করে। সকেট তার মেথডগুলো ব্যবহার করলে বিট এবং বাইট বিনিময় করে।
এক উদাহরণ দিয়ে মেথডগুলো ইলাস্ট্রেট করি। ধরুন দুটি কম্পিউটার আছে, A এবং B। কম্পিউটার A কম্পিউটার B-কে কিছু বলতে চাইছে। তাই কম্পিউটার B কম্পিউটার A কী বলছে সেটা শুনতে চাইছে। এটার রূপ নিচের মতো হবে।

বাফার পড়া
এটু একটু অদ্ভুত লাগছে, ঠিক না? একটা, দুটি কম্পিউটারই মাঝপথে একটি বারের দিকে পয়েন্ট করছে, যেটার টাইটেল 'buffer'।
বাফার কী? বাফার একটি মেমরি স্ট্যাক। এটি হলো যেখানে প্রতিটি কম্পিউটারের ডেটা সংরক্ষিত থাকে, এবং কার্নেল দ্বারা এলোকেট করা হয়।
পরবর্তী, কেন তারা উভয় একই বাফারের দিকে পয়েন্ট করছে? ঠিক বললে, এটি পুরোপুরি সঠিক নয়। প্রতিটি কম্পিউটারের নিজস্ব বাফার থাকে নিজস্ব কার্নেল দ্বারা এলোকেটেড এবং নেটওয়ার্ক দুটি আলাদা বাফারের মাঝে ডেটা ট্রান্সপোর্ট করে। কিন্তু, আমি নেটওয়ার্ক ডিটেইলসে যেতে চাই না, তাই আমরা অনুমান করব দুটি কম্পিউটারের একই বাফার অ্যাক্সেস আছে "কहीं ভয়েডে মাঝপথে"।
ঠিক আছে, এখন আমরা ভিজ্যুয়ালি কী দেখতে পাচ্ছি সেটা জানি, এটাকে কোডে অ্যাবস্ট্রাক্ট করি।
#Computer A sends data computerA.send(data) #Computer B receives data computerB.recv(1024)এই কোড স্নিপেটটি উপরের ছবির সেটাই রিপ্রেজেন্ট করে। ছাড়া একটি কৌতূহল, আমরা computerB.recv(data) বলি না। বদলে, ডেটার জায়গায় একটি যেন র্যান্ডম নাম্বার স্পেসিফাই করি।
কারণটি সিম্পল। নেটওয়ার্কের ওপর ডেটা বিট আকারে ট্রান্সমিট হয়। তাই যখন কম্পিউটার B-এ recv করি, আমরা স্পেসিফাই করি আমরা একসময় কত বিট রিসিভ করতে ইচ্ছুক।
আমি কেন ১০২৪ বাইট রিসিভ করার জন্য পিক করলাম? কোনো স্পেসিফিক কারণ নেই। সাধারণত ২-এর পাওয়ারের সংখ্যা স্পেসিফাই করা ভালো। আমি ১০২৪ পিক করলাম যেটা ২¹⁰।
তাহলে বাফার এটাকে কীভাবে বুঝে? খুব সিম্পল। কম্পিউটার A তার সাথে সংরক্ষিত যেকোনো ডেটা বাফারে লিখে বা পাঠায়। কম্পিউটার B সেটি রিড বা রিসিভ করতে সিদ্ধান্ত নেয় বাফারে সংরক্ষিত প্রথম ১০২৪ বাইট।
বেশ ভালো! কিন্তু, এই দুটি কম্পিউটার কীভাবে একে অপরের সাথে কথা বলতে পারে সেটা জানে? উদাহরণস্বরূপ, যখন কম্পিউটার A এই বাফারে লিখে, কীভাবে সেটা জানে কম্পিউটার B সেটা পিক আপ করবে? অন্য কথায়, দুটি কম্পিউটারের কানেকশন কীভাবে একটি ইউনিক বাফার নিশ্চিত করতে পারে?
পোর্টিং ইন্টু আইপিজ

উপরের ছবিতে একই দুটি কম্পিউটার দেখানো হয়েছে যাদের সাথে আমরা সবসময় কাজ করছিলাম একটি এক্সট্রা ডিটেইলসহ। প্রতিটি কম্পিউটারের সামনে একটি বড় বারের দৈর্ঘ্যে কিছু নাম্বার লিস্টেড আছে।
প্রতিটি কম্পিউটারের সামনে লম্বা বারের কথা চিন্তা করুন যেটা একটি নির্দিষ্ট কম্পিউটারকে ইন্টারনেটের সাথে কানেক্ট করে রাউটার। সেই বারে লিস্টেড নাম্বারগুলোকে পোর্ট বলা হয়। আপনার কম্পিউটারের এখন হাজার হাজার পোর্ট উপলব্ধ। প্রতিটি পোর্ট একটি সকেট কানেকশন অনুমতি দেয়। ছবিতে আমি শুধু ৬টি পোর্ট দেখিয়েছি, কিন্তু আপনি আইডিয়া পেলেন।
২৫৫-এর নিচের পোর্টগুলো সাধারণত সিস্টেম কল এবং লো-লেভেল কানেকশনের জন্য রিজার্ভড। সাধারণত ৪-ডিজিটের হাই নম্বরের পোর্টে, যেমন ৮০০০, কানেকশন খোলা উচিত। আমি উপরের ছবিতে বাফার আঁকিনি, কিন্তু আপনি অনুমান করতে পারেন প্রতিটি পোর্টের নিজস্ব বাফার আছে।
বারটাও একটি নাম্বারের সাথে জুড়ে আছে। এই নাম্বারকে IP অ্যাড্রেস বলা হয়। IP অ্যাড্রেসের সাথে একসাথে অনেক পোর্ট জুড়ে থাকে। এটাকে এইভাবে চিন্তা করুন:
127.0.0.1 / | \ / | \ / | \ 8000 8001 8002চলুন কম্পিউটার A এবং B-এর মাঝে একটি নির্দিষ্ট পোর্টে কানেকশন সেটআপ করি।
# computerA.pyimport socket computerA = socket.socket() # localhost:8000-এ কানেক্ট করা computerA.connect(('127.0.0.1', 8000)) string = 'abcd' encoded_string = string.encode('utf-8') computerA.send(encoded_string)এখানে computerB.py-র কোড:
# computerB.py import socket computerB = socket.socket() # localhost:8000-এ লিসেন করা computerB.bind(('127.0.0.1', 8000)) computerB.listen(1) client_socket, address = computerB.accept() data = client_socket.recv(2048) print(data.decode('utf-8'))লাগছে আমরা কোডে একটু আগে চলে গেলাম, কিন্তু আমি স্টেপ বাই স্টেপ ব্যাখ্যা করব। আমরা জানি দুটি কম্পিউটার আছে, A এবং B। তাই একটা ডেটা পাঠাবে এবং একটা ডেটা রিসিভ করবে।
আমি באופן خود থেকেই A-কে ডেটা পাঠাতে এবং B-কে ডেটা রিসিভ করতে সিলেক্ট করেছি। এই লাইনে computerA.connect((‘127.0.0.1’, 8000), আমি কম্পিউটার A-কে IP অ্যাড্রেস 127.0.0.1-এ পোর্ট ৮০০০-এ কানেক্ট করছি।
নোট: 127.0.0.1 সাধারণত লোকালহোস্ট বোঝায়, যা আপনার মেশিনকে রেফার করে
তারপর, কম্পিউটার B-র জন্য, আমি এটাকে IP অ্যাড্রেস 127.0.0.1-এ পোর্ট ৮০০০-এ বাইন্ড করছি। এখন, আপনি চিন্তা করছেন কেন দুই ভিন্ন কম্পিউটারের জন্য একই IP অ্যাড্রেস।
এটি কারণ আমি চেটিং করছি। আমি একটি কম্পিউটার ব্যবহার করছি সকেট কীভাবে ব্যবহার করা যায় তা দেখানোর জন্য (আমি বেসিক্যালি একই কম্পিউটার থেকে এবং একই কম্পিউটারে কানেক্ট করছি সিম্পলিসিটির জন্য)। সাধারণত দুটি ভিন্ন কম্পিউটারের দুটি ভিন্ন IP অ্যাড্রেস থাকবে।
আমরা ইতিমধ্যে জানি শুধু বিট ডেটা প্যাকেটের অংশ হিসেবে পাঠানো যেতে পারে, তাই আমরা স্ট্রিংকে এনকোড করি পাঠানোর আগে। একইভাবে, কম্পিউটার B-এ স্ট্রিং ডিকোড করি। আপনি যদি উপরের দুটি ফাইল লোকালি রান করতে চান, computerB.py ফাইলটি প্রথমে রান করুন নিশ্চিত করে। আপনি যদি computerA.py ফাইলটি প্রথমে রান করেন, তবে কানেকশন রিফিউজড এরর পাবেন।
ক্লায়েন্টদের সার্ভ করা

আমি নিশ্চিত অনেকের কাছে এটি ইতিমধ্যে স্পষ্ট যে আমি এতক্ষণ যা বর্ণনা করছি সেটা একটি খুব সিম্পলিস্টিক ক্লায়েন্ট-সার্ভার মডেল। ফ্যাক্ট, উপরের ছবি থেকে দেখতে পাচ্ছেন, আমি শুধু কম্পিউটার A-কে ক্লায়েন্ট এবং কম্পিউটার B-কে সার্ভার রিপ্লেস করলাম।
ক্লায়েন্ট এবং সার্ভারের মাঝে একটি কনস্ট্যান্ট কমিউনিকেশন স্ট্রিম চলতে থাকে। আমাদের আগের কোড উদাহরণে, আমরা এক শট ডেটা ট্রান্সফার বর্ণনা করেছি। বদলে, আমরা যা চাই সেটা হলো ক্লায়েন্ট থেকে সার্ভারের तरफ একটি কনস্ট্যান্ট ডেটা স্ট্রিম পাঠানো। যাইহোক, আমরা চাই জানা যে ডেটা ট্রান্সফার কখন কমপ্লিট হয়েছে, যাতে আমরা জানি আমরা লিসেনিং বন্ধ করতে পারি।
এটি আরও এক্সপ্লোর করতে একটি এনালজি ব্যবহার করি। নিচের কথোপকথন দুজন মানুষের মাঝে কল্পনা করুন।

দুই জন মানুষ একে অপরের সাথে পরিচয় করাতে চাইছে। যাইহোক, তারা একসাথে কথা বলতে চাইবে না। ধরুন রাজ প্রথমে যায়। জন তারপর রাজ তার পরিচয় শেষ না হওয়া পর্যন্ত অপেক্ষা করবে তারপর নিজের পরিচয় দিবে। এটি কিছু লার্নড হিউরিস্টিক্সের ওপর ভিত্তি করে কিন্তু আমরা সাধারণত উপরের প্রোটোকাল হিসেবে বর্ণনা করতে পারি।
আমাদের ক্লায়েন্ট এবং সার্ভারদের একটি সিমিলার প্রোটোকল দরকার। অন্যথায়, তারা কীভাবে জানবে তাদের পালা ডেটা প্যাকেট পাঠানোর?
আমরা একটা সিম্পল জিনিস করব এটি ইলাস্ট্রেট করতে। ধরুন আমরা কিছু ডেটা পাঠাতে চাই যেটা হয়ে গেছে একটি স্ট্রিং অ্যারে। ধরুন অ্যারেটি নিচের মত:
arr = ['random', 'strings', 'that', 'need', 'to', 'be', 'transferred', 'across', 'the', 'network', 'using', 'sockets']
উপরের ডেটাটি ক্লায়েন্ট থেকে সার্ভারে লেখা হবে। আরেকটি কনস্ট্রেইন্ট তৈরি করি। সার্ভারকে ডেটা এক্সেপ্ট করতে হবে যা একজাক্টলি সেটি স্ট্রিং দ্বারা অকুপাইড ডেটার সমান যেটা সেই মুহূর্তে পাঠানো হচ্ছে।
উদাহরণস্বরূপ, যদি ক্লায়েন্ট 'random' স্ট্রিংটি পাঠাতে চায়, এবং ধরুন প্রতিটি ক্যারেক্টার ১ বাইট একোকেয়পাই করে, তবে স্ট্রিংটি ৬ বাইট একোকেয়পাই করে। ৬ বাইট তখন ৬*৮ = ৪৮ বিট সমান। তাই, 'random' স্ট্রিংটি সকেটস দিয়ে ক্লায়েন্ট থেকে সার্ভারে ট্রান্সফার করার জন্য, সার্ভারকে জানতে হবে যে সেটিকে ৪৮ বিট অ্যাক্সেস করতে হবে সেই স্পেসিফিক ডেটা প্যাকেটের জন্য।
এটি সমস্যাটি ব্রেক ডাউন করার একটি ভালো সুযোগ। কিছু জিনিস আমাদের আগে বের করতে হবে।
পাইথনে একটি স্ট্রিং কত বাইট একোকেয়পাই করে সেটা কীভাবে বের করব?
যদি, আমরা স্ট্রিংর লেন্থ বের করে শুরু করি। এটি সহজ, শুধু len() কল। কিন্তু, আমাদের এখনও স্ট্রিংর আকুপাইড বাইট নম্বর জানতে হবে, শুধু লেন্থ নয়।
আমরা স্ট্রিংকে বাইনারি কনভার্ট করব প্রথমে, এবং তারপর রেজাল্টিং বাইনারি রিপ্রেজেন্টেশনের লেন্থ বের করব। এটা আমাদের ব্যবহৃত বাইট নম্বর দেবে।
len(‘random’.encode(‘utf-8’)) আমাদের যা দরকার সেটা দেবে
প্রতিটি স্ট্রিংর অকুপাইড বাইট নম্বর সার্ভারে কীভাবে পাঠাব?
সহজ, আমরা বাইট নম্বর (যেটা একটি ইন্টিজার) কে সেই নম্বরটির বাইনারি রিপ্রেজেন্টেশনে কনভার্ট করব, এবং সার্ভারে পাঠাব। এখন, সার্ভার স্ট্রিং রিসিভ করার আগে স্ট্রিংর লেনথ এক্সপেক্ট করতে পারবে।
সার্ভার কীভাবে জানে ক্লায়েন্ট সব স্ট্রিং পাঠাতে ফিনিশ করেছে?
কথোপকথনের এনালজি থেকে মনে রাখবেন, ডেটা ট্রান্সফার কমপ্লিট হয়েছে কিনা জানার একটি উপায় দরকার। কম্পিউটারদের নিজস্ব হিউরিস্টিক্স নেই যেগুলোর ওপর তারা নির্ভর করতে পারে। তাই, আমরা একটি র্যান্ডম রুল দিব। আমরা বলব, যখন আমরা 'end' স্ট্রিংটি পাঠাব, তখন মানে সার্ভার সকল স্ট্রিং রিসিভ করেছে এবং কানেকশন বন্ধ করতে পারে। অবশ্যই, এর মানে 'end' স্ট্রিংটি আমরা আমাদের অ্যারেতে অন্য কোথাও ব্যবহার করতে পারব না খুব শেষে ছাড়া।
এতক্ষণে আমরা যে প্রোটোকল ডিজাইন করেছি:

স্ট্রিংর লেন্থ ২ বাইট হবে, তারপর আসল স্ট্রিংটি যেটা ভেরিয়েবল লেন্থ। এটি পिछলো প্যাকেটে পাঠানো স্ট্রিংর লেন্থের ওপর নির্ভর করবে, এবং আমরা স্ট্রিং লেন্থ এবং আসল স্ট্রিংর মাঝে অ্যাল্টারনেট করব। EOT বলতে End Of Transmission, এবং 'end' স্ট্রিং পাঠানোর মানে আর কোনো ডেটা পাঠাতে নেই।
নোট: এগিয়ে যাওয়ার আগে, একটা জিনিস পয়েন্ট আউট করি। এটি একটি খুব সিম্পল এবং স্টুপিড প্রোটোকল। আপনি যদি একটি ভালো ডিজাইন করা প্রোটোকল দেখতে চান, HTTP প্রোটোকল চেক করুন।
এটা কোড করি। নিচের গিস্টগুলিতে কমেন্ট ইনক্লুড করা আছে তাই এটা সেলফ-এক্সপ্লানেটরি।
বেশ, আমাদের একটি ক্লায়েন্ট রানিং আছে। এখন সার্ভার দরকার।
উপরের গিস্টগুলিতে কিছু স্পেসিফিক লাইন কোড এক্সপ্লেইন করি। প্রথমটি, clientSocket.py ফাইল থেকে।
len_in_bytes = (len_of_string).to_bytes(2, byteorder='little')
উপরের কোডটি একটি নাম্বারকে বাইটে কনভার্ট করে। to_bytes ফাংশনে পাস করা প্রথম প্যারামিটার len_of_string-কে এর বাইনারি রিপ্রেজেন্টেশনে কনভার্ট করার ফলাফলের জন্য এলোকেটেড বাইট নম্বর।
দ্বিতীয় প্যারামিটার ব্যবহার করা হয় লিটল এন্ডিয়ান ফরম্যাট অথবা বিগ এন্ডিয়ান ফরম্যাট ফলো করা হবে কিনা ডিসাইড করতে। আপনি এখানে পড়তে পারেন। এখন জানুন যে আমরা সবসময় সেই প্যারামিটারের জন্য little ব্যবহার করব।
পরবর্তী লাইন কোড যেটা আমি দেখাতে চাই:
client_socket.send(string.encode(‘utf-8’))
আমরা স্ট্রিংকে ‘utf-8’ এনকোডিং ব্যবহার করে বাইনারি ফরম্যাটে কনভার্ট করছি।
পরবর্তী, serverSocket.py ফাইলে:
data = client_socket.recv(2) str_length = int.from_bytes(data, byteorder='little')উপরের প্রথম লাইন কোড ক্লায়েন্ট থেকে ২ বাইট ডেটা রিসিভ করে। মনে রাখুন clientSocket.py-এ স্ট্রিংর লেন্থ বাইনারি ফরম্যাটে কনভার্ট করার সময়, আমরা রেজাল্ট ২ বাইটে স্টোর করার সিদ্ধান্ত নিয়েছিলাম। এটাই কারণ আমরা সেটি ডেটার জন্য ২ বাইট রিড করছি।
পরবর্তী লাইন বাইনারি ফরম্যাটকে ইন্টিজারে কনভার্ট করে। এখানে byteorder "little", ক্লায়েন্টে ব্যবহার করা byteorder-এর সাথে মেলাতে।
আপনি যদি দুটি সকেট রান করেন, আপনি দেখবেন সার্ভার ক্লায়েন্ট যে স্ট্রিংগুলো পাঠাচ্ছে সেগুলো প্রিন্ট করছে। আমরা কমিউনিকেশন স্থাপন করেছি!
উপসংহার
ঠিক আছে, আমরা এতক্ষণে অনেক কিছু কভার করেছি। নামত, সকেট কী, আমরা কীভাবে সেগুলো ব্যবহার করি, এবং খুব সিম্পল এবং স্টুপিড একটি প্রোটোকল কীভাবে ডিজাইন করতে হয়। আপনি যদি সকেট কীভাবে কাজ করে তার সম্পর্কে আরও জানতে চান, আমি প্রबलভাবে রিকমেন্ড করছি Beej’s Guide To Network Programming পড়ুন। সেই ই-বুকে অনেক দুর্দান্ত জিনিস আছে।
আপনি অবশ্যই এই আর্টিকেলে যা পড়েছেন সেটা নেওয়া যাবে এবং আরও কমপ্লেক্স সমস্যা যেমন রাসপবেরি পাই ক্যামেরা থেকে আপনার কম্পিউটারে ইমেজ স্ট্রিমিং-এ অ্যাপ্লাই করতে পারেন। এর সাথে মজা করুন!
আপনি চাইলে টুইটারে বা GitHub-এ ফলো করতে পারেন। আমার ব্লগও চেক আউট করতে পারেন এখানে। আমি সবসময় উপলব্ধ আপনি আমাকে যোগাযোগ করতে চাইলে!
মূলত https://redixhumayun.github.io/networking/2019/02/14/how-the-internet-speaks.html-এ প্রকাশিত ১৪ ফেব্রুয়ারি, ২০১৯।