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

নেটওয়ার্ক প্রোগ্রামিং প্যাটার্ন — ব্লকিং, নন-ব্লকিং, থ্রেডেড সার্ভার

Network programming patterns — blocking, non-blocking, threaded servers
৮ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ব্লকিং I/O কী এবং কেন এটি একটি single-threaded সার্ভারকে সীমাবদ্ধ করে
  • মাল্টি-থ্রেডেড/মাল্টি-প্রসেস সার্ভার কীভাবে সমান্তরালতা যোগ করে, এবং এর ওভারহেড ট্রেড-অফ
  • নন-ব্লকিং/asynchronous I/O মডেল (select/epoll/asyncio) কী সমস্যা সমাধান করে
  • Python দিয়ে blocking বনাম concurrent সার্ভিং-এর throughput পার্থক্য সিমুলেট করা

১ · ব্লকিং I/O — সহজ কিন্তু সীমাবদ্ধ

L35-L36-এ আমরা দেখেছি একটি সার্ভার conn.recv() বা s.recvfrom() কল করে ডেটার জন্য অপেক্ষা করে। এই কলগুলো ডিফল্টভাবে ব্লকিংBlocking I/Oএকটি I/O কল যা ডেটা প্রস্তুত না হওয়া পর্যন্ত প্রোগ্রামের এক্সিকিউশন থামিয়ে রাখে — যতক্ষণ না ডেটা আসে বা connection গ্রহণযোগ্য হয়, ততক্ষণ পরবর্তী লাইন এক্সিকিউট হয় না। — মানে ডেটা না আসা পর্যন্ত প্রোগ্রামের সেই লাইনেই থেমে থাকে, পরের কোনো কাজ করতে পারে না।

এটি single-threaded একটি সার্ভারের জন্য একটি বাস্তব ও গুরুত্বপূর্ণ সীমাবদ্ধতা তৈরি করে — সার্ভার যখন Client A-এর সাথে ব্যস্ত (তার ডেটার জন্য ব্লক করে বসে আছে), তখন সে এমনকি Client B-এর নতুন connection request-টুকুও accept() করতে পারে না, সার্ভ করা তো দূরের কথা। ফলে সব ক্লায়েন্টকে একে একে, সিরিয়ালি সার্ভ করতে হয়।

মূল সীমাবদ্ধতা

একটি সরল single-threaded ব্লকিং সার্ভার একসাথে ঠিক এক ক্লায়েন্টকে সার্ভ করতে পারে। বাকি সব ক্লায়েন্টকে তাদের পালা আসা পর্যন্ত সারিতে অপেক্ষা করতে হয় — একটি ধীর ক্লায়েন্ট (যেমন বড় ফাইল আপলোড করা কেউ) বাকি সবাইকে আটকে রাখতে পারে।

২ · সমাধান ১ — মাল্টি-থ্রেডেড / মাল্টি-প্রসেস সার্ভার

সবচেয়ে সরাসরি সমাধান — প্রতিটি accept করা connection-এর জন্য একটি নতুন থ্রেড/প্রসেসThread / Processপ্রোগ্রামের একটি স্বতন্ত্র এক্সিকিউশন প্রবাহ, যা মূল প্রোগ্রামের সাথে সমান্তরালে চলতে পারে — প্রতিটি ক্লায়েন্টের জন্য আলাদা থ্রেড/প্রসেস স্পন করলে একটির অপেক্ষা অন্যটিকে ব্লক করে না। স্পন করা। মূল সার্ভার লুপ শুধু নতুন connection accept করেই যায় — প্রতিটি ক্লায়েন্টের প্রকৃত সার্ভিসিং একটি আলাদা থ্রেড/প্রসেসে চলে যায়, যা স্বাধীনভাবে ব্লক হতে পারে অন্যদের প্রভাবিত না করে।

সুবিধা
প্রোগ্রাম করা তুলনামূলক সহজ — যুক্তি অনেকটা সিঙ্গেল-ক্লায়েন্ট কোডের মতোই, শুধু প্রতি connection-এ থ্রেড/প্রসেস র‍্যাপ করা।
সীমাবদ্ধতা
প্রতিটি থ্রেড/প্রসেসের নিজস্ব মেমরি ও শিডিউলিং ওভারহেড আছে — হাজার হাজার একসাথে connection-এ এই ওভারহেডই বড় বাধা হয়ে দাঁড়ায়।

৩ · সমাধান ২ — নন-ব্লকিং / Asynchronous I/O

বড় স্কেলে (একসাথে হাজার হাজার connection) থ্রেড/প্রসেসের ওভারহেড সমস্যা হয়ে দাঁড়ায়। বিকল্প পদ্ধতি — নন-ব্লকিং/asynchronous I/ONon-blocking / Async I/Oএকটি single thread যা একসাথে বহু socket-কে মনিটর করে এবং কোনোটির জন্যই ব্লক না হয়ে শুধু যেগুলোতে ডেটা প্রস্তুত সেগুলো হ্যান্ডেল করে। উদাহরণ: select, epoll, Python-এর asyncio। — যেখানে একটি মাত্র থ্রেড একসাথে বহু socket মনিটর করে, আর যেই socket-এ ডেটা প্রস্তুত হয় শুধু সেটিকেই হ্যান্ডেল করে, কোনোটিতেই না-ব্লক করে বসে না থেকে। Python-এর select মডিউল, লিনাক্সের epoll, এবং উচ্চ-স্তরের asyncio ফ্রেমওয়ার্ক এই মডেলের উদাহরণ (এখানে শুধু নাম উল্লেখ করা হলো, গভীর সিনট্যাক্স নয়)।

এই মডেল প্রোগ্রাম করা তুলনামূলক জটিল (কোড আর সরল সিকোয়েন্সিয়াল লজিকের মতো দেখায় না), কিন্তু থ্রেড/প্রসেসের প্রতি-connection ওভারহেড ছাড়াই অনেক বেশি concurrent connection সামলাতে পারে — বড় স্কেলের ওয়েব সার্ভার (nginx-এর মতো) এই মডেলের উপর ভিত্তি করে তৈরি।

তিনটি মডেলের সারাংশ

ব্লকিং (সরল, কিন্তু এক-এক-করে) → থ্রেডেড/মাল্টি-প্রসেস (সহজ প্রোগ্রামিং, সমান্তরাল, কিন্তু প্রতি-connection ওভারহেড) → নন-ব্লকিং/async (জটিল প্রোগ্রামিং মডেল, কিন্তু সর্বোচ্চ স্কেল কম ওভারহেডে) — এটি নেটওয়ার্ক প্রোগ্রামিংয়ের একটি ক্লাসিক ট্রেড-অফ স্পেকট্রাম, সরলতা বনাম স্কেল।

Python
# ব্লকিং বনাম থ্রেডেড সার্ভিসিং — মোট সময়ের সিমুলেশন
# (আসল থ্রেডিং নয় — শুধু দুই মডেলের "মোট সময়" গাণিতিকভাবে তুলনা করা হচ্ছে)

def blocking_server_process(clients, processing_time_per_client):
    """এক-এক করে সার্ভ করা — মোট সময় = সবার processing time-এর যোগফল।"""
    total_time = 0
    log = []
    for name in clients:
        t = processing_time_per_client[name]
        total_time += t
        log.append(f"  {name}: {t}s কাজ শেষ -> মোট এখন পর্যন্ত {total_time}s")
    return total_time, log

def threaded_server_process(clients, processing_time_per_client):
    """সমান্তরালে সার্ভ করা (প্রতি ক্লায়েন্টে এক থ্রেড ধরে নিয়ে) —
    মোট সময় ≈ সবচেয়ে ধীর ক্লায়েন্টের সময়, কারণ সবাই একই সাথে চলে।"""
    times = [processing_time_per_client[name] for name in clients]
    total_time = max(times)
    log = [f"  {name}: {processing_time_per_client[name]}s কাজ (সমান্তরালে চলছে)" for name in clients]
    return total_time, log

clients = ["Client-A", "Client-B", "Client-C", "Client-D"]
processing_time = {"Client-A": 2, "Client-B": 3, "Client-C": 1, "Client-D": 4}

b_total, b_log = blocking_server_process(clients, processing_time)
print("ব্লকিং (এক এক করে):")
print("\n".join(b_log))
print("মোট সময়:", b_total, "সেকেন্ড\n")

t_total, t_log = threaded_server_process(clients, processing_time)
print("থ্রেডেড (সমান্তরালে):")
print("\n".join(t_log))
print("মোট সময়:", t_total, "সেকেন্ড (সবচেয়ে ধীর ক্লায়েন্টের সমান)")

print(f"\nস্পিডআপ: {b_total / t_total:.1f}x")

    
ব্লকিং (সিরিয়াল): Client-A (2s) Client-B (3s) Client-C (1s) D (4s) থ্রেডেড (সমান্তরাল): Client-A (2s) Client-B (3s) C (1s) সবাই একসাথে শুরু, D (4s) সবচেয়ে দেরিতে শেষ
ব্লকিং মডেলে মোট সময় = যোগফল (২+৩+১+৪=১০s), থ্রেডেড মডেলে মোট সময় ≈ সবচেয়ে ধীর ক্লায়েন্ট (৪s)।
বাস্তবে থ্রেডেড মডেলে সামান্য শিডিউলিং ওভারহেড থাকে, তাই প্রকৃত স্পিডআপ ঠিক এই সিমুলেশনের মতো নিখুঁত হয় না — কিন্তু মূল ধারণা (সমান্তরাল কাজ ≈ সবচেয়ে ধীর অংশের সময়, যোগফল নয়) বাস্তব সার্ভার ডিজাইনেও ঠিক এভাবেই কাজ করে।
মূল কথা · Key takeaway

ব্লকিং I/O সরল কিন্তু সিরিয়াল, থ্রেডেড/মাল্টি-প্রসেস সার্ভার সমান্তরালতা আনে ওভারহেডের বিনিময়ে, আর নন-ব্লকিং/async I/O সেই ওভারহেড ছাড়াই সর্বোচ্চ স্কেল অর্জন করে জটিলতার বিনিময়ে — M7 এখানেই শেষ হলো, পরবর্তী M8 আমাদের তারবিহীন ও মোবাইল নেটওয়ার্কিংয়ের জগতে নিয়ে যাবে।

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

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

প্র ০১ একটি single-threaded ব্লকিং সার্ভার কেন একই সাথে দ্বিতীয় ক্লায়েন্টের connection request-টুকুও গ্রহণ করতে পারে না?

কারণ সার্ভারের মূল লুপ একটি একক থ্রেডে চলে — যখন সেই থ্রেড প্রথম ক্লায়েন্টের recv()-এ ব্লক হয়ে বসে আছে (ডেটার জন্য অপেক্ষা করছে), তখন প্রোগ্রামের এক্সিকিউশন সেই লাইনেই আটকে থাকে — accept() কল করার মতো পরবর্তী কোনো কোড লাইনেই পৌঁছাতে পারে না, যতক্ষণ না প্রথম ক্লায়েন্টের কাজ শেষ হয়।

প্র ০২ মাল্টি-থ্রেডেড সার্ভার হাজার হাজার concurrent connection-এ কেন সমস্যায় পড়তে পারে?

প্রতিটি থ্রেড/প্রসেসের নিজস্ব মেমরি (স্ট্যাক স্পেস ইত্যাদি) ও অপারেটিং সিস্টেমের শিডিউলিং ওভারহেড লাগে। হাজার হাজার থ্রেড একসাথে চালালে মেমরি ব্যবহার ও context-switching খরচ এত বেড়ে যায় যে সার্ভারের প্রকৃত কার্যকারিতা কমে যেতে পারে — এই কারণেই বড় স্কেলের সিস্টেম non-blocking/async I/O-এর দিকে যায়।

প্র ০৩ নন-ব্লকিং/async I/O মডেল কীভাবে থ্রেডিংয়ের প্রতি-connection ওভারহেড ছাড়াই বহু connection সামলায়?

একটি মাত্র থ্রেড select/epoll-এর মতো মেকানিজম ব্যবহার করে একসাথে বহু socket-কে "নজরে" রাখে — কোনো একটি socket-এর জন্য ব্লক হয়ে বসে না থেকে, শুধু জিজ্ঞেস করে "এই মুহূর্তে কোন কোন socket-এ ডেটা প্রস্তুত?" এবং শুধু সেগুলোকেই দ্রুত হ্যান্ডেল করে। ফলে প্রতি connection-এ আলাদা থ্রেড/প্রসেস তৈরির খরচ ছাড়াই একটি মাত্র থ্রেড হাজার হাজার connection পরিচালনা করতে পারে।

অনুশীলন

  1. চিন্তা করুন: উপরের কোড সেলে processing_time-এ একটি পঞ্চম ক্লায়েন্ট যোগ করুন যার processing time সবচেয়ে বেশি (যেমন ১০ সেকেন্ড)। ব্লকিং ও থ্রেডেড — দুই মোট সময় কীভাবে বদলায়?

    ব্লকিং মোট সময় ১০ সেকেন্ড বেড়ে যাবে (যোগফলে যোগ হয়), কিন্তু থ্রেডেড মোট সময়ও ঠিক ১০ সেকেন্ডে গিয়ে দাঁড়াবে (কারণ এটি এখন সবচেয়ে ধীর ক্লায়েন্ট) — অর্থাৎ থ্রেডেড মডেলে একটি একক ধীর ক্লায়েন্ট পুরো ব্যাচের মোট সময় নির্ধারণ করে দেয়, বাকি ক্লায়েন্টের সংখ্যা যতই বাড়ুক না কেন (যতক্ষণ তারা দ্রুততর)।

  2. পরীক্ষা করুন: সব ক্লায়েন্টের processing time যদি সমান হয় (যেমন সবাই ২ সেকেন্ড), তাহলে স্পিডআপ (b_total / t_total) কত হবে বলে মনে হয়? কোড চালিয়ে যাচাই করুন।

    যদি n জন ক্লায়েন্ট প্রত্যেকে t সময় নেয়, ব্লকিং মোট সময় = n×t, থ্রেডেড মোট সময় = t — তাই স্পিডআপ ঠিক n (ক্লায়েন্ট সংখ্যা) হবে। এটি স্পষ্ট দেখায় কেন সমান্তরালতা বেশি সংখ্যক অভিন্ন-দৈর্ঘ্যের কাজে সবচেয়ে বেশি লাভজনক।

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

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