সরাসরি পর্বে যান
পর্ব ৪২ · ৬২-এর মধ্যে · মডিউল ৭

UDP socket প্রোগ্রামিং — datagram, broadcast আর timeout

UDP Socket Programming in C — sendto/recvfrom, One Socket for Many Clients, Timeouts, Backoff & Jitter, Reply Caches, Broadcast & Multicast

Taught in Bangla, this lesson writes UDP programs in C: socket, bind, sendto and recvfrom with one server socket for many clients, timeouts with SO_RCVTIMEO or poll, retransmission with request IDs, exponential backoff and jitter, idempotency and reply caches for duplicate requests, and broadcast and multicast sockets, with live C programs and simulators.

এই পর্বে যা শিখবেন

  • UDP সার্ভার আর client-এর system call-এর ক্রম, আর কেন একটি UDP socket একসাথে অনেক client সামলাতে পারে — C-তে চালিয়ে দেখে
  • sendto, recvfrom, sendmsg, recvmsg কখন কোনটি; buffer-এর আকার আর ১,৪৭২ / ১,২৩২ বাইটের বাস্তব সীমা
  • নিজের হাতে একটি নির্ভরযোগ্য request/response protocol: request ID, timeout, exponential backoff, jitter, duplicate ধরা আর reply cache — ইচ্ছাকৃত packet হারিয়ে প্রমাণসহ
  • Broadcast আর multicast socket বাস্তবে: SO_BROADCAST, IP_ADD_MEMBERSHIP, TTL, loop, SO_REUSEADDR/SO_REUSEPORT, mDNS আর SSDP
  • IPv6 UDP, ICMP ত্রুটি, প্রেরকের গতি নিয়ন্ত্রণ আর নিরাপত্তার নিয়ম — উৎস যাচাই, amplification আর spoofing

১ · UDP socket-এর জীবনচক্র — চারটি call, একটি socket, অনেক client

পর্ব ৪১-এ (TCP socket প্রোগ্রামিং) দেখেছেন, TCP সার্ভার বানাতে লাগে socket(), bind(), listen(), তারপর প্রতিটি client-এর জন্য accept() — আর প্রতিটি সংযোগের জন্য আলাদা একটি নতুন socket. UDP-তে এর অর্ধেকই লাগে না। কারণ UDP-তে “সংযোগ” বলে কিছু নেই (পর্ব ২৮): প্রতিটি datagram নিজের খামে নিজের ঠিকানা নিয়ে আসে।

ধাপTCP সার্ভারUDP সার্ভারUDP client
১socket(AF_INET, SOCK_STREAM, 0)socket(AF_INET, SOCK_DGRAM, 0)socket(AF_INET, SOCK_DGRAM, 0)
২bind() — পরিচিত portbind() — পরিচিত portসাধারণত bind নয়; প্রথম sendto()-তে kernel একটি ephemeral port দেয়
৩listen()— (লাগে না)— (ঐচ্ছিক: connect(), অংশ ৮)
৪accept() → প্রতি client-এ নতুন socket— (লাগে না)—
৫recv()/send() ওই নতুন socket-এrecvfrom() → কে পাঠাল জানা যায়; sendto() ওই ঠিকানায়sendto() → recvfrom()
শেষেclose() — FIN দিয়ে সংযোগ বন্ধclose() — তারে কিছু যায় নাclose()

মূল ভূমিকা bind()-এর: socket-কে একটি স্থানীয় ঠিকানা আর port-এ বসায়, যাতে ওই port-এ আসা datagram kernel এই socket-এ জমা করে। recvfrom() একটি datagram পড়ে, সাথে প্রেরকের IP ও port একটি struct sockaddr_in-এ ভরে দেয়; উত্তর দিতে ওই struct-টাই sendto()-কে দিলেই হলো। sockaddr_in, htons(), network byte order, getaddrinfo() আর ত্রুটি সামলানোর ভিত্তি পর্ব ৪১-এ; এখানে শুধু মনে রাখুন — INADDR_ANY (0.0.0.0) মানে “যেকোনো interface-এ আসা”, INADDR_LOOPBACK (127.0.0.1) মানে শুধু এই কম্পিউটারের ভেতর থেকে।

TCP সার্ভার যেন ব্যাংকের কাউন্টার: প্রতিটি গ্রাহক এলে আলাদা একজন কর্মী তাঁর সাথে বসে পুরো কাজ শেষ করেন (প্রতি সংযোগে আলাদা socket). UDP সার্ভার যেন অফিসের রিসেপশনের চিঠির বাক্স: সবার চিঠি একই বাক্সে পড়ে, প্রতিটি খামের ওপর ফেরত-ঠিকানা লেখা থাকে। রিসেপশনিস্ট একটি একটি খাম খোলেন, উত্তর লিখে ওই ফেরত-ঠিকানায় পাঠিয়ে দেন, তারপর পরের খাম। কে আগে লিখেছিলেন, কার সাথে কথা চলছে — এসব মনে রাখার কোনো খাতা নেই। খামের ফেরত-ঠিকানাই একমাত্র “সংযোগ”।

নিচের প্রোগ্রাম loopback-এ একটি সার্ভার socket খোলে, তারপর fork() দিয়ে পরপর দুটি client প্রক্রিয়া বানায়। কোনো client নিজে bind() করে না। সার্ভার প্রতিটি অনুরোধে recvfrom()-এর দেওয়া ঠিকানা ছাপে আর সেখানেই উত্তর পাঠায়। আউটপুটের ক্রম যাতে প্রতিবার এক থাকে, সার্ভার প্রতিটি client শেষ হওয়ার জন্য wait() করে, তারপর পরেরটি বানায়।

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
static void client(const struct sockaddr_in *srv, const char *msg) {
    int c = socket(AF_INET, SOCK_DGRAM, 0);         /* no bind(): kernel picks a port */
    char buf[128];
    sendto(c, msg, strlen(msg), 0, (const struct sockaddr *)srv, sizeof *srv);
    ssize_t n = recvfrom(c, buf, sizeof buf, 0, NULL, NULL);
    printf("  client %s got: %.*s\n", msg, (int)(n > 0 ? n : 0), buf);
}
int main(void) {
    int s = socket(AF_INET, SOCK_DGRAM, 0);         /* ONE server socket for everybody */
    struct sockaddr_in srv = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)}, from;
    socklen_t len = sizeof srv;
    if (bind(s, (struct sockaddr *)&srv, sizeof srv) < 0) { perror("bind"); return 1; }
    getsockname(s, (struct sockaddr *)&srv, &len);
    printf("server: 127.0.0.1:%d - no listen(), no accept()\n", ntohs(srv.sin_port));
    const char *who[2] = {"A", "B"};
    for (int i = 0; i < 2; i++) {
        fflush(stdout);
        if (fork() == 0) { client(&srv, who[i]); return 0; }
        char buf[64], rep[128];
        len = sizeof from;                          /* recvfrom fills in WHO sent it */
        ssize_t n = recvfrom(s, buf, sizeof buf - 1, 0, (struct sockaddr *)&from, &len);
        buf[n > 0 ? n : 0] = '\0';
        printf("server: \"%s\" from %s:%d\n", buf, inet_ntoa(from.sin_addr), ntohs(from.sin_port));
        int m = snprintf(rep, sizeof rep, "hello %s, you are port %d", buf, ntohs(from.sin_port));
        fflush(stdout);
        sendto(s, rep, m, 0, (struct sockaddr *)&from, len);   /* reply to that address */
        wait(NULL);                                 /* child prints, then we continue */
    }
}

আউটপুট (একটি চালানো থেকে; port নম্বরগুলো প্রতিবার আলাদা হবে):

server: 127.0.0.1:60254 - no listen(), no accept()
server: "A" from 127.0.0.1:43436
  client A got: hello A, you are port 43436
server: "B" from 127.0.0.1:59466
  client B got: hello B, you are port 59466

একটি socket, দুটি client: সার্ভার listen() বা accept() ছাড়াই দুজনকে উত্তর দিল — দুটোই একই s দিয়ে। ঠিকানা আসে datagram-এর সাথে: A আর B কেউ bind করেনি, তাই প্রথম sendto()-র সময় kernel প্রত্যেককে একটি ephemeral port দিয়েছে (এই চালানোয় ৪৩৪৩৬ আর ৫৯৪৬৬)। সার্ভার তা জেনেছে শুধু recvfrom()-এর from থেকে, আর উত্তর গেছে ঠিক সেই port-এ। নিজে চালালে সংখ্যা বদলাবে, কিন্তু client-এর লাইনের port আর সার্ভারের দেখা port সবসময় মিলবে। লক্ষ করুন, len = sizeof from প্রতিবার আবার বসানো হয়েছে: recvfrom()-এর শেষ argument একই সাথে ইনপুট (ঠিকানার struct কত বড়) আর আউটপুট (প্রেরকের ঠিকানা আসলে কত বাইটের)।

মূল ধারণা

UDP সার্ভারে “কোন client” প্রশ্নের উত্তর socket নয়, datagram-এর উৎস-ঠিকানা (IP, port). তাই একটি socket লাখো client-এর কাজ করতে পারে, আর সার্ভারকে নিজে থেকে কিছু মনে রাখতে হয় না — যদি না আপনি নিজে সিদ্ধান্ত নেন (অংশ ৪-এর reply cache). সীমাবদ্ধতাও এখানেই: একটি অনুরোধে সার্ভার অনেকক্ষণ আটকে থাকলে বাকিরা অপেক্ষা করে। তখন কাজ ভাগ করতে হয় একাধিক thread বা প্রক্রিয়ায় (পর্ব ৪৩) বা event loop-এ (পর্ব ৪৪)।

২ · পাঠানো আর পড়ার call, buffer-এর আকার আর বাস্তব সীমা

UDP socket-এ পাঠানো আর পড়ার চার জোড়া call আছে। সবাই একটি করে পুরো datagram নিয়ে কাজ করে — পর্ব ২৮-এ message boundary আর ছোট buffer-এ কাটা পড়া (MSG_TRUNC) চালিয়ে দেখেছেন, এখানে আর পুনরাবৃত্তি করছি না।

Callকী করেকখন ব্যবহার
sendto() / recvfrom()প্রতিবার ঠিকানা দিয়ে পাঠানো; পড়ার সময় প্রেরকের ঠিকানা পাওয়াসার্ভার, যে অনেকের সাথে কথা বলে — সবচেয়ে প্রচলিত
send() / recv()ঠিকানা ছাড়া — শুধু connected UDP socket-এclient, যে একটিই সার্ভারের সাথে কথা বলে (অংশ ৪-এর কোড)
sendmsg() / recvmsg()একাধিক buffer (scatter/gather) আর “ancillary data” — যেমন কোন স্থানীয় ঠিকানায় datagram এল, TTL কত, কাটা পড়ল কি নাএকাধিক IP-ওয়ালা সার্ভার, DNS সার্ভার, QUIC বাস্তবায়ন
recvmmsg() / sendmmsg()একবারের system call-এ অনেক datagram (Linux-নির্দিষ্ট)প্রতি সেকেন্ডে লাখো datagram সামলানো সার্ভার

recvmsg()-এর একটি বাস্তব প্রয়োজন: সার্ভারের দুটি IP, socket bind করা 0.0.0.0-এ। Client অনুরোধ পাঠাল দ্বিতীয় ঠিকানায়, কিন্তু উত্তর routing অনুযায়ী প্রথম ঠিকানা থেকে বেরোল — client দেখবে অচেনা উৎস। সমাধান: Linux-এ IP_PKTINFO চালু করে recvmsg() দিয়ে জানা কোন স্থানীয় ঠিকানায় datagram এসেছিল, আর sendmsg()-এ সেটিই উৎস বলে দেওয়া (BSD/macOS-এ IP_RECVDSTADDR, IPv6-এ IPV6_RECVPKTINFO).

Buffer কত বড় রাখবেন

  • অ্যাপের buffer (recvfrom()-এ দেওয়া): আপনার protocol-এ সবচেয়ে বড় যে datagram আসতে পারে, তার চেয়ে অন্তত এক বাইট বড়। ফল যদি buffer-এর পুরো আকার হয়, ধরে নিন কিছু কাটা পড়েছে। অজানা উৎস হলে ৬৫,৫৩৬ বাইটের buffer নিরাপদ — একটি UDP datagram এর বেশি হতেই পারে না।
  • Kernel-এর buffer (socket receive buffer): অ্যাপ পড়ার আগে যত datagram জমতে পারে। হঠাৎ অনেক datagram এলে এটি ভরে যায়, আর তখন নতুন datagram নীরবে বাদ পড়ে। setsockopt(fd, SOL_SOCKET, SO_RCVBUF, …) দিয়ে বাড়ানো যায়; Linux চাওয়া মানের দ্বিগুণ রাখে (হিসাবের খরচসহ) আর সর্বোচ্চ সীমা ঠিক করে net.core.rmem_max.
  • পাঠানোর আকার: একটি datagram-এ কত payload? IPv4 Ethernet-এ fragmentation ছাড়া সর্বোচ্চ ১,৫০০ − ২০ − ৮ = ১,৪৭২ বাইট; IPv6-এর সর্বনিম্ন MTU ধরে যেকোনো পথে নিরাপদ ১,২৮০ − ৪০ − ৮ = ১,২৩২ বাইট. কেন fragmentation এড়াতে হয়, তার হিসাব পর্ব ২৮-এ। বড় ডেটা হলে অ্যাপ নিজেই টুকরো করে, প্রতিটিতে নিজের ক্রমিক নম্বর দেয়।

recvfrom() -1 ফেরত দিলে errno দেখুন: EINTR (signal এসে বাধা দিয়েছে — আবার ডাকুন), EAGAIN/EWOULDBLOCK (timeout বা non-blocking socket-এ এখন কিছু নেই), ECONNREFUSED (connected socket-এ ICMP port unreachable এসেছিল — অংশ ৮)। আর sendto() সফল হওয়া মানে শুধু “datagram kernel-এর হাতে দেওয়া গেছে” — পৌঁছেছে, এমন কোনো প্রমাণ নয়।

৩ · নির্ভরযোগ্যতা নিজে বানানো (১) — request ID, timeout, backoff আর jitter

UDP অনুরোধ বা উত্তর পথে হারালে কেউ জানাবে না। তাই প্রতিটি request/response protocol-এ — DNS, NTP, DHCP, SNMP, গেম সার্ভারের প্রশ্ন — client-এর দিকে তিনটি জিনিস থাকে:

Request ID

প্রতিটি নতুন অনুরোধে একটি সংখ্যা (request ID), যা সার্ভার উত্তরে ফেরত দেয়। এতে উত্তর কোন প্রশ্নের, তা মেলানো যায়; পুরোনো বা দেরিতে আসা উত্তর চেনা যায়। আবার পাঠানোর সময় ID বদলায় না — বদলালে সার্ভার duplicate চিনতে পারবে না।

DNS-এর ১৬ bit ID

Timeout

উত্তরের জন্য কতক্ষণ অপেক্ষা। সবচেয়ে সহজ পথ SO_RCVTIMEO: সময় পেরোলে recv() -1 আর EAGAIN দেয়। একসাথে অনেক socket বা অনুরোধ হলে poll()/select()/epoll-এর timeout (পর্ব ৪৪)।

SO_RCVTIMEO, poll

Retransmission আর backoff

Timeout হলে একই অনুরোধ আবার পাঠানো (retransmission), প্রতিবার অপেক্ষা দ্বিগুণ (exponential backoff), একটি সর্বোচ্চ সীমা আর চেষ্টার সংখ্যার সীমা রেখে। শেষে হাল ছেড়ে অ্যাপকে ত্রুটি জানানো।

200, 400, 800 ms…

এভাবে পাওয়া যায় stop-and-wait ধরনের একটি protocol (পর্ব ১০-এর ARQ-এর আত্মীয়): একটি অনুরোধ, উত্তরের অপেক্ষা, না এলে আবার। নিচে একটি অনুরোধ একবার হারিয়ে কীভাবে উদ্ধার পায়, ধাপে ধাপে দেখুন — client-এর socket-এর চোখ দিয়ে, LAN-এ RTT ২০ মিলিসেকেন্ড ধরে।

  1. Clientid = 7sendto, তারপর recv
  2. নেটওয়ার্কreq হারালভিড়ের queue
  3. TimeoutEAGAINঅপেক্ষা দ্বিগুণ
  4. সার্ভারid = 7প্রথমবার পেল
  5. উত্তরid মিললt = 220 ms
Client-এর অবস্থা
সময়
চেষ্টা │ অপেক্ষার সীমা
recv()-এর অবস্থা
পথে কী
  1. পাঠানো আর অপেক্ষা শুরু। Client নতুন অনুরোধের জন্য ID 7 নিল, SO_RCVTIMEO ২০০ মিলিসেকেন্ড বসাল, sendto() করল, তারপর recv()-এ আটকে রইল।
  2. অনুরোধ পথে হারাল। কোনো ভিড়ের router-এর queue ভরা ছিল, datagram বাদ পড়ল। UDP কাউকে কিছু জানায় না — client শুধু নীরবতা দেখছে।
  3. Timeout: recv() ফিরল -1 আর EAGAIN নিয়ে। ২০০ মিলিসেকেন্ড পেরিয়েছে। Client একই ID 7 দিয়েই অনুরোধ আবার পাঠাল, আর এবার অপেক্ষার সীমা দ্বিগুণ — ৪০০ মিলিসেকেন্ড। বাস্তবের কোডে এই অপেক্ষায় এলোমেলো jitter-ও যোগ হয় (নিচে দেখুন)।
  4. সার্ভার প্রথমবার অনুরোধটি পেল। t = ২১০-এ (একমুখী ১০ মিলিসেকেন্ড) সার্ভার ID 7 পেল, কাজ করল, উত্তরে একই ID বসিয়ে পাঠাল। সার্ভারের কাছে এটি প্রথম দেখা — সে জানেই না এর আগে একটি কপি হারিয়েছিল।
  5. উত্তর এল, ID মিলল। t = ২২০-এ উত্তর পৌঁছাল, ID 7 অপেক্ষমাণ অনুরোধের সাথে মিলল, client থামল। মোট সময় ২২০ মিলিসেকেন্ড — ২০০ গেল শুধু অপেক্ষায়। এই উত্তর থেকে RTT মাপা ঠিক নয়: client জানে না উত্তরটি প্রথম না দ্বিতীয় কপির — এটাই Karn-এর নিয়ম।

Backoff-এর হিসাব

প্রথম timeout T0, প্রতিবার দ্বিগুণ, মোট n বার চেষ্টা — সব কটিই ব্যর্থ হলে client হাল ছাড়ার আগে মোট অপেক্ষা করে:

মোট অপেক্ষা = T0 + 2T0 + 4T0 + … + 2n−1T0 = T0(2n − 1)
T0 = ২০০ ms, n = ৫ → ২০০ × ৩১ = ৬,২০০ ms; বাস্তবে প্রতিটি অপেক্ষার একটি সর্বোচ্চ সীমা (cap) থাকে

গুণোত্তর ধারা বলে শেষ অপেক্ষাটিই মোটের প্রায় অর্ধেক। দ্বিগুণ করার যুক্তি: loss যদি ভিড়ের কারণে হয়, সবাই সমান তালে আবার পাঠালে ভিড় কমবে না; পিছিয়ে গেলে নেটওয়ার্ক শ্বাস নেওয়ার সময় পায় (পর্ব ৩২-এর congestion control-এর একই দর্শন)। প্রতিটি চেষ্টা স্বাধীনভাবে p সম্ভাবনায় ব্যর্থ হলে n বারেই ব্যর্থতার সম্ভাবনা pn; অনুরোধ আর উত্তর দুটিকেই পৌঁছাতে হয় বলে প্রতিমুখী loss q হলে p = ১ − (১ − q)২.

Jitter — সবাই একসাথে যেন আবার না পাঠায়

লোডশেডিং শেষে বিদ্যুৎ ফিরলে পাড়ার সব ফ্রিজ, পাম্প আর এসি একই মুহূর্তে চালু হয় — ট্রান্সফরমার সেই ধাক্কায় আবার বসে যেতে পারে। নেটওয়ার্কেও তাই: সার্ভার কয়েক সেকেন্ড বন্ধ থাকলে হাজারো client একসাথে timeout পায়, একই সূত্রে একই মুহূর্তে আবার পাঠায় (thundering herd). প্রতিকার jitter: প্রতিটি অপেক্ষায় এলোমেলো অংশ, যাতে client-রা ছড়িয়ে পড়ে। প্রচলিত “full jitter”-এ অপেক্ষা = ০ থেকে cap-এর মধ্যে এলোমেলো, যেখানে cap = min(সর্বোচ্চ সীমা, T0·2k); আরেক ধরনে অর্ধেক স্থির, অর্ধেক এলোমেলো। অংশ ৫-এর simulator দুটিই ছাপে।

SO_RCVTIMEO না poll? একটি socket, একটি অনুরোধ হলে SO_RCVTIMEO সবচেয়ে সরল। কিন্তু ভুল ID-র একটি পুরোনো উত্তর এলে recv() ফিরে আসে, আর আবার ডাকলে টাইমার শুরু থেকে গোনে। poll()-এ প্রতিবার বাকি সময় দেওয়া যায়, আর একসাথে অনেক socket ও অনেক অপেক্ষমাণ অনুরোধ সামলানো যায় (পর্ব ৪৪, event-driven সার্ভার)। অনুশীলন ৪-এ poll দিয়ে লেখা অপেক্ষা দেখুন।

৪ · নির্ভরযোগ্যতা নিজে বানানো (২) — duplicate, idempotent আর reply cache

আবার পাঠানোর একটি অনিবার্য পরিণাম আছে। Client-এর চোখে “অনুরোধ হারাল” আর “উত্তর হারাল” একই রকম — দুই ক্ষেত্রেই শুধু নীরবতা। দ্বিতীয় ক্ষেত্রে সার্ভার কাজটি আগেই করে ফেলেছে, আর আবার পাঠানো কপি পেয়ে কাজটি দ্বিতীয়বার করবে — যদি না সে duplicate চিনতে পারে।

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

অনুরোধদুবার চললেধরন
“abcltech.com-এর IP কত?” (DNS)একই উত্তর — কোনো ক্ষতি নেইidempotent
“এখন সময় কত?” (NTP)নতুন সময় — তাতেও ক্ষতি নেইনিরাপদে আবার করা যায়
“তাপমাত্রা ২৫ ডিগ্রি বসানো” (স্মার্ট যন্ত্র)দুবার বসালেও ২৫ — ক্ষতি নেইidempotent (মান বসানো)
“তাপমাত্রা ১ ডিগ্রি বাড়ানো”২ ডিগ্রি বেড়ে যায়idempotent নয়
“১০০ টাকা জমা”২০০ টাকা জমা হয়idempotent নয়

প্রথম শিক্ষা নকশার: সম্ভব হলে অনুরোধকে idempotent বানান (“১ বাড়ানো” নয়, “২৫ বসানো”)। যেখানে তা সম্ভব নয়, সেখানে সার্ভারে লাগে একটি reply cache — একটি ছোট টেবিল, যার চাবি (client-এর IP, client-এর port, request ID), আর মান সেই অনুরোধের পাঠানো উত্তর। নতুন অনুরোধ এলে সার্ভার আগে টেবিলে খোঁজে:

  • পাওয়া গেল (hit): এটি duplicate. কাজ আবার না চালিয়ে জমা উত্তরটিই আবার পাঠানো — এভাবে client শেষ পর্যন্ত উত্তর পায়, কাজ হয় একবার।
  • পাওয়া গেল না (miss): নতুন অনুরোধ। কাজ চালানো, উত্তর টেবিলে রাখা, তারপর পাঠানো।

চাবিতে শুধু ID রাখা ভুল — দুজন ভিন্ন client-এরই ID 7 থাকতে পারে। আর সারি চিরকাল রাখা যায় না: হয় নির্দিষ্ট সময় পর মোছা (client-এর সর্বোচ্চ retry সময়ের চেয়ে বেশি), নয়তো প্রতি client-এর ID শুধু বাড়ে ধরে নিয়ে শুধু শেষ ID রাখা — তার চেয়ে ছোট ID এলে সেটি পুরোনো, চুপচাপ বাদ।

  1. অনুরোধ.5:40001 · id 7deposit 100
  2. উত্তর হারালok 100পথে
  3. Cache hitএকই চাবিকাজ নয়
  4. অন্য client.9:51000 · id 7আলাদা চাবি
  5. নতুন ID.5:40001 · id 8পুরোনো সারি বদল
সার্ভারের অবস্থা (একই হিসাবে দুজন জমা দিচ্ছেন)
আসা datagram
Cache-এ খোঁজ
কাজ
Balance
Cache-এর সারি
  1. নতুন অনুরোধ — miss, কাজ চলল। 10.0.0.5-এর port 40001 থেকে ID 7: “১০০ জমা”। টেবিলে এই চাবি নেই, তাই সার্ভার কাজ করল (balance ০ → ১০০), উত্তর “ok 100” টেবিলে রাখল, তারপর পাঠাল।
  2. উত্তর পথে হারাল। সার্ভারের কাছে কাজ শেষ; client-এর কাছে শুধু নীরবতা। Cache-এর সারিটি রয়ে গেল — এটিই পরের ধাপে কাজে লাগবে।
  3. Duplicate — hit, কাজ আর চলল না। Timeout-এর পর client একই ID 7 আবার পাঠাল। চাবি (10.0.0.5, 40001, 7) টেবিলে আছে, তাই সার্ভার জমা উত্তর “ok 100” আবার পাঠাল। Balance ১০০-ই রইল — cache না থাকলে হতো ২০০.
  4. অন্য client-এর ID 7 — আলাদা চাবি। 10.0.0.9-এর port 51000 থেকেও ID 7 এল, কিন্তু ঠিকানা আলাদা বলে এটি miss: সত্যিকারের নতুন অনুরোধ, কাজ চলল (balance ২০০)। চাবিতে ঠিকানা না থাকলে এই বৈধ জমাটি ভুল করে “duplicate” ধরা পড়ত।
  5. প্রথম client এগিয়ে গেল — পুরোনো সারি বদল। 10.0.0.5 এবার ID 8 পাঠাল। Stop-and-wait-এ নতুন ID মানে client আগের উত্তর পেয়ে গেছে, তাই ID 7-এর সারি আর লাগবে না — সেখানে ID 8-এর উত্তর বসল (balance ২৫০)। পরে ID 7-এর কোনো দেরিতে আসা কপি এলে “শেষ ID-র চেয়ে ছোট” নিয়মে বাদ পড়বে।

তিন রকম প্রতিশ্রুতি

  • At-least-once — শুধু retry, সার্ভারে কোনো cache নেই: কাজ অন্তত একবার হবে, কিন্তু একাধিকবারও হতে পারে। শুধু idempotent অনুরোধে নিরাপদ।
  • At-most-once — retry + reply cache: কাজ একবারের বেশি হবে না। কিন্তু সব চেষ্টা ব্যর্থ হলে client জানে না কাজ শূন্যবার হয়েছে না একবার।
  • “Exactly-once” — client, সার্ভার আর নেটওয়ার্ক যেকোনো মুহূর্তে ব্যর্থ হতে পারলে শুধু বার্তা দিয়ে এটি নিশ্চিত করা যায় না। বাস্তবে এই নামে যা চলে, তা at-least-once পাঠানো + সার্ভারে duplicate ছাঁকা (প্রায়ই টেকসই storage-এ) — ফলাফল একবারের মতো।

এবার পুরো protocol চালিয়ে দেখা যাক। সার্ভার একটি “জমা” সেবা (idempotent নয়), fork() করা আলাদা প্রক্রিয়ায় চলে, আর ইচ্ছা করে প্রথম অনুরোধ আর প্রথম উত্তর “হারায়”। Client connect() করা socket-এ ID 1 আর 2 পাঠায়, প্রথম timeout ২০০ মিলিসেকেন্ড, প্রতিবার দ্বিগুণ। Cache সবচেয়ে সরল: শুধু শেষ অনুরোধের চাবি আর উত্তর। দুই প্রক্রিয়া unbuffered stdout-এ ছাপে, প্রতিটি লাইন পরের ঘটনার আগে — তাই সময়রেখার ক্রম প্রতিবার একই।

#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
static void server(int s) {                         /* "deposit 100" is NOT idempotent */
    int balance = 0, len = 0; uint32_t id, last_id = 0; char reply[32]; struct sockaddr_in c, last = {0};
    for (int k = 1; k <= 4; k++) {
        socklen_t l = sizeof c; recvfrom(s, &id, sizeof id, 0, (struct sockaddr *)&c, &l); id = ntohl(id);
        if (k == 1) { printf("        server: req %u LOST (simulated)\n", id); continue; }
        int dup = id == last_id && c.sin_addr.s_addr == last.sin_addr.s_addr && c.sin_port == last.sin_port;
        if (!dup) { balance += 100; last_id = id; last = c;   /* cache key = client addr:port + id */
                    len = snprintf(reply, sizeof reply, "id=%u balance=%d", id, balance); }
        printf("        server: req %u %s, balance=%d\n", id, dup ? "DUPLICATE -> cached reply, NOT executed" : "executed", balance);
        if (k == 2) printf("        server: reply %u LOST (simulated)\n", id); else sendto(s, reply, len, 0, (struct sockaddr *)&c, l);
    }
}
int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0); int t = 0;    /* unbuffered: both processes print at once */
    int s = socket(AF_INET, SOCK_DGRAM, 0), c = socket(AF_INET, SOCK_DGRAM, 0);
    struct sockaddr_in a = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
    socklen_t al = sizeof a; bind(s, (struct sockaddr *)&a, al); getsockname(s, (struct sockaddr *)&a, &al);
    if (fork() == 0) { server(s); return 0; }
    connect(c, (struct sockaddr *)&a, sizeof a);    /* replies accepted only from the server */
    for (uint32_t id = 1; id <= 2; id++)
        for (int try = 1, wait_ms = 200; try <= 4; try++, t += wait_ms, wait_ms *= 2) {  /* 200, 400, 800 */
            struct timeval tv = {wait_ms / 1000, wait_ms % 1000 * 1000};    /* recv() gives up after tv */
            setsockopt(c, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
            printf("t=%4d client: send req %u (try %d, wait %d ms)\n", t, id, try, wait_ms);
            uint32_t nid = htonl(id); char r[32] = ""; unsigned rid = 0; send(c, &nid, sizeof nid, 0);
            if (recv(c, r, sizeof r - 1, 0) < 0) { printf("       client: timeout\n"); continue; }
            if (sscanf(r, "id=%u", &rid) == 1 && rid == id) { printf("       client: got \"%s\"\n", r); break; }
        }
    wait(NULL);
}

আউটপুট:

t=   0 client: send req 1 (try 1, wait 200 ms)
        server: req 1 LOST (simulated)
       client: timeout
t= 200 client: send req 1 (try 2, wait 400 ms)
        server: req 1 executed, balance=100
        server: reply 1 LOST (simulated)
       client: timeout
t= 600 client: send req 1 (try 3, wait 800 ms)
        server: req 1 DUPLICATE -> cached reply, NOT executed, balance=100
       client: got "id=1 balance=100"
t= 600 client: send req 2 (try 1, wait 200 ms)
        server: req 2 executed, balance=200
       client: got "id=2 balance=200"

প্রথম চেষ্টা: অনুরোধ “হারাল” — সার্ভার পড়ে ফেলে দিল, client ২০০ মিলিসেকেন্ড পর timeout পেল। দ্বিতীয় চেষ্টা (অপেক্ষা ৪০০): সার্ভার কাজ করল, balance ১০০, কিন্তু উত্তর “হারাল”। তৃতীয় চেষ্টা (অপেক্ষা ৮০০): একই ID 1 আবার এল, চাবি মিলল, সার্ভার জমা উত্তর পাঠাল — NOT executed, balance=100. Client পেল id=1 balance=100: ঠিক যা হওয়া উচিত। তারপর ID 2 প্রথমবারেই সফল, balance ২০০. Cache না থাকলে তৃতীয় চেষ্টায় balance হতো ২০০, শেষে ৩০০ — একটি জমা দুবার।

t= সংখ্যা client নিজে হিসাব করে (timeout-এর অপেক্ষা যোগ করে), যাতে আউটপুট প্রতিবার একই থাকে; ঘড়ি দিয়ে মাপা একটি সংস্করণে এসেছিল প্রায় ২১০ আর ৬২০ মিলিসেকেন্ড — kernel-এর timer সামান্য দেরিতে জাগে। দুটি সরলীকরণ: এক সারির cache “শেষ ID-র চেয়ে ছোট হলে বাদ” নিয়ম মানে না (অনুশীলন ৩), আর ভুল ID-র উত্তর এলে কোড বাকি সময় অপেক্ষা না করে পরের চেষ্টায় যায় (অনুশীলন ৪)। Request ID যায় htonl() করে, network byte order-এ।

দেরিতে আসা আর ক্রম ভাঙা উত্তর

UDP-তে উত্তর আগে-পরে আসতে পারে, একই উত্তর দুবারও আসতে পারে (একটি দেরিতে আসা কপি, একটি cache থেকে)। Client-এর নিয়ম: যে উত্তরের ID এখন অপেক্ষমাণ কোনো অনুরোধের সাথে মেলে না, তা বাদ। একসাথে অনেক অনুরোধ চললে client একটি টেবিল রাখে — ID → অপেক্ষমাণ অনুরোধ — আর উত্তর যে ক্রমেই আসুক, ID দিয়ে মেলায়; স্বাভাবিক কাঠামো একটি hash table (DSA: hash table). DNS resolver এভাবেই একসাথে শত শত প্রশ্ন সামলায়।

৫ · Simulator — বিভিন্ন loss-এর ধরনে সময় আর duplicate

নিচের simulator একটি “জমা” অনুরোধের stop-and-wait client: প্রথম timeout ২০০ মিলিসেকেন্ড, প্রতিবার দ্বিগুণ, সর্বোচ্চ ৫ বার, RTT ২০ মিলিসেকেন্ড। প্রতিটি চেষ্টার ভাগ্য একটি অক্ষরে লেখা: o ঠিক আছে, q অনুরোধ হারাল, r উত্তর হারাল, d উত্তর ২৫০ মিলিসেকেন্ড দেরিতে এল। প্রতিটি ধরনের জন্য হিসাব হয় কতবার চেষ্টা, কখন শেষ, আর সার্ভারে কাজ কতবার চলল — cache ছাড়া আর cache-সহ। শেষে backoff-এর সারির পাশে full jitter-এর একটি নমুনা।

// Stop-and-wait request/response over a lossy channel: one NON-idempotent request.
// Pattern letter per try: o = ok, q = request lost, r = reply lost, d = reply 250 ms late.
const RTT = 20, T0 = 200, MAX = 5;                  // ms; first timeout 200 ms, doubled each try
function run(pattern, cache) {
  let t = 0, T = T0, done = Infinity, tries = 0, execs = 0, seen = false;
  for (let k = 0; k < MAX && t < done; k++, t += T, T *= 2) {   // next try only if no reply yet
    tries++;
    const ev = pattern[k] || "o";
    if (ev === "q") continue;                       // server never sees this copy
    if (!(cache && seen)) execs++;                  // with a reply cache a duplicate is not re-run
    seen = true;
    if (ev === "r") continue;                       // server did the work, the reply vanished
    done = Math.min(done, t + RTT + (ev === "d" ? 250 : 0));
  }
  return { tries, execs, end: done === Infinity ? "gave up at " + t : "done at " + done };
}
console.log("pattern  tries  client result      executions: no cache / with cache");
for (const p of ["o", "qo", "ro", "do", "rro", "qrqo", "qqqqq", "rrrrr"]) {
  const a = run(p, false), b = run(p, true);
  console.log(p.padEnd(8), String(a.tries).padStart(5), " ", (a.end + " ms").padEnd(18),
              String(a.execs).padStart(6), "/", b.execs, a.execs > 1 ? " <- duplicate!" : "");
}
let seed = 987654;                                      // fixed seed: same "random" jitter every run
const rnd = () => (seed = seed * 16807 % 2147483647) / 2147483647;
const caps = [0, 1, 2, 3, 4].map(k => T0 * 2 ** k);
console.log("backoff       :", caps.join(" "), "ms");
console.log("+ full jitter :", caps.map(c => Math.round(rnd() * c)).join(" "), "ms (each in 0..cap)");

আউটপুট:

pattern  tries  client result      executions: no cache / with cache
o            1   done at 20 ms           1 / 1
qo           2   done at 220 ms          1 / 1
ro           2   done at 220 ms          2 / 1  <- duplicate!
do           2   done at 220 ms          2 / 1  <- duplicate!
rro          3   done at 620 ms          3 / 1  <- duplicate!
qrqo         4   done at 1420 ms         2 / 1  <- duplicate!
qqqqq        5   gave up at 6200 ms      0 / 0
rrrrr        5   gave up at 6200 ms      5 / 1  <- duplicate!
backoff       : 200 400 800 1600 3200 ms
+ full jitter : 146 329 495 1020 2218 ms (each in 0..cap)

qo বনাম ro: দুটিতেই client-এর অভিজ্ঞতা হুবহু এক — ২ চেষ্টা, ২২০ মিলিসেকেন্ড। কিন্তু ro-তে cache ছাড়া কাজ চলল দুবার। Client-এর দিক থেকে দেখে এই পার্থক্য ধরার কোনো উপায় নেই — এজন্যই duplicate ধরার দায় সার্ভারের। do: উত্তর হারায়নি, শুধু দেরি করেছে; তবু timeout আগে ফুরিয়েছে, দ্বিতীয় কপি গেছে, cache ছাড়া কাজ দুবার। শেষে client ২২০-এ দ্বিতীয় কপির উত্তর নিল, আর ২৭০-এ আসা প্রথমটি দেরিতে আসা duplicate হিসেবে বাদ পড়বে। qrqo: ২০০ + ৪০০ + ৮০০ + ২০ = ১,৪২০ মিলিসেকেন্ড। qqqqq আর rrrrr: দুটিতেই client ৬,২০০ মিলিসেকেন্ডে (= ২০০ × ৩১) হাল ছাড়ে আর অ্যাপকে “ব্যর্থ” জানায় — কিন্তু প্রথমটিতে কাজ শূন্যবার হয়েছে, দ্বিতীয়টিতে cache ছাড়া পাঁচবার, cache-সহ একবার! “ব্যর্থ” মানে “হয়নি” নয়, মানে “জানি না”। শেষের দুই লাইন: backoff-এর সীমা ২০০ থেকে ৩,২০০; full jitter প্রতিটিকে ০ থেকে সেই সীমার মধ্যে এলোমেলো বসায় — হাজার client-এর অপেক্ষা এভাবে ছড়িয়ে যায়। Seed স্থির, তাই প্রতিবার একই সংখ্যা।

৬ · Broadcast — LAN-এর সবাইকে একসাথে

Broadcast মানে একটি datagram একই LAN-এর সব host-এর কাছে। পাড়ার মসজিদের মাইকে ঘোষণার মতো — যাঁর দরকার শোনেন, বাকিরাও শুনতে বাধ্য, আর ঘোষণা পাড়ার বাইরে যায় না। IPv4-এ দুই রকম ঠিকানা:

ধরনউদাহরণকোথায় যায়Router কী করে
Limited broadcast255.255.255.255যে interface দিয়ে বেরোল, সেই link-এর সবাইকখনো অন্য নেটওয়ার্কে পাঠায় না
Directed broadcast192.168.1.255 (192.168.1.0/24-এর জন্য)ওই subnet-এর সবাইRFC 2644 (১৯৯৯) থেকে ডিফল্টভাবে পাঠায় না

Router broadcast পার করলে একটি ভুল বা দূষিত packet পুরো ইন্টারনেটে ছড়াত। ১৯৯০-এর দশকের শেষে Smurf আক্রমণ ঠিক directed broadcast-এর অপব্যবহার করেছিল, তার পর router-এর ডিফল্ট বদলায়। তাই broadcast একটি broadcast domain-এর ভেতরে সীমিত (পর্ব ১২–১৩)।

Socket-এ broadcast — SO_BROADCAST

ভুল করে broadcast পাঠানো ঠেকাতে OS একটি সচেতন অনুমতি চায়: socket-এ SO_BROADCAST চালু না করলে broadcast ঠিকানায় sendto() ব্যর্থ হয় — Linux-এ EACCES (“Permission denied”). শোনার দিকে কিছু বিশেষ লাগে না: 0.0.0.0-এ ওই port-এ bind করা socket broadcast পায়।

int on = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof on);
dst.sin_addr.s_addr = htonl(INADDR_BROADCAST);     /* 255.255.255.255 */
dst.sin_port = htons(47000);
sendto(fd, msg, len, 0, (struct sockaddr *)&dst, sizeof dst);

কারা broadcast ব্যবহার করে

  • DHCP: নতুন যন্ত্রের তখনো IP নেই, সার্ভারের ঠিকানাও জানা নেই। তাই DISCOVER যায় 0.0.0.0:68 থেকে 255.255.255.255:67-এ (পর্ব ১৮)। অন্য subnet-এর সার্ভারে পৌঁছাতে router-এ DHCP relay লাগে — কারণ router broadcast পার করে না।
  • LAN-এ সেবা বা গেম খোঁজা: অনেক LAN গেম আর পুরোনো সেবা “কেউ সার্ভার চালাচ্ছেন?” প্রশ্নটি broadcast করে, সার্ভার unicast-এ উত্তর দেয় — একই subnet-এ কাজ করে, অন্যটিতে নয়।
  • Wake-on-LAN: ঘুমন্ত কম্পিউটার জাগানোর “magic packet” প্রায়ই UDP broadcast-এ পাঠানো হয় (Wake-on-LAN) — ঘুমন্ত যন্ত্রের IP stack চলে না, network card নিজেই packet-এর ভেতরে নিজের MAC খোঁজে।

দাম: broadcast LAN-এর প্রত্যেক host-কে datagram-টি দেখতে বাধ্য করে, যার আগ্রহ নেই তাকেও। এজন্য IPv6-এ broadcast নেই — তার জায়গায় multicast, যেমন সব node-এর দল ff02::1.

৭ · Multicast socket — group-এ যোগ দেওয়া আর পাঠানো

Multicast-এ datagram যায় শুধু তাদের কাছে, যারা একটি group-এ যোগ দিয়েছে — মাইকে ঘোষণা নয়, অনেকটা কোচিং-এর একটি ব্যাচের নোটিশের মতো: ব্যাচে নাম লেখালে পান, না লেখালে নয়। ঠিকানার পরিসর, IGMP আর router-এর ভূমিকা পর্ব ২৬-এ (multicast আর QoS) দেখেছেন। এখানে socket-এর দিক থেকে কী কী করতে হয়:

দিকOption / callকাজ
প্রাপকSO_REUSEADDR (দরকারে SO_REUSEPORT)একই যন্ত্রে একাধিক প্রোগ্রাম একই group port-এ শুনতে পারে; bind-এর আগে বসাতে হয়
প্রাপকbind(0.0.0.0 বা group ঠিকানা, port)Group-এর port-এ শোনা; Linux-এ group ঠিকানায় bind করলে ওই port-এর অন্য datagram ঢোকে না
প্রাপকIP_ADD_MEMBERSHIP + struct ip_mreqGroup-এ যোগ: কোন group (imr_multiaddr), কোন interface-এ (imr_interface). Kernel IGMP report পাঠায়, network card-এর MAC ফিল্টারে group-এর MAC যোগ করে
প্রাপকIP_DROP_MEMBERSHIP বা close()Group ছাড়া; যন্ত্রের শেষ সদস্য গেলে kernel IGMP-তে জানায়
প্রেরকIP_MULTICAST_TTLকত router পার হবে; ডিফল্ট ১ — শুধু এই link
প্রেরকIP_MULTICAST_LOOPনিজের যন্ত্রের সদস্যরাও কপি পাবে কি না; Linux-এ ডিফল্ট চালু
প্রেরকIP_MULTICAST_IFকোন interface দিয়ে বেরোবে (একাধিক interface থাকলে জরুরি)

প্রেরকের জন্য group-এ যোগ দেওয়া লাগে না — যেকোনো UDP socket group ঠিকানায় sendto() করতে পারে। নিজের প্রতিষ্ঠানের ভেতরের ব্যবহারে 239.0.0.0/8 (administratively scoped) পরিসর থেকে ঠিকানা বেছে নেওয়াই প্রচলিত। নিচে একটি host-এ group-এ যোগ দেওয়া থেকে datagram পৌঁছানো পর্যন্ত — LAN-এ, যেখানে 192.168.1.20-এর দুটি প্রোগ্রাম শুনছে আর 192.168.1.10 পাঠাচ্ছে।

  1. প্রোগ্রাম A, Bbind :47000SO_REUSEADDR
  2. Join239.1.2.3IGMP report
  3. প্রেরক.10 → groupTTL 1
  4. প্রাপকের kernel২টি কপিA আর B
  5. LeaveDROPশেষ সদস্য গেলে
192.168.1.20-এর ভেতরে আর তারে
Socket-এর অবস্থা
eth0-এ group তালিকা
NIC-এর MAC ফিল্টার
তারে
  1. দুটি প্রোগ্রাম একই port-এ। A আর B দুজনেই SO_REUSEADDR বসিয়ে 0.0.0.0:47000-এ bind করল — option ছাড়া দ্বিতীয় bind EADDRINUSE দিত। এখনো কেউ group-এ নেই, তাই 239.1.2.3-এর datagram এলেও এদের কাছে পৌঁছাবে না।
  2. Group-এ যোগ — IP_ADD_MEMBERSHIP. struct ip_mreq-এ group 239.1.2.3 আর interface 192.168.1.20. Kernel eth0-এ group যোগ করল, network card-কে group-এর MAC 01:00:5e:01:02:03 নিতে বলল (ঠিকানার নিচের ২৩ bit: 1.2.3 → 01:02:03), আর একটি IGMP Membership Report পাঠাল — IGMP snooping-ওয়ালা switch আর router জানল এখানে সদস্য আছে। B-ও যোগ দিল; kernel সদস্য গোনে, যন্ত্রের পক্ষে একবার জানানোই যথেষ্ট।
  3. প্রেরক group-এ পাঠাল। 192.168.1.10 কোনো যোগ ছাড়াই IP_MULTICAST_TTL ১ বসিয়ে 239.1.2.3:47000-এ একটি datagram পাঠাল। Frame-এর গন্তব্য MAC group-এর MAC; TTL ১ বলে কোনো router এটি অন্য নেটওয়ার্কে নেবে না।
  4. প্রাপকের kernel — একটি datagram, দুটি কপি। Network card-এর ফিল্টারে MAC মিলল; IP স্তর দেখল eth0-এ 239.1.2.3-এর সদস্য আছে; UDP স্তর port 47000-এ দুটি socket পেল — multicast বলে প্রত্যেকে একটি করে কপি পেল (unicast হলে পেত একজনই)। নিচের কোডে loopback-এ ঠিক এটিই দেখা যাবে।
  5. Group ছাড়া। A বন্ধ হলো (বা IP_DROP_MEMBERSHIP ডাকল)। B এখনো সদস্য, তাই যন্ত্রটি group-এ থাকে, তারে কিছু যায় না। B-ও গেলে kernel IGMP-তে ছাড়ার খবর দেয় (IGMPv2-তে Leave, v3-এ state-change report), আর router যাচাই করে এই link-এ group-এর ট্রাফিক পাঠানো বন্ধ করে।

নিচের প্রোগ্রাম এই runner-এর ভেতরেই multicast আর broadcast দুটোই পরীক্ষা করে। দুটি “শ্রোতা” socket SO_REUSEADDR দিয়ে একই port 47000-এ bind করে; প্রেরক IP_MULTICAST_IF দিয়ে loopback (127.0.0.1) বেছে নেয়, TTL ১ আর loop চালু রাখে। প্রথমে group-এ কেউ যোগ না দিয়ে পাঠানো, তারপর যোগ দিয়ে, শেষে SO_BROADCAST ছাড়া আর সহ 127.255.255.255-এ broadcast.

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
static void try_recv(int s, const char *who) {     /* SO_RCVTIMEO: wait at most 200 ms */
    char b[32]; ssize_t n = recv(s, b, sizeof b, 0);
    if (n < 0) printf("  %s: nothing (timeout)\n", who); else printf("  %s: got \"%.*s\"\n", who, (int)n, b);
}
int main(void) {
    int one = 1, r[2], t = socket(AF_INET, SOCK_DGRAM, 0);
    struct sockaddr_in any = {.sin_family = AF_INET, .sin_port = htons(47000), .sin_addr.s_addr = htonl(INADDR_ANY)};
    struct timeval tv = {0, 200000}; struct in_addr lo = {htonl(INADDR_LOOPBACK)}; unsigned char ttl = 1, loop = 1;
    for (int i = 0; i < 2; i++) {                   /* two listeners on the SAME port */
        r[i] = socket(AF_INET, SOCK_DGRAM, 0);
        setsockopt(r[i], SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
        setsockopt(r[i], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
        if (bind(r[i], (struct sockaddr *)&any, sizeof any) < 0) { perror("bind"); return 1; }
    }
    setsockopt(t, IPPROTO_IP, IP_MULTICAST_IF, &lo, sizeof lo);      /* send via lo (127.0.0.1) */
    setsockopt(t, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, 1);            /* never leave this link */
    setsockopt(t, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, 1);          /* local members get a copy */
    struct sockaddr_in grp = any; grp.sin_addr.s_addr = inet_addr("239.1.2.3");
    puts("1) send to 239.1.2.3 before anyone joined:");
    sendto(t, "hi group", 8, 0, (struct sockaddr *)&grp, sizeof grp); try_recv(r[0], "listener 0");
    struct ip_mreq m = {.imr_multiaddr = grp.sin_addr, .imr_interface = lo};
    for (int i = 0; i < 2; i++)
        printf("2) listener %d IP_ADD_MEMBERSHIP: %s\n", i, setsockopt(r[i], IPPROTO_IP, IP_ADD_MEMBERSHIP, &m, sizeof m) ? strerror(errno) : "ok");
    sendto(t, "hi group", 8, 0, (struct sockaddr *)&grp, sizeof grp); try_recv(r[0], "listener 0"); try_recv(r[1], "listener 1");
    struct sockaddr_in bc = any; bc.sin_addr.s_addr = inet_addr("127.255.255.255");
    printf("3) broadcast without SO_BROADCAST: %s\n", sendto(t, "hi all", 6, 0, (struct sockaddr *)&bc, sizeof bc) < 0 ? strerror(errno) : "sent");
    setsockopt(t, SOL_SOCKET, SO_BROADCAST, &one, sizeof one);
    printf("4) with SO_BROADCAST: %s\n", sendto(t, "hi all", 6, 0, (struct sockaddr *)&bc, sizeof bc) < 0 ? strerror(errno) : "sent");
    try_recv(r[0], "listener 0"); try_recv(r[1], "listener 1");
}

আউটপুট:

1) send to 239.1.2.3 before anyone joined:
  listener 0: nothing (timeout)
2) listener 0 IP_ADD_MEMBERSHIP: ok
2) listener 1 IP_ADD_MEMBERSHIP: ok
  listener 0: got "hi group"
  listener 1: got "hi group"
3) broadcast without SO_BROADCAST: Permission denied
4) with SO_BROADCAST: sent
  listener 0: got "hi all"
  listener 1: got "hi all"

যোগ দেওয়ার আগে কিছুই আসেনি — kernel group-এর সদস্য না হলে datagram socket পর্যন্ত পৌঁছায় না, socket ঠিক port-এ বসে থাকলেও। যোগ দেওয়ার পর একটি sendto()-র দুটি কপি — দুই শ্রোতাই “hi group” পেল। Broadcast: SO_BROADCAST ছাড়া sendto() ব্যর্থ হলো “Permission denied” (EACCES) দিয়ে; option চালু করার পর পাঠানো গেল, আর দুই শ্রোতাই কপি পেল। 127.255.255.255 হলো Linux-এ loopback নেটওয়ার্ক 127.0.0.0/8-এর broadcast ঠিকানা; সত্যিকারের LAN-এ আপনি পাঠাতেন 255.255.255.255 বা 192.168.1.255-এর মতো ঠিকানায়।

একটি সৎ সতর্কতা: আলাদা একটি পরীক্ষায় এই runner-এর interface-গুলোর flag দেখা হয়েছিল (getifaddrs() দিয়ে) — lo-তে MULTICAST flag নেই। তবু multicast কাজ করল, কারণ IP_MULTICAST_LOOP চালু থাকলে Linux নিজের যন্ত্রের সদস্যদের জন্য কপি ভেতরেই পৌঁছে দেয়, আর IP_MULTICAST_IF দিয়ে বেরোনোর পথ স্পষ্ট বলা ছিল। অন্য kernel, অন্য container বা অন্য OS-এ ফল ভিন্ন হতে পারে — loopback-এ multicast-এর আচরণ মানে লেখা নিশ্চয়তা নয়। নিজের যন্ত্রে ip maddr show দিয়ে কোন interface কোন group-এ আছে দেখতে পারেন।

SO_REUSEADDR আর SO_REUSEPORT — একই port-এ অনেক শ্রোতা

UDP-তে এই দুটি option-এর অর্থ TCP থেকে আলাদা, OS ভেদেও আলাদা। Linux-এ: সব socket SO_REUSEADDR বসালে একাধিক UDP socket একই ঠিকানা-port-এ bind করতে পারে; multicast আর broadcast-এ সবাই কপি পায় (ওপরের আউটপুট), unicast-এ পায় একজনই। SO_REUSEPORT (Linux ৩.৯ থেকে) unicast datagram উৎস-ঠিকানার hash দিয়ে socket-গুলোর মধ্যে ভাগ করে — একাধিক thread-এ UDP সার্ভারের ভার ছড়ানোর প্রচলিত কৌশল। BSD আর macOS-এ একাধিক multicast শ্রোতার জন্য সাধারণত SO_REUSEPORT লাগে, Windows-এর নিয়ম আবার ভিন্ন — প্রতিটি লক্ষ্য-OS-এ পরীক্ষা করুন।

বাস্তবের দুটি multicast protocol

ProtocolGroup আর portকী করে
mDNS (RFC 6762), সাথে DNS-SD (RFC 6763)224.0.0.251:5353, IPv6-এ ff02::fbDNS সার্ভার ছাড়াই LAN-এ নাম খোঁজা (printer.local) আর সেবা খোঁজা — প্রিন্টার, Chromecast-ধরনের যন্ত্র, ফাইল শেয়ার; Apple-এর Bonjour আর Linux-এর Avahi এর বাস্তবায়ন
SSDP (UPnP-এর অংশ)239.255.255.250:1900, IPv6-এ ff02::cHTTP-র মতো লেখা বার্তা UDP-তে: M-SEARCH দিয়ে যন্ত্র খোঁজা, NOTIFY দিয়ে নিজের উপস্থিতি জানানো — স্মার্ট টিভি, মিডিয়া সার্ভার, router

দুটিই শুধু স্থানীয় নেটওয়ার্কের জন্য (224.0.0.251 router পার করে না, আর SSDP-র বার্তাও খুব ছোট TTL নিয়ে পাঠানোর কথা)। আর ভুল করে ইন্টারনেটের দিকে খোলা SSDP যন্ত্র ছোট M-SEARCH-এর জবাবে বড় উত্তর দেয় বলে amplification আক্রমণে ব্যাপক অপব্যবহৃত হয়েছে (অংশ ৯)।

৮ · IPv6 UDP, ICMP ত্রুটি আর connected socket

IPv6-এ একই কোড, অন্য struct

IPv6-এ UDP-র নিয়ম একই, শুধু ঠিকানার struct বদলায় — sockaddr_in6: sin6_family = AF_INET6, sin6_port, ১২৮ bit-এর sin6_addr (যেকোনো ঠিকানা = in6addr_any, loopback = in6addr_loopback, অর্থাৎ ::1), আর দুটি নতুন ঘর sin6_flowinfo ও sin6_scope_id. শেষেরটি জরুরি link-local ঠিকানায় (fe80::…): একই fe80::1 প্রতিটি interface-এ থাকতে পারে, তাই কোন interface — তা interface index দিয়ে বলতে হয় (লেখায় fe80::1%eth0). পর্ব ৪১-এর মতো getaddrinfo() ব্যবহার করলে IPv4 আর IPv6 দুটোর জন্য একই কোড লেখা যায় — এটিই পরামর্শ।

  • Broadcast নেই: LAN-এর সবাইকে পেতে link-local সব-node group ff02::1-এ পাঠান — তবে সত্যিই সবাইকে দরকার না হলে নিজের একটি group ব্যবহার করুন (পর্ব ২১, IPv6).
  • Multicast-এ যোগ: IPV6_JOIN_GROUP + struct ipv6_mreq — interface এখানে ঠিকানা নয়, index (if_nametoindex("eth0")). প্রেরকের দিকে IPV6_MULTICAST_HOPS, IPV6_MULTICAST_LOOP, IPV6_MULTICAST_IF.
  • Dual-stack: ::-এ bind করা IPv6 socket Linux-এ ডিফল্টভাবে IPv4 datagram-ও পায় (ঠিকানা ::ffff:192.0.2.1 রূপে); শুধু IPv6 চাইলে IPV6_V6ONLY. এই ডিফল্ট OS ভেদে আলাদা।

ICMP ত্রুটি কোথায় যায়

বন্ধ port-এ datagram পৌঁছালে প্রাপক সাধারণত ICMP port unreachable ফেরত পাঠায় (পর্ব ২০)। পর্ব ২৮-এ চালিয়ে দেখেছেন: connected socket-এ Linux এটি পরের call-এ ECONNREFUSED হিসেবে জানায়; unconnected socket-এ সাধারণত কিছুই জানায় না, কারণ kernel জানে না ত্রুটিটি কোন sendto()-এর। Linux-এ IP_RECVERR চালু করলে unconnected socket-ও ICMP ত্রুটি গন্তব্যসহ একটি আলাদা error queue থেকে পড়তে পারে (recvmsg(…, MSG_ERRQUEUE)). তবে ICMP firewall-এ আটকাতে বা rate limit-এ বাদ পড়তে পারে — এটি বাড়তি সংকেত, timeout-এর বিকল্প নয়।

৯ · ভদ্র প্রেরক আর নিরাপদ সার্ভার

গতি সীমিত রাখা — rate limiting আর pacing

TCP নিজে congestion control করে (পর্ব ৩২); UDP-তে তা অ্যাপের দায়। একটি লুপে যত দ্রুত সম্ভব sendto() করলে কী হয়? প্রথমে নিজের socket-এর send buffer আর network card-এর queue ভরে, তারপর পথের সবচেয়ে সরু link-এ queue জমে, তারপর loss — আর loss মানে আরও retry, আরও ভিড়। চরম অবস্থায় congestion collapse. তাই প্রেরক দুটি জিনিস করে:

  • Rate limiting: প্রতি সেকেন্ডে সর্বোচ্চ কত datagram বা বাইট — প্রায়ই token bucket দিয়ে: প্রতি সেকেন্ডে r টি token জমে, সর্বোচ্চ b টি পর্যন্ত, প্রতিটি datagram পাঠাতে একটি token লাগে।
  • Pacing: একসাথে ঝাঁক বেঁধে না পাঠিয়ে সমান ফাঁক রেখে পাঠানো — ১০ Mbps-এ ১,২০০ বাইটের datagram মানে প্রতি ০.৯৬ মিলিসেকেন্ডে একটি (১,২০০ × ৮ ÷ ১০৭ সেকেন্ড)। Linux-এ SO_MAX_PACING_RATE option আর fq queueing এতে সাহায্য করে।

IETF-এর UDP নির্দেশিকা RFC 8085 (BCP 145) মোটা দাগে বলে: বেশি ডেটার অ্যাপ TCP-র সাথে তুলনীয় congestion control রাখবে; অল্প ডেটার request/response অ্যাপ প্রতি RTT-তে মোটামুটি একটির বেশি অনুরোধ পাঠাবে না, RTT অজানা হলে রক্ষণশীল (সেকেন্ডের ঘরের) timeout নেবে আর retry-তে backoff করবে। সঠিক শর্তগুলো RFC-তেই পড়ুন — এই পর্বের ২০০ মিলিসেকেন্ড loopback-এ শেখানোর জন্য, ইন্টারনেটের ডিফল্ট নয়।

নিরাপত্তা — UDP-র উৎস-ঠিকানাকে বিশ্বাস নয়

UDP-তে handshake নেই, তাই datagram-এর উৎস-ঠিকানা প্রেরক যা লিখেছে তা-ই। যেসব নেটওয়ার্ক বাইরে যাওয়া packet-এর উৎস যাচাই করে না (BCP 38 না মানা), সেখান থেকে অন্যের ঠিকানা বসানো সম্ভব — IP spoofing. সার্ভার লেখার সময় এর তিনটি পরিণাম মাথায় রাখুন:

নিয়মকেনকীভাবে
উৎস যাচাই করুনঅচেনা কেউ উত্তর সেজে বা অনুমতিহীন ঠিকানা থেকে অনুরোধ পাঠাতে পারেClient-এ connect() (শুধু সার্ভারের উত্তর ঢোকে); সার্ভারে অনুমোদিত ঠিকানা-তালিকা, firewall নিয়ম; এলোমেলো request ID আর এলোমেলো উৎস port — অনুমান কঠিন করে
যাচাইয়ের আগে বড় উত্তর নয়জাল উৎস দিয়ে ছোট অনুরোধ, বড় উত্তর শিকারের দিকে — amplification (পর্ব ২৮)উত্তর অনুরোধের চেয়ে বড় না রাখা, বা আগে একটি cookie/token ফেরত দিয়ে ঠিকানা যাচাই — যেমন QUIC যাচাইয়ের আগে পাওয়া বাইটের ৩ গুণের বেশি পাঠায় না
ঠিকানা দিয়ে পরিচয় প্রমাণ নয়“10.0.0.5 থেকে এসেছে, তাই বিশ্বস্ত” — spoofing-এ ভাঙে; reply cache-এও জাল ঠিকানার অনুরোধ জায়গা দখল করতে পারেগুরুত্বপূর্ণ কাজে cryptographic যাচাই (DTLS, QUIC-এর TLS, বা বার্তায় MAC); cache-এর আকার ও প্রতি ঠিকানার সারির সীমা; প্রতি উৎসে rate limit

আরেকটি সূক্ষ্ম বিষয়: recvfrom()-এর ঠিকানা client-এর স্থায়ী পরিচয় নয়। NAT চুপ থাকার পর নতুন port দিতে পারে (NAT rebinding, পর্ব ১৯), আর Wi-Fi থেকে মোবাইল ডেটায় গেলে IP-ই বদলায় — তখন (ঠিকানা, port, ID) চাবির cache retry-কে নতুন অনুরোধ ভাববে। দীর্ঘ কথোপকথনের protocol তাই নিজস্ব শনাক্তকারী রাখে, যেমন QUIC-এর connection ID (পর্ব ৩৩)।

এই আলোচনা শুধু প্রতিরক্ষার জন্য। অন্যের নেটওয়ার্কে broadcast বা multicast ছড়ানো, জাল উৎস-ঠিকানায় packet পাঠানো বা অন্যের সেবা পরীক্ষা করা অনৈতিক আর আইনত দণ্ডনীয় হতে পারে। নিজের যন্ত্রে, নিজের ল্যাবে বা loopback-এ পরীক্ষা করুন; নিজের সার্ভারে ss -ulpn দিয়ে দেখুন কোন UDP port কে খুলে রেখেছে।

আরও গভীরে: Linux-এ 0.0.0.0-এ bind করা socket যেকোনো group-এর datagram পায়, যে group-এ ওই যন্ত্রের অন্য কোনো socket যোগ দিয়েছে — কারণ IP_MULTICAST_ALL option ডিফল্ট চালু; শুধু নিজের যোগ দেওয়া group চাইলে এটি বন্ধ করুন বা group ঠিকানায় bind করুন। দ্রুত UDP সার্ভার recvmmsg()/sendmmsg() দিয়ে একবারে অনেক datagram সামলায়, SO_REUSEPORT দিয়ে প্রতি CPU core-এ একটি socket রাখে, আর QUIC বাস্তবায়নগুলো UDP GSO/GRO দিয়ে kernel-এ datagram ভাগ-জোড়ার খরচ কমায়। NFS-এর মতো পুরোনো UDP-ভিত্তিক RPC সার্ভারগুলোর “duplicate request cache” এই পর্বের reply cache-এরই বাস্তব রূপ (ভাবনার প্রশ্ন ১)।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • UDP সার্ভারে listen() আর accept() লাগে না কেন? — সংযোগ নেই; প্রতিটি datagram নিজের উৎস-ঠিকানা নিয়ে আসে, recvfrom() তা জানায়, এক socket-এ সব client.
  • UDP-র ওপর নির্ভরযোগ্য request/response কীভাবে বানাবেন? — request ID, timeout, একই ID-তে retry, exponential backoff + jitter, সার্ভারে reply cache দিয়ে duplicate ছাঁকা।
  • Idempotent অনুরোধ কী, retry-তে কেন জরুরি? — একাধিকবার চালালেও ফল এক; উত্তর হারালে retry-তে কাজ দুবার হয়, তাই idempotent নয় এমন অনুরোধে dedup লাগে।
  • 255.255.255.255-এ sendto() EACCES দিল — কেন? — socket-এ SO_BROADCAST চালু করা হয়নি।
  • Multicast শুনতে কী কী লাগে? — group-এর port-এ bind (একাধিক শ্রোতা হলে SO_REUSEADDR), IP_ADD_MEMBERSHIP দিয়ে group আর interface জানানো; kernel IGMP report পাঠায়।

১০ · AI-এর যুগে UDP socket কোড — চলে, কিন্তু টেকে কি?

AI-কে একটি UDP client-server লিখতে বললে যে কোড আসে, loopback-এ তা প্রায় সবসময় চলে — কারণ loopback-এ কিছু হারায় না, কিছু দেরি করে না। সমস্যা ধরা পড়ে বাস্তব নেটওয়ার্কে। UDP socket কোডে AI-এর পরিচিত ভুলগুলো:

AI-এর ভুলফলকীভাবে ধরবেন
Timeout ছাড়া recvfrom()একটি হারানো উত্তরে প্রোগ্রাম চিরকাল আটকেSO_RCVTIMEO বা poll আছে কি না খুঁজুন
প্রতি retry-তে নতুন request IDসার্ভার duplicate চিনতে পারে না — reply cache অকেজোRetry লুপের ভেতরে ID বদলাচ্ছে কি না দেখুন
শুধু ID দিয়ে reply cacheভিন্ন client-এর একই ID “duplicate” ভেবে বৈধ অনুরোধ বাদচাবিতে ঠিকানা আর port আছে কি না দেখুন
স্থির ব্যবধানে দ্রুত retry, jitter নেই, সীমা নেইভিড় বাড়ায়, thundering herd; RFC 8085-এর পরামর্শের বিপরীতঅপেক্ষা দ্বিগুণ হচ্ছে, cap আর সর্বোচ্চ চেষ্টা আছে কি না
Multicast শুনতে শুধু bind, IP_ADD_MEMBERSHIP নেই; বা SO_BROADCAST দিয়ে multicast পাঠানোর চেষ্টাকিছুই আসে না, বা অপ্রয়োজনীয় optionএই পর্বের ছক মিলিয়ে দেখুন; ip maddr show দিয়ে join যাচাই
SO_REUSEADDR/SO_REUSEPORT-এর আচরণ সব OS-এ এক বলাLinux-এ চলে, macOS বা Windows-এ ভাঙেman 7 socket, প্রতিটি লক্ষ্য-OS-এ পরীক্ষা
ifconfig, netstat -anu-কে একমাত্র পথ বলা; বানানো RFC নম্বরআধুনিক Linux-এ ip, ss; ভুল উৎসrfc-editor.org-এ শিরোনাম মেলান: mDNS 6762, DNS-SD 6763, UDP নির্দেশিকা 8085, BCP 38 = RFC 2827

AI-কে কোড লিখতে দেওয়ার সময় বাস্তব নেটওয়ার্কের শর্তগুলো আগেই বলে দিন, আর পরীক্ষার উপায়ও চেয়ে নিন:

আমি Linux-এ C-তে একটি UDP request/response client আর server চাই (loopback-এ পরীক্ষা করব)। ১. Client প্রতিটি নতুন অনুরোধে একটি request ID দেবে; retry-তে একই ID রাখবে। প্রথম timeout 200 ms (SO_RCVTIMEO বা poll), প্রতিবার দ্বিগুণ, সর্বোচ্চ 2 সেকেন্ড, full jitter, সর্বোচ্চ 5 বার। ২. Server-এ অনুরোধটি idempotent নয় ("deposit"), তাই (client IP, port, request ID) চাবির একটি reply cache রাখো; duplicate এলে কাজ না চালিয়ে আগের উত্তর পাঠাও। Cache-এর সারি কখন মুছবে, ব্যাখ্যা করো। ৩. Client শুধু server-এর ঠিকানা থেকে আর মিলে যাওয়া ID-র উত্তর গ্রহণ করবে। ৪. পরীক্ষার জন্য server-এ ইচ্ছাকৃতভাবে প্রথম অনুরোধ আর প্রথম উত্তর বাদ দেওয়ার একটি flag রাখো, আর পুরো সময়রেখা ছাপো। ৫. gcc -Wall -Wextra-এ কোনো warning যেন না থাকে; প্রতিটি system call-এর ফল যাচাই করো। কোনো RFC বা OS-ভেদের কথা বললে উৎস দাও, নিশ্চিত না হলে স্পষ্ট বলো।

AI-এর UDP কোড ব্যবহারের আগে — চেকলিস্ট

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

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

প্র ০১NFS-এর মতো পুরোনো ফাইল-সেবা UDP-র ওপর RPC দিয়ে চলত। “ফাইল মোছা” ধরনের অনুরোধ retry-তে কী সমস্যা করত, আর duplicate request cache কীভাবে এর সমাধান হলো?

১৯৮০-এর দশকে Sun Microsystems-এর Network File System (NFS) একটি সরল নকশা নেয়: client প্রতিটি ফাইল-কাজ (পড়া, লেখা, মোছা, নাম বদলানো) একটি RPC হিসেবে UDP datagram-এ পাঠায়, প্রতিটিতে একটি transaction ID (XID), আর উত্তর না এলে একই XID দিয়ে আবার পাঠায়। সার্ভার ছিল “stateless” — কোনো client-এর কথা মনে রাখত না, ফলে সার্ভার রিবুট হলেও client শুধু retry করে কাজ চালিয়ে যেতে পারত। পড়া বা লেখার মতো অনুরোধের বেশিরভাগ idempotent হওয়ায় নকশাটি সুন্দর কাজ করত।

সমস্যা ছিল idempotent নয় এমন অনুরোধে। ধরুন client “report.txt মোছা”-র অনুরোধ পাঠাল, সার্ভার মুছল, কিন্তু উত্তর হারাল। Retry এলে সার্ভার আবার মোছার চেষ্টা করে দেখে ফাইলটিই নেই — উত্তর দেয় “এমন ফাইল নেই”। ব্যবহারকারী দেখেন ত্রুটি, অথচ কাজটি সফল হয়েছিল। আরও খারাপ: “ফাইল বানানো, আগে থাকলে ব্যর্থতা” ধরনের অনুরোধ retry-তে নিজের বানানো ফাইলের কারণেই ব্যর্থ হয়, আর মাঝে অন্য কেউ একই নামে নতুন ফাইল বানালে retry-র মোছার অনুরোধ সেই নিরীহ নতুন ফাইলটিকেই মুছে দিতে পারে।

সমাধান এল সার্ভারের দিকে একটি ছোট স্মৃতি দিয়ে — duplicate request cache: সাম্প্রতিক অনুরোধের (client-এর ঠিকানা, XID, কাজের ধরন) আর তার উত্তর। একই চাবির অনুরোধ আবার এলে সার্ভার কাজ না করে আগের উত্তর পাঠায়। ১৯৮৯ সালের দিকে প্রকাশিত একটি পরিচিত গবেষণায় দেখানো হয়, এই cache NFS সার্ভারের নির্ভুলতা আর গতি দুটোই বাড়ায় — অপ্রয়োজনীয় কাজও বাঁচে। কিন্তু cache মেমরিতে, সীমিত আকারের, সার্ভার রিবুটে মুছে যায় — তাই নিশ্চয়তা ছিল “বেশিরভাগ সময়”। পরে NFS TCP-র দিকে সরে যায়, আর NFSv4.1 (RFC 5661, ২০১০) session আর slot-এর ধারণা দিয়ে reply cache-কে protocol-এর অংশ বানায়, যাকে মান-দলিলটি “exactly once semantics” বলে।

মূল উপলব্ধি: “stateless সার্ভার + retry” idempotent কাজে চমৎকার, কিন্তু idempotent নয় এমন কাজে সার্ভারকে কিছুটা স্মৃতি রাখতেই হয়। প্রশ্ন শুধু কতটুকু, কতক্ষণ, আর রিবুটের পরও টিকবে কি না।

প্র ০২“Exactly-once delivery অসম্ভব” — এই দাবির পেছনের যুক্তি কী? তাহলে পেমেন্ট সেবাগুলো কীভাবে একই অর্থ দুবার না কেটে নিরাপদে retry করতে দেয়?

দুটি প্রক্রিয়া শুধু অনির্ভরযোগ্য চ্যানেলে বার্তা দিয়ে কথা বলছে, আর যেকোনো বার্তা হারাতে পারে — এই অবস্থাকে বিতরণকৃত সিস্টেমের সাহিত্যে প্রায়ই “দুই সেনাপতির সমস্যা” দিয়ে বোঝানো হয়: শেষ বার্তাটি পৌঁছাল কি না, প্রেরক কখনো নিশ্চিত হতে পারে না, কারণ নিশ্চিতকরণের বার্তাও হারাতে পারে। আমাদের client-এর দিক থেকে: উত্তর না এলে দুটি সম্ভাবনা — কাজ হয়নি, বা কাজ হয়েছে কিন্তু উত্তর হারিয়েছে — আর দুটিকে আলাদা করার কোনো তথ্য client-এর কাছে নেই। আবার না পাঠালে কাজ শূন্যবার হতে পারে; পাঠালে দুবার হতে পারে। সার্ভারও যদি যেকোনো মুহূর্তে বিকল হতে পারে (কাজ করার পর, উত্তর লেখার আগে), তাহলে শুধু বার্তার স্তরে “ঠিক একবার পৌঁছানো” নিশ্চিত করার কোনো সাধারণ উপায় নেই।

১৯৮৪ সালে Andrew Birrell আর Bruce Nelson-এর বিখ্যাত RPC গবেষণাপত্রে এই বাস্তবতা স্বীকার করে লক্ষ্য রাখা হয় “at-most-once”: প্রতিটি call-এর একটি অনন্য পরিচয়, সার্ভারে duplicate ছাঁকা, আর ব্যর্থ হলে client-কে জানানো যে কাজ হয়েছে কি না অজানা। বাস্তবের ব্যবস্থাগুলো তারপর অন্য পথে এগোয়: বার্তা পৌঁছানো থাকবে at-least-once, আর প্রভাব হবে একবারের মতো।

আধুনিক পেমেন্ট API-গুলো এজন্য “idempotency key” ব্যবহার করে: client প্রতিটি লেনদেনের জন্য একটি এলোমেলো, অনন্য চাবি বানায় আর প্রতিটি retry-তে একই চাবি পাঠায়। সার্ভার চাবি আর ফলাফল টেকসই database-এ রাখে — প্রায়ই লেনদেনের সাথে একই database transaction-এ, যাতে “টাকা কাটা হলো কিন্তু চাবি লেখা হলো না” অবস্থা না হয়। একই চাবি আবার এলে আগের ফলাফল ফেরত যায়। HTTP-র জন্য এমন একটি Idempotency-Key header মানে রূপ দেওয়ার কাজও IETF-এ চলছে। এটি আমাদের reply cache-এরই বড় সংস্করণ — পার্থক্য হলো চাবি client-এর ঠিকানার ওপর নির্ভর করে না (ঠিকানা বদলালেও কাজ করে), আর স্মৃতি রিবুটেও টেকে।

মূল উপলব্ধি: বার্তা ঠিক একবার পৌঁছানো নিশ্চিত করা যায় না, কিন্তু কাজের প্রভাব একবার রাখা যায় — অনন্য চাবি, টেকসই dedup আর idempotent নকশা দিয়ে। “Exactly-once” আসলে “at-least-once + duplicate ছাঁকা”-র ডাকনাম।

প্র ০৩Exponential backoff-এর ধারণা এসেছে Ethernet আর TCP-র ইতিহাস থেকে, আর jitter-এর প্রয়োজন প্রমাণ হয়েছে routing protocol-এর অদ্ভুত আচরণে। এই ইতিহাস UDP অ্যাপের retry নকশায় কী শেখায়?

Backoff-এর শিকড় ভাগাভাগি করা মাধ্যমে। ১৯৭০-এর দশকে ALOHAnet আর তারপর Xerox PARC-এর Ethernet-এ একটি তারে অনেক যন্ত্র; দুজন একসাথে পাঠালে collision, দুজনেই আবার পাঠায় — একই মুহূর্তে আবার পাঠালে আবার collision. ১৯৭৬ সালের Metcalfe আর Boggs-এর Ethernet গবেষণাপত্রে বর্ণিত সমাধান: প্রতিটি collision-এর পর একটি এলোমেলো অপেক্ষা, আর পরপর collision হলে সেই অপেক্ষার পরিসর দ্বিগুণ — binary exponential backoff (পর্ব ১১)। এখানে দুটি উপাদান একসাথে: দ্বিগুণ (চাপ বুঝে পিছিয়ে যাওয়া) আর এলোমেলো (একে অপরের সাথে সমলয় ভাঙা)।

১৯৮৬ সালে ইন্টারনেটের কিছু link-এ throughput হঠাৎ কয়েকশো গুণ পড়ে যায় — congestion collapse. Van Jacobson-এর ১৯৮৮ সালের কাজ TCP-তে retransmission timer-এর exponential backoff আর congestion control যোগ করে (পর্ব ৩০, ৩২): ক্ষতি হলে পিছিয়ে যাওয়া। এরপর ১৯৯০-এর দশকের শুরুতে Sally Floyd আর Van Jacobson একটি অপ্রত্যাশিত ঘটনা বিশ্লেষণ করেন — নির্দিষ্ট বিরতিতে বার্তা পাঠানো router-গুলো (routing update) আলাদা আলাদা শুরু করলেও সময়ের সাথে নিজে থেকেই সমলয়ে চলে আসে, আর তখন সবার বার্তা একসাথে এসে নেটওয়ার্কে ঢেউ তোলে। তাঁদের পরামর্শ ছিল বিরতিতে যথেষ্ট এলোমেলোতা যোগ করা। আজ cloud-এর অনেক client লাইব্রেরি retry-তে “exponential backoff with jitter” ডিফল্ট রাখে; একটি বড় cloud প্রতিষ্ঠানের প্রকৌশল-লেখায় simulation দিয়ে দেখানো হয়েছিল যে jitter ছাড়া backoff সার্ভারের কাজ কমায় সামান্যই, কারণ সব client একই মুহূর্তে আবার আঘাত করে।

UDP অ্যাপের জন্য শিক্ষা তিনটি। এক, retry হলো নেটওয়ার্কের ওপর একটি বাড়তি চাপ — কারণ ক্ষতি প্রায়ই ভিড় থেকেই আসে; তাই পিছিয়ে যাওয়া বাধ্যতামূলক, ঐচ্ছিক নয়। দুই, একই কোড চালানো হাজারো client একটি সমলয়িত ব্যবস্থা — নির্ধারিত (deterministic) backoff তাদের একসাথে রাখে, jitter আলাদা করে। তিন, সীমা লাগে: সর্বোচ্চ অপেক্ষা (cap), সর্বোচ্চ চেষ্টা, আর অনেক ব্যবস্থায় একটি “retry budget” — মোট অনুরোধের নির্দিষ্ট শতাংশের বেশি retry নয়, যাতে বিপদের সময় retry-ই বিপদ না বাড়ায়।

মূল উপলব্ধি: backoff শেখায় চাপ দেখলে সরে দাঁড়ানো, jitter শেখায় সবাই মিলে একই মুহূর্তে আবার ফিরে না আসা। দুটোই পঞ্চাশ বছরের ভুল থেকে শেখা, আর দুটোই আপনার পাঁচ লাইনের retry লুপে জায়গা পাওয়ার যোগ্য।

প্র ০৪IPv6 broadcast পুরোপুরি বাদ দিল, আর LAN-এ যন্ত্র খোঁজার কাজ এখন mDNS আর SSDP-র মতো multicast protocol করে। এই পরিবর্তনের কারণ কী, আর multicast কি সব সমস্যা মিটিয়েছে?

IPv4-এ broadcast অনেক কাজের ভরসা — ARP, DHCP, পুরোনো Windows-এর নাম খোঁজা (NetBIOS), নানা LAN গেম। কিন্তু প্রতিটি broadcast LAN-এর প্রত্যেক যন্ত্রকে থামিয়ে packet দেখতে বাধ্য করে। বড় flat নেটওয়ার্কে এই বোঝা জমে বড় হয়, আর লুপ থাকলে broadcast storm পুরো নেটওয়ার্ক অচল করে দেয় (পর্ব ১২)। নিরাপত্তায়ও এর দাম ছিল: ১৯৯০-এর দশকের শেষে Smurf আক্রমণ directed broadcast দিয়ে একটি ping-কে পুরো subnet-এর উত্তরে বহুগুণ করত, যার পর RFC 2644 router-এর ডিফল্ট বদলায়।

IPv6-এর নকশাকারীরা তাই broadcast রাখেননি। ARP-এর জায়গায় Neighbor Discovery যায় solicited-node multicast ঠিকানায় — প্রায় শুধু লক্ষ্য-যন্ত্রটির কাছে (পর্ব ২১)। “সবাই”-কে পাঠানোর দরকার হলে আছে ff02::1, কিন্তু সেটিও একটি multicast group. ফলে আগ্রহহীন যন্ত্রের network card hardware ফিল্টারেই packet বাদ দিতে পারে।

যন্ত্র খোঁজার জগতেও একই পথ। Apple-এর Stuart Cheshire-এর নেতৃত্বে “Zero Configuration Networking”-এর কাজ থেকে আসে mDNS আর DNS-SD (২০১৩ সালে RFC 6762 আর 6763): পরিচিত DNS বার্তার কাঠামো, কিন্তু 224.0.0.251 আর ff02::fb group-এ, প্রতিটি যন্ত্র নিজের নামের উত্তর নিজে দেয়। Microsoft আর অন্যদের UPnP উদ্যোগ থেকে আসে SSDP. দুটিতেই ক্যাশিং, উত্তর দেওয়ার আগে এলোমেলো দেরি (যাতে সবাই একসাথে উত্তর না দেয়), আর অপ্রয়োজনীয় পুনরাবৃত্তি কমানোর নিয়ম আছে।

তবু সমস্যা পুরো মেটেনি। Wi-Fi-তে multicast প্রায়ই সবচেয়ে ধীর হারে পাঠানো হয় আর ACK পায় না, তাই বড় Wi-Fi নেটওয়ার্কে mDNS-এর ট্রাফিক বোঝা হয়ে দাঁড়ায়; অনেক প্রতিষ্ঠানের Wi-Fi এজন্য multicast সীমিত বা রূপান্তর করে। Link-local হওয়ায় mDNS অন্য subnet-এ যায় না — বড় ক্যাম্পাসে প্রিন্টার খুঁজতে “gateway” বা “proxy” লাগে। আর ভুল কনফিগারেশনে ইন্টারনেটে খোলা SSDP আর mDNS সেবা amplification-এর প্রতিফলক হয়েছে। Multicast বোঝা কমায়, কিন্তু “কে শুনবে, কতদূর যাবে, কে উত্তর দেবে” — এই প্রশ্নগুলোর দায় নকশাকারীরই থাকে।

মূল উপলব্ধি: broadcast থেকে multicast মানে “সবাইকে বিরক্ত করা” থেকে “শুধু আগ্রহীদের ডাকা”-য় যাওয়া। এতে দক্ষতা বাড়ে, কিন্তু scope, rate আর নিরাপত্তার সিদ্ধান্ত protocol-কেই নিতে হয়।

প্র ০৫একটি DNS সার্ভার বা গেম সার্ভার কি একটিই UDP socket আর একটি thread দিয়ে চলতে পারে? কখন আর কীভাবে একাধিক socket বা thread-এ ভাগ করা হয়, আর UDP-তে “সংযোগ” না থাকায় এটি TCP-র চেয়ে সহজ না কঠিন?

এক socket আর এক thread-এর UDP সার্ভার আশ্চর্য রকম শক্তিশালী। প্রতিটি অনুরোধ ছোট আর স্বাধীন হলে (যেমন cache থেকে DNS উত্তর), লুপটি শুধু “recvfrom → উত্তর বানানো → sendto” — কোনো accept নেই, প্রতি client-এর আলাদা অবস্থা নেই, প্রতি সংযোগের মেমরি নেই। TCP সার্ভারে এক লাখ client মানে এক লাখ সংযোগের অবস্থা; UDP-তে এক লাখ client মানে শুধু এক লাখ datagram. এজন্য DNS-এর মতো সেবা দীর্ঘদিন UDP-তে চলে আসছে।

সীমা আসে দুই জায়গা থেকে। প্রথমত CPU: একটি thread একটি core-এর বেশি ব্যবহার করতে পারে না, আর প্রতি datagram-এ system call-এর খরচ আছে। প্রথম ধাপ recvmmsg()/sendmmsg() দিয়ে একবারে অনেক datagram সামলানো। পরের ধাপ একাধিক thread — কিন্তু একই socket-এ অনেক thread recvfrom() করলে kernel-এর ভেতরে একটি তালায় সবাই লাইন দেয়। তাই Linux-এ প্রচলিত কৌশল SO_REUSEPORT: প্রতি thread বা core-এ আলাদা socket, একই port-এ; kernel উৎস-ঠিকানার hash দিয়ে datagram ভাগ করে, ফলে একই client-এর datagram সাধারণত একই socket-এ যায়। দ্বিতীয় সীমা ধীর কাজ: একটি অনুরোধে database বা অন্য সার্ভারের উত্তরের অপেক্ষা করলে পুরো লুপ আটকে যায় — তখন event loop আর non-blocking নকশা (পর্ব ৪৪) বা কাজের জন্য thread pool (পর্ব ৪৩)।

“সংযোগ” না থাকা এখানে দুই দিকেই কাটে। সহজ দিক: কোনো সংযোগ কোনো thread-এর সাথে বাঁধা নয়, যেকোনো worker যেকোনো datagram-এর উত্তর দিতে পারে, আর সার্ভার রিস্টার্টে client-রা শুধু retry করে। কঠিন দিক: যে অবস্থা রাখতেই হয় — reply cache, গেমের খেলোয়াড়ের অবস্থান, rate limit-এর হিসাব — তা thread-গুলোর মধ্যে ভাগ করতে হয়, আর চাবি ঠিকানা হলে NAT rebinding বা ঠিকানা বদলে ভুল হয়। এজন্য বড় গেম সার্ভার আর QUIC সার্ভার প্যাকেটে নিজস্ব session বা connection ID রাখে, আর load balancer সেই ID দেখে ঠিক worker-এ পাঠায়।

মূল উপলব্ধি: UDP সার্ভারের সরলতা আসে অবস্থা না রাখা থেকে। যতক্ষণ প্রতিটি অনুরোধ স্বাধীন, ততক্ষণ এক socket অনেক দূর যায়; অবস্থা রাখতে হলে সেই অবস্থার চাবি আর তার ভাগাভাগিই হয়ে ওঠে আসল নকশার প্রশ্ন।

অনুশীলন

খাতা-কলম নিন — সময়, সম্ভাবনা আর টেবিলের সারি ধরে ধরে হিসাব করুন।

  1. হিসাব (backoff আর সম্ভাবনা): একটি client-এর প্রথম timeout ২৫০ মিলিসেকেন্ড, প্রতিবার দ্বিগুণ, কিন্তু কোনো অপেক্ষা ৪ সেকেন্ডের বেশি নয় (cap), সর্বোচ্চ ৬ বার চেষ্টা। (ক) সব চেষ্টা ব্যর্থ হলে client কত সময় পরে হাল ছাড়বে? (খ) Full jitter-এ প্রতিটি অপেক্ষা ০ থেকে cap-এর মধ্যে সমভাবে এলোমেলো হলে গড়ে কত সময়? (গ) প্রতিমুখী loss ১০% হলে একটি চেষ্টা সফল হওয়ার সম্ভাবনা কত, আর ৬ বারেই ব্যর্থ হওয়ার সম্ভাবনা কত?

    (ক) অপেক্ষাগুলো: ২৫০, ৫০০, ১,০০০, ২,০০০, ৪,০০০, তারপর ৮,০০০ হওয়ার কথা কিন্তু cap ৪,০০০. যোগফল = ২৫০ + ৫০০ + ১,০০০ + ২,০০০ + ৪,০০০ + ৪,০০০ = ১১,৭৫০ মিলিসেকেন্ড (প্রায় ১১.৮ সেকেন্ড)। Cap না থাকলে হতো ২৫০ × (২৬ − ১) = ১৫,৭৫০.

    (খ) ০ থেকে c-এর মধ্যে সমভাবে এলোমেলো মানের গড় c/২, তাই গড় মোট = ১১,৭৫০ ÷ ২ = ৫,৮৭৫ মিলিসেকেন্ড. Jitter গড় অপেক্ষা কমায়, আর হাজার client-কে সময়ের ওপর ছড়িয়ে দেয়।

    (গ) অনুরোধ আর উত্তর দুটিকেই পৌঁছাতে হবে: ০.৯ × ০.৯ = ০.৮১. একটি চেষ্টা ব্যর্থ হওয়ার সম্ভাবনা ০.১৯, ৬ বারেই ব্যর্থ = ০.১৯৬ ≈ ০.০০০০৪৭, অর্থাৎ প্রায় ২১,০০০-এ একবার। কিন্তু লক্ষ করুন — এই ব্যর্থতার একটি অংশে উত্তরই শুধু হারিয়েছে, সার্ভারে কাজ হয়েছে।

  2. আউটপুট অনুমান করুন: অংশ ৫-এর simulator-এ pattern-এর তালিকায় শুধু "dqo" রাখা হলো (প্রথম চেষ্টার উত্তর ২৫০ মিলিসেকেন্ড দেরিতে আসে, দ্বিতীয় চেষ্টার অনুরোধ হারায়)। টেবিলের লাইনটি কী হবে? তৃতীয় চেষ্টা কি পাঠানো হবে?
    dqo          2   done at 270 ms          1 / 1 

    চেষ্টা ১ যায় t = ০-তে; সার্ভার কাজ করে (১ বার), উত্তর আসবে ০ + ২০ + ২৫০ = ২৭০-এ। t = ২০০-তে timeout — উত্তর তখনো আসেনি, তাই চেষ্টা ২ যায়, কিন্তু অনুরোধ হারায় (সার্ভার কিছু দেখে না)। পরের চেষ্টার সময় ২০০ + ৪০০ = ৬০০, কিন্তু তার আগেই ২৭০-এ প্রথম চেষ্টার দেরিতে আসা উত্তর পৌঁছায়, ID মেলে — client থামে। তাই তৃতীয় চেষ্টা পাঠানো হয় না, মোট ২ চেষ্টা, কাজ একবারই (cache থাকুক বা না থাকুক)। শিক্ষা: client আগের যেকোনো কপির উত্তর গ্রহণ করে, যদি ID মেলে — সব কপির ID একই হওয়ার এটিই সুবিধা।

  3. হাতে trace (reply cache): একটি “জমা” সার্ভার (শুরুতে balance ০) ক্রমে এই ছয়টি datagram পায় — A = 10.0.0.5:40001, B = 10.0.0.9:51000: (১) A id 1 “+200”, (২) B id 1 “+300”, (৩) A id 1 আবার, (৪) A id 2 “+100”, (৫) B id 1 আবার, (৬) A id 1-এর একটি অনেক দেরিতে আসা কপি। শেষ balance কত হবে — (ক) cache ছাড়া, (খ) প্রতি client-এর শুধু শেষ ID মেলানো cache-এ (“id == শেষ id হলে duplicate”), (গ) “id ≤ শেষ id হলে duplicate বা পুরোনো” নিয়মে? সঠিক উত্তর কোনটি?

    (ক) সব ছয়টি চলে: ২০০ + ৩০০ + ২০০ + ১০০ + ৩০০ + ২০০ = ১,৩০০.

    (খ) (১) miss → ২০০, A-র শেষ id 1; (২) miss → ৫০০, B-র শেষ id 1; (৩) A id 1 = শেষ id → hit, কাজ নয়; (৪) A id 2 নতুন → ৬০০, A-র শেষ id এখন 2; (৫) B id 1 → hit; (৬) A id 1 ≠ 2 → “নতুন” ভেবে চালানো → ৮০০. অংশ ৪-এর C কোডের cache আরও সরল (সব client মিলিয়ে শুধু একটি সারি), তাই এই ভুল তারও আছে — সেখানে (৫)-ও আবার চলত।

    (গ) (৬)-এ id 1 ≤ A-র শেষ id 2 → পুরোনো, বাদ (stop-and-wait client id 2 পাঠিয়েছে মানে id 1-এর উত্তর সে পেয়ে গেছে, তাই উত্তরও লাগে না)। Balance ৬০০ = ২০০ + ৩০০ + ১০০ — সঠিক উত্তর এটিই, কারণ আসল জমা ছিল তিনটি। এই নিয়মের শর্ত: প্রতি client-এর ID শুধু বাড়ে, আর client একটির উত্তর না পেয়ে পরেরটি পাঠায় না।

  4. কোড লিখুন: অংশ ৪-এর client-এ SO_RCVTIMEO-এর বদলে poll() ব্যবহার করে একটি চেষ্টার “অপেক্ষা” অংশটি লিখুন, যাতে ভুল ID-র উত্তর এলে বাদ দিয়ে বাকি সময় অপেক্ষা চলতে থাকে। ধরে নিন connected socket c, অপেক্ষমাণ id, আর এই চেষ্টার সীমা wait_ms.
    #include <poll.h>
    #include <time.h>
    static long now_ms(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t);
        return t.tv_sec * 1000L + t.tv_nsec / 1000000; }
    
    /* returns 1 if the matching reply arrived, 0 on timeout */
    static int wait_reply(int c, unsigned id, int wait_ms, char *r, size_t rlen) {
        long deadline = now_ms() + wait_ms;
        for (;;) {
            long left = deadline - now_ms();
            if (left <= 0) return 0;                       /* this try timed out */
            struct pollfd p = { .fd = c, .events = POLLIN };
            int k = poll(&p, 1, (int)left);
            if (k == 0) return 0;                           /* timeout */
            if (k < 0) continue;                            /* EINTR: try again */
            ssize_t n = recv(c, r, rlen - 1, 0);
            if (n < 0) continue;                            /* e.g. ECONNREFUSED: keep waiting */
            r[n] = '\0';
            unsigned rid;
            if (sscanf(r, "id=%u", &rid) == 1 && rid == id) return 1;
            /* stale or foreign reply: ignore it and wait for the time that is LEFT */
        }
    }

    মূল কৌশল একটি deadline: চেষ্টার শুরুতে শেষ মুহূর্ত ঠিক করে প্রতিবার poll()-কে শুধু বাকি সময় দেওয়া। ভুল ID-র উত্তর এলে লুপ ঘুরে বাকি সময় অপেক্ষা করে — টাইমার শুরু থেকে গোনে না, আবার চেষ্টাও আগেভাগে ফুরোয় না। CLOCK_MONOTONIC ব্যবহার করা হয়েছে, কারণ দেয়ালঘড়ি (NTP বা ব্যবহারকারী) পেছনে-সামনে সরতে পারে। একই কাঠামোয় অনেক socket-এর pollfd একসাথে দিলে একটি লুপে অনেক অনুরোধ সামলানো যায় — সেটিই পর্ব ৪৪-এর event loop-এর বীজ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমLinux-এ একটি multicast শ্রোতা প্রোগ্রামের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. socket(AF_INET, SOCK_DGRAM, 0) দিয়ে UDP socket খোলা
  2. SO_REUSEADDR চালু করা, যাতে একই port-এ অন্য শ্রোতাও থাকতে পারে
  3. Group-এর port-এ (যেমন 0.0.0.0:47000) bind() করা
  4. IP_ADD_MEMBERSHIP দিয়ে group আর interface জানিয়ে যোগ দেওয়া
  5. লুপে recvfrom() দিয়ে datagram আর প্রেরকের ঠিকানা পড়া

একটি UDP সার্ভার লিখতে নিচের কোন system call দুটি লাগে না?

  • socket() আর bind()
  • recvfrom() আর sendto()
  • listen() আর accept()
  • close() আর setsockopt()

একটি UDP “জমা” সার্ভার retry-র কারণে আসা duplicate অনুরোধ চিনতে reply cache রাখে। Cache-এর চাবি কী হওয়া উচিত?

  • শুধু request ID
  • শুধু client-এর IP ঠিকানা
  • অনুরোধের বিষয়বস্তু (যেমন “deposit 100”)
  • client-এর IP, client-এর port আর request ID — তিনটি একসাথে

একটি UDP client-এর প্রথম timeout ১০০ মিলিসেকেন্ড, প্রতিবার দ্বিগুণ, সর্বোচ্চ ৫ বার চেষ্টা (কোনো cap বা jitter নেই)। সব চেষ্টা ব্যর্থ হলে হাল ছাড়ার আগে মোট কত সময় অপেক্ষা করবে?

  • ৫০০ মিলিসেকেন্ড
  • ৩,১০০ মিলিসেকেন্ড
  • ১,৬০০ মিলিসেকেন্ড
  • ৩,২০০ মিলিসেকেন্ড

Linux-এ একটি সাধারণ UDP socket থেকে 255.255.255.255-এ sendto() করলে ত্রুটি আসে EACCES (Permission denied). সবচেয়ে সম্ভাব্য কারণ কী?

  • Socket-এ SO_BROADCAST চালু করা হয়নি
  • শুধু root broadcast পাঠাতে পারেন
  • আগে IP_ADD_MEMBERSHIP দিয়ে যোগ দেওয়া হয়নি
  • Router broadcast পার করে না বলে

একই যন্ত্রে দুটি প্রোগ্রাম SO_REUSEADDR দিয়ে 0.0.0.0:47000-এ bind করেছে, আর দুজনেই 239.1.2.3 group-এ যোগ দিয়েছে। একজন প্রেরক 239.1.2.3:47000-এ একটি datagram পাঠাল। Linux-এ কী ঘটে?

  • শুধু যে প্রোগ্রাম শেষে bind করেছে, সে পায়
  • কেউ পায় না, কারণ port-এ দুটি socket থাকায় দ্বন্দ্ব
  • দুজনেই একটি করে কপি পায়
  • Datagram-টি দুই ভাগ হয়ে দুজনের কাছে যায়

একটি client “১০০ টাকা জমা” অনুরোধ পাঠাল, উত্তর পেল না, timeout-এর পর একই অনুরোধ আবার পাঠাল। সার্ভারে কোনো duplicate ধরার ব্যবস্থা নেই। আসলে প্রথম অনুরোধ পৌঁছেছিল, শুধু উত্তর হারিয়েছিল। ফল কী, আর এই ব্যবস্থার প্রতিশ্রুতির নাম কী?

  • ১০০ টাকা জমা; at-most-once
  • ২০০ টাকা জমা; at-least-once
  • ০ টাকা জমা; exactly-once
  • ১০০ টাকা জমা; exactly-once

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. UDP-র ক্রম: socket(SOCK_DGRAM) → bind → recvfrom/sendto; listen, accept, connect লাগে না। একটি সার্ভার socket সব client-এর — recvfrom-এর ঠিকানাই “সংযোগ”।
  2. আকার আর buffer: অ্যাপের buffer সবচেয়ে বড় datagram-এর চেয়ে বড়; kernel-এর buffer (SO_RCVBUF) হঠাৎ ভিড়ের জন্য; payload ১,৪৭২ (IPv4 Ethernet) বা ১,২৩২ (যেকোনো IPv6 পথ) বাইটের মধ্যে।
  3. নিজের নির্ভরযোগ্যতা: request ID, timeout (SO_RCVTIMEO বা poll), একই ID-তে retry, exponential backoff (মোট T0(২n − ১)), cap আর jitter.
  4. Duplicate: উত্তর হারালে retry-তে কাজ দুবার — idempotent নয় এমন অনুরোধে (IP, port, ID) চাবির reply cache, পুরোনো ID বাদ। শুধু retry = at-least-once, retry + cache = at-most-once.
  5. Broadcast, multicast আর নিরাপত্তা: SO_BROADCAST ছাড়া EACCES, router broadcast পার করে না; multicast-এ IP_ADD_MEMBERSHIP, TTL ডিফল্ট ১, loop ডিফল্ট চালু, একাধিক শ্রোতায় SO_REUSEADDR; উৎস যাচাই, যাচাইয়ের আগে বড় উত্তর নয়, প্রেরকের গতি সীমিত।

পরের পর্বে: একটি সার্ভার একসাথে অনেক client-কে কীভাবে সামলায় — প্রতি client-এ fork, প্রতি client-এ thread, আর thread pool; প্রতিটির খরচ, সীমা আর কখন কোনটি বেছে নেবেন।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান