Loading content...
The sender in a sliding window protocol carries the heavier burden. It must orchestrate multiple simultaneous transmissions, track acknowledgments, manage retransmission timers, maintain buffers for potentially unacknowledged frames, and respond correctly to timeout events—all while presenting a simple interface to upper layers.
Understanding the sender window in depth is essential because:
This page dissects every aspect of the sender window.
By the end of this page, you will master sender window state variables and their semantics, buffer allocation and management strategies, retransmission timer implementation, the complete sender state machine, and how to analyze sender behavior under various network conditions.
The sender maintains a precise set of state variables that together define its operational state. While implementations may vary in naming conventions, the following canonical variables capture essential sender state:
Core Window Variables:
| Variable | Description | Valid Range |
|---|---|---|
| Sₙ (Send Next) | Sequence number for the next new frame to transmit | [Sᵦ, Sᵦ + Wₛ) |
| Sᵦ (Send Base) | Sequence number of the oldest unacknowledged frame | Advances with ACKs |
| Wₛ (Send Window) | Maximum number of outstanding (unacknowledged) frames | Fixed or receiver-advertised |
Derived Quantities:
| Quantity | Formula | Meaning |
|---|---|---|
| Frames in flight | Sₙ - Sᵦ | Currently transmitted but unacknowledged |
| Window available | Wₛ - (Sₙ - Sᵦ) | Slots available for new transmissions |
| Can send? | Sₙ < Sᵦ + Wₛ | True if window has capacity |
Critical Invariant:
The sender must always maintain:
Sᵦ ≤ Sₙ ≤ Sᵦ + Wₛ
Sequence Number Arithmetic:
When sequence numbers wrap (modular arithmetic over the sequence space), comparisons require care:
// For n-bit sequence numbers (range 0 to 2^n - 1)
seq_lt(a, b) = ((b - a) & MASK) < (1 << (n-1)) // a < b (modularly)
seq_le(a, b) = (a == b) || seq_lt(a, b) // a ≤ b (modularly)
This handles wrap-around correctly, treating the sequence space as circular. We'll see why this matters when analyzing sequence number requirements for different protocol variants.
TCP uses slightly different nomenclature: SND.UNA (= Sᵦ), SND.NXT (= Sₙ), SND.WND (= Wₛ). HDLC uses V(S) for Sₙ. The concepts remain identical—only names change.
The sender must retain copies of all transmitted-but-unacknowledged frames to enable retransmission. This retransmission buffer (or send buffer) is a critical component affecting both correctness and performance.
Buffer Requirements:
Buffer capacity ≥ Wₛ × (maximum frame size)
For a window of 127 frames with 1500-byte frames: 127 × 1500 = 190,500 bytes per connection.
Buffer Organization:
Two common implementations exist:
1. Circular Array (Ring Buffer):
buffer[MAX_SEQ_NUM + 1] // Array of frame slots
index = sequence_number % (MAX_SEQ_NUM + 1)
2. Linked Data Structure:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
Sender Buffer: Ring Buffer Implementation━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Data Structure:──────────────────────────────────────────────────────────────────────────────struct SenderBuffer { Frame frames[MAX_SEQ + 1]; // Frame storage (circular) bool in_use[MAX_SEQ + 1]; // Slot occupancy flags Time tx_time[MAX_SEQ + 1]; // Transmission timestamps int retry_count[MAX_SEQ + 1]; // Retransmission attempt counters SeqNum base; // Sᵦ - oldest unACKed SeqNum next; // Sₙ - next to send int window_size; // Wₛ} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Operation: buffer_frame(data) → returns sequence number assigned━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ function buffer_frame(data): // Check if window has capacity if (next - base) >= window_size: return BUFFER_FULL // Flow control back-pressure seq = next idx = seq mod (MAX_SEQ + 1) // Map to circular buffer frames[idx] = create_frame(seq, data) in_use[idx] = true tx_time[idx] = 0 // Not yet transmitted retry_count[idx] = 0 next = next + 1 return seq ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Operation: mark_acked(ack_num) [For cumulative ACK]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ function mark_acked(ack_num): // Cumulative ACK: all frames with seq < ack_num are acknowledged if not seq_in_range(ack_num, base, next): return INVALID_ACK // Out of expected range // Release buffers for acknowledged frames while base < ack_num: idx = base mod (MAX_SEQ + 1) in_use[idx] = false frames[idx] = null // Free memory base = base + 1 return SUCCESS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Operation: get_frame_for_retransmit(seq) → returns frame copy━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ function get_frame_for_retransmit(seq): idx = seq mod (MAX_SEQ + 1) if not in_use[idx] or frames[idx].seq != seq: return ERROR // Frame not buffered (bug!) retry_count[idx] = retry_count[idx] + 1 if retry_count[idx] > MAX_RETRIES: return GIVEUP // Too many retries - report failure return frames[idx] // Return copy for retransmissionIf the sender buffer fills completely (all Wₛ slots occupied) and no ACKs arrive, the sender blocks. This is correct behavior—it's flow control working. But if the network or receiver has failed, this becomes a deadlock. Timers and connection aborts prevent indefinite blocking.
Buffer Lifecycle:
┌──────────┐ buffer_frame() ┌──────────┐ transmit() ┌──────────┐
│ Empty │ ──────────────────> │ Buffered │ ───────────────> │ In-Use │
└──────────┘ └──────────┘ └──────────┘
│
ack_received()
│
v
┌──────────┐
│ Released │
└──────────┘
At any time:
Retransmission timers are the sender's mechanism for detecting and recovering from frame or ACK loss. Timer design profoundly affects protocol performance:
Timer Strategies:
Different sliding window protocols use different timer approaches:
1. Single Timer (Go-Back-N style):
2. Per-Frame Timers (Selective Repeat style):
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
Timer Strategy Comparison━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Strategy 1: Single Window Timer (Go-Back-N)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ State: Timer retransmit_timer bool timer_running = false On Frame Transmission: if not timer_running and (base != next): // Frames outstanding start_timer(retransmit_timer, TIMEOUT) timer_running = true On ACK Received: if base == next: // All frames acknowledged stop_timer(retransmit_timer) timer_running = false else: restart_timer(retransmit_timer, TIMEOUT) // Reset for oldest On Timeout: // Retransmit ALL frames from base to next-1 (expensive!) for seq = base to next - 1: retransmit(frames[seq]) restart_timer(retransmit_timer, TIMEOUT) Pros: Simple, single timerCons: Wastes bandwidth retransmitting correctly-received frames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Strategy 2: Per-Frame Timers (Selective Repeat)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ State: Timer frame_timer[MAX_SEQ + 1] // One timer per sequence number On Frame Transmission (seq): start_timer(frame_timer[seq], TIMEOUT) On ACK Received (seq): // Individual ACK stop_timer(frame_timer[seq]) On Timeout (seq): // Specific frame timed out retransmit(frames[seq]) start_timer(frame_timer[seq], TIMEOUT) Pros: Only retransmit lost frames, efficientCons: Many timers to manage (typically use timer wheel) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Timer Wheel Implementation (for many timers efficiently):━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Slots: [0] [1] [2] [3] [4] [5] [6] [7] ... [WHEEL_SIZE-1] ^ | current - Each slot = time granule (e.g., 10ms) - Timers inserted at (current + ticks) mod WHEEL_SIZE - Per-tick: advance current, fire all timers in that slot - O(1) insert, O(1) per-tick processing - Used in Linux kernel, high-performance network stacksTimeout Value Selection:
The timeout value (RTO - Retransmission Timeout) should exceed the expected RTT but not by too much:
RTO = RTT + safety_margin
In data link protocols, RTT is often predictable (fixed-distance link), so static timeouts suffice. In transport protocols (TCP), RTT varies dramatically, requiring adaptive timeout calculation.
For Data Link Layer (typical values):
Exponential Backoff:
After repeated timeouts (suggesting persistent congestion or failure), doubling the RTO prevents congestion collapse:
RTO (retry n) = min(RTO × 2^n, MAX_RTO)
After successful transmission, RTO resets to base value.
When a frame is retransmitted and then ACKed, should that ACK update RTT estimates? No—we can't know if the ACK was for the original or retransmission (ambiguity). Karn's algorithm says: never use retransmitted segments for RTT measurement. This prevents RTT underestimation during congestion.
Let's now define the complete sender state machine, integrating all components: buffering, transmission, acknowledgment processing, and timer handling.
Events the Sender Handles:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
Sliding Window Sender: Complete State Machine━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Sender State Variables:────────────────────────────────────────────────────────────────────────────── Sᵦ : SeqNum // Send base (oldest unACKed) Sₙ : SeqNum // Send next (next to transmit) Wₛ : int // Window size buffer[] : Frame[Wₛ] // Retransmission buffer timer : Timer // Retransmission timer (or per-frame timers) MAX_SEQ : int // Maximum sequence number (2^n - 1) Helper Functions:────────────────────────────────────────────────────────────────────────────── inc(seq) = (seq + 1) mod (MAX_SEQ + 1) // Increment modularly in_window(seq, base, size) = // Check if seq is in window 0 ≤ (seq - base) mod (MAX_SEQ + 1) < size ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━EVENT 1: Upper Layer Request (send data)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ procedure on_data_request(data): // Check window capacity outstanding = (Sₙ - Sᵦ) mod (MAX_SEQ + 1) if outstanding >= Wₛ: // Window full - cannot accept more data queue_for_later(data) // Buffer in upper-layer queue return BLOCKED // Create and buffer frame frame = new Frame() frame.seq = Sₙ frame.data = data frame.checksum = compute_checksum(frame) buffer[Sₙ mod Wₛ] = frame // Store for retransmission // Transmit physical_send(frame) // Start timer if this is first outstanding frame if Sᵦ == Sₙ: start_timer(timeout_value) // Advance Sₙ Sₙ = inc(Sₙ) return SUCCESS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━EVENT 2: ACK Received (cumulative acknowledgment)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ procedure on_ack_received(ack): // Validate ACK is within expected range // ACK 'n' means: "all frames with seq < n received" if not in_window(ack, Sᵦ, Wₛ + 1): // Duplicate or invalid ACK - ignore return IGNORED if ack == Sᵦ: // Duplicate ACK for already-acknowledged frame return DUPLICATE // Slide window forward old_base = Sᵦ Sᵦ = ack // Release buffers for acknowledged frames for seq = old_base to ack - 1: // (modular iteration) release_buffer(seq mod Wₛ) // Manage timer if Sᵦ == Sₙ: // All outstanding frames acknowledged stop_timer() else: // Reset timer for new oldest frame restart_timer(timeout_value) // Potentially unblock upper layer signal_window_available() return SUCCESS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━EVENT 3: Timeout ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ procedure on_timeout(): // [GO-BACK-N VARIANT] // Retransmit ALL outstanding frames for seq = Sᵦ to Sₙ - 1: // (modular iteration) frame = buffer[seq mod Wₛ] physical_send(frame) // [SELECTIVE REPEAT VARIANT - would be] // Only retransmit the specific timed-out frame // frame = buffer[timed_out_seq mod Wₛ] // physical_send(frame) // Restart timer (possibly with exponential backoff) timeout_value = min(timeout_value * 2, MAX_TIMEOUT) start_timer(timeout_value) return ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━EVENT 4: NAK Received (for protocols supporting NAK)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ procedure on_nak_received(nak_seq): // NAK indicates specific frame was lost/corrupted if not in_window(nak_seq, Sᵦ, Wₛ): return IGNORED // Invalid NAK // Immediately retransmit the NAK'd frame frame = buffer[nak_seq mod Wₛ] physical_send(frame) // Note: Some protocols reset timer, others don't return SUCCESSReal implementations are event-driven—responding to interrupts or callbacks for frame arrival, timer expiration, and upper-layer requests. The state machine above captures the logical behavior; actual code involves interrupt handlers, kernel threads, or async I/O frameworks.
Let's trace the sender window through a detailed scenario, observing how state variables evolve.
Scenario Parameters:
| Time | Event | Sᵦ | Sₙ | Outstanding | Available | Action |
|---|---|---|---|---|---|---|
| T0 | Initial | 0 | 0 | 0 | 4 | — |
| T1 | Send data | 0 | 1 | 1 | 3 | Transmit F0, start timer |
| T2 | Send data | 0 | 2 | 2 | 2 | Transmit F1 |
| T3 | Send data | 0 | 3 | 3 | 1 | Transmit F2 (will be lost!) |
| T4 | Send data | 0 | 4 | 4 | 0 | Transmit F3 |
| T5 | Send data | 0 | 4 | 4 | 0 | BLOCKED - window full |
| T6 | ACK(1) arrives | 1 | 4 | 3 | 1 | Slide window, restart timer |
| T7 | Send data | 1 | 5 | 4 | 0 | Transmit F4 |
| T8 | ACK(2) arrives | 2 | 5 | 3 | 1 | Slide window (F0,F1 done) |
| T9 | Send data | 2 | 6 | 4 | 0 | Transmit F5 |
| T10 | Waiting... | 2 | 6 | 4 | 0 | No ACK for F2... |
| T11 | TIMEOUT | 2 | 6 | 4 | 0 | Retransmit F2-F5 (GBN) |
Analysis of Key Moments:
T5 - Blocking: With 4 frames outstanding and Wₛ = 4, no slots remain. The sender must wait—this is flow control working correctly. Without this limit, the sender would overwhelm the receiver.
T6 - Window Slide: ACK(1) means "received all frames up to and including frame 0." Frame 0 is acknowledged, so:
T11 - Timeout Recovery: Frame 2 was lost. The receiver couldn't acknowledge F2 or anything after it (cumulative ACK semantics in Go-Back-N). After timeout:
The distinction between these strategies is fundamental to the next modules.
Imagine the sequence number space as a number line. The window is a fixed-width 'lens' that the sender looks through. Only frames visible through this lens can be transmitted. As ACKs arrive, the lens slides right, revealing new sequence numbers for transmission while hiding acknowledged ones.
Several implementation choices affect sender performance:
1. Buffer Copying vs. Zero-Copy:
High-performance stacks (DPDK, kernel bypass) emphasize zero-copy.
2. Transmission Pacing:
Even if the window allows sending W frames instantly, bursty transmission can cause:
Pacing spreads transmissions over time:
inter_frame_gap = window_size × RTT / frames_per_window
TCP implementations (like Linux's FQ scheduler) use pacing for fairness.
3. ACK Processing Efficiency:
Each ACK triggers:
Batching ACK processing (handling multiple ACKs before acting) reduces overhead.
Even with optimal sender implementation, throughput is limited by: min(sender rate, link bandwidth, receiver processing rate). Identifying the bottleneck requires measuring at multiple points—the sender may be fast but blocked waiting for window advancement.
We've examined the sender window in comprehensive detail. Let's consolidate the key concepts:
What's Next:
With the sender window understood, we turn to the receiver window. The receiver's role complements the sender's—receiving frames, detecting losses or errors, generating acknowledgments, and managing its own buffering. The receiver's design choices (cumulative vs. selective ACKs, buffering out-of-order frames) define whether the protocol is Go-Back-N or Selective Repeat.
You now understand the sender window in depth—its state variables, buffering requirements, timer mechanisms, and complete state machine. This knowledge forms the foundation for analyzing any sliding window protocol implementation. Next, we explore the receiver's perspective.