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

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

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

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

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

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

M7/L35-এ আমরা দেখেছি TCP সার্ভারের bind → listen → accept ধারা। UDP-তে এই ধারা অনেক সহজ, কারণ UDP connectionlessConnectionlessকোনো আগাম হ্যান্ডশেক বা প্রতিষ্ঠিত কানেকশন-স্টেট ছাড়াই সরাসরি ডেটা পাঠানো — M5/L23-এর UDP-এর মূল বৈশিষ্ট্য। (M5/L23) — কোনো হ্যান্ডশেক নেই, তাই connect() বা accept()-এর প্রয়োজনই নেই —

সার্ভার
s.bind((host, port))
data, client_addr = s.recvfrom(1024)
s.sendto(response, client_addr)
ক্লায়েন্ট
s.sendto(request, (host, port))
data, _ = s.recvfrom(1024)

২ · TCP-এর সাথে মূল পার্থক্য — প্রতিটি recvfrom()-এ প্রেরকের ঠিকানা

M7/L35-এ TCP সার্ভারে accept() প্রতিটি ক্লায়েন্টের জন্য একটি ডেডিকেটেড conn সকেট তৈরি করে — সেই conn নিজেই "মনে রাখে" এটি কোন ক্লায়েন্টের সাথে কথা বলছে। UDP-তে এমন কোনো ডেডিকেটেড কানেকশন অবজেক্ট নেই — একটিমাত্র সকেট সব ক্লায়েন্টের ডেটাগ্রাম গ্রহণ করে। তাই প্রতিটি recvfrom() কল ডেটার পাশাপাশি প্রেরকের ঠিকানাও ফেরত দেয় — অ্যাপ্লিকেশনকেই নিজে সিদ্ধান্ত নিতে হয় কীভাবে বিভিন্ন প্রেরককে আলাদা রাখবে (যেমন একটি ঠিকানা-ভিত্তিক ডিকশনারি ব্যবহার করে), যদি একাধিক ক্লায়েন্টকে ভিন্নভাবে সাড়া দেওয়ার প্রয়োজন হয়।

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

TCP-তে "কে পাঠিয়েছে" প্রশ্নের উত্তর কানেকশন-স্তরেই এমবেডেড থাকে (আপনি জানেন এই নির্দিষ্ট conn মানেই সেই নির্দিষ্ট ক্লায়েন্ট) — UDP-তে এই তথ্য প্রতিটি বার্তার সাথে আলাদাভাবে বহন করতে হয়, কারণ কোনো প্রতিষ্ঠিত কানেকশন-স্টেট নেই যা সেটি মনে রাখবে। এটিই M5/L23-এর "connectionless" ধারণার কোড-স্তরের সরাসরি প্রতিফলন।

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

M7/L34-L35-এর মতোই, Pyodide-এর ব্রাউজার-স্যান্ডবক্স real bind()/recvfrom()/ sendto() চালাতে পারে না। তাই নিচের কোড সেল একটি MockUDPServer ব্যবহার করে, যা উপরের real API-এর recvfrom/sendto গঠন অনুসরণ করে কিন্তু ভেতরে শুধু ইন-মেমরি ডিকশনারি দিয়ে কাজ করে — কোনো real পোর্ট বাইন্ড হচ্ছে না।

Python
# *** সিমুলেশন — real bind()/recvfrom()/sendto()-এর in-memory প্রতিরূপ ***
# real সার্ভার কোড হতো: s.bind(...); data, client_addr = s.recvfrom(1024)
#                        s.sendto(response, client_addr)
# real ক্লায়েন্ট কোড হতো: s.sendto(request, (host, port)); s.recvfrom(1024)

class MockUDPServer:
    def __init__(self, address):
        self.address = address
        # UDP-তে কোনো persistent per-client কানেকশন নেই — শুধু কে কী পাঠিয়েছে তার একটি লগ
        self.received_log = {}
        print(f"[server] bind({address}) — connectionless: connect()/accept()-এর দরকার নেই")

    def recvfrom(self, data, sender_addr):
        self.received_log.setdefault(sender_addr, []).append(data)
        print(f"[server] recvfrom() -> data={data!r}, sender={sender_addr}")
        return data, sender_addr

    def sendto(self, response, dest_addr):
        print(f"[server] sendto({response!r}, {dest_addr})")
        return response


def mock_udp_client(name, client_addr, server, request):
    print(f"[client:{name}] sendto({request!r}, {server.address})")
    data, sender_addr = server.recvfrom(request, client_addr)
    response = f"ACK from server: {data}"
    server.sendto(response, sender_addr)          # ঠিক সেই প্রেরকের ঠিকানাতেই ফেরত পাঠানো হচ্ছে
    print(f"[client:{name}] recvfrom() -> {response!r}")
    return response


server = MockUDPServer(("0.0.0.0", 9500))

reply_a = mock_udp_client("client-A", ("192.168.1.10", 51000), server, "query-1")
print()
reply_b = mock_udp_client("client-B", ("192.168.1.11", 52000), server, "query-2")

print("\nসার্ভারের received_log (প্রতিটি প্রেরক-ঠিকানা অনুযায়ী):")
for addr, messages in server.received_log.items():
    print(" ", addr, "->", messages)

print("\nclient-A রেসপন্স:", reply_a)
print("client-B রেসপন্স:", reply_b)

    
লক্ষ্য করুন — MockUDPServer-এর কোনো per-client persistent কানেকশন অবজেক্ট নেই (M7/L35-এর MockTCPConnection-এর মতো কিছু নেই); বরং প্রতিটি recvfrom()-এর সাথে আসা sender_addr দিয়েই সার্ভার জানতে পারছে কাকে রেসপন্স পাঠাতে হবে। received_log ডিকশনারিটি দেখায় সার্ভার একই সময়ে একাধিক ভিন্ন প্রেরককে ঠিকানা দিয়ে আলাদা রাখছে।
মূল কথা · Key takeaway

UDP প্রোগ্রামিং TCP-এর চেয়ে সহজ (কোনো হ্যান্ডশেক/accept ধাপ নেই), কিন্তু এই সরলতার বিনিময়ে অ্যাপ্লিকেশনকে নিজে প্রতিটি বার্তার প্রেরক ঠিকানা ট্র্যাক করতে হয় — M5/L23-এর "connectionless" ট্রেড-অফ সরাসরি কোডের দায়িত্বে রূপান্তরিত হয়।

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

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

প্র ০১ UDP সার্ভারে কেন accept()-এর মতো কোনো ধাপ নেই, অথচ TCP সার্ভারে আছে?

accept() একটি TCP-নির্দিষ্ট ধাপ কারণ TCP-তে একটি কানেকশন "প্রতিষ্ঠিত" (established) হতে হয় থ্রি-ওয়ে হ্যান্ডশেকের (M5/L24) মাধ্যমে — accept() সেই হ্যান্ডশেক সম্পূর্ণ করে একটি নতুন কানেকশন অবজেক্ট তৈরি করে। UDP-তে কোনো হ্যান্ডশেক বা প্রতিষ্ঠিত কানেকশন-স্টেট নেই (M5/L23) — প্রতিটি ডেটাগ্রাম স্বাধীন, তাই "গ্রহণ করার" কোনো আলাদা ধাপের প্রয়োজন নেই, সরাসরি recvfrom()-এই ডেটা পাওয়া যায়।

প্র ০২ একটি UDP সার্ভার যদি নিজে sender_addr ট্র্যাক না রাখে (যেমন উপরের কোডে received_log না থাকলে), তাহলে ব্যবহারিকভাবে কী সমস্যা হতে পারে?

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

প্র ০৩ DNS (M6/L30) সাধারণত UDP ব্যবহার করে — উপরের কোড প্যাটার্নের সাথে এটি কীভাবে সামঞ্জস্যপূর্ণ?

একটি DNS সার্ভার একই সময়ে হাজার হাজার ভিন্ন ক্লায়েন্ট থেকে ছোট, স্বতন্ত্র প্রশ্ন (query) পায় — প্রতিটি প্রশ্নের জন্য একটি TCP-এর মতো ভারী হ্যান্ডশেক/ডেডিকেটেড কানেকশন প্রয়োজন হলে তা অপ্রয়োজনীয় ওভারহেড হতো। উপরের MockUDPServer প্যাটার্নের মতোই real DNS সার্ভার প্রতিটি ইনকামিং recvfrom()-এ পাওয়া প্রেরক ঠিকানায় সরাসরি sendto() দিয়ে উত্তর পাঠায় — কোনো persistent কানেকশন-স্টেট ছাড়াই, যা DNS-এর মতো ছোট, দ্রুত request-response প্যাটার্নের জন্য আদর্শ।

অনুশীলন

  1. চিন্তা করুন: M7/L35-এর MockTCPServer ও এই পাঠের MockUDPServer-এর গঠন পাশাপাশি রেখে তুলনা করুন — কোন ক্লাসে "per-client dedicated অবজেক্ট" আছে, আর কোনটিতে নেই?

    MockTCPServer-এ প্রতিটি ক্লায়েন্টের জন্য accept() একটি আলাদা MockTCPConnection অবজেক্ট তৈরি করে (per-client dedicated অবস্থা)। MockUDPServer-এ এমন কোনো আলাদা অবজেক্ট নেই — একটিমাত্র সার্ভার অবজেক্টই সব ক্লায়েন্টের ডেটাগ্রাম হ্যান্ডল করে, শুধু ঠিকানা দিয়ে আলাদা করে চেনে। এই পার্থক্যটাই TCP-এর কানেকশন-ওরিয়েন্টেড বনাম UDP-এর connectionless প্রকৃতির কোড-স্তরের সবচেয়ে স্পষ্ট প্রতিফলন।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় ক্লায়েন্ট (client-C, ভিন্ন ঠিকানা ও প্রশ্নসহ) যোগ করে received_log-এ তিনটি ভিন্ন এন্ট্রি দেখা যায় কি না পরীক্ষা করুন।

    হ্যাঁ — mock_udp_client("client-C", ("192.168.1.12", 53000), server, "query-2")-এর মতো একটি নতুন কল যোগ করলে received_log-এ তৃতীয় একটি ঠিকানা-চাবি যোগ হবে, প্রতিটি এন্ট্রি স্বাধীনভাবে থেকে যাবে — এটিই দেখায় সার্ভার কোনো নির্দিষ্ট সংখ্যক ক্লায়েন্টে সীমাবদ্ধ নয়, প্রতিটি নতুন ঠিকানার জন্য স্বয়ংক্রিয়ভাবে একটি নতুন এন্ট্রি তৈরি হয়।

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

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