পাঠ ৫৪ · ৫৭-এর মধ্যে · মডিউল ১২
Home / Courses / Computer Networks / চ্যাট অ্যাপ্লিকেশন ডিজাইন

কেস স্টাডি: সকেট দিয়ে একটি চ্যাট অ্যাপ্লিকেশন বানানো

Case study: building a chat app with sockets
১০ মিনিট পড়া উন্নত · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • একটি TCP-ভিত্তিক চ্যাট অ্যাপ্লিকেশনের সার্ভার-সাইড ডিজাইন
  • ব্রডকাস্ট প্যাটার্ন — এক ক্লায়েন্টের মেসেজ বাকি সবার কাছে পাঠানো
  • TCP-এর কাঁচা বাইট স্ট্রিমের উপর কেন ও কীভাবে মেসেজ সীমানা নির্ধারণ করতে হয়
  • Python দিয়ে সম্পূর্ণ ইন-মেমরি সিমুলেশনে এই প্যাটার্ন যাচাই

১ · দৃশ্যপট (Scenario)

ধরুন আপনি একটি সাধারণ গ্রুপ চ্যাট অ্যাপ্লিকেশন বানাচ্ছেন — একাধিক ব্যবহারকারী একটি সার্ভারে সংযুক্ত হবেন এবং যেকোনো একজনের পাঠানো মেসেজ বাকি সবাই দেখতে পাবেন। এটি সাধারণ request-response প্যাটার্নের (যেমন একটি ওয়েব সার্ভার) চেয়ে ভিন্ন — এখানে সার্ভারকে একাধিক সক্রিয় কানেকশন একসাথে পরিচালনা করতে হয় এবং একটি কানেকশন থেকে আসা ডেটা বাকি কানেকশনগুলোতে ফরোয়ার্ড করতে হয়।

২ · ওয়াকথ্রু (Walkthrough)

সার্ভার একটি well-known পোর্টে শোনে (M5/L22-এ শেখা পোর্ট-ভিত্তিক মাল্টিপ্লেক্সিং), এবং প্রতিটি নতুন ক্লায়েন্ট সংযোগের জন্য একটি আলাদা কানেকশন গ্রহণ করে — M7/L35-এ শেখা accept() প্যাটার্নের অনুরূপ, যেখানে প্রতিটি ক্লায়েন্ট তার নিজস্ব ডেডিকেটেড কানেকশন পায়। কিন্তু এখানে সার্ভারকে একটি ধাপ এগিয়ে যেতে হয় — শুধু একটি ক্লায়েন্টের সাথে কথা বলা নয়, বরং সক্রিয় ক্লায়েন্টদের একটি তালিকা বজায় রাখা এবং যেকোনো একজনের মেসেজ বাকি সবার কাছে broadcast করা — M7/L37-এ আলোচিত মাল্টি-ক্লায়েন্ট-হ্যান্ডলিং প্যাটার্নের একটি বাস্তব প্রয়োগ।

একটি বাস্তব ও প্রায়ই বিস্মিত করা বিস্তারিত — মেসেজ সীমানা

M5/L25-এ শেখা TCP-এর নির্ভরযোগ্যতার গ্যারান্টি নিশ্চিত করে যে বাইট সঠিক ক্রমে ও সম্পূর্ণ পৌঁছাবে — কিন্তু TCP নিজে "একটি চ্যাট মেসেজ" ধারণাটি জানে না, এটি শুধু বাইটের একটি অবিচ্ছিন্ন স্ট্রিম। তাই অ্যাপ্লিকেশনকেই নিজের মেসেজ সীমানা নির্ধারণ করতে হয় — সবচেয়ে সাধারণ দুটি সমাধান হলো একটি ডিলিমিটার ক্যারেক্টার (যেমন নিউলাইন \n) অথবা একটি length-prefix হেডার (মেসেজের দৈর্ঘ্য আগে থেকে পাঠানো)। এটি নতুনদের জন্য প্রায়ই একটি বিস্ময়কর বাস্তবতা — "TCP তো নির্ভরযোগ্য, তাহলে মেসেজ আলাদা করার দরকার কেন?"

Python
# এই সিমুলেশন কোনো প্রকৃত socket খোলে না — Pyodide sandbox real port bind করতে
# পারে না, তাই এটি real socket API-এর আচরণের একটি ইচ্ছাকৃত ইন-মেমরি সিমুলেশন।

class MockSocket:
    """M7-এর মক-সকেট প্যাটার্নের পুনর্ব্যবহার — recv()-এর মতো একটি ইনবক্স রাখে।"""
    def __init__(self, client_id):
        self.client_id = client_id
        self.inbox = []

    def deliver(self, message):
        """সার্ভার এই মেথড কল করে এই ক্লায়েন্টকে একটি মেসেজ 'পাঠায়' (real sendall()-এর সিমুলেশন)।"""
        self.inbox.append(message)


class ChatServer:
    """একটি well-known পোর্টে (M5/L22) 'শোনে' — একাধিক ক্লায়েন্ট কানেকশনের
    তালিকা রাখে (M7/L35 accept(), M7/L37 multi-client handling)।"""
    def __init__(self, port=5000):
        self.port = port
        self.clients = {}  # client_id -> MockSocket

    def connect(self, client_id):
        sock = MockSocket(client_id)
        self.clients[client_id] = sock
        return sock

    def broadcast(self, sender_id, message, clients):
        """প্রেরক ছাড়া বাকি সব সংযুক্ত ক্লায়েন্টের কাছে মেসেজ পাঠায়।"""
        framed_message = f"[{sender_id}] {message}\n".strip()  # নিউলাইন ডিলিমিটার = মেসেজ সীমানা
        for cid, sock in clients.items():
            if cid != sender_id:
                sock.deliver(framed_message)


# --- সিমুলেশন: ৩ জন ক্লায়েন্ট একটি চ্যাট সার্ভারে সংযুক্ত ---
server = ChatServer(port=5000)
alice = server.connect("Alice")
bob = server.connect("Bob")
carol = server.connect("Carol")

print(f"সার্ভার পোর্ট {server.port}-এ 'শুনছে', সংযুক্ত ক্লায়েন্ট: {list(server.clients)}\n")

server.broadcast("Alice", "সবাই কেমন আছেন?", server.clients)

print("Alice-এর ইনবক্স (নিজের পাঠানো মেসেজ নিজে পায় না):", alice.inbox)
print("Bob-এর ইনবক্স:  ", bob.inbox)
print("Carol-এর ইনবক্স:", carol.inbox)

assert alice.inbox == []
assert bob.inbox == carol.inbox == ["[Alice] সবাই কেমন আছেন?"]
print("\nব্রডকাস্ট প্যাটার্ন সঠিক — প্রেরক ছাড়া বাকি সবাই মেসেজ পেয়েছে।")

    
বাস্তব জীবনে ChatServer-এর প্রতিটি MockSocket আসলে একটি real socket.socket কানেকশন হবে (M7/L34-L35-এ শেখানো real API অনুযায়ী), এবং সার্ভার সাধারণত threading বা asyncio ব্যবহার করে একাধিক ক্লায়েন্ট একসাথে পরিচালনা করে। কিন্তু broadcast()-এর মূল যুক্তি — "প্রেরক ছাড়া বাকি সবাইকে পাঠাও" — ঠিক এই সিমুলেশনের মতোই থাকে।
মূল কথা · Key takeaway

একটি চ্যাট অ্যাপ্লিকেশন সাধারণ request-response সার্ভারের চেয়ে ভিন্ন কারণ এটি একাধিক সক্রিয় কানেকশনের মধ্যে ডেটা ফরোয়ার্ড করে, শুধু একটির উত্তর দেয় না — আর TCP নির্ভরযোগ্য বাইট ডেলিভারি দিলেও, সেই বাইটের মধ্যে "একটি মেসেজ কোথায় শেষ" তা নির্ধারণ করার দায়িত্ব সবসময় অ্যাপ্লিকেশন স্তরেরই।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ প্রেরক নিজেই কেন তার নিজের পাঠানো মেসেজ ফেরত পায় না — এটি কি একটি বাগ?

না, এটি ইচ্ছাকৃত ডিজাইন — প্রেরকের নিজের UI ইতিমধ্যেই তার পাঠানো মেসেজ দেখায় (স্থানীয়ভাবে, সার্ভারে পাঠানোর সাথে সাথেই)। সার্ভার যদি প্রেরককেও মেসেজ ফেরত পাঠাত, তাহলে সেই মেসেজ দুইবার দেখা যেত — তাই broadcast() ইচ্ছাকৃতভাবে প্রেরককে বাদ দেয়।

প্র ০২ যদি একজন ক্লায়েন্টের মেসেজে নিজেই একটি নিউলাইন ক্যারেক্টার (\n) থাকে, তাহলে নিউলাইন-ডিলিমিটার পদ্ধতিতে কী সমস্যা হতে পারে?

রিসিভার ভুলভাবে সেই একটি মেসেজকে দুটি আলাদা মেসেজ হিসেবে পড়তে পারে, কারণ ডিলিমিটার শুধু "মেসেজ শেষ" বোঝাতে ব্যবহৃত হয় — মেসেজের ভেতরের কনটেন্ট থেকে আলাদা করা যায় না। এই সমস্যা এড়াতে length-prefix পদ্ধতি প্রায়ই বেশি নির্ভরযোগ্য, কারণ সেখানে মেসেজের দৈর্ঘ্য আগে থেকেই জানা থাকে, কনটেন্ট নির্বিশেষে।

প্র ০৩ এই সার্ভার যদি একসাথে ১০,০০০ ক্লায়েন্ট পরিচালনা করতে হয়, তাহলে self.clients-এর মতো একটি সাধারণ ডিকশনারি যথেষ্ট হবে কি?

যুক্তিগতভাবে হ্যাঁ, কিন্তু ব্যবহারিকভাবে প্রতিটি কানেকশনের জন্য একটি ব্লকিং থ্রেড ব্যবহার করলে (M7/L37-এ আলোচিত ব্লকিং বনাম থ্রেডেড প্যাটার্ন) হাজার হাজার থ্রেড ম্যানেজ করা ব্যয়বহুল হয়ে ওঠে — বাস্তব বড় স্কেলের চ্যাট সিস্টেম সাধারণত asyncio বা event-driven I/O ব্যবহার করে একই থ্রেডে হাজার হাজার কানেকশন দক্ষতার সাথে পরিচালনা করে।

অনুশীলন

  1. চিন্তা করুন: কোড সেলে একটি চতুর্থ ক্লায়েন্ট ("Dave") যোগ করুন এবং Bob একটি মেসেজ পাঠালে কারা কারা সেটি পাবে তা যাচাই করুন।

    Bob ছাড়া বাকি সবাই — Alice, Carol ও Dave — মেসেজটি পাবে। broadcast() ফাংশনটি সর্বদা শুধু প্রেরককে বাদ দিয়ে বাকি সব সংযুক্ত ক্লায়েন্টের কাছে মেসেজ পাঠায়, ক্লায়েন্ট সংখ্যা নির্বিশেষে।

  2. পরীক্ষা করুন: একটি ক্লায়েন্ট "ডিসকানেক্ট" করার একটি disconnect(client_id) মেথড যোগ করুন যা self.clients থেকে সেই ক্লায়েন্ট সরিয়ে দেয়, তারপর যাচাই করুন ডিসকানেক্ট হওয়া ক্লায়েন্ট আর ব্রডকাস্ট মেসেজ পায় না।

    def disconnect(self, client_id): del self.clients[client_id] — এরপর broadcast() কল করলে সেই ক্লায়েন্ট আর self.clients-এ না থাকায় তার MockSocket.inbox-এ নতুন কোনো মেসেজ যোগ হবে না, কারণ ফাংশনটি শুধু বর্তমানে সংযুক্ত ক্লায়েন্টদের উপর iterate করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
কেস স্টাডি: একটি প্যাকেটের যাত্রা