পাইথন সকেট প্রোগ্রামিং : TCP চ্যাট অ্যাপ
এর আগের পোস্টে ইন্টারনেটের লেয়ার ও ডেটা প্রটোকল নিয়ে লেখার চেষ্টা করেছিলাম। এই পোস্টে আমরা দেখব কিভাবে TCP প্রটোকল ব্যবহার করে একটা সিম্পল চ্যাট অ্যাপ বানানো যায়। এজন্য আমরা পাইথনের socket মডিউল ব্যবহার করব।
শুরুতে কয়েকটা প্রাথমিক ধারণা নিয়ে একটু কথা বলে নেই।
আমরা জানি যে, নেটওয়ার্কে কানেক্টেড প্রত্যেকটা ডিভাইসকে একটা ইউনিক IP এড্রেস এসাইন করা থাকে, যাতে করে সেটাকে নির্দিষ্ট করে নেটওয়ার্কে খুঁজে পাওয়া যায়। এটা হচ্ছে অনেকটা বাড়ির ঠিকানার মত, অর্থাৎ নেটওয়ার্কের মধ্যে ঐ ডিভাইসের ঠিকানা। এটাকে তাই ঐ ডিভাইসের নেটওয়ার্ক এড্রেসও বলা হয়। IPV4 ভার্সনে এটা দেখতে অনেকটা 192.178.100.111
এরকম টাইপের একটা নাম্বার হয়। এখন ঐ ডিভাইসে একটা নির্দিষ্ট সময়ে তো একাধিক প্রোগ্রাম রানিং থাকতে পারে যারা নেটওয়ার্কের সাথে কানেক্টেড থেকে কাজ করবে। হয় নেটওয়ার্ক থেকে ডাটা রিড করবে নাহলে নেটওয়ার্কে ডাটা পাস করবে। যেমন মনে করেন আপনার কম্পিউটারের ব্রাউজারও নেটওয়ার্কের সাথে কানেক্টেড আবার আপনার চ্যাট মেসেঞ্জার যেমন skype, messenger এগুলোও নেটওয়ার্কের সাথে কানেক্টেড। তাহলে আরেকটা ডিভাইস বা সার্ভার থেকে কীভাবে আপনার ডিভাইসকে জানাবে যে সে আপনার কোন প্রোগ্রামটার জন্য ডাটা পাঠাচ্ছে? বা নেটওয়ার্কে ডাটা আসলেই বা আপনার ডিভাইস কীভাবে বুঝবে সেটা কোন প্রোগ্রামের কাছে পাঠাতে হবে? এ সমস্যা সমাধানের জন্যই প্রত্যেকটা ডিভাইসে অনেকগুলো Port থাকে। নেটওয়ার্ক এড্রেসকে যদি আপনি একটা এপার্টমেন্টের ঠিকানা মনে করেন, তাহলে পোর্টকে চিন্তা করতে পারেন সেই এপার্টমেন্টের একেকটা ফ্ল্যাটের মত। তারমানে আপনি যদি সেই এপার্টমেন্টে কারও কাছে কোনো চিঠি পাঠাতে চান আপনাকে শুধু এপার্টমেন্ট নাম্বার বললেই হবে না, সাথে ফ্ল্যাট নাম্বারও বলে দিতে হবে। IP Address আর Port ও ঠিক এভাবেই কাজ করে। সাধারণত সর্বমোট প্রায় ৬৫০০০ পোর্ট ( 0 থেকে 65535) এভেইলেবল থাকে প্রতিটা ডিভাইসে। তারমানে একটা ডিভাইসে এতগুলো সংখ্যক প্রোগ্রাম একইসাথে নেটওয়ার্কের সাথে কানেক্টেড থেকে কাজ করতে পারবে। কিছু কমন পোর্ট আছে যেগুলো ইউনিভার্সালি কিছু নির্দিষ্ট কাজের জন্য নির্ধারিত। যেমন প্রতিটা ডিভাইসের পোর্ট 80 নির্দিষ্ট থাকে HTTP প্রোটোকলের জন্য। তারমানে আপনার কম্পিউটারের HTTP প্রোটোকল রিলেটেড সব তথ্য পোর্ট 80 দিয়ে আদান প্রদান হবে। এমন না যে এই পোর্ট ছাড়া অন্য পোর্টে আপনি চাইলেও HTTP রিলেটেড তথ্য আদান প্রদান করতে পারবেন না। বরং এটা হচ্ছে একটা কনভেনশনের মত। সবাই এটা মেনে চলে। যেমন আপনি কোনো একটা সার্ভারের সাথে HTTP প্রোটোকলে যোগাযোগ করতে চাচ্ছেন Port 90 তে। এখন সার্ভার তো বসে আছে আপনার জন্য Port 80 তে, তাহলে তো হবে না, তাই না? এজন্যই এরকম কমন কমন কাজের জন্য সবাই কোন Port ব্যবহার করে এটা জানা থাকা ভালো। কমন Port গুলোর লিস্ট পাওয়া যাবে এখানে।
এখন দুইটা ডিভাইস A এবং B যখন একে অপরের সাথে কানেক্ট হতে চায় তখন তাঁদের মধ্যে প্রথমে একটা কানেকশন তৈরি করে নিতে হয়। এখন এই কানেকশনটাকে যদি আমরা ‘ক্যাবল’ বা ‘তার’এর মত চিন্তা করি তাহলে এর তো দুটি প্রান্ত থাকবে, তাই না? একটা প্রান্ত যুক্ত হবে A তে আরেকটা B তে। কিন্তু যুক্ত হওয়ার জন্য তাকে কয়েকটা জিনিস নির্দিষ্ট করে দিতে হবে। যেমন এই ক্যাবলটা A তে যুক্ত হতে চাইলে তাকে বলে দিতে হবে সে কোন IP Address, কোন Port এবং কোন প্রোটোকলে (TCP or UDP) যুক্ত হতে চায়। বোঝাই যাচ্ছে প্রথমে IP Address দিয়ে সে নির্দিষ্ট করবে কোন ডিভাইস অর্থাৎ A, তারপরে তার কোন Port নাম্বারে এবং তার সাথে নেটওয়ার্ক লেয়ারের কোন প্রোটোকলের মাধ্যমে কানেক্ট হবে। এই জিনিসগুলো দিয়ে সে A তে তার কানেকশনের একটা প্রান্ত এস্টাব্লিশ করবে। এই প্রান্তটাকেই মূলত বলা হয় Socket। একই রকমভাবে আরেকটা সকেট তৈরি হবে B তে। এভাবে দুইটা সকেট দিয়ে একটা সাকসেসফুল কানেকশন তৈরি হয়। তারপরে আর কি, এই কানেকশন দিয়ে দুজনের মাঝে কথা চালাচালি চলতে থাকে।
আমাদের চ্যাট অ্যাপে যাওয়ার আগে একটি সিম্পল TCP প্রোগ্রাম দেখি, যেখানে ক্লায়েন্ট সার্ভারকে ‘Ping!’ বলবে আর সার্ভার উত্তরে ক্লায়েন্টকে ‘Pong!’ বলে কানেকশন অফ করে দিবে।
ক্লায়েন্ট
প্রথমে আমরা সকেট মডিউল ইম্পোর্ট করি।
import socket
host = 'localhost'
port = 8000
তারপরে আমরা যে হোস্টের সার্ভারের সাথে কমিউনিকেট করব সেই হোস্টের হোস্টনেম ও পোর্ট দুইটি ভ্যারিয়েবলে রাখি। এখানে আমরা আপাতত লোকাল হোস্টেই সার্ভার রান করব। তাই হোস্ট হিসেবে localhost
নিলাম। ইচ্ছা করলে আমরা লোকালহোস্টের আইপি এড্রেসও দিতে পারতাম। পোর্ট নাম্বারটি হল সার্ভারের সকেটের পোর্ট নাম্বার (যেটার সাথে আমরা কমিউনিকেট করব) । ক্লায়েন্টের সকেটেরও একটি পোর্ট নাম্বার থাকে। তবে সেটি আমরা বলে দিব না। অপারেটিং সিস্টেম ইচ্ছা মত যেকোন একটি পোর্ট এসাইন করবে।
তার পরের লাইনে আমরা একটা সকেট অবজেক্ট ক্রিয়েট করি। এখানে দুটি আর্গুমেন্টের প্রথমটি দিয়ে বলে দিলাম যে সকেটটি IPv4 প্রটোকল ব্যবহার করবে। আর দ্বিতীয়টির অর্থ হল সকেটটি একটি SOCK_STREAM
অর্থাৎ TCP সকেট।
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
তার পরের লাইনে আমরা সার্ভারের সাথে সকেট কানেক্ট করব। এখানে একটি tuple এ সার্ভারের হোস্ট আর পোর্ট দিতে হবে।
sock.connect((host, port))
এখন আমরা সার্ভারে একটি মেসেজ ‘Ping!’ পাঠাব। কিন্তু সকেটের send
মেথডটি শুধু বাইট নেয়। তাই পাঠানোর আগে আমাদের স্ট্রিংটিকে byte এ encode করে নেব। (ইচ্ছা করলে b'Ping!'
এভাবেও দেয়া যেত)
sock.send(‘Ping!’.encode())
পরের লাইনদুটির প্রথমটিতে আমরা রিসিভ মেথড কল করে সার্ভারের রিপ্লাইয়ের জন্য অপেক্ষা করব। এখানে আর্গুমেন্টটি দিয়ে বাফার সাইজ বলে দেয়া হয়েছে। তারপর রিপ্লাই আসলে সেটা প্রিন্ট করব। এই মেথডটিও বাইট রিটার্ন করে। তাই প্রিন্ট করার সময় ডিকোড করে প্রিন্ট করব।
message = sock.recv(4096)
print('Server said: ' + message.decode())
তারপরে আমরা সকেটটি ক্লোজ করে দেই। এখানে বলে রাখা ভাল যে সকেট ফাইলের মতই অপারেটিং সিস্টেম রিসোর্স। তাই আমাদের ব্যবহার শেষ হলে সব সময়ই ক্লোজ করে দেয়া উচিত।
sock.close()
সার্ভার
আগের মতই সকেট অবজেক্ট ইম্পোর্ট করে সকেট অবজেক্ট ক্রিয়েট করলাম। তারপর যা করি তা হল সকেট অবজেক্টটিকে একটি পোর্টের সাথে এসোসিয়েট করি bind
মেথড কল করে। এখানে আমরা হোস্টের বদলে এম্পটি স্ট্রিং দিচ্ছি।
sock.bind(('', port))
এর পরের লাইনের মাধ্যমে সকেটটি টিসিপি কানেকশনের জন্য অপেক্ষা করে। এখানে আর্গুমেন্ট ১ এর মাধ্যমে বলা হয়েছে সর্বোচ্চ একটি কানেকশন queue তে থাকবে।
sock.listen(1)
যখন কোন ক্লায়েন্ট কানেশনের জন্য রিকোয়েস্ট করে তখন প্রোগ্রাম এক্সিকিউশন পরের লাইনে যায়। পরের লাইনে আমরা কানেকশনটি এক্সেপ্ট করি। এই মেথডটি একটি নতুন সকেট অবজেক্ট ও এড্রেস রিটার্ন করে। এই নতুন সকেট অবজেক্ট দিয়ে আমরা ক্লায়েন্টের সাথে যোগাযোগ করতে পারব। এই পর্যায়ে ক্লায়েন্ট-সার্ভার হ্যান্ডশেক হয়ে যায়।
client_sock, addr = sock.accept()
তারপর আমরা ক্লায়েন্ট থেকে মেসেজের জন্য অপেক্ষা করি। মেসেজ পেলে সেটা প্রিন্ট করি এবং ক্লায়েন্টকে রিপ্লাই পাঠাই। সর্বশেষে সকেট ক্লোজ করে দেই।
এখন প্রথমে সার্ভার প্রোগ্রামটি চালিয়ে পরে ক্লায়েন্ট প্রোগ্রামটি চালালে আমরা এমন আউটপুট দেখতে পাই।
Server
Client said: Ping!
Client
Server said: Pong!
চ্যাট অ্যাপ
এখন আমরা শিখে গেলাম কিভাবে সকেট ও টিসিপি দিয়ে মেসেজ আদান প্রদান করতে হয়। তাহলে আমাদের চ্যাট অ্যাপ বানানো কঠিন হবে না। শুধু একটা মেসেজ পেয়ে কানেকশন ক্লোজ করে না দিয়ে আবার পরবর্তী মেসেজের জন্য অপেক্ষা করতে হবে।
পুরো কোড দিয়ে দিলাম। আশা করি নিজেই পড়ে বুঝতে পারবেন। এখানে ক্লায়েন্ট সার্ভারের জন্য দুটি আলাদা আলাদা স্ক্রিপ্ট না লিখে একই স্ক্রিপ্টে দুটি ফাংশন লিখলাম।
স্ক্রিপ্টটা অনেকদিক থেকেই ত্রুটিপূর্ণ। যেমন এখানে একটা মেসেজ পাঠিয়ে উত্তরের জন্য অপেক্ষা করতে হয়। উত্তর পাওয়ার পরেই আরেকটা মেসেজ পাঠানো যায়। থ্রেডিং ব্যবহার করে এই সমস্যা সমাধান করা যেত।
আরেকটা দুঃখজনজ সমস্যা হল এটি পাবলিক ইন্টারনেটে কাজ করবে না। কারণ আজকাল আমরা যেসব উপায়ে ইন্টারনেট ব্যবহার করি (যেমন ওয়াই ফাই, থ্রিজি, টুজি) তাতে প্রায় কোন ডিভাইসেরই ডেডিকেটেড পাবলিক আইপি থাকে না। ডিভাইসগুলো NAT ডিভাইসের (যেমন আপনার বাসার রাউটার) মাধ্যমে কনফিগার করা থাকে। এতে করে NAT ডিভাইসের শুধু পাবলিক আইপি হয় আর বাকি ডিভাইসগুলোর একটা করে লোকাল আইপি হয়। NAT (Network Address Translation) কিভাবে কাজ করে বা আমাদের সমস্যাটাই বা কোথায় হচ্ছে জানতে ইন্টারনেটে সার্চ করতে পারেন। এখানে বিস্তারিত লিখছি না।
হ্যাঁ, এটা লোকাল নেটওয়ার্কে কাজ করবে। অর্থাৎ একই ওয়াইফাই নেটওয়ার্কে কানেক্টেড এক ডিভাইস থেকে অন্য ডিভাইসে ব্যবহার করা যাবে। আমি আমার ল্যাপটপ আর অ্যান্ড্রয়েডে (QPython অ্যাপ দিয়ে) টেস্ট করেছি। এজন্য প্রথমে এন্ড্রয়েডের মোবাইল হটস্পট চালু করে ল্যাপটপ কানেক্ট করি। তারপর ল্যাপটপে স্ক্রিপ্টটা রান করে হোস্ট সিলেক্ট করি। তারপর মোবাইলে স্ক্রিপ্টটা রান করে জয়েন সিলেক্ট করি। তারপর হোস্টের আইপি এড্রেস দেই।
আপনি চাইলে দুটো কম্পিউটারেও টেস্ট করতে পারেন। এক্ষেত্রে দুটো কম্পিউটার একই নেটওয়ার্কে কানেক্টেড থাকতে হবে। এখন খেয়াল করুন, যদি হোস্টের আইপি 127.0.01 বা 0.0.0.0 দেখায় তাহলে বুঝতে হবে আইপি বের করার ফাংশনটা কাজ করেনি। সেক্ষেত্রে অন্যভাবে ( আপনার অপারেটিং সিস্টেম অনুসারে) হোস্টের আইপি বের করে ক্লায়েন্টে ইনপুট দিতে হবে।
পোর্ট, আইপি আর সকেটের ডিটেইল ব্যাখ্যার জন্য ভাইকে ধন্যবাদ :)
আজ এ পর্যন্তই। কোন কিছু বুঝতে অসুবিধা হলে বলবেন। ভুল ত্রুটি হলে ক্ষমাসুন্দর দৃষ্টিতে দেখতে হবে না, শুধু ধরিয়ে দিলেই হবে :)