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

TCP flow control — sliding window আর receive window

TCP Flow Control — Receive Window, Sliding Window, Zero-Window Probes, Silly Window Syndrome and Window Scaling

Taught in Bangla, this lesson explains TCP flow control, which protects the receiver: the receive window formula, the sender's sliding window bounded by min(rwnd, cwnd), zero windows and persist-timer probes, silly window syndrome with Clark's rule and Nagle's algorithm, and window scaling with the throughput limit of window divided by RTT, using simulators and a live bash terminal.

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

  • Flow control আর congestion control-এর পার্থক্য — কে কাকে রক্ষা করে
  • Receive buffer, rwnd আর প্রেরকের byte-স্রোতের চার ভাগ — সংখ্যা দিয়ে হাতে হিসাব
  • Zero window, persist timer আর window probe — কেন না থাকলে deadlock হয়
  • Silly window syndrome আর তার দুই দিকের প্রতিকার
  • Window scaling, window/RTT সীমা, BDP আর Linux-এ buffer-এর আকার দেখা ও বদলানো

১ · সমস্যা — দ্রুত প্রেরক, ধীর প্রাপক

পর্ব ৩০-এ (TCP-র নির্ভরযোগ্যতা) দেখেছেন, TCP প্রতিটি byte-এ নম্বর দেয়, ACK নেয় আর হারানো byte আবার পাঠায়। কিন্তু সব byte ঠিকঠাক পৌঁছালেও একটি সমস্যা থেকে যায়। ধরুন আপনার ফোনে একটি ভিডিও অ্যাপ সার্ভার থেকে ডেটা নামাচ্ছে। সার্ভার শক্তিশালী মেশিন, সেকেন্ডে কয়েকশো মেগাবিট ঠেলতে পারে। অথচ ফোনের অ্যাপটি তখন ব্যস্ত — ভিডিও decode করছে, বা ব্যবহারকারী অন্য অ্যাপে চলে গেছেন। অ্যাপ socket থেকে পড়ছে না।

তাহলে আসা byte-গুলো যায় কোথায়? Kernel প্রতিটি TCP সংযোগের জন্য একটি receive buffer রাখে — অ্যাপ recv() ডেকে না পড়া পর্যন্ত byte সেখানে জমে থাকে। Buffer-এর আকার সীমিত। প্রেরক থামার কোনো সংকেত না পেলে buffer ভরে যাবে, তারপর নতুন byte ফেলে দিতে হবে — আর ফেলে দেওয়া byte আবার পাঠাতে হবে, যা নিছক অপচয়। এই সমস্যার সমাধানকেই বলে flow control: প্রেরকের গতিকে প্রাপকের পড়ার গতির সাথে মেলানো।

পুরান ঢাকার একটি পাইকারি আড়ত থেকে মহল্লার মুদি দোকানে চালের বস্তা যাচ্ছে ভ্যানে। দোকানের গুদামে জায়গা মাত্র ৪০ বস্তার, আর দোকানদার দিনে যতটা বিক্রি করেন ততটাই গুদাম খালি হয়। আড়তদার যদি গুদামের খবর না রেখে ভ্যানের পর ভ্যান পাঠান, বাড়তি বস্তা রাস্তায় পড়ে থাকবে। তাই প্রতিটি রসিদের সাথে দোকানদার লিখে দেন: “আর ১২ বস্তার জায়গা আছে।” আড়তদার এর বেশি পাঠান না। রসিদে লেখা এই “জায়গা আছে” সংখ্যাটিই TCP-র receive window.

Flow control বনাম congestion control — গুলিয়ে ফেলবেন না

দুটিই প্রেরককে ধীর করে, তাই নতুনরা প্রায়ই এক ভাবেন। কিন্তু দুটি সম্পূর্ণ ভিন্ন সমস্যার সমাধান:

বিষয়Flow control (এই পর্ব)Congestion control (পর্ব ৩২)
কাকে রক্ষা করেপ্রাপকের buffer — একটি নির্দিষ্ট hostপথের router আর link — পুরো নেটওয়ার্ক
সীমা কে জানায়প্রাপক নিজে, header-এর window ঘরে স্পষ্ট সংখ্যা দিয়েকেউ সরাসরি জানায় না; প্রেরক loss, delay বা ECN দেখে অনুমান করে
প্রেরকের ভেতরের চলকrwnd (প্রাপকের ঘোষণা)cwnd (প্রেরকের নিজের হিসাব)
উপমাদোকানের গুদামে জায়গাআড়ত থেকে দোকানের পথে ঢাকার রাস্তার জ্যাম
সমস্যা কোথায়প্রাপকের অ্যাপ ধীরে পড়ছেপথের কোনো link-এ ভিড়, router-এর queue ভরা
মূল সূত্র

যেকোনো মুহূর্তে প্রেরক ACK ছাড়া সর্বোচ্চ যত byte পথে রাখতে পারে, তা দুই সীমার ছোটটি: প্রেরকের window = min(rwnd, cwnd). গুদাম খালি থাকলেও রাস্তা জ্যাম হলে কম পাঠাতে হবে, আবার রাস্তা ফাঁকা থাকলেও গুদাম ভরা হলে থামতে হবে। এই পর্বে আমরা ধরে নেব রাস্তা ফাঁকা (cwnd অনেক বড়), যাতে শুধু rwnd-এর প্রভাব দেখা যায়; cwnd কীভাবে বাড়ে-কমে, তা পর্ব ৩২-এ (TCP congestion control).

পর্ব ১০-এ (Framing আর ARQ) link স্তরে flow control আর sliding window দেখেছেন — সেখানে window মাপা হতো frame-এর সংখ্যায়, প্রেরক আর প্রাপক একই link-এর দুই মাথা। TCP-র sliding window একই ধারণা, কিন্তু তিনটি বড় পার্থক্য: (১) window মাপা হয় byte-এ, segment-এ নয়; (২) window-এর আকার স্থির নয় — প্রাপক প্রতিটি segment-এ নতুন করে জানায়; (৩) দুই প্রান্তের মাঝে পুরো ইন্টারনেট, তাই ACK হারাতে পারে, দেরিতে আসতে পারে। Window-এর আকার বনাম efficiency-র হিসাব (W ≥ 1 + 2a) পর্ব ১০-এই আছে; এখানে আর পুনরাবৃত্তি করব না।

২ · প্রাপকের দিক — receive buffer আর rwnd

প্রাপকের buffer-কে একটি লম্বা তাক ভাবুন, যেখানে byte বাঁ থেকে ডানে সাজানো। দুটি নম্বর দিয়ে এর অবস্থা পুরোপুরি বোঝা যায় (Kurose-Ross-এর পাঠ্যবইয়ের নাম ব্যবহার করছি, বিশ্ববিদ্যালয়ের পরীক্ষায় এগুলোই বেশি আসে):

  • LastByteRead — অ্যাপ recv() দিয়ে সর্বশেষ যে byte পর্যন্ত পড়ে নিয়েছে। এর আগের byte buffer থেকে বিদায় নিয়েছে।
  • LastByteRcvd — নেটওয়ার্ক থেকে এসে buffer-এ বসা সর্বশেষ byte-এর নম্বর (ধরে নিচ্ছি সব ক্রম মেনে এসেছে)।

তাহলে buffer-এ জমে থাকা, অ্যাপের না-পড়া byte = LastByteRcvd − LastByteRead. Buffer উপচে না পড়ার শর্ত: LastByteRcvd − LastByteRead ≤ RcvBuffer. বাকি খালি জায়গাই প্রাপক ঘোষণা করে:

rwnd = RcvBuffer − (LastByteRcvd − LastByteRead)

প্রাপকের receive buffer

Receive buffer: data waiting for the application, and free space (rwnd)

      অ্যাপ পড়ে নিয়েছে        buffer-এ জমা (না-পড়া)      খালি জায়গা
  ... ──────────────────┬──────────────────────────┬───────────────────┐
                        │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│                   │
  ... ──────────────────┴──────────────────────────┴───────────────────┘
                  LastByteRead               LastByteRcvd
                        │◄──────────────── RcvBuffer ─────────────────►│
                                                   │◄────── rwnd ─────►│
ছবি ৩১.১ — অ্যাপ পড়লে বাঁ দিক খালি হয় (rwnd বাড়ে), নেটওয়ার্ক থেকে byte এলে ডান দিক ভরে (rwnd কমে)।

এই rwnd প্রাপক বসায় তার পাঠানো প্রতিটি segment-এর TCP header-এর ১৬-bit Window ঘরে — আলাদা কোনো বার্তা লাগে না। ACK-এর সাথে window-ও যায়, আর দুই দিকে ডেটা চললে ডেটার segment-এই দুটি ভেসে যায়। Header-এর পুরো নকশা পর্ব ২৯-এ (TCP সংযোগ)। TCP full-duplex, তাই দুই প্রান্তেরই নিজস্ব receive buffer আর নিজস্ব rwnd আছে — প্রতিটি দিকের flow control আলাদা।

প্রেরকের দিক — একটি মাত্র অসমতা

প্রেরক দুটি নম্বর রাখে: LastByteSent (সর্বশেষ পাঠানো byte) আর LastByteAcked (যে পর্যন্ত cumulative ACK এসেছে)। এদের ব্যবধানই পথে থাকা, অস্বীকৃত byte — “bytes in flight”. নিয়ম:

LastByteSent − LastByteAcked ≤ rwnd

কেন এটি buffer উপচানো ঠেকায় — ছোট প্রমাণ: ধরুন প্রাপক এক মুহূর্তে ACK পাঠাল, যখন তার অবস্থা LastByteRcvd = r, LastByteRead = p. ACK-এ থাকে “r পর্যন্ত পেয়েছি” আর rwnd = RcvBuffer − (r − p). প্রেরক এই ACK পেয়ে LastByteAcked = r ধরে, তাই সে সর্বোচ্চ LastByteSent = r + rwnd = RcvBuffer + p পর্যন্ত পাঠাতে পারে। এই সব byte পৌঁছালে buffer-এ জমা হবে LastByteSent − LastByteReadতখন ≤ RcvBuffer + p − LastByteReadতখন. অ্যাপ শুধু সামনে পড়ে, পেছনে যায় না, তাই LastByteReadতখন ≥ p — ফলে জমা ≤ RcvBuffer. অর্থাৎ প্রাপক যত দেরিতেই পড়ুক, প্রেরক নিয়ম মানলে buffer কখনো উপচায় না। (শর্ত একটাই: প্রাপক আগে দেওয়া প্রতিশ্রুতি ফিরিয়ে নেবে না — অংশ ৩-এর সতর্কবার্তা দেখুন।)

হাতে-কলমে উদাহরণ

প্রাপকের RcvBuffer = ১০,০০০ byte. এখন LastByteRead = ৩,০০০, LastByteRcvd = ৭,০০০.

  • Buffer-এ জমা = ৭,০০০ − ৩,০০০ = ৪,০০০ byte; rwnd = ১০,০০০ − ৪,০০০ = ৬,০০০ byte.
  • প্রাপক ACK পাঠায় “পরের byte চাই ৭,০০১” (ACK নম্বর = পরের প্রত্যাশিত byte), সাথে window = ৬,০০০.
  • প্রেরকের কাছে LastByteAcked = ৭,০০০, আর ইতিমধ্যে LastByteSent = ৯,০০০ — পথে আছে ২,০০০ byte.
  • আর পাঠানো যাবে ৬,০০০ − ২,০০০ = ৪,০০০ byte, অর্থাৎ LastByteSent সর্বোচ্চ ৭,০০০ + ৬,০০০ = ১৩,০০০.
  • যাচাই: অ্যাপ আর কিছু না পড়লে আর সব পৌঁছালে buffer-এ জমা হবে ১৩,০০০ − ৩,০০০ = ১০,০০০ = RcvBuffer — ঠিক কানায় কানায়, এক byte-ও উপচায় না।

৩ · প্রেরকের byte-স্রোতে sliding window

প্রেরকের দিক থেকে পাঠানোর অপেক্ষায় থাকা সব ডেটাকে একটি লম্বা byte-এর সারি ভাবুন। যেকোনো মুহূর্তে সারিটি চার ভাগে ভাগ হয়:

█ পাঠানো ও স্বীকৃত

LastByteAcked পর্যন্ত। এগুলোর কাজ শেষ — প্রেরক send buffer থেকে মুছে ফেলতে পারে।

window-এর বাঁয়ে

▒ পাঠানো, স্বীকৃত নয়

LastByteAcked + 1 থেকে LastByteSent — পথে আছে। হারালে আবার পাঠাতে হবে, তাই কপি রাখতে হয়।

window-এর ভেতরে

○ পাঠানো যায় (usable)

LastByteSent + 1 থেকে LastByteAcked + rwnd. প্রাপক জায়গা দিয়েছে, এখনই পাঠানো চলে।

window-এর ভেতরে

· এখনো নয়

LastByteAcked + rwnd-এর পরের byte. ACK এসে window সামনে না সরা পর্যন্ত অপেক্ষা।

window-এর ডানে

Window-এর বাঁ কিনারা = LastByteAcked + 1, ডান কিনারা = LastByteAcked + rwnd. নতুন ACK এলে বাঁ কিনারা ডানে সরে (window “বন্ধ হয়” বাঁ দিক থেকে)। প্রাপকের অ্যাপ পড়লে নতুন rwnd বড় হয়, তাই ডান কিনারা ডানে সরে (window “খোলে”)। নিচের stepper-এ ১,০০০ byte-এর একটি সারি, প্রতিটি ঘর ১০০ byte; প্রেরক ১০০ byte-এর segment পাঠায়।

Byte 1–1000, প্রতি ঘর 100 byte · █ স্বীকৃত ▒ পথে ○ পাঠানো যায় · এখনো নয়
সর্বশেষ ACK, rwnd
LastByteAcked
LastByteSent
Window (byte)
সারির ছবি
পথে (in flight)
এখনই পাঠানো যায়
  1. শুরু। সংযোগ স্থাপনের সময় প্রাপক জানিয়েছে rwnd = ৪০০. কিছুই পাঠানো হয়নি, তাই window 1–400 পুরোটাই পাঠানো যায় — চারটি ঘর ○.
  2. তিনটি segment গেল। প্রেরক 1–300 পাঠাল (তিনটি ১০০ byte-এর segment). এখন পথে ৩০০ byte, আর পাঠানো যায় মাত্র ৪০০ − ৩০০ = ১০০ byte (301–400).
  3. ACK 201 এলো, rwnd 400. 1–200 স্বীকৃত (█), বাঁ কিনারা সরে 201-এ, ডান কিনারা 200 + 400 = 600. পথে শুধু 201–300 (১০০ byte), পাঠানো যায় 301–600 — ৩০০ byte. ACK জানালাটিকে সামনে ঠেলে দিল।
  4. Window ভর্তি। প্রেরক 301–600 পাঠাল। পথে ৪০০ = rwnd, তাই LastByteSent − LastByteAcked = ৪০০ ≤ ৪০০ — সমতা। আর এক byte-ও পাঠানো যাবে না, ACK-এর অপেক্ষা।
  5. ACK 501, কিন্তু rwnd 300. প্রাপকের অ্যাপ ধীরে পড়ছে, তাই rwnd কমে ৩০০. বাঁ কিনারা 501, ডান কিনারা 500 + 300 = 800. পথে 501–600 (১০০), পাঠানো যায় 601–800 (২০০)। খেয়াল করুন, জানালা সামনে সরেছে কিন্তু ছোট হয়েছে।
  6. ACK 601, rwnd 200. বাঁ কিনারা 601, ডান কিনারা 600 + 200 = 800 — আগের জায়গাতেই। Window বাঁ দিক থেকে বন্ধ হলো, ডান দিক নড়ল না: প্রাপক নতুন byte পেয়েছে কিন্তু অ্যাপ সেই পরিমাণ পড়েনি। পথে ০, পাঠানো যায় 601–800.

Window “shrink” করা নিষেধের কাছাকাছি। ধাপ ৬-এ ডান কিনারা স্থির ছিল — সেটি স্বাভাবিক। কিন্তু প্রাপক যদি এমন rwnd ঘোষণা করে যাতে ডান কিনারা বাঁয়ে সরে যায় (ধরুন ACK 601-এর সাথে rwnd 100, ফলে ডান কিনারা 700 < 800), তাহলে প্রেরক আগের প্রতিশ্রুতি মেনে যা পাঠিয়ে ফেলেছে তা হঠাৎ window-এর বাইরে পড়ে যাবে। এজন্য TCP-র মানদণ্ড (RFC 1122, বর্তমানে RFC 9293) বলে প্রাপকের window shrink করা উচিত নয়, আর প্রেরককে তবু এমন পরিস্থিতি সামলানোর মতো শক্ত হতে হবে। Linux-এর প্রাপক সাধারণত ডান কিনারা পিছিয়ে না নেওয়ার চেষ্টা করে।

৪ · Simulator — দ্রুত প্রেরক, ধীর অ্যাপ

এবার সূত্রগুলো একসাথে চালিয়ে দেখি। নিচের simulator-এ প্রাপকের buffer ৪,০০০ byte, প্রেরকের হাতে ৯,০০০ byte-এর একটি ফাইল। প্রতিটি ধাপ = একটি RTT: প্রেরক যতটা পারে পাঠায়, প্রাপক পায়, অ্যাপ appReads তালিকা অনুযায়ী কিছু পড়ে, তারপর প্রাপক ACK আর নতুন rwnd পাঠায়। ধাপ ৩–৪-এ অ্যাপ কিছুই পড়ে না (ব্যবহারকারী অন্য অ্যাপে), আর ধাপ ৫-এ অ্যাপ পড়লে প্রাপক যে window update পাঠায় সেটি পথে হারিয়ে যায় — ইচ্ছে করে বানানো ঘটনা, যাতে persist timer-এর কাজ দেখা যায়। সব কিছু নির্ধারিত, তাই প্রতিবার একই ফল।

// TCP flow control: fast sender, 4000-byte receive buffer, slow app. 1 step = 1 RTT.
// LB = LastByte (Sent / Acked by the sender; Read / Rcvd at the receiver)
const RcvBuffer = 4000, FILE = 9000;
const appReads = [1000, 1000, 0, 0, 2000, 0, 2000, 2000, 2000];  // bytes the app reads per step
let lastSent = 0, lastAcked = 0, rwnd = RcvBuffer;   // sender's view (rwnd = last advertised)
let lastRcvd = 0, lastRead = 0, probeAt = 0, backoff = 1;
const pad = (x, n) => String(x).padStart(n);
console.log("step sent LBSent inFlight LBAcked | read LBRead LBRcvd rwnd | note");
for (let t = 1; lastRead < FILE; t++) {
  let sent = 0, note = "", ackArrives = true;
  if (rwnd > 0) {                                     // rule: LastByteSent - LastByteAcked <= rwnd
    sent = Math.min(rwnd - (lastSent - lastAcked), FILE - lastSent);
    lastSent += sent; lastRcvd += sent;               // fits: the receiver promised this space
  } else if (t === probeAt) note = "persist timer fired: window probe ->";
  else { note = "zero window, sender waits"; ackArrives = false; }
  const inFlight = lastSent - lastAcked;
  const wasZero = lastRcvd - lastRead === RcvBuffer;
  const read = Math.min(appReads[t - 1], lastRcvd - lastRead);
  lastRead += read;
  const adv = RcvBuffer - (lastRcvd - lastRead);      // rwnd = RcvBuffer - (LastByteRcvd - LastByteRead)
  if (wasZero && adv > 0 && rwnd === 0 && t !== probeAt) {
    note += "; window update rwnd=" + adv + " LOST"; ackArrives = false;
  }
  if (ackArrives) {
    lastAcked = lastRcvd; rwnd = adv;
    if (rwnd === 0) { probeAt = t + backoff; note += (note ? " " : "") + "rwnd=0, next probe at step " + probeAt; backoff *= 2; }
    else if (note) note += " reply rwnd=" + rwnd + ", resume";
  }
  console.log(`${pad(t, 3)} ${pad(sent, 5)} ${pad(lastSent, 6)} ${pad(inFlight, 8)} ${pad(lastAcked, 7)} | ${pad(read, 4)} ${pad(lastRead, 6)} ${pad(lastRcvd, 6)} ${pad(adv, 4)} | ${note}`);
}
console.log("Without the persist timer, a lost window update means deadlock:");
console.log("sender waits for rwnd > 0, receiver waits for data. Neither ever sends again.");

আউটপুট:

step sent LBSent inFlight LBAcked | read LBRead LBRcvd rwnd | note
  1  4000   4000     4000    4000 | 1000   1000   4000 1000 |
  2  1000   5000     1000    5000 | 1000   2000   5000 1000 |
  3  1000   6000     1000    6000 |    0   2000   6000    0 | rwnd=0, next probe at step 4
  4     0   6000        0    6000 |    0   2000   6000    0 | persist timer fired: window probe -> rwnd=0, next probe at step 6
  5     0   6000        0    6000 | 2000   4000   6000 2000 | zero window, sender waits; window update rwnd=2000 LOST
  6     0   6000        0    6000 |    0   4000   6000 2000 | persist timer fired: window probe -> reply rwnd=2000, resume
  7  2000   8000     2000    8000 | 2000   6000   8000 2000 |
  8  1000   9000     1000    9000 | 2000   8000   9000 3000 |
  9     0   9000        0    9000 | 1000   9000   9000 4000 |
Without the persist timer, a lost window update means deadlock:
sender waits for rwnd > 0, receiver waits for data. Neither ever sends again.

ধাপ ১: rwnd = ৪,০০০, তাই প্রেরক একবারে ৪,০০০ byte পাঠাল — inFlight ঠিক rwnd-এর সমান। অ্যাপ পড়ল ১,০০০, তাই rwnd = ৪,০০০ − (৪,০০০ − ১,০০০) = ১,০০০. ধাপ ২–৩: এখন প্রেরক প্রতি RTT-তে মাত্র ১,০০০ byte পাঠাতে পারছে — অ্যাপের পড়ার গতিতে বাঁধা পড়েছে। এটাই flow control-এর আসল ফল: প্রেরকের গতি নেমে আসে প্রাপকের অ্যাপের গতিতে, link যত দ্রুতই হোক। ধাপ ৩-এ অ্যাপ কিছু পড়েনি, buffer-এ ৬,০০০ − ২,০০০ = ৪,০০০ = পূর্ণ, তাই rwnd = ০. ধাপ ৪–৬ অংশ ৫-এর stepper-এ বিস্তারিত: probe, হারানো update, আবার probe. ধাপ ৭–৯: পাঠানো আবার চালু, শেষ ১,০০০ byte ধাপ ৮-এ যায়, ধাপ ৯-এ অ্যাপ শেষ byte-টি (৯,০০০) পড়ে আর buffer আবার পুরো খালি (rwnd ৪,০০০)। প্রতিটি সারিতে নিজে মিলিয়ে দেখুন: rwnd = ৪০০০ − (LBRcvd − LBRead).

অ্যাপ ধীরে পড়লে window কেন ছোট হয়

উপরের ধাপ ২-এ একটি গুরুত্বপূর্ণ বাস্তব শিক্ষা লুকিয়ে আছে। Link ১ Gbps হোক বা ১০ Mbps, প্রতি RTT-তে প্রেরক পাঠাতে পারছে ঠিক ততটাই, যতটা অ্যাপ আগের RTT-তে পড়েছে। সার্ভার থেকে ফাইল নামানো ধীর লাগলে সমস্যা সবসময় নেটওয়ার্কে নয় — হতে পারে আপনার নিজের প্রোগ্রাম socket থেকে ছোট ছোট টুকরোয়, ধীরে পড়ছে (যেমন প্রতিটি টুকরো পড়ে ডিস্কে লিখে তারপর আবার পড়া, আর ডিস্ক ধীর)। এমন হলে প্যাকেট capture-এ দেখা যায় প্রাপকের ঘোষিত window ক্রমশ ছোট হচ্ছে, কখনো ০ — Wireshark এগুলোকে “TCP ZeroWindow” আর “TCP Window Full” নামে চিহ্নিত করে। একে বলে backpressure: ধীর ভোক্তার চাপ উল্টো দিকে প্রেরক পর্যন্ত পৌঁছায়। ভালো খবর হলো এটাই কাঙ্ক্ষিত আচরণ — নইলে প্রেরক প্রাপকের মেমরি ভরিয়ে ফেলত।

৫ · Zero window, persist timer আর window probe

rwnd = ০ হলে প্রেরক থামে — একে বলে zero window. এবার কল্পনা করুন অ্যাপ পড়ল, buffer-এ জায়গা হলো। প্রাপক একটি window update পাঠায় — ডেটাহীন একটি ACK, যার window ঘরে নতুন সংখ্যা। কিন্তু TCP ডেটাহীন ACK-এর কোনো ACK নেয় না, আর হারালে আবার পাঠায়ও না (পর্ব ৩০)। তাহলে এই update হারালে কী হবে?

  • প্রেরক ভাবছে rwnd = ০ — সে অপেক্ষা করছে প্রাপকের খবরের জন্য।
  • প্রাপক ভাবছে সে জায়গা ঘোষণা করে দিয়েছে — সে অপেক্ষা করছে ডেটার জন্য।

দুই পক্ষই অন্যের অপেক্ষায়, কেউ আর কিছু পাঠাবে না — এক চিরস্থায়ী deadlock. অপারেটিং সিস্টেমের deadlock-এর মতোই, চক্রাকার অপেক্ষা। সমাধান প্রেরকের দায়িত্বে: zero window দেখলেই প্রেরক একটি persist timer চালু করে। Timer ফুরালে সে একটি window probe পাঠায় — একটি ছোট segment যা প্রাপককে উত্তর দিতে বাধ্য করে, আর সেই উত্তরে থাকে বর্তমান rwnd. উত্তর এলেও window ০ থাকলে timer আবার চলে, প্রতিবার ব্যবধান দ্বিগুণ করে (exponential backoff), একটি সর্বোচ্চ সীমা পর্যন্ত। অর্থাৎ হারানো update-এর ক্ষতি সর্বোচ্চ একটি probe-ব্যবধান দেরি — চিরস্থায়ী নয়।

RcvBuffer = 4000 byte · প্রতিটি ধাপ = একটি RTT
Simulator-এর ধাপ
প্রাপকের buffer (না-পড়া / খালি)
প্রাপক যা পাঠাল
প্রেরকের জানা rwnd
Persist timer
LastByteSent / LastByteAcked
  1. ধাপ ৩ — window বন্ধ। অ্যাপ কিছু পড়েনি, buffer-এ ৪,০০০ byte, খালি ০. ACK-এর সাথে rwnd = ০ গেল। প্রেরক থামল আর persist timer চালু করল — প্রথম probe ধাপ ৪-এ।
  2. ধাপ ৪ — প্রথম probe. প্রেরক probe পাঠাল। অ্যাপ এখনো পড়েনি, তাই উত্তরে আবার rwnd = ০. Timer আবার চালু, এবার ব্যবধান দ্বিগুণ — পরের probe ধাপ ৬-এ।
  3. ধাপ ৫ — update হারাল। অ্যাপ ২,০০০ byte পড়ল, খালি হলো ২,০০০. প্রাপক window update পাঠাল, কিন্তু সেটি পথে হারাল। প্রেরক এখনো জানে rwnd = ০, প্রাপক ভাবছে সে জানিয়ে দিয়েছে। Persist timer না থাকলে এখানেই চিরকালের deadlock.
  4. ধাপ ৬ — probe বাঁচাল। নির্ধারিত সময়ে probe গেল; উত্তরে প্রাপক বর্তমান অবস্থা জানাল: rwnd = ২,০০০. প্রেরক timer বন্ধ করল। হারানো update-এর দাম মাত্র এক ধাপ দেরি।
  5. ধাপ ৭ — আবার চালু। প্রেরক ২,০০০ byte পাঠাল (LastByteSent ৮,০০০), সব পৌঁছাল আর স্বীকৃত হলো। প্রাপকের অ্যাপও ২,০০০ পড়ল, তাই rwnd আবার ২,০০০. স্বাভাবিক প্রবাহ ফিরে এলো।

Probe দেখতে কেমন

RFC 793 থেকেই নিয়ম ছিল, window ০ হলেও প্রেরককে মাঝে মাঝে অন্তত এক byte নতুন ডেটা পাঠানোর চেষ্টা করতে হবে; RFC 1122 (১৯৮৯) zero-window probing-কে বাধ্যতামূলক (MUST) করে, আর বর্তমান TCP মানদণ্ড RFC 9293 (২০২২) একই কথা বলে। অনেক পাঠ্যবই আর পুরোনো implementation probe হিসেবে ১ byte ডেটা পাঠায় — প্রাপকের জায়গা না থাকলে সে byte-টি ফেলে দিয়ে ACK দেয়, আর ACK-এ থাকে window. আমাদের জানামতে Linux সাধারণত ডেটা ছাড়া, ইচ্ছে করে একটি পুরোনো sequence নম্বরসহ segment পাঠায়, যা প্রাপককে “ডুপ্লিকেট” দেখে সাথে সাথে ACK দিতে বাধ্য করে। ফল একই: উত্তরে বর্তমান rwnd. RFC 1122 আরও বলে, probe-এর উত্তর আসতে থাকলে প্রাপক যত দিন খুশি window বন্ধ রাখতে পারে, আর প্রেরক সংযোগ ভাঙবে না — এর নিরাপত্তা-দিক ভাবনার প্রশ্ন ২-এ।

Persist timer আর keepalive এক নয়। Persist timer চলে শুধু তখন, যখন প্রেরকের হাতে পাঠানোর ডেটা আছে কিন্তু window ০. Keepalive চলে উল্টো অবস্থায় — দীর্ঘক্ষণ কোনো ডেটাই নেই, অপর প্রান্ত বেঁচে আছে কি না দেখার জন্য, আর সেটি ঐচ্ছিক ও সাধারণত বন্ধ থাকে। আবার retransmission timer চলে পাঠানো অস্বীকৃত ডেটার জন্য। একটি TCP সংযোগে এই তিনটি আলাদা timer-এর কাজ আলাদা।

৬ · Silly window syndrome — ছোট window, ছোট segment

Flow control-এর একটি অদ্ভুত পার্শ্বপ্রতিক্রিয়া আছে। ধরুন প্রাপকের buffer ভরা, আর অ্যাপ প্রতিবার মাত্র ২০ byte পড়ে। সরল নিয়মে প্রাপক সাথে সাথে ঘোষণা করে “rwnd = ২০”। প্রেরকের হাতে প্রচুর ডেটা, সে সেই ২০ byte-এর জায়গা ভরাতে একটি ২০ byte-এর segment পাঠায় — যার সাথে অন্তত ৪০ byte-এর TCP আর IPv4 header (২০ + ২০)। Buffer আবার ভরা, অ্যাপ আবার ২০ পড়ে, আবার ২০ byte-এর segment… প্রতি ৬০ byte-এ কাজের ডেটা মাত্র ২০ — তারে যাওয়া byte-এর দুই-তৃতীয়াংশই header! David Clark ১৯৮২ সালে RFC 813-এ এই অবস্থার নাম দেন silly window syndrome (SWS).

দোকানের গুদাম ভরা, দোকানদার একবারে এক বস্তা বিক্রি করেই আড়তে ফোন দিচ্ছেন “এক বস্তার জায়গা হয়েছে”, আর আড়ত একটি পুরো ভ্যান ভাড়া করে এক বস্তা পাঠাচ্ছে। ভ্যানের ভাড়া (header) চালের দামের চেয়ে বেশি পড়ছে। সমাধান দুই দিক থেকেই: দোকানদার যেন অন্তত অর্ধেক গুদাম খালি হলে তবেই ফোন দেন, আর আড়তও যেন ভ্যান ভরার মতো মাল না হলে না পাঠায়।

প্রাপকের দিকের প্রতিকার — Clark-এর নিয়ম

প্রাপক ছোট window ঘোষণা করবে না। Clark-এর সমাধান অনুযায়ী (RFC 1122-এ মানদণ্ড হিসেবে লেখা): window ০ থেকে বাড়ানোর খবর তখনই দেওয়া হবে, যখন খালি জায়গা অন্তত min(MSS, RcvBuffer ÷ ২) হয়। তার আগ পর্যন্ত প্রাপক window ০-ই দেখাতে থাকে (এবং ডান কিনারা পিছিয়ে না নিয়ে)। RFC 1122 এখানে অনুপাত হিসেবে ১/২ সুপারিশ করে।

প্রেরকের দিকের প্রতিকার — Nagle আর “পুরো segment-এর অপেক্ষা”

SWS প্রেরকের দিক থেকেও আসতে পারে: অ্যাপ নিজেই একটি একটি byte লেখে (যেমন পুরোনো telnet-এ প্রতিটি কী-চাপ)। এর প্রতিকার Nagle-এর algorithm (RFC 896, ১৯৮৪): অস্বীকৃত ডেটা পথে থাকলে ছোট segment না পাঠিয়ে জমিয়ে রাখা — বিস্তারিত আর delayed ACK-এর সাথে তার বিরোধ পর্ব ৩০-এ। RFC 1122 প্রেরকের জন্য আরও একটি নিয়ম দেয়: ছোট window দেওয়া হলেও প্রেরক পাঠাবে কেবল যদি পুরো একটি MSS পাঠানো যায়, বা প্রাপকের দেখা সর্বোচ্চ window-এর অন্তত অর্ধেক পাঠানো যায়, বা পাঠানোর সব ডেটাই পাঠানো যায় আর কিছু অস্বীকৃত নেই।

RcvBuffer 4000 · MSS 1000 · অ্যাপ প্রতি tick-এ 20 byte পড়ে · header 40 byte
Tick
প্রাপকের খালি জায়গা
Clark ছাড়া ঘোষণা
Clark-সহ ঘোষণা
Segment / efficiency
  1. শুরু — buffer ভরা। প্রেরক দ্রুত, প্রাপকের ৪,০০০ byte-এর buffer পুরো ভরা, rwnd = ০. অ্যাপ খুব ধীর: প্রতি tick-এ মাত্র ২০ byte পড়ে।
  2. Clark ছাড়া, tick ১. ২০ byte খালি হতেই প্রাপক rwnd = ২০ ঘোষণা করল, প্রেরক সাথে সাথে ২০ byte-এর segment পাঠাল — তারে গেল ২০ + ৪০ = ৬০ byte, কাজের ডেটা ৩৩.৩%. Buffer আবার ভরা।
  3. Clark ছাড়া, ২০০ tick. প্রতি tick-এ একই চক্র: ২০০টি segment, ৪,০০০ byte ডেটার জন্য ৮,০০০ byte header — efficiency ৪,০০০ ÷ ১২,০০০ = ৩৩.৩%, সাথে ২০০টি ACK উল্টো দিকে। প্রতিটি segment-এর জন্য দুই প্রান্তের CPU-ও খাটে।
  4. Clark-সহ, tick ১–৪৯. খালি জায়গা ২০, ৪০ … ৯৮০ — সবই সীমা min(১,০০০, ৪,০০০ ÷ ২) = ১,০০০-এর নিচে। প্রাপক window ০-ই দেখায়; প্রেরক অপেক্ষা করে।
  5. Clark-সহ, tick ৫০. খালি জায়গা ১,০০০ = সীমা। এবার rwnd = ১,০০০ ঘোষণা, প্রেরক একটি পূর্ণ ১,০০০ byte-এর segment পাঠাল: ১,০০০ ÷ ১,০৪০ = ৯৬.২%.
  6. Clark-সহ, ২০০ tick. t = ৫০, ১০০, ১৫০, ২০০-এ চারটি segment — সেই একই ৪,০০০ byte ডেটা, header মাত্র ১৬০ byte, efficiency ৯৬.২%. অ্যাপ যে গতিতে পড়ছে, ডেটা সেই গতিতেই গেল; শুধু অপচয় কমল ৫০ গুণ।

একই হিসাব প্রোগ্রামে — প্রেরক এখানে ইচ্ছে করে “বোকা”: যত ছোট window-ই দেওয়া হোক, সাথে সাথে ভরিয়ে দেয় (প্রেরকের দিকের প্রতিকার বন্ধ), যাতে শুধু প্রাপকের নিয়মের প্রভাব দেখা যায়।

// Silly window syndrome: full 4000-byte buffer, app reads only 20 bytes per tick,
// sender always has data and sends whatever window it is offered. 40 bytes of TCP+IP header.
const BUF = 4000, MSS = 1000, HDR = 40, READ = 20, TICKS = 200;
function run(clark) {
  let used = BUF, segs = 0, data = 0;
  const log = [];
  for (let t = 1; t <= TICKS; t++) {
    used -= READ;                                    // the app reads a little
    const free = BUF - used;
    // Clark (RFC 813/1122): keep advertising 0 until free >= min(MSS, BUF/2)
    const adv = clark && free < Math.min(MSS, BUF / 2) ? 0 : free;
    if (adv > 0) {                          // window update -> sender fills it at once
      segs++; data += adv; used += adv;
      if (log.length < 3) log.push(`t=${t}: rwnd=${adv} -> ${adv}-byte segment`);
    }
  }
  const wire = data + segs * HDR;
  console.log((clark ? "WITH Clark's rule" : "WITHOUT Clark's rule") + ": " + log.join(", "));
  console.log(`   ${segs} segments, ${data} data bytes, ${segs * HDR} header bytes,` +
              ` efficiency ${(100 * data / wire).toFixed(1)}% (plus ${segs} ACKs back)`);
}
run(false);
run(true);

আউটপুট:

WITHOUT Clark's rule: t=1: rwnd=20 -> 20-byte segment, t=2: rwnd=20 -> 20-byte segment, t=3: rwnd=20 -> 20-byte segment
   200 segments, 4000 data bytes, 8000 header bytes, efficiency 33.3% (plus 200 ACKs back)
WITH Clark's rule: t=50: rwnd=1000 -> 1000-byte segment, t=100: rwnd=1000 -> 1000-byte segment, t=150: rwnd=1000 -> 1000-byte segment
   4 segments, 4000 data bytes, 160 header bytes, efficiency 96.2% (plus 4 ACKs back)

দুই ক্ষেত্রেই ২০০ tick-এ ঠিক ৪,০০০ byte ডেটা গেছে — কারণ শেষ পর্যন্ত গতি ঠিক করে অ্যাপ (২০ × ২০০ = ৪,০০০)। পার্থক্য শুধু খরচে: ২০০টি segment বনাম ৪টি, ৮,০০০ বনাম ১৬০ byte header, ৩৩.৩% বনাম ৯৬.২% efficiency. READ ১-এ নামিয়ে দেখুন — Clark ছাড়া efficiency হয় ১ ÷ ৪১ ≈ ২.৪%, যা ১৯৮০-এর দশকের terminal-ট্রাফিকের আসল দুঃস্বপ্ন ছিল।

৭ · Window scaling আর throughput ≤ window ÷ RTT

TCP header-এর Window ঘর ১৬ bit, তাই সর্বোচ্চ মান 216 − 1 = ৬৫,৫৩৫ byte (প্রায় ৬৪ KiB). ১৯৮১ সালে, যখন সবচেয়ে দ্রুত দূরপাল্লার link ছিল কিলোবিটের ঘরে, এটি বিশাল মনে হতো। আজ এই সংখ্যাই হয়ে দাঁড়ায় throughput-এর ছাদ। কেন, তা একটি সরল যুক্তিতে:

প্রমাণ: প্রেরক ACK ছাড়া সর্বোচ্চ W byte পথে রাখতে পারে। কোনো byte পাঠানোর পর তার ACK ফিরতে অন্তত এক RTT লাগে, আর ACK না আসা পর্যন্ত window সামনে সরে না। তাই যেকোনো এক RTT সময়ের মধ্যে প্রেরক W-এর বেশি নতুন byte পাঠাতে পারে না — rate ≤ W ÷ RTT. আবার link-এর নিজের rate R-এর বেশিও সম্ভব নয়। ফলে

Throughput ≤ min(R, W ÷ RTT)   ·   পূর্ণ গতির শর্ত: W ≥ R × RTT = BDP

R × RTT হলো পর্ব ০৫-এর bandwidth-delay product (Delay, loss আর throughput) — পথে একসাথে কত byte “ভাসতে” পারে। Window তার চেয়ে ছোট হলে প্রেরক window ভরিয়ে বসে থাকে, পাইপের বাকিটা খালি যায়। পর্ব ১০-এর W ≥ 1 + 2a শর্তটিও আসলে একই কথা, frame-এর এককে।

সমাধান করা অঙ্ক — কেন ৬৪ KiB-এ ১০০ ms পথ ৫.২ Mbps-এ আটকে যায়
  • W = ৬৫,৫৩৫ byte = ৫,২৪,২৮০ bit; RTT = ১০০ ms = ০.১ s.
  • W ÷ RTT = ৫,২৪,২৮০ ÷ ০.১ = ৫২,৪২,৮০০ bit/s ≈ ৫.২৪ Mbps.
  • Link ১০০ Mbps হলেও throughput ৫.২৪ Mbps-এর বেশি নয় — link-এর মাত্র ৫.২%. Link ১ Gbps হলে মাত্র ০.৫২%!
  • Link পূর্ণ করতে লাগবে W ≥ BDP = ১০৮ × ০.১ ÷ ৮ = ১২,৫০,০০০ byte (প্রায় ১.২ MB).

Window scale option (RFC 7323)

সমাধান window scaling: header-এ ১৬ bit-ই থাকবে, কিন্তু দুই প্রান্ত সংযোগের শুরুতে একমত হবে যে সংখ্যাটিকে 2S দিয়ে গুণ করে পড়তে হবে (বাঁয়ে S bit shift). প্রস্তাবটি এসেছিল ১৯৮৮ সালে RFC 1072-এ, প্রচলিত হয় RFC 1323 (১৯৯২) দিয়ে, আর বর্তমান মানদণ্ড RFC 7323 (২০১৪)। মূল নিয়মগুলো:

  • Option-টি (kind 3, দৈর্ঘ্য ৩ byte, এক byte-এ shift count S) শুধু SYN আর SYN-ACK-এ যায় — অর্থাৎ পর্ব ২৯-এর handshake-এর সময়েই ঠিক হয়। দুই পক্ষই পাঠালে তবেই চালু; একজন না পাঠালে কেউই scaling ব্যবহার করে না।
  • প্রতিটি দিকের S আলাদা — প্রতিটি প্রান্ত নিজের receive buffer অনুযায়ী নিজের S ঘোষণা করে।
  • S সর্বোচ্চ ১৪. সর্বোচ্চ window = ৬৫,৫৩৫ × 214 = ১,০৭,৩৭,২৫,৪৪০ byte ≈ ১ GiB. সীমার কারণ: ৩২-bit sequence নম্বরের চক্রে পুরোনো আর নতুন byte আলাদা রাখতে window-কে 230-এর নিচে থাকতে হয়।
  • SYN segment-এর নিজের window ঘর কখনো scale হয় না। আর scaling-এর দাম: window এখন 2S-এর গুণিতকে মাপা, তাই সূক্ষ্মতা কমে।

উদাহরণ: S = ৭ আর header-এ Window = ৫১২ হলে আসল rwnd = ৫১২ × ১২৮ = ৬৫,৫৩৬ byte. উল্টো দিকে, উপরের ১০০ Mbps × ১০০ ms পথে লাগবে ১২,৫০,০০০ ÷ ৬৫,৫৩৫ ≈ ১৯.১ গুণ, তাই S = ৫ (25 = ৩২ ≥ ১৯.১)।

যে পথে R আর RTT দুটোই বড় — মহাদেশ পেরোনো ১০ Gbps fiber, উপগ্রহ — তাকে RFC 7323-এর ভাষায় বলে long fat network (LFN, উচ্চারণ “elephant”). নিচের calculator কয়েকটি পরিস্থিতিতে window-সীমা, link-সীমা, BDP আর প্রয়োজনীয় S হিসাব করে:

// Max TCP throughput = min(bandwidth, window / RTT); BDP = bandwidth x RTT; window-scale needed.
const cases = [  // [name, bandwidth in bit/s, RTT in s, window in bytes]
  ["no scaling, 100 ms path",  100e6, 0.100, 65535],
  ["no scaling, 10 ms path",   1e9,   0.010, 65535],
  ["home link, 256 KiB",       20e6,  0.050, 256 * 1024],
  ["GEO satellite, 64 KiB",    50e6,  0.600, 65535],
  ["10G long fat network",     10e9,  0.100, 16 * 1024 * 1024],
];
const mbps = b => (b / 1e6).toFixed(2).padStart(9) + " Mbps";
const kb = B => (B / 1024).toFixed(0).padStart(7) + " KiB";
function scaleNeeded(bytes) {            // smallest shift s with 65535 * 2^s >= bytes (max 14)
  let s = 0;
  while (65535 * 2 ** s < bytes && s < 14) s++;
  return 65535 * 2 ** s >= bytes ? String(s) : "more than 14";
}
console.log(`${"case".padEnd(24)} ${"window/RTT".padStart(14)} ${"link".padStart(14)} ${"max".padStart(14)} ${"BDP".padStart(11)}  scale`);
for (const [name, bw, rtt, win] of cases) {
  const wr = win * 8 / rtt, tput = Math.min(bw, wr), bdp = bw * rtt / 8;
  const who = wr < bw ? "window-limited" : "link-limited";
  console.log(`${name.padEnd(24)} ${mbps(wr)} ${mbps(bw)} ${mbps(tput)} ${kb(bdp)}  ${scaleNeeded(bdp).padStart(2)}  ${who}`);
}
console.log("largest window with scale 14 = " + 65535 * 2 ** 14 + " bytes (about 1 GiB)");

আউটপুট:

case                         window/RTT           link            max         BDP  scale
no scaling, 100 ms path       5.24 Mbps    100.00 Mbps      5.24 Mbps    1221 KiB   5  window-limited
no scaling, 10 ms path       52.43 Mbps   1000.00 Mbps     52.43 Mbps    1221 KiB   5  window-limited
home link, 256 KiB           41.94 Mbps     20.00 Mbps     20.00 Mbps     122 KiB   1  link-limited
GEO satellite, 64 KiB         0.87 Mbps     50.00 Mbps      0.87 Mbps    3662 KiB   6  window-limited
10G long fat network       1342.18 Mbps  10000.00 Mbps   1342.18 Mbps  122070 KiB  11  window-limited
largest window with scale 14 = 1073725440 bytes (about 1 GiB)

প্রথম দুই সারি একই BDP (১২,৫০,০০০ byte ≈ ১২২১ KiB) দেখায় — ১০০ Mbps × ১০০ ms আর ১ Gbps × ১০ ms গুণফলে সমান। Scaling ছাড়া দুটিই window-বাঁধা: ৫.২৪ আর ৫২.৪৩ Mbps; দুটিতেই S = ৫ লাগবে। তৃতীয় সারি উল্টো ছবি: ২৫৬ KiB window ২০ Mbps × ৫০ ms পথের BDP (১২২ KiB)-এর চেয়ে বড়, তাই সীমা টানছে link নিজে (link-limited), window বাড়িয়ে আর লাভ নেই। Geostationary উপগ্রহ (RTT ধরা হয়েছে ৬০০ ms) ৬৪ KiB window-এ ৫০ Mbps-এর link থেকে মাত্র ০.৮৭ Mbps পায়। ১০ Gbps LFN-এ ১৬ MiB window-ও কম পড়ে: BDP ১২৫ MB, লাগবে S = ১১. শেষ লাইন RFC 7323-এর সর্বোচ্চ window — ১,০৭,৩৭,২৫,৪৪০ byte.

বাফারের আকার কত রাখবেন — নিয়মটি সহজ: প্রাপকের buffer (আর প্রেরকের send buffer) অন্তত BDP-র সমান হওয়া উচিত, নইলে flow control নিজেই throughput-এর ছাদ। বাস্তবে Linux buffer-এর একটি অংশ kernel-এর হিসাবরক্ষণে খরচ করে, তাই প্রায়ই BDP-র দেড় থেকে দুই গুণ রাখা হয়। অন্যদিকে প্রয়োজনের চেয়ে অনেক বড় buffer মানে প্রতি সংযোগে অকারণ মেমরি — হাজার সংযোগের সার্ভারে সেটি বড় খরচ। এই tuning-এর বাস্তব কৌশল পর্ব ৫৬-এ।

৮ · Linux-এ বাস্তবে — buffer, autotuning আর SO_RCVBUF

আধুনিক Linux প্রতিটি সংযোগের receive buffer নিজে থেকেই বাড়ায়-কমায় — একে বলে receive-buffer autotuning. Kernel প্রতি RTT-তে অ্যাপ কতটা পড়ছে তা মেপে buffer প্রয়োজনমতো বড় করে, একটি সীমা পর্যন্ত। সীমাগুলো থাকে তিনটি sysctl মানে:

  • net.ipv4.tcp_rmem = min default max — receive buffer-এর সর্বনিম্ন, শুরুর আর autotuning-এর সর্বোচ্চ মান (byte).
  • net.ipv4.tcp_wmem — send buffer-এর জন্য একই তিনটি মান।
  • net.ipv4.tcp_window_scaling — ১ হলে SYN-এ window scale option যায় (প্রায় সব সিস্টেমে ডিফল্ট ১)।

ডিফল্ট মান kernel সংস্করণ আর distribution-ভেদে আলাদা; অনেক সাধারণ সিস্টেমে tcp_rmem-এর সর্বোচ্চ কয়েক MiB. অ্যাপ নিজে SO_RCVBUF / SO_SNDBUF socket option দিয়ে আকার ঠিক করে দিতে পারে, কিন্তু Linux-এ দুটি চমক আছে: kernel চাওয়া মানকে দ্বিগুণ করে রাখে (হিসাবরক্ষণের বাড়তি জায়গার জন্য — man 7 socket-এ লেখা আছে), আর চাওয়া মান net.core.rmem_max দিয়ে সীমিত। এছাড়া নিজে SO_RCVBUF ঠিক করলে সাধারণত সেই socket-এর autotuning বন্ধ হয়ে যায় — তাই না বুঝে ছোট মান বসানো উল্টো ফল দিতে পারে।

নিচের C প্রোগ্রাম loopback-এ পুরো ঘটনাটি দেখায়: /proc থেকে তিনটি মান পড়ে, buffer-এর আকার চেয়ে ফেরত পড়ে, তারপর একটি সংযোগ বানায় যার সার্ভার কখনো recv() ডাকে না। Client socket-কে non-blocking করে ১,০০০ byte করে send() চলতে থাকে, যতক্ষণ না kernel আর নিতে অস্বীকার করে। শেষে TCP_INFO দিয়ে kernel-এর ভেতরের হিসাব পড়া হয় — glibc-র struct-এ tcpi_snd_wnd ঘরটি নেই, তাই kernel-এর <linux/tcp.h> ব্যবহার করা হয়েছে, আর ঘরটি kernel আসলে ফেরত দিল কি না দৈর্ঘ্য দেখে যাচাই করা হয়েছে।

#include <stdio.h>
#include <stddef.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <linux/tcp.h>          /* kernel's struct tcp_info: has tcpi_snd_wnd */
static void show(const char *f) { char b[96] = "not readable\n"; FILE *p = fopen(f, "r");
    if (p) { if (!fgets(b, sizeof b, p)) strcpy(b, "?\n"); fclose(p); } printf("%-19s %s", f + 19, b); }
static int opt(int s, int o) { int v = 0; socklen_t n = sizeof v; getsockopt(s, SOL_SOCKET, o, &v, &n); return v; }
int main(void) {
    show("/proc/sys/net/ipv4/tcp_rmem"); show("/proc/sys/net/ipv4/tcp_wmem"); show("/proc/sys/net/ipv4/tcp_window_scaling");
    int L = socket(AF_INET, SOCK_STREAM, 0), C = socket(AF_INET, SOCK_STREAM, 0), big = 1000000, v = 16384;
    setsockopt(C, SOL_SOCKET, SO_RCVBUF, &big, sizeof big);  printf("SO_RCVBUF 1000000 -> %d\n", opt(C, SO_RCVBUF));
    setsockopt(L, SOL_SOCKET, SO_RCVBUF, &v, sizeof v);      /* before listen(): accepted socket inherits it */
    setsockopt(C, SOL_SOCKET, SO_SNDBUF, &v, sizeof v);
    printf("SO_RCVBUF 16384 -> %d (server), SO_SNDBUF 16384 -> %d (client)\n", opt(L, SO_RCVBUF), opt(C, SO_SNDBUF));
    struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; socklen_t al = sizeof a;
    bind(L, (struct sockaddr *)&a, sizeof a); listen(L, 1); getsockname(L, (struct sockaddr *)&a, &al);
    if (connect(C, (struct sockaddr *)&a, sizeof a) < 0) { perror("connect"); return 1; }
    int S = accept(L, NULL, NULL);                          /* the server never calls recv() */
    fcntl(C, F_SETFL, O_NONBLOCK); char buf[1000] = {0}; long total = 0; ssize_t n;
    while ((n = send(C, buf, sizeof buf, 0)) > 0) total += n;
    printf("send() stopped: %s\n", strerror(errno));
    usleep(300000); int inq = 0, outq = 0; ioctl(S, FIONREAD, &inq); ioctl(C, TIOCOUTQ, &outq);
    printf("accepted %ld bytes = %d in server's receive queue + %d still in client\n", total, inq, outq);
    struct tcp_info ts, tc; socklen_t ls = sizeof ts, lc = sizeof tc;
    if (getsockopt(S, IPPROTO_TCP, TCP_INFO, &ts, &ls) == 0)
        printf("server: rcv_space=%u rcv_wscale=%u, client's wscale=%u\n", ts.tcpi_rcv_space, ts.tcpi_rcv_wscale, ts.tcpi_snd_wscale);
    if (getsockopt(C, IPPROTO_TCP, TCP_INFO, &tc, &lc) == 0 && lc >= offsetof(struct tcp_info, tcpi_snd_wnd) + 4)
        printf("client: snd_wnd=%u snd_mss=%u unacked=%u\n", tc.tcpi_snd_wnd, tc.tcpi_snd_mss, tc.tcpi_unacked);
    else printf("client: this kernel does not report tcpi_snd_wnd\n");
    return 0;
}

আউটপুট (অনলাইন runner-এর container-এ):

tcp_rmem            4096	87380	33554432
tcp_wmem            4096	87380	33554432
tcp_window_scaling  1
SO_RCVBUF 1000000 -> 425984
SO_RCVBUF 16384 -> 32768 (server), SO_SNDBUF 16384 -> 32768 (client)
send() stopped: Resource temporarily unavailable
accepted 50768 bytes = 29192 in server's receive queue + 21576 still in client
server: rcv_space=16384 rcv_wscale=0, client's wscale=2
client: snd_wnd=0 snd_mss=8192 unacked=0

প্রথম তিন লাইন: এই host-এ tcp_rmem আর tcp_wmem-এর সর্বোচ্চ ৩২ MiB (33554432) — host-এর প্রশাসক বাড়িয়ে রেখেছেন; আপনার মেশিনে অন্য সংখ্যা আসবে। Window scaling চালু। দ্বিগুণের চমক: ১৬,৩৮৪ চেয়ে পাওয়া গেল ৩২,৭৬৮ — দুটিতেই ঠিক দ্বিগুণ। ১০,০০,০০০ চেয়ে পাওয়া গেল ৪,২৫,৯৮৪ = ২ × ২,১২,৯৯২: অর্থাৎ চাওয়া মান আগে rmem_max (এখানে স্পষ্টতই ২,১২,৯৯২, Linux-এর পরিচিত ডিফল্ট) দিয়ে কাটা হয়েছে, তারপর দ্বিগুণ। Buffer ভরানো: send() শেষে EAGAIN (“Resource temporarily unavailable”) দিল — blocking socket হলে এখানেই প্রোগ্রাম আটকে থাকত। মোট ৫০,৭৬৮ byte নেওয়া হয়েছে, যার ২৯,১৯২ byte সার্ভারের receive queue-তে (অ্যাপ পড়েনি) আর ২১,৫৭৬ byte এখনো client-এর send buffer-এ; যোগফল মিলে যায়। সার্ভারের ৩২,৭৬৮ byte-এর buffer-এ ২৯,১৯২ byte ডেটা ধরেছে, কারণ বাকিটা kernel-এর হিসাবরক্ষণে খরচ হয়। TCP_INFO: client-এর snd_wnd=0 — সার্ভার zero window ঘোষণা করেছে; unacked=0 মানে পাঠানো সবই স্বীকৃত, বাকি ডেটা শুধু window খোলার অপেক্ষায় (persist timer-এর অবস্থা)। সার্ভারের buffer ছোট বলে তার rcv_wscale=0 — ৩২ KiB ১৬ bit-এই ধরে; client-এর বড় buffer-এর জন্য client ঘোষণা করেছে wscale 2. Loopback-এর MTU বড় (সাধারণত ৬৫,৫৩৬), তবু snd_mss=8192 — আমাদের ধারণা, Linux segment-কে প্রাপকের দেখা সর্বোচ্চ window-এর অর্ধেকে সীমিত রাখে (প্রায় ১৬ KiB-এর অর্ধেক), যা অংশ ৬-এর প্রেরক-দিকের SWS নিয়মের সাথে মেলে। rcv_space autotuning-এর মাপ, অ্যাপ কিছু পড়েনি বলে বাড়েনি। সংখ্যাগুলো kernel-এর সংস্করণ আর সময়ের ওপর নির্ভর করে, আপনার মেশিনে কিছুটা আলাদা হতে পারে।

নিজের Linux মেশিনে চলমান সংযোগের window আর buffer দেখতে এই কমান্ডগুলো কাজে লাগে (চালানোর আগে নিজের মেশিনের man ss দেখে নিন):

sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.ipv4.tcp_window_scaling
ss -tmi state established     # প্রতিটি সংযোগে wscale, rcv_space, snd_wnd, skmem(r,rb,t,tb)
cat /proc/sys/net/core/rmem_max

Windows-এ autotuning-এর নাম “Receive Window Auto-Tuning”, Windows Vista থেকে ডিফল্ট চালু; অবস্থা দেখা যায় এভাবে (প্রশাসক হিসেবে বদলানো যায়, তবে কারণ ছাড়া বদলানো উচিত নয়):

netsh interface tcp show global

আরও গভীরে: Linux শুরু থেকেই পুরো buffer-কে window হিসেবে ঘোষণা করে না, কারণ প্রতিটি packet-এর সাথে sk_buff নামের metadata-ও buffer-এর হিসাবে ধরা হয়। পুরোনো kernel-এ এই অনুপাত ঠিক করত tcp_adv_win_scale (ডিফল্টে মোটামুটি অর্ধেক), আর ২০২৩ সালের দিকের ৬.x kernel থেকে প্রকৃত ডেটা-বনাম-metadata অনুপাত মেপে window হিসাব করা হয়। Receive autotuning-এর ধারণা এসেছে ২০০০-এর দশকের শুরুর “Dynamic Right-Sizing” গবেষণা থেকে; এটি প্রতি RTT-তে অ্যাপের পড়া byte মেপে buffer-কে মোটামুটি তার দ্বিগুণে রাখে, যাতে window পরের RTT-তে cwnd-এর বৃদ্ধির সাথে তাল রাখতে পারে। এর ফলে ব্যবহারকারীকে আর হাতে buffer বসাতে হয় না — আর হাতে বসালে বরং এই বুদ্ধিমান ব্যবস্থাটি বন্ধ হয়।

৯ · অন্য স্তরেও flow control

“ধীর প্রাপক দ্রুত প্রেরককে থামাবে” — এই সমস্যা নেটওয়ার্কের প্রায় প্রতিটি স্তরে আছে, আর সমাধানের ধরনও প্রায় এক: হয় স্পষ্ট থামার সংকেত, নয়তো credit বা window.

কোথায়কৌশলকার জন্য
Ethernet link (IEEE 802.3x)PAUSE frame — “এত সময় পাঠাবেন না”; data center-এ priority-ভিত্তিক PFC (802.1Qbb)পাশের switch বা NIC — এক hop
Data link ARQ (পর্ব ১০)Frame-সংখ্যার sliding windowএক link-এর দুই প্রান্ত
TCP (এই পর্ব)Byte-ভিত্তিক rwnd, প্রতিটি segment-এপুরো সংযোগ — দুই host-এর kernel
HTTP/2 (পর্ব ৩৭)প্রতিটি stream আর পুরো সংযোগের আলাদা window, WINDOW_UPDATE frame দিয়ে বাড়ানো; শুরুর মান ৬৫,৫৩৫ byteএকই TCP সংযোগের ভেতরের অনেক request
QUIC (পর্ব ৩৩)MAX_DATA (পুরো সংযোগ) আর MAX_STREAM_DATA (প্রতি stream) frame — credit-এর মতো সর্বোচ্চ byte-সীমাUDP-র ওপর অনেক stream

HTTP/2 আর QUIC-কে আলাদা flow control রাখতে হয় কারণ TCP-র rwnd পুরো সংযোগের — একটি সংযোগে দশটি ফাইল একসাথে চললে, একটি ধীর ভোক্তা (ধরুন বড় একটি download যা অ্যাপ ধীরে লিখছে) পুরো সংযোগের window আটকে বাকি নয়টিকেও থামিয়ে দিত। Stream-ভিত্তিক window প্রতিটি stream-কে আলাদা করে থামাতে দেয়। বিস্তারিত পর্ব ৩৩ আর ৩৭-এ।

পরীক্ষা ও ইন্টারভিউতে — প্রায়ই আসে এমন প্রশ্ন:

  • Flow control আর congestion control-এর পার্থক্য কী? — প্রথমটি প্রাপকের buffer রক্ষা করে (rwnd, প্রাপক জানায়), দ্বিতীয়টি নেটওয়ার্ক রক্ষা করে (cwnd, প্রেরক অনুমান করে); পাঠানোর সীমা min(rwnd, cwnd).
  • RcvBuffer, LastByteRcvd, LastByteRead দিয়ে rwnd লিখুন। — rwnd = RcvBuffer − (LastByteRcvd − LastByteRead); প্রেরকে LastByteSent − LastByteAcked ≤ rwnd.
  • Window ০ হলে TCP কীভাবে deadlock এড়ায়? — প্রেরকের persist timer নির্দিষ্ট ব্যবধানে window probe পাঠায়, উত্তরে বর্তমান rwnd আসে।
  • ৬৪ KiB window আর ২০০ ms RTT-তে সর্বোচ্চ throughput? — ৬৫,৫৩৫ × ৮ ÷ ০.২ ≈ ২.৬২ Mbps; বেশি পেতে window scaling লাগবে।
  • Silly window syndrome কী, প্রতিকার কী? — ছোট window-এ ছোট segment আর অনুপাতে বিশাল header; প্রাপকে Clark (min(MSS, buffer/2) খালি না হলে window ০), প্রেরকে Nagle আর পূর্ণ MSS-এর অপেক্ষা।

১০ · AI-এর যুগে TCP window-এর হিসাব আর tuning

“Linux-এ TCP throughput বাড়াব কীভাবে?” — AI-কে জিজ্ঞেস করলে প্রায়ই একগাদা sysctl লাইন আসে, যার কিছু অপ্রয়োজনীয়, কিছু পুরোনো, কিছু ক্ষতিকর। Window-এর অঙ্কেও AI-এর ভুলগুলোর ধরন চেনা:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
Flow control আর congestion control এক করে ফেলা (“rwnd কমে কারণ নেটওয়ার্কে ভিড়”)rwnd প্রাপকের buffer-এর খবর, ভিড়ের নয়প্রশ্ন করুন: সংখ্যাটি কে ঘোষণা করে?
W ÷ RTT-তে byte আর bit গুলিয়ে ৮ গুণ ভুলWindow byte-এ, rate bit/s-এএককসহ নিজে আবার গুণ করুন
Window scale-এর সর্বোচ্চ ১৬ বা ৩২ বলা, বা “scale প্রতিটি segment-এ বদলায়”সর্বোচ্চ ১৪, ঠিক হয় শুধু SYN-এRFC 7323-এর অংশ ২ পড়ুন
“SO_RCVBUF যত বড় তত ভালো, সব socket-এ বসান”Linux দ্বিগুণ করে, rmem_max-এ কাটে, আর autotuning বন্ধ করে দেয়setsockopt-এর পর getsockopt দিয়ে পড়ুন; ss -tmi দেখুন
পুরোনো কমান্ড (netstat, ifconfig) বা অস্তিত্বহীন sysctl নামআধুনিক Linux-এ ss, ip; sysctl নাম kernel-ভেদে বদলায়sysctl -a | grep tcp_ আর kernel-এর ip-sysctl ডকুমেন্টেশন মেলান
বানানো RFC নম্বর বা ভুল সাল (যেমন window scaling-কে RFC 793-এর অংশ বলা)Window scaling এসেছে পরে: RFC 1072 → 1323 → 7323rfc-editor.org-এ নম্বর খুলে শিরোনাম মেলান

AI-কে TCP window-এর অঙ্ক বা tuning-প্রশ্ন দিলে অনুমান আর একক আগেই বেঁধে দিন:

একটি TCP সংযোগের হিসাব ধাপে ধাপে করো। Link 200 Mbps, RTT 80 ms, প্রাপকের ঘোষিত window field 4096, window scale shift 6. cwnd-কে বাধা ধরবে না। ১. আসল rwnd byte-এ কত? গুণের ধাপটি দেখাও। ২. সর্বোচ্চ throughput = min(R, W/RTT) হিসাব করো, প্রতিটি সংখ্যার একক (byte, bit, s) লিখে। ৩. BDP কত, আর link পুরো ব্যবহার করতে ন্যূনতম কত shift count লাগবে? ১৪-এর সীমা মনে রাখো। ৪. Linux-এ প্রাপকের buffer কোথায় দেখব আর কীভাবে বদলাব, তা বলো — কিন্তু প্রতিটি sysctl বা socket option-এর নাম আর আচরণের জন্য man page বা kernel ডকুমেন্টেশনের নাম দাও, আর কোন কাজের জন্য প্রশাসকের অনুমতি লাগে তা আলাদা করে বলো। ৫. কোনো সংখ্যা নিজে থেকে ধরে নেবে না; নিশ্চিত না হলে বলবে।

AI-এর উত্তর মানার আগে — চেকলিস্ট

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

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

প্র ০১প্রাপক তো সরাসরি “stop” আর “go” সংকেত পাঠাতে পারত (XON/XOFF-এর মতো)। TCP কেন বদলে “এত byte জায়গা আছে” — একটি সংখ্যা — পাঠানোর নকশা বেছে নিল?

Serial লাইন আর পুরোনো terminal-এ XON/XOFF সত্যিই ব্যবহার হতো: প্রাপক একটি বিশেষ অক্ষর (Ctrl-S) পাঠালে প্রেরক থামত, আরেকটি (Ctrl-Q) পাঠালে আবার চলত। এক মিটার তারে এটি চমৎকার, কারণ “stop” বার্তা প্রায় সাথে সাথে পৌঁছায়। কিন্তু ইন্টারনেটে দুই প্রান্তের মাঝে RTT শত মিলিসেকেন্ড হতে পারে। প্রাপক “stop” পাঠানোর মুহূর্তে প্রেরক হয়তো আরও এক RTT-র ডেটা পাঠিয়ে ফেলেছে, যা পৌঁছে buffer উপচে দেবে। অর্থাৎ stop/go নকশায় প্রাপককে সবসময় বাড়তি এক BDP-র জায়গা খালি রেখে “stop” বলতে হতো, আর সেই BDP পথভেদে বদলায় — প্রাপক জানেই না কত।

সংখ্যা-ভিত্তিক window, যাকে credit-ভিত্তিক flow control-ও বলা হয়, এই সমস্যা মূল থেকে কেটে দেয়। প্রাপক বলে “ACK নম্বরের পর আর ঠিক এত byte পাঠাতে পারেন”। প্রতিশ্রুতিটি একটি নির্দিষ্ট byte-সীমা, সময়ের ওপর নির্ভর করে না। ACK দেরিতে এলে প্রেরক শুধু পুরোনো সীমা পর্যন্ত পাঠিয়ে থেমে থাকে — কখনো সীমা পেরোয় না। অংশ ২-এর প্রমাণটি ঠিক এই কারণেই RTT ছাড়া লেখা গেছে। তাছাড়া একই সংখ্যা দিয়ে “পুরো গতি” থেকে “একদম বন্ধ” পর্যন্ত যেকোনো মাঝামাঝি গতিও বোঝানো যায়, যা stop/go পারে না।

১৯৭৪ সালে Vint Cerf আর Bob Kahn-এর বিখ্যাত প্রবন্ধ “A Protocol for Packet Network Intercommunication”-এ, যা থেকে TCP-র জন্ম, window-ভিত্তিক flow control-এর ধারণা ছিল; ১৯৮১ সালের RFC 793 তাকে byte-স্তরে, ACK-এর সাথে জুড়ে মানদণ্ড করে। একই credit-ধারণা পরে আবার এসেছে ATM, InfiniBand-এর link-স্তরের credit, HTTP/2-এর WINDOW_UPDATE আর QUIC-এর MAX_DATA-তে — নকশাটি এতটাই টেকসই। তবে link-স্তরে, যেখানে দূরত্ব ছোট, Ethernet-এর PAUSE frame-এর মতো stop/go আজও চলে।

মূল উপলব্ধি: দীর্ঘ delay-এর পথে সময়-নির্ভর “stop” সংকেত নিরাপদ নয়; byte-সীমার প্রতিশ্রুতি (credit বা window) delay যতই হোক buffer উপচানো ঠেকায়।

প্র ০২RFC 1122 অনুযায়ী প্রাপক probe-এর উত্তর দিতে থাকলে window অনির্দিষ্টকাল বন্ধ রাখতে পারে। একটি সার্ভারের জন্য এতে কী ঝুঁকি, আর রক্ষার উপায় কী?

নিয়মটি মানবিক কারণে যুক্তিসংগত: একটি প্রিন্টারের কাগজ ফুরিয়েছে, বা ব্যবহারকারী একটি download থামিয়ে রেখেছেন — প্রাপক ঠিকই বেঁচে আছে, শুধু এখন পড়তে পারছে না। এমন সংযোগ ভেঙে দিলে ক্ষতি। তাই RFC 1122 বলে, probe-এর উত্তর আসতে থাকলে প্রেরক সংযোগ বন্ধ করবে না।

কিন্তু উল্টো দিক থেকে ভাবুন। একটি ওয়েব সার্ভার বড় একটি ফাইল পাঠাচ্ছে। Client-এর দিকে কেউ ইচ্ছে করে একদম না পড়ে, শুধু window ০ রাখে আর probe-এর উত্তর দেয়। সার্ভারের send buffer ভরা থাকে, সংযোগের সব kernel-কাঠামো, আর অনেক সার্ভারে প্রতি সংযোগের একটি thread বা প্রক্রিয়া, বসে থাকে। এমন হাজার হাজার সংযোগ খুললে সার্ভারের মেমরি আর সংযোগ-সীমা ফুরিয়ে যায়, অথচ আক্রমণকারীর খরচ প্রায় শূন্য — প্রতিটি probe-এর জবাবে একটি ছোট ACK. এটি এক ধরনের ধীর resource-exhaustion আক্রমণ, “slow read” নামে পরিচিত ধারণার আত্মীয়; ২০০৯ সালের দিকে কয়েকটি TCP stack-এর জন্য এ বিষয়ে নিরাপত্তা-সতর্কতাও প্রকাশিত হয়েছিল।

রক্ষার উপায় স্তরে স্তরে। প্রথমত অ্যাপ্লিকেশনের নিজস্ব timeout: ওয়েব সার্ভার বা reverse proxy সাধারণত নির্দিষ্ট সময়ে কোনো অগ্রগতি না হলে সংযোগ বন্ধ করে — nginx-এর send_timeout এর পরিচিত উদাহরণ। দ্বিতীয়ত kernel-এর সীমা: Linux-এ TCP_USER_TIMEOUT socket option দিয়ে বলা যায় কতক্ষণ অগ্রগতি না হলে সংযোগ ভাঙতে হবে, আর নতুন kernel-এ এটি zero-window probe-এর ক্ষেত্রেও প্রযোজ্য। তৃতীয়ত সংযোগের সংখ্যা ও প্রতি-IP সীমা, আর event-driven সার্ভার নকশা (পর্ব ৪৪), যেখানে একটি আটকে থাকা সংযোগ একটি পুরো thread দখল করে না। আক্রমণ আর প্রতিরক্ষার বড় ছবি পর্ব ৫০-এ।

শিক্ষাটি protocol নকশার একটি স্থায়ী সমস্যা: “ভালো আচরণকারী ধীর প্রাপক” আর “ইচ্ছাকৃত ধীর আক্রমণকারী” protocol-এর দৃষ্টিতে হুবহু এক। তাই protocol যা অনুমতি দেয়, অ্যাপ্লিকেশনকে তার ওপর নিজের নীতি বসাতে হয়।

মূল উপলব্ধি: Zero window protocol-এর দিক থেকে বৈধ ও প্রয়োজনীয়, কিন্তু সীমাহীন অপেক্ষা resource-এর ঝুঁকি; সমাধান আসে অ্যাপ আর kernel-এর timeout ও সংযোগ-সীমা থেকে।

প্র ০৩২০০০-এর দশকে Linux আর Windows বড় window আর autotuning চালু করার পর কিছু ব্যবহারকারীর ইন্টারনেট হঠাৎ ধীর হয়ে গেল। নতুন প্রযুক্তি কীভাবে পুরোনো যন্ত্রের সাথে ঠোকাঠুকি খেল?

Window scaling-এর option আছে ১৯৯২ সাল থেকে, কিন্তু বহু বছর বেশিরভাগ সিস্টেম ছোট buffer নিয়ে চলত, ফলে shift count হতো ০ বা খুব ছোট। ২০০৬ সালের দিকে Linux kernel receive autotuning-এর সাথে বড় সর্বোচ্চ buffer ডিফল্ট করে, ফলে SYN-এ বড় shift count যেতে শুরু করে; ২০০৭ সালে Windows Vista-ও “Receive Window Auto-Tuning” ডিফল্ট চালু করে।

তখন দেখা গেল কিছু বাড়ির router, firewall আর load balancer — যাদের ভেতরে TCP header “বোঝার” একটি সরল কোড ছিল — window scale option-কে ঠিকমতো বোঝে না। কেউ SYN থেকে option-টি মুছে দিত, কিন্তু অপর প্রান্ত ততক্ষণে ধরে নিয়েছে scaling চালু। কেউ আবার scale না জেনে window ঘরের ছোট সংখ্যা দেখে সংযোগের “অবৈধ” packet ফেলে দিত। ফল: প্রাপক হয়তো window = ৫১২ লিখেছে মানে ৫১২ × ১২৮ = ৬৫,৫৩৬ byte, কিন্তু প্রেরক scaling না জেনে সেটি ৫১২ byte পড়ছে — throughput ধসে পড়ে, কিছু সাইট খোলেই না। LWN-এর মতো প্রকাশনায় ২০০৭ সালে “broken routers আর window scaling” নিয়ে লেখা হয়, আর kernel ডেভেলপাররা ব্যবহারকারীদের সাময়িকভাবে tcp_window_scaling বা buffer কমিয়ে পরীক্ষা করতে বলেন, যদিও স্থায়ী সমাধান ছিল ভাঙা যন্ত্রগুলোর firmware ঠিক করা।

এই ঘটনা ইন্টারনেট প্রকৌশলের একটি বড় সমস্যার নমুনা — ossification (জমে শক্ত হয়ে যাওয়া)। পথের middlebox-গুলো protocol-এর যা দেখে অভ্যস্ত, তার বাইরে কিছু এলেই ভাঙে, ফলে বৈধ কিন্তু কম-ব্যবহৃত বৈশিষ্ট্য বাস্তবে ব্যবহার করা কঠিন হয়ে পড়ে। TCP-তে নতুন option যোগ করা আজও কঠিন এই কারণেই। QUIC (পর্ব ৩৩) তার header-এর প্রায় সব অংশ encrypt করে, অংশত ঠিক এই সমস্যা এড়াতে — middlebox যা দেখতে পায় না, তার ওপর নির্ভর করে ভাঙতেও পারে না।

মূল উপলব্ধি: একটি protocol বৈশিষ্ট্য মানদণ্ডে থাকলেই বাস্তবে কাজ করে না; পথের প্রতিটি যন্ত্রকে তা ঠিকমতো বুঝতে হয়, আর না বুঝলে ভুল দেখা দেয় রহস্যময় ধীরগতি হিসেবে।

প্র ০৪BDP-র চেয়ে বড় buffer রাখলে throughput বাড়ে না, তবু কি ক্ষতি হতে পারে? “Bufferbloat”-এর সাথে flow control-এর সম্পর্ক কী?

প্রাপকের receive buffer বড় রাখার খরচ মূলত মেমরি: window বড় ঘোষণা করলেও প্রেরক তা পুরো ভরাবে কেবল যদি প্রাপকের অ্যাপ ধীর হয়। প্রাপক দ্রুত পড়লে buffer প্রায় খালিই থাকে। তাই বড় receive buffer নিজে latency বাড়ায় না।

কিন্তু বড় window প্রেরককে অনুমতি দেয় অনেক ডেটা পথে রাখার। Throughput-এর সূত্রে W ÷ RTT — W যদি BDP-র চেয়ে বড় হয়, তবু link-এর চেয়ে দ্রুত পাঠানো সম্ভব নয়; বাড়তি ডেটা যায় কোথায়? পথের সবচেয়ে ধীর link-এর আগের router-এর queue-তে। Queue লম্বা হলে প্রতিটি packet-কে লাইনে অপেক্ষা করতে হয়, তাই RTT বাড়ে — একই সংযোগে, আর একই link ভাগ করা সবার জন্য। ২০১০-১১ সালে Jim Gettys বাড়ির router আর cable modem-এ এই সমস্যা মেপে নাম দেন “bufferbloat”: বড় একটি upload চললে একই বাড়ির ভিডিও কল বা গেমের ping কয়েক সেকেন্ড পর্যন্ত বেড়ে যেত, কারণ যন্ত্রের অতিরিক্ত বড় buffer ভরে থাকত।

এখানে flow control-এর ভূমিকা সীমিত — সেটি কেবল প্রাপকের buffer দেখে, পথের queue দেখে না। পথের queue সামলানো congestion control-এর কাজ (পর্ব ৩২): loss-ভিত্তিক algorithm queue ভরা পর্যন্ত cwnd বাড়ায়, তাই bloated buffer-এ খারাপ করে; BBR-এর মতো algorithm queue খালি রাখার চেষ্টা করে। Router-এ CoDel আর fq_codel-এর মতো queue ব্যবস্থাপনা (AQM) packet-কে বেশিক্ষণ লাইনে থাকতে দেয় না। আর প্রেরকের নিজের ভেতরেও send buffer অকারণে বড় হলে অ্যাপের লেখা ডেটা kernel-এ জমে থাকে; Linux-এর TCP_NOTSENT_LOWAT option দিয়ে এই না-পাঠানো অংশ ছোট রাখা যায়, যাতে অ্যাপ শেষ মুহূর্তে বেশি জরুরি ডেটা আগে পাঠাতে পারে।

বাংলাদেশের ঘরে ঘরে ছোট Wi-Fi router আর ভাগ করা broadband সংযোগে সন্ধ্যার ভিড়ে “ডাউনলোড চলছে তাই ভিডিও কল আটকাচ্ছে” — এই অভিজ্ঞতার একটি অংশ সম্ভবত এই সমস্যারই রূপ, যদিও প্রতিটি ক্ষেত্রে মেপে দেখা ছাড়া নিশ্চিত বলা যায় না।

মূল উপলব্ধি: window-এর সঠিক আকার প্রায় BDP; বেশি হলে লাভ নেই, আর পথে বা প্রেরকে বাড়তি ডেটা জমে latency বাড়ে — flow control একা এটা সামলাতে পারে না।

প্র ০৫TCP-র নিজেরই flow control আছে, তবু HTTP/2 আর QUIC আবার stream-স্তরে flow control বানাল কেন? এতে কি নতুন সমস্যাও এসেছে?

HTTP/1.1-এ ব্রাউজার একটি সাইটের জন্য কয়েকটি আলাদা TCP সংযোগ খুলত, প্রতিটিতে একটি করে request; প্রতিটির নিজস্ব rwnd, তাই একটি ধীর download অন্যকে আটকাত না। HTTP/2 (প্রথমে RFC 7540, ২০১৫; বর্তমানে RFC 9113) সব request একটি TCP সংযোগে multiplex করে। এখন TCP-র rwnd পুরো সংযোগের: সার্ভারের দিকে কোনো একটি stream-এর ডেটা অ্যাপ ধীরে নিলে — ধরুন একটি বড় upload, যা সার্ভার ধীর ডিস্কে লিখছে — সেটি সংযোগের receive buffer ভরিয়ে পুরো সংযোগের window ০ করে দিতে পারত, ফলে একই সংযোগের ছোট, জরুরি request-ও আটকে যেত।

তাই HTTP/2 প্রতিটি stream-এর আর পুরো সংযোগের আলাদা window রাখে, শুরুর মান ৬৫,৫৩৫ byte, আর প্রাপক WINDOW_UPDATE frame পাঠিয়ে credit বাড়ায়। QUIC (RFC 9000, ২০২১) একই ধারণা MAX_STREAM_DATA আর MAX_DATA frame-এ নেয়, আর যেহেতু QUIC-এর নিচে TCP নেই, এটিই তার একমাত্র flow control. Stream-স্তরে প্রাপক একটি stream-কে থামিয়ে অন্যগুলো চালু রাখতে পারে।

নতুন সমস্যাও এলো, আর মজার ব্যাপার হলো সেটি TCP-র পুরোনো সমস্যারই পুনরাবৃত্তি। HTTP/2-এর শুরুর ৬৪ KiB stream window অংশ ৭-এর হিসাবের মতোই throughput-এর ছাদ টানে: কোনো implementation window না বাড়ালে বড় RTT-র পথে একটি stream-এর upload সেই W ÷ RTT-তে আটকে যায়, নিচের TCP-র window যত বড়ই হোক। তাই ভালো implementation-গুলো সংযোগের শুরুতেই বড় window ঘোষণা করে, কেউ কেউ autotuning-এর মতো মাপজোখ করে window বাড়ায়। আরেকটি সূক্ষ্মতা: দুই স্তরের flow control একসাথে থাকলে সীমা টানে যেটি ছোট, আর debug করার সময় কোন স্তরে আটকেছে খুঁজে বের করা কঠিন হয়। Flow control-এর ঘোষণা দিয়ে resource আটকে রাখার আক্রমণের ধারণাও (প্রশ্ন ২) stream-স্তরে নতুন রূপে আসে, তাই implementation-গুলোকে সীমা বসাতে হয়।

মূল উপলব্ধি: Flow control যে স্তরে multiplexing হয়, সেই স্তরেই দরকার; আর প্রতিটি নতুন স্তরে window/RTT-র পুরোনো অঙ্ক আবার প্রযোজ্য হয়।

অনুশীলন

খাতা-কলম আর ক্যালকুলেটর নিন — প্রতিটি সংখ্যার একক (byte, bit, s) লিখে হিসাব করুন।

  1. হাতে হিসাব (rwnd): Host B-র RcvBuffer = ৮,১৯২ byte. এক মুহূর্তে LastByteRcvd = ১৩,০০০ আর LastByteRead = ৯,০০০. (ক) B কত rwnd ঘোষণা করবে, আর ACK নম্বর কত লিখবে? (খ) Host A-র কাছে LastByteAcked = ১৩,০০০, LastByteSent = ১৪,৫০০. A আর কত byte পাঠাতে পারবে? (গ) এরপর B-র অ্যাপ ২,০০০ byte পড়ল আর পথের ১,৫০০ byte পৌঁছাল। নতুন rwnd কত?

    (ক) জমা = ১৩,০০০ − ৯,০০০ = ৪,০০০; rwnd = ৮,১৯২ − ৪,০০০ = ৪,১৯২ byte. ACK নম্বর = পরের প্রত্যাশিত byte = ১৩,০০১.

    (খ) পথে = ১৪,৫০০ − ১৩,০০০ = ১,৫০০; শর্ত LastByteSent − LastByteAcked ≤ ৪,১৯২, তাই আর পাঠানো যায় ৪,১৯২ − ১,৫০০ = ২,৬৯২ byte (LastByteSent সর্বোচ্চ ১৩,০০০ + ৪,১৯২ = ১৭,১৯২)।

    (গ) LastByteRcvd = ১৪,৫০০, LastByteRead = ১১,০০০; জমা ৩,৫০০; rwnd = ৮,১৯২ − ৩,৫০০ = ৪,৬৯২ byte. যাচাই: A যদি (খ)-এর পুরো ২,৬৯২ byte পাঠিয়ে দেয় আর অ্যাপ আর না পড়ে, জমা হবে ১৭,১৯২ − ১১,০০০ = ৬,১৯২ ≤ ৮,১৯২ — উপচায় না।

  2. হাতে হিসাব (window scaling): একটি ১ Gbps পথ, RTT ৮০ ms. (ক) Window scaling ছাড়া (৬৫,৫৩৫ byte) সর্বোচ্চ throughput কত? এই গতিতে ১০০ MB (১০৮ byte) ফাইল পাঠাতে কত সময় লাগবে? (খ) BDP কত, আর link পূর্ণ ব্যবহারে ন্যূনতম shift count কত? (গ) সেই shift-এ header-এর window ঘরে কত লিখলে window অন্তত BDP হয়?

    (ক) W ÷ RTT = ৬৫,৫৩৫ × ৮ ÷ ০.০৮ = ৬৫,৫৩,৫০০ bit/s ≈ ৬.৫৫ Mbps (link-এর ০.৬৬%). সময় = ৮ × ১০৮ ÷ ৬৫,৫৩,৫০০ ≈ ১২২ s — দুই মিনিটের বেশি।

    (খ) BDP = ১০৯ × ০.০৮ ÷ ৮ = ১,০০,০০,০০০ byte (১০ MB). প্রয়োজনীয় গুণ = ১,০০,০০,০০০ ÷ ৬৫,৫৩৫ ≈ ১৫২.৬; 27 = ১২৮ কম, 28 = ২৫৬ যথেষ্ট — S = ৮.

    (গ) Window ঘর ≥ ১,০০,০০,০০০ ÷ ২৫৬ = ৩৯,০৬২.৫, অর্থাৎ ৩৯,০৬৩ (৩৯,০৬৩ × ২৫৬ = ১,০০,০০,১২৮ byte). তখন একই ফাইল তাত্ত্বিকভাবে প্রায় ০.৮ s-এ যায় (১০৮ × ৮ ÷ ১০৯), শুরুর handshake আর slow start (পর্ব ৩২) বাদে।

  3. আউটপুট অনুমান করুন: অংশ ৪-এর simulator-এ appReads বদলে [1000, 1000, 0, 0, 0, 0, 0, 3000, 2000, 2000, 2000, 2000] করলে (অ্যাপ ধাপ ৩ থেকে ৭ পর্যন্ত কিছু পড়ে না, ধাপ ৮-এ ৩,০০০ পড়ে) কোন কোন ধাপে probe যাবে, কোন ধাপে পাঠানো আবার শুরু হবে, আর মোট কত ধাপ লাগবে? আগে হাতে বের করুন, তারপর চালিয়ে মেলান।

    ধাপ ১–৩ আগের মতোই, ধাপ ৩-এ rwnd = ০. Probe-এর ব্যবধান ১, ২, ৪ করে দ্বিগুণ হয়: probe ধাপ ৪-এ (উত্তর rwnd ০, পরেরটি ৪ + ২ = ৬), ধাপ ৬-এ (উত্তর আবার ০, পরেরটি ৬ + ৪ = ১০)। ধাপ ৮-এ অ্যাপ ৩,০০০ পড়ে, window update (rwnd ৩,০০০) হারায়; ধাপ ৯-এ অ্যাপ বাকি ১,০০০ পড়ে, buffer খালি — কিন্তু প্রেরক জানে না। ধাপ ১০-এর probe-এর উত্তরে rwnd = ৪,০০০, আর ধাপ ১১-এ প্রেরক বাকি ৩,০০০ byte একবারে পাঠায় (LastByteSent ৯,০০০)। ধাপ ১১-এ অ্যাপ ২,০০০, ধাপ ১২-এ ১,০০০ পড়ে — মোট ১২ ধাপ.

    চালালে ধাপ ১০-এর সারি হয় 10 0 6000 0 6000 | 0 6000 6000 4000 | persist timer fired: window probe -> reply rwnd=4000, resume. শিক্ষা: exponential backoff-এর কারণে window আসলে খুলেছিল ধাপ ৮-এ, কিন্তু প্রেরক জানল ধাপ ১০-এ — হারানো update-এর দাম এখানে দুই RTT.

  4. হিসাব আর কোড-বদল (silly window): (ক) একটি terminal অ্যাপ প্রতিটি কী-চাপে ১ byte পাঠায়, প্রতিটি segment-এ ২০ byte TCP আর ২০ byte IPv4 header. Efficiency কত? (খ) প্রাপকের RcvBuffer = ৪,০৯৬ byte আর MSS = ১,৪৬০ হলে Clark-এর নিয়মে খালি জায়গা কমপক্ষে কত হলে window ০ থেকে বাড়ানো ঘোষণা হবে? (গ) অংশ ৬-এর প্রোগ্রামে এই মানগুলো বসাতে কোন লাইন কীভাবে বদলাবেন, আর READ = 20, TICKS = 200 রেখে Clark-সহ লাইনে কয়টি segment আসবে?

    (ক) ১ ÷ (১ + ৪০) = ১/৪১ ≈ ২.৪৪% — প্রতি byte-এর জন্য ৪১ byte তারে যায়।

    (খ) সীমা = min(MSS, RcvBuffer ÷ ২) = min(১,৪৬০, ২,০৪৮) = ১,৪৬০ byte.

    (গ) const BUF দিয়ে শুরু হওয়া লাইনটি বদলে লিখুন const BUF = 4096, MSS = 1460, HDR = 40, READ = 20, TICKS = 200;. Buffer প্রথমে পুরো ভরা ধরা হয়, প্রতি tick-এ ২০ byte খালি হয়, ১,৪৬০ ÷ ২০ = ৭৩ tick-এ সীমা ছোঁয়। Segment যাবে t = ৭৩ আর ১৪৬-এ; t = ২১৯ সীমার বাইরে — তাই ২টি segment, ২,৯২০ byte ডেটা, efficiency ২,৯২০ ÷ ৩,০০০ ≈ ৯৭.৩%. (প্রোগ্রাম শুধু শুরুর তিনটি ঘটনা দেখায়, এখানে দুটিই দেখাবে।)

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

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

ক্রমTCP-তে zero window থেকে স্বাভাবিক প্রবাহে ফেরা — ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রাপকের অ্যাপ পড়া বন্ধ রাখে, buffer ভরে যায়
  2. প্রাপক ACK-এ rwnd = 0 ঘোষণা করে, প্রেরক থামে
  3. প্রেরক persist timer চালু করে
  4. অ্যাপ পড়ে, প্রাপকের পাঠানো window update পথে হারায়
  5. Persist timer ফুরায়, প্রেরক window probe পাঠায়
  6. Probe-এর উত্তরে নতুন rwnd আসে, প্রেরক আবার পাঠায়

একটি TCP প্রাপকের RcvBuffer = ৬,০০০ byte, LastByteRcvd = ৯,৫০০ আর LastByteRead = ৭,০০০. প্রাপক কত rwnd ঘোষণা করবে?

  • ২,৫০০ byte
  • ৬,০০০ byte
  • ৩,৫০০ byte
  • ৩,৫০০ − ৭,০০০ = ঋণাত্মক, তাই ০

একটি TCP সংযোগে window scale shift count = ৬ ঠিক হয়েছে। একটি segment-এর header-এর Window ঘরে লেখা ১,০০০. প্রেরক আসল window কত ধরবে?

  • ১,০০০ byte
  • ৬৪,০০০ byte
  • ৬,০০০ byte
  • ৬,৫৫,৩৬,০০০ byte

Window scaling ছাড়া (সর্বোচ্চ window ৬৫,৫৩৫ byte) RTT ৫০ ms-এর একটি ১ Gbps পথে একটি TCP সংযোগের সর্বোচ্চ throughput প্রায় কত?

  • প্রায় ১০.৫ Mbps
  • ১ Gbps
  • প্রায় ১.৩১ Mbps
  • প্রায় ১০.৫ kbps

একটি TCP প্রেরক প্রাপকের কাছ থেকে rwnd = 0 পেয়েছে, আর তার হাতে পাঠানোর ডেটা আছে। মানদণ্ড অনুযায়ী প্রেরক কী করে?

  • কিছুক্ষণ পর সংযোগ বন্ধ করে দেয়
  • rwnd উপেক্ষা করে ধীরে ধীরে পাঠাতে থাকে
  • প্রাপকের window update না আসা পর্যন্ত চুপচাপ অপেক্ষা করে
  • Persist timer চালু করে, নির্দিষ্ট ব্যবধানে window probe পাঠায়

একটি TCP প্রাপকের RcvBuffer = ৮,০০০ byte, MSS = ১,৪৬০. বর্তমানে window ০ ঘোষিত। Clark-এর silly window প্রতিকার অনুযায়ী খালি জায়গা কমপক্ষে কত হলে প্রাপক window বাড়ানোর ঘোষণা দেবে?

  • ১ byte
  • ৪,০০০ byte
  • ১,৪৬০ byte
  • ৮,০০০ byte

একটি Linux প্রোগ্রাম setsockopt(fd, SOL_SOCKET, SO_RCVBUF, …) দিয়ে ৫০,০০০ চাইল (net.core.rmem_max = ২,১২,৯৯২), তারপর getsockopt দিয়ে পড়ল। কোন মান আসবে বলে আশা করা যায়?

  • ৫০,০০০
  • ১,০০,০০০
  • ২,১২,৯৯২
  • ৪,২৫,৯৮৪

নিচের কোনটি TCP congestion control-এর নয়, flow control-এর বৈশিষ্ট্য?

  • সীমাটি প্রাপক header-এর একটি ঘরে স্পষ্ট সংখ্যা দিয়ে জানায়
  • Packet loss দেখে প্রেরক পাঠানোর হার কমায়
  • পথের router-এর queue উপচানো ঠেকায়
  • সংযোগের শুরুতে slow start-এ ধীরে ধীরে বাড়ে

শব্দকোষ

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

মনে রাখুন

  1. Flow control প্রাপকের buffer রক্ষা করে (rwnd, প্রাপক জানায়); congestion control নেটওয়ার্ক রক্ষা করে (cwnd); প্রেরকের window = min(rwnd, cwnd).
  2. rwnd = RcvBuffer − (LastByteRcvd − LastByteRead); প্রেরকে LastByteSent − LastByteAcked ≤ rwnd — এই শর্ত মানলে buffer কখনো উপচায় না।
  3. Window ০ হলে persist timer আর window probe deadlock ঠেকায়; probe-এর ব্যবধান দ্বিগুণ হয়ে বাড়ে।
  4. Silly window syndrome: প্রাপকে Clark — min(MSS, buffer/2) খালি না হলে window ০; প্রেরকে Nagle আর পূর্ণ MSS-এর অপেক্ষা।
  5. Throughput ≤ min(R, W ÷ RTT); ১৬-bit window-এ ৬৪ KiB, window scaling (RFC 7323) shift ≤ ১৪, সর্বোচ্চ প্রায় ১ GiB; buffer ≈ BDP. Linux-এ autotuning, আর SO_RCVBUF দ্বিগুণ হয়।

পরের পর্বে: প্রাপক জায়গা দিলেও পথের রাস্তায় জ্যাম থাকতে পারে। প্রেরক কীভাবে অনুমান করে নেটওয়ার্ক কতটা নিতে পারবে — slow start, AIMD, CUBIC আর BBR, আর cwnd-এর ওঠানামা।

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

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