পাঠ ৩৫ · ৫৭-এর মধ্যে · মডিউল ৭
Home / Courses / Computer Networks / TCP প্রোগ্রামিং

TCP ক্লায়েন্ট-সার্ভার প্রোগ্রামিং

TCP client-server programming
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • real Python TCP সার্ভার ও ক্লায়েন্ট কোডের সম্পূর্ণ গঠন, ধাপে ধাপে
  • কেন accept() প্রতিটি ক্লায়েন্টের জন্য একটি নতুন, ডেডিকেটেড সকেট ফেরত দেয় — এবং মূল listening সকেট কেন তবুও সচল থাকে
  • একাধিক ক্লায়েন্টের একই সার্ভারের সাথে সংযোগ স্থাপনের ইন-মেমরি সিমুলেশন
  • TCP-এর কানেকশন-ওরিয়েন্টেড প্রকৃতি কীভাবে এই কোড-স্তরের ডিজাইনে প্রতিফলিত হয়

১ · real TCP সার্ভার ও ক্লায়েন্ট কোডের গঠন

M7/L34-এ আমরা সকেট API-এর পরিচিতি পেয়েছি। এখন দেখি TCP-এর জন্য real Python কোড ঠিক কেমন দেখায় (এটি এই পাঠের প্রোজ-ব্যাখ্যা — নিচে দেখানো হবে কেন কোড সেলে এটি হুবহু চালানো হয় না) —

সার্ভার
s.bind((host, port))
s.listen()
conn, addr = s.accept()
data = conn.recv(1024)
conn.sendall(response)
ক্লায়েন্ট
s.connect((host, port))
s.sendall(request)
data = s.recv(1024)

২ · কেন accept() একটি নতুন সকেট ফেরত দেয়

একটি বারবার ভুল বোঝা বিষয় — conn, addr = s.accept() লাইনে conn আসলে s-এর মতো একই সকেট নয়, বরং একটি সম্পূর্ণ নতুন সকেট অবজেক্ট, যা শুধুমাত্র সেই নির্দিষ্ট ক্লায়েন্ট কানেকশনের জন্য নিবেদিত। মূল s (listening সকেট) accept() কল করার পরও অক্ষত ও সচল থাকে — এটি আরও নতুন ক্লায়েন্টদের কানেকশন রিকোয়েস্ট গ্রহণ করতে থাকতে পারে, এমনকি আগের conn-এর সাথে ডেটা আদান-প্রদান চলাকালীনও।

এটিই কারণ কেন একটি TCP সার্ভার একই সময়ে একাধিক ক্লায়েন্টের সাথে সংযুক্ত থাকতে পারে — প্রতিটি ক্লায়েন্টের জন্য একটি আলাদা conn সকেট, কিন্তু সবাই একই একটিমাত্র পোর্টে listen() করা মূল সকেটের মাধ্যমেই সংযুক্ত হয়েছিল। এটি সরাসরি TCP-এর কানেকশন-ওরিয়েন্টেড প্রকৃতির (M5/L24-এর থ্রি-ওয়ে হ্যান্ডশেক) ফলাফল — প্রতিটি প্রতিষ্ঠিত কানেকশনের নিজস্ব সিকোয়েন্স নাম্বার ও অবস্থা থাকে, যা একে অপর থেকে স্বাধীন রাখা প্রয়োজন।

মূল অন্তর্দৃষ্টি

মূল listening সকেট আর প্রতিটি accept() থেকে পাওয়া conn সকেট — এই দুটোকে আলাদা ভাবতে পারা TCP সার্ভার কোড বোঝার একটি চাবিকাঠি। M7/L37-এ আমরা দেখব কীভাবে একাধিক conn একই সাথে সামলানোর জন্য থ্রেডিং বা নন-ব্লকিং I/O ব্যবহার করা হয়।

গুরুত্বপূর্ণ — কেন কোড সেল real সকেট ব্যবহার করে না

M7/L34-এর মতোই, Pyodide-এর ব্রাউজার-স্যান্ডবক্স real bind()/accept()/ connect() চালাতে পারে না। তাই নিচের কোড সেল উপরের real API-এর গঠন হুবহু অনুসরণ করে এমন MockTCPServer ও MockTCPConnection ক্লাস ব্যবহার করে, যা দুইটি ইন-মেমরি পাইথন অবজেক্টকে সরাসরি একে অপরের মেথড কল করিয়ে পুরো রিকোয়েস্ট/রেসপন্স বিনিময় সিমুলেট করে — কোনো real সকেট বা থ্রেড ছাড়াই।

Python
# *** সিমুলেশন — real accept()/recv()/sendall()-এর in-memory প্রতিরূপ ***
# real সার্ভার কোড হতো: s.bind(...); s.listen(); conn, addr = s.accept()
#                        data = conn.recv(1024); conn.sendall(response)
# real ক্লায়েন্ট কোড হতো: s.connect(...); s.sendall(request); s.recv(1024)

class MockTCPConnection:
    """accept() থেকে পাওয়া একটি নির্দিষ্ট ক্লায়েন্টের জন্য ডেডিকেটেড কানেকশন।"""
    def __init__(self, client_name):
        self.client_name = client_name

    def recv(self, request):
        print(f"[server:{self.client_name}] recv() <- {request!r}")
        response = f"ECHO: {request.upper()}"
        return response

    def sendall(self, response):
        print(f"[server:{self.client_name}] sendall({response!r})")
        return response


class MockTCPServer:
    def __init__(self, address):
        self.address = address
        print(f"[server] bind({address}); listen() — সংযোগের অপেক্ষায়")

    def accept(self, client_name):
        # প্রতিটি accept() নতুন, ডেডিকেটেড conn অবজেক্ট তৈরি করে —
        # মূল listening সকেট (এই সার্ভার) নতুন ক্লায়েন্টদের জন্য এখনো চালু থাকে।
        print(f"[server] accept() -> {client_name}-এর জন্য নতুন ডেডিকেটেড সংযোগ")
        return MockTCPConnection(client_name)


def mock_tcp_client(name, server, request):
    print(f"[client:{name}] connect({server.address}) — সিমুলেটেড থ্রি-ওয়ে হ্যান্ডশেক")
    conn = server.accept(name)              # সার্ভার-পাশে নতুন ডেডিকেটেড conn তৈরি হলো
    print(f"[client:{name}] sendall({request!r})")
    processed = conn.recv(request)          # সার্ভার রিকোয়েস্ট প্রসেস করছে
    response = conn.sendall(processed)      # সার্ভার রেসপন্স পাঠাচ্ছে
    print(f"[client:{name}] recv() -> {response!r}")
    return response


server = MockTCPServer(("127.0.0.1", 9000))

reply_a = mock_tcp_client("client-A", server, "hello server")
print()
reply_b = mock_tcp_client("client-B", server, "second request")

print("\nক্লায়েন্ট-A রেসপন্স:", reply_a)
print("ক্লায়েন্ট-B রেসপন্স:", reply_b)
print("মূল listening সকেট এখনো নতুন ক্লায়েন্ট accept করতে পারছে:", True)

    
লক্ষ্য করুন — client-A ও client-B দুজনেই একই server অবজেক্টের accept() কল করেছে এবং প্রতিবারই একটি সম্পূর্ণ আলাদা MockTCPConnection পেয়েছে। এটিই হুবহু বাস্তব TCP সার্ভারের আচরণ — server নিজে কখনো "ব্যস্ত" হয়ে যায় না, শুধু প্রতিটি নতুন ক্লায়েন্টের জন্য একটি নতুন conn বরাদ্দ করে যায়।
মূল কথা · Key takeaway

TCP ক্লায়েন্ট-সার্ভার প্রোগ্রামিং-এর কেন্দ্রীয় ধারণা হলো listening সকেট বনাম per-client conn সকেটের পার্থক্য — এই পার্থক্যটি বুঝলেই M7/L37-এর থ্রেডেড/নন-ব্লকিং সার্ভার প্যাটার্নগুলো স্বাভাবিকভাবেই বোধগম্য হয়ে ওঠে।

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

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

প্র ০১ conn, addr = s.accept()-এর পর যদি প্রোগ্রামার ভুলে conn-এর বদলে মূল s-এ recv() কল করে, কী সমস্যা হতে পারে?

মূল s শুধু listening সকেট — এটি নিজে কোনো নির্দিষ্ট ক্লায়েন্টের সাথে ডেটা বিনিময়ের জন্য প্রস্তুত নয়, শুধু নতুন কানেকশন রিকোয়েস্ট গ্রহণের জন্য তৈরি। s.recv() কল করলে তা এরর দেবে বা ভুল আচরণ করবে, কারণ real ডেটা আদান-প্রদান শুধু accept()-থেকে পাওয়া conn সকেটেই সম্ভব — এই বিভ্রান্তি এড়াতে conn ও s-কে সবসময় স্পষ্টভাবে আলাদা ভেরিয়েবলে রাখা জরুরি।

প্র ০২ যদি একটি সার্ভার একই সময়ে ১০০ জন ক্লায়েন্টের সাথে সংযুক্ত থাকে, তাহলে কয়টি সকেট অবজেক্ট থাকবে সার্ভার প্রসেসে?

১০১টি — একটি মূল listening সকেট (যা কখনো ডেটা আদান-প্রদান করে না, শুধু নতুন কানেকশন গ্রহণ করে) এবং প্রতিটি সংযুক্ত ক্লায়েন্টের জন্য একটি করে ডেডিকেটেড conn সকেট — মোট ১০০টি। এই গণনাটাই স্পষ্ট করে কেন উচ্চ-ট্রাফিক সার্ভারে সকেট/মেমরি ম্যানেজমেন্ট গুরুত্বপূর্ণ একটি ইঞ্জিনিয়ারিং বিবেচনা (M7/L37-এ আরও দেখব)।

প্র ০৩ উপরের কোড সেলে mock_tcp_client একটি ফাংশন হিসেবে লেখা হয়েছে, প্রতিবার সরাসরি server.accept() কল করছে — real জীবনে ক্লায়েন্ট কি নিজে সরাসরি সার্ভারের accept() কল করতে পারে?

না — এটি এই সিমুলেশনের একটি সরলীকরণ। real জীবনে ক্লায়েন্ট শুধু connect() কল করে, আর accept() শুধুমাত্র সার্ভার-পাশের প্রসেসেই কল হয় (ভিন্ন মেশিনে/প্রসেসে), OS-এর নেটওয়ার্ক স্ট্যাক ক্লায়েন্টের connect() রিকোয়েস্টকে সার্ভারের অপেক্ষমাণ accept() কলের সাথে মিলিয়ে দেয়। এই পাঠের সিমুলেশনে যেহেতু ক্লায়েন্ট ও সার্ভার একই পাইথন প্রসেসে থাকা দুটি অবজেক্ট মাত্র, তাই ধাপগুলো সরলীকৃতভাবে একসাথে দেখানো হয়েছে — মূল ধারণা (প্রতি ক্লায়েন্টে নতুন conn) অপরিবর্তিত থাকে।

অনুশীলন

  1. চিন্তা করুন: একটি চ্যাট সার্ভার যদি প্রতিটি ব্যবহারকারীর জন্য আলাদা conn সকেট রাখে, তাহলে সার্ভার কীভাবে একজন ব্যবহারকারীর পাঠানো বার্তা বাকি সবাইকে পৌঁছে দেবে?

    সার্ভারকে সব সক্রিয় conn সকেটের একটি তালিকা/ডিকশনারি রাখতে হবে (ঠিক যেমন এই পাঠের MockTCPServer-এ প্রতিটি ক্লায়েন্টের জন্য আলাদা কানেকশন তৈরি হয়েছিল) — একজনের বার্তা এলে সার্ভার সেই তালিকার প্রতিটি conn-এ আলাদাভাবে sendall() কল করে একই বার্তা সবার কাছে পৌঁছে দেয়। M12/L54-এর কেস স্টাডিতে এই ধরনের একটি সকেট-ভিত্তিক চ্যাট অ্যাপ্লিকেশন বিস্তারিত দেখব।

  2. পরীক্ষা করুন: উপরের কোড সেলে server.accept(...)-এর বদলে একইভাবে mock_tcp_client-কে তিনটি ভিন্ন ক্লায়েন্ট-নামে তিনবার কল করে দেখুন — প্রতিবারই কি একটি সম্পূর্ণ আলাদা MockTCPConnection অবজেক্ট তৈরি হয়?

    হ্যাঁ — প্রতিটি কলে MockTCPServer.accept() একটি নতুন MockTCPConnection(client_name) ইনস্ট্যান্স তৈরি করে রিটার্ন করে, যার নিজস্ব client_name থাকে — এটিই real accept()-এর "প্রতি ক্লায়েন্টে নতুন সকেট" আচরণের সরাসরি প্রতিফলন।

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

আগের পাঠ
সকেট প্রোগ্রামিং পরিচিতি