17  Transport Protocols: Hands-On Lab

In 60 Seconds

This hands-on ESP32 lab builds a reliable message transport system from scratch. You will implement the core mechanisms that make TCP work – sequence numbers for ordering, acknowledgments for delivery confirmation, timeouts with exponential backoff for loss recovery, and real-time statistics tracking throughput and retransmission rates – using LEDs to visualize ACK/NACK/timeout states.

Key Concepts
  • TCP Server Lab Setup: Python socket server: socket(AF_INET, SOCK_STREAM) → setsockopt(SO_REUSEADDR) → bind((host, port)) → listen(backlog) → accept() loop → recv/send → close()
  • UDP Echo Lab: Python UDP: socket(AF_INET, SOCK_DGRAM) → bind((host, port)) → recvfrom(buffer_size) → sendto(data, addr) → loop; tests basic UDP datagram exchange
  • Latency Measurement Lab: Use time.time() or time.perf_counter() around send/recv pairs to measure RTT; compare TCP (3-way handshake overhead on first measurement) vs UDP (no connection overhead)
  • Packet Loss Simulation: tc netem: sudo tc qdisc add dev lo root netem loss 5% → test protocol behavior under 5% loss → observe retransmission behavior in TCP, silence in UDP
  • Throughput Test: iperf3 -c server -u -b 10M (UDP, 10 Mbps target) vs iperf3 -c server (TCP); compare achieved throughput and CPU utilization between protocols
  • Wireshark Lab Exercise: Capture TCP 3-way handshake: filter “tcp.flags.syn==1”; capture UDP exchange: filter “udp.port==5683”; analyze MQTT traffic: filter “tcp.port==1883”
  • CoAP Lab: Libcoap coap-client CLI: coap-client -m get coap://server/sensors/temp; tests CoAP over UDP with confirmable/non-confirmable options
  • SSL/TLS Lab: Python ssl.wrap_socket() for TLS over TCP; openssl s_client for testing DTLS; capture and analyze TLS handshake in Wireshark (TLS dissector)

17.1 Learning Objectives

By the end of this lab, you will be able to:

  • Implement message sequencing: Use sequence numbers to detect missing, duplicate, and out-of-order messages
  • Build an acknowledgment system: Create ACK/NACK responses to confirm or reject message delivery
  • Design timeout and retry logic: Handle message loss with configurable timeouts and exponential backoff
  • Analyze transport layer statistics: Calculate throughput, loss rate, and retransmission overhead in real-time

In this hands-on lab, you will build and test a reliable message delivery system on an ESP32. You will observe the difference between reliable delivery (TCP) and fast delivery (UDP) firsthand. You will identify which protocol fits which scenario – it is like comparing express mail with a postcard – and you will measure the trade-offs in real time.

“In this lab, you build TCP-like reliability from scratch on an ESP32,” said Max the Microcontroller. “Sequence numbers, acknowledgments, timeouts, retransmissions – all the building blocks that make data delivery reliable.”

“The LEDs make it visual,” added Sammy the Sensor. “Green lights up for a successful acknowledgment. Red lights up for a timeout or negative acknowledgment. Yellow blinks during transmission. Blue lights when a message is received. You can literally see the transport protocol in action!”

“Exponential backoff is the clever part,” explained Lila the LED. “When a message fails, you wait 500 ms before retrying. If it fails again, wait 1,000 ms. Then 2,000 ms. Then 4,000 ms. This prevents the network from getting overwhelmed with retries during congestion.”

“By the end, you will be able to explain why TCP works the way it does,” said Bella the Battery. “Building these mechanisms yourself gives you intuition that reading a textbook never can. You will identify exactly why sequence numbers matter and diagnose what happens without timeout logic.”

17.2 Lab: Build a Reliable Message Transport System

This hands-on lab demonstrates core transport layer concepts by building a reliable message delivery system on an ESP32. You will implement the fundamental mechanisms that make protocols like TCP reliable: sequence numbers, acknowledgments, timeouts, and retransmissions.

17.2.1 Components Needed

Component Quantity Purpose
ESP32 DevKit 1 Microcontroller simulating sender and receiver
Green LED 1 ACK received indicator
Red LED 1 NACK/Timeout indicator
Yellow LED 1 Transmission in progress
Blue LED 1 Message received
220 ohm Resistors 4 Current limiting for LEDs
Breadboard 1 Circuit assembly
Jumper Wires Several Connections

17.2.2 Wokwi Simulator

Use the embedded simulator below to build and test your reliable transport system. Click “Start Simulation” to begin.

17.2.3 Circuit Diagram

ESP32 breadboard circuit with four LEDs: green LED on GPIO 4 (ACK indicator), red LED on GPIO 2 (NACK/timeout indicator), yellow LED on GPIO 5 (transmit indicator), and blue LED on GPIO 18 (receive indicator), each connected through a 220 ohm resistor to GND
Figure 17.1: Circuit diagram showing ESP32 connected to four LEDs (green on GPIO 4, red on GPIO 2, yellow on GPIO 5, blue on GPIO 18) each with a 220 ohm current-limiting resistor to GND

17.2.4 Complete Code

Copy this code into the Wokwi editor and upload to the ESP32:

/*
 * Reliable Message Transport System for ESP32
 *
 * This program demonstrates core transport layer concepts:
 * - Message sequencing with sequence numbers
 * - Acknowledgment system (ACK/NACK)
 * - Timeout and retry mechanism with exponential backoff
 * - Simulated packet loss for testing reliability
 * - Real-time throughput and loss statistics
 *
 * LED Indicators:
 * - Green (GPIO 4):  ACK received - message delivered successfully
 * - Red (GPIO 2):    NACK/Timeout - message delivery failed
 * - Yellow (GPIO 5): Transmitting - message in transit
 * - Blue (GPIO 18):  Received - new message arrived
 *
 * Transport Layer Concepts Demonstrated:
 * 1. Sequence Numbers: Each message has a unique ID for ordering
 * 2. Acknowledgments: Receiver confirms each message
 * 3. Timeouts: Sender waits limited time for ACK
 * 4. Retransmission: Failed messages are resent
 * 5. Statistics: Track throughput and reliability
 */

// ============= PIN DEFINITIONS =============
#define LED_ACK       4   // Green LED - ACK received
#define LED_NACK      2   // Red LED - NACK/Timeout
#define LED_TRANSMIT  5   // Yellow LED - Transmission in progress
#define LED_RECEIVED  18  // Blue LED - Message received

// ============= TRANSPORT CONFIGURATION =============
#define INITIAL_TIMEOUT_MS    500   // Initial timeout for ACK (milliseconds)
#define MAX_TIMEOUT_MS        4000  // Maximum timeout after backoff
#define MAX_RETRIES           3     // Maximum retransmission attempts
#define PACKET_LOSS_PERCENT   20    // Simulated packet loss rate (0-100)
#define WINDOW_SIZE           1     // Stop-and-wait (1), increase for sliding window

// ============= MESSAGE TYPES =============
#define MSG_DATA    0x01  // Data message
#define MSG_ACK     0x02  // Positive acknowledgment
#define MSG_NACK    0x03  // Negative acknowledgment (request retransmit)

// ============= MESSAGE STRUCTURE =============
struct TransportMessage {
  uint8_t  type;           // MSG_DATA, MSG_ACK, or MSG_NACK
  uint16_t sequenceNum;    // Sequence number (0-65535)
  uint16_t ackNum;         // Acknowledgment number (for ACK/NACK)
  uint8_t  payloadLen;     // Length of payload
  char     payload[64];    // Message payload
  uint16_t checksum;       // Simple checksum for integrity
};

// ============= STATISTICS TRACKING =============
struct TransportStats {
  uint32_t messagesSent;
  uint32_t messagesReceived;
  uint32_t acksReceived;
  uint32_t nacksReceived;
  uint32_t timeouts;
  uint32_t retransmissions;
  uint32_t bytesTransmitted;
  uint32_t simulatedDrops;
  unsigned long startTime;
};

// ============= GLOBAL VARIABLES =============
uint16_t nextSequenceNum = 0;       // Next sequence number to use
uint16_t expectedSequenceNum = 0;   // Expected sequence number (receiver)
TransportStats stats;
bool verboseMode = true;            // Print detailed logs

// ============= FUNCTION PROTOTYPES =============
void initializeLEDs();
void setLED(int led, bool state);
void blinkLED(int led, int times, int delayMs);
uint16_t calculateChecksum(TransportMessage* msg);
bool verifyChecksum(TransportMessage* msg);
void createDataMessage(TransportMessage* msg, const char* data);
void createAckMessage(TransportMessage* ack, uint16_t ackNum);
void createNackMessage(TransportMessage* nack, uint16_t nackNum);
bool simulatePacketLoss();
bool sendWithReliability(const char* data);
void processReceivedMessage(TransportMessage* msg);
void printMessage(TransportMessage* msg, const char* direction);
void printStatistics();
float calculateThroughput();

// ============= SETUP =============
void setup() {
  Serial.begin(115200);
  delay(1000);

  // Initialize random seed for packet loss simulation
  randomSeed(analogRead(0));

  // Initialize LEDs
  initializeLEDs();

  // Initialize statistics
  memset(&stats, 0, sizeof(TransportStats));
  stats.startTime = millis();

  // Startup sequence
  Serial.println("\n");
  Serial.println("╔══════════════════════════════════════════════════════════╗");
  Serial.println("║     RELIABLE MESSAGE TRANSPORT SYSTEM - ESP32 LAB        ║");
  Serial.println("║                                                          ║");
  Serial.println("║  Demonstrating Transport Layer Concepts:                 ║");
  Serial.println("║  - Sequence Numbers    - Acknowledgments                 ║");
  Serial.println("║  - Timeouts/Retries    - Packet Loss Handling            ║");
  Serial.println("╚══════════════════════════════════════════════════════════╝");
  Serial.println();

  // Show configuration
  Serial.println("=== CONFIGURATION ===");
  Serial.printf("Initial Timeout:    %d ms\n", INITIAL_TIMEOUT_MS);
  Serial.printf("Max Timeout:        %d ms\n", MAX_TIMEOUT_MS);
  Serial.printf("Max Retries:        %d\n", MAX_RETRIES);
  Serial.printf("Packet Loss Rate:   %d%%\n", PACKET_LOSS_PERCENT);
  Serial.printf("Window Size:        %d (Stop-and-Wait)\n", WINDOW_SIZE);
  Serial.println();

  // LED test
  Serial.println("=== LED TEST ===");
  Serial.println("Testing all LEDs...");
  blinkLED(LED_ACK, 2, 200);
  blinkLED(LED_NACK, 2, 200);
  blinkLED(LED_TRANSMIT, 2, 200);
  blinkLED(LED_RECEIVED, 2, 200);
  Serial.println("LED test complete.\n");

  delay(1000);
}

// ============= MAIN LOOP =============
void loop() {
  // Demonstrate reliable message transmission
  static int messageCount = 0;
  char messageBuffer[64];

  // Create a test message
  snprintf(messageBuffer, sizeof(messageBuffer),
           "Sensor reading #%d: Temp=%.1fC",
           messageCount, 20.0 + (random(100) / 10.0));

  Serial.println("════════════════════════════════════════════════════════════");
  Serial.printf(">>> SENDING MESSAGE %d <<<\n", messageCount);
  Serial.println("════════════════════════════════════════════════════════════");

  // Send with reliability (handles retries, timeouts, etc.)
  bool success = sendWithReliability(messageBuffer);

  if (success) {
    Serial.println("Result: MESSAGE DELIVERED SUCCESSFULLY");
  } else {
    Serial.println("Result: MESSAGE DELIVERY FAILED (max retries exceeded)");
  }

  // Print statistics every 5 messages
  if (messageCount % 5 == 4) {
    printStatistics();
  }

  messageCount++;
  Serial.println();

  // Wait before next transmission
  delay(3000);
}

// ============= LED FUNCTIONS =============
void initializeLEDs() {
  pinMode(LED_ACK, OUTPUT);
  pinMode(LED_NACK, OUTPUT);
  pinMode(LED_TRANSMIT, OUTPUT);
  pinMode(LED_RECEIVED, OUTPUT);

  // All LEDs off initially
  digitalWrite(LED_ACK, LOW);
  digitalWrite(LED_NACK, LOW);
  digitalWrite(LED_TRANSMIT, LOW);
  digitalWrite(LED_RECEIVED, LOW);
}

void setLED(int led, bool state) {
  digitalWrite(led, state ? HIGH : LOW);
}

void blinkLED(int led, int times, int delayMs) {
  for (int i = 0; i < times; i++) {
    digitalWrite(led, HIGH);
    delay(delayMs);
    digitalWrite(led, LOW);
    delay(delayMs);
  }
}

// ============= CHECKSUM FUNCTIONS =============
uint16_t calculateChecksum(TransportMessage* msg) {
  uint16_t sum = 0;
  sum += msg->type;
  sum += msg->sequenceNum;
  sum += msg->ackNum;
  sum += msg->payloadLen;

  for (int i = 0; i < msg->payloadLen; i++) {
    sum += (uint8_t)msg->payload[i];
  }

  return ~sum;  // One's complement
}

bool verifyChecksum(TransportMessage* msg) {
  uint16_t expected = calculateChecksum(msg);
  return (msg->checksum == expected);
}

// ============= MESSAGE CREATION =============
void createDataMessage(TransportMessage* msg, const char* data) {
  msg->type = MSG_DATA;
  msg->sequenceNum = nextSequenceNum;
  msg->ackNum = 0;
  msg->payloadLen = strlen(data);

  if (msg->payloadLen > 63) msg->payloadLen = 63;
  strncpy(msg->payload, data, msg->payloadLen);
  msg->payload[msg->payloadLen] = '\0';

  msg->checksum = calculateChecksum(msg);
}

void createAckMessage(TransportMessage* ack, uint16_t ackNum) {
  ack->type = MSG_ACK;
  ack->sequenceNum = 0;
  ack->ackNum = ackNum;
  ack->payloadLen = 0;
  ack->payload[0] = '\0';
  ack->checksum = calculateChecksum(ack);
}

void createNackMessage(TransportMessage* nack, uint16_t nackNum) {
  nack->type = MSG_NACK;
  nack->sequenceNum = 0;
  nack->ackNum = nackNum;
  nack->payloadLen = 0;
  nack->payload[0] = '\0';
  nack->checksum = calculateChecksum(nack);
}

// ============= PACKET LOSS SIMULATION =============
bool simulatePacketLoss() {
  int roll = random(100);
  bool lost = (roll < PACKET_LOSS_PERCENT);

  if (lost) {
    stats.simulatedDrops++;
    if (verboseMode) {
      Serial.printf("  [SIMULATED] Packet lost! (roll=%d < %d%%)\n",
                    roll, PACKET_LOSS_PERCENT);
    }
  }
  return lost;
}

// ============= RELIABLE SEND WITH RETRIES =============
bool sendWithReliability(const char* data) {
  TransportMessage msg;
  TransportMessage response;
  int retryCount = 0;
  int currentTimeout = INITIAL_TIMEOUT_MS;

  // Create the data message
  createDataMessage(&msg, data);

  printMessage(&msg, "SEND");

  while (retryCount <= MAX_RETRIES) {
    // Turn on transmit LED
    setLED(LED_TRANSMIT, true);

    if (verboseMode) {
      if (retryCount > 0) {
        Serial.printf("\n--- RETRY %d/%d (timeout=%dms) ---\n",
                      retryCount, MAX_RETRIES, currentTimeout);
        stats.retransmissions++;
      }
      Serial.printf("  [TX] Sending SEQ=%d, waiting for ACK...\n",
                    msg.sequenceNum);
    }

    stats.messagesSent++;
    stats.bytesTransmitted += sizeof(TransportMessage);

    // Simulate network transmission delay
    delay(50);

    // Turn off transmit LED
    setLED(LED_TRANSMIT, false);

    // Simulate the "receiver" processing
    // Check if packet was "lost" in transit
    if (simulatePacketLoss()) {
      // Packet lost - wait for timeout
      if (verboseMode) {
        Serial.printf("  [WAIT] Waiting %dms for ACK (packet was lost)...\n",
                      currentTimeout);
      }
      delay(currentTimeout);

      // Timeout occurred
      setLED(LED_NACK, true);
      delay(200);
      setLED(LED_NACK, false);

      stats.timeouts++;
      if (verboseMode) {
        Serial.println("  [TIMEOUT] No ACK received - will retry");
      }

      // Exponential backoff: double the timeout up to MAX_TIMEOUT_MS
      retryCount++;
      currentTimeout = min(currentTimeout * 2, MAX_TIMEOUT_MS);
      continue;
    }

    // Packet arrived at receiver
    setLED(LED_RECEIVED, true);
    stats.messagesReceived++;

    if (verboseMode) {
      Serial.printf("  [RX] Message received at destination\n");
    }

    // Receiver processes message and sends ACK
    processReceivedMessage(&msg);

    // Simulate receiver creating ACK
    // Small chance of ACK being lost too
    if (simulatePacketLoss()) {
      setLED(LED_RECEIVED, false);
      if (verboseMode) {
        Serial.println("  [SIMULATED] ACK was lost in transit!");
        Serial.printf("  [WAIT] Waiting %dms for ACK...\n", currentTimeout);
      }
      delay(currentTimeout);

      setLED(LED_NACK, true);
      delay(200);
      setLED(LED_NACK, false);

      stats.timeouts++;
      retryCount++;
      currentTimeout = min(currentTimeout * 2, MAX_TIMEOUT_MS);
      continue;
    }

    // ACK received successfully
    createAckMessage(&response, msg.sequenceNum);
    printMessage(&response, "RECV");

    setLED(LED_RECEIVED, false);
    setLED(LED_ACK, true);
    stats.acksReceived++;

    if (verboseMode) {
      Serial.printf("  [ACK] Received ACK for SEQ=%d\n", response.ackNum);
    }

    delay(300);
    setLED(LED_ACK, false);

    // Success! Increment sequence number for next message
    nextSequenceNum++;
    return true;
  }

  // Max retries exceeded
  Serial.println("  [FAIL] Max retries exceeded!");
  blinkLED(LED_NACK, 3, 150);
  return false;
}

// ============= RECEIVER MESSAGE PROCESSING =============
void processReceivedMessage(TransportMessage* msg) {
  if (verboseMode) {
    // Check sequence number
    if (msg->sequenceNum == expectedSequenceNum) {
      Serial.printf("  [RX] Sequence OK (expected=%d, got=%d)\n",
                    expectedSequenceNum, msg->sequenceNum);
      expectedSequenceNum++;
    } else if (msg->sequenceNum < expectedSequenceNum) {
      Serial.printf("  [RX] DUPLICATE detected (expected=%d, got=%d)\n",
                    expectedSequenceNum, msg->sequenceNum);
    } else {
      Serial.printf("  [RX] OUT OF ORDER (expected=%d, got=%d)\n",
                    expectedSequenceNum, msg->sequenceNum);
    }

    // Verify checksum
    if (verifyChecksum(msg)) {
      Serial.println("  [RX] Checksum verified OK");
    } else {
      Serial.println("  [RX] CHECKSUM FAILED - would send NACK");
      stats.nacksReceived++;
    }
  }
}

// ============= MESSAGE PRINTING =============
void printMessage(TransportMessage* msg, const char* direction) {
  if (!verboseMode) return;

  Serial.println();
  Serial.printf("  ┌─────────────── %s MESSAGE ───────────────┐\n", direction);
  Serial.printf("  │ Type:        %-28s\n",
                msg->type == MSG_DATA ? "DATA" :
                msg->type == MSG_ACK ? "ACK" : "NACK");
  Serial.printf("  │ Sequence:    %-28d\n", msg->sequenceNum);
  Serial.printf("  │ Ack Number:  %-28d\n", msg->ackNum);
  Serial.printf("  │ Payload Len: %-28d\n", msg->payloadLen);

  if (msg->payloadLen > 0) {
    Serial.printf("  │ Payload:     %-28s\n", msg->payload);
  }

  Serial.printf("  │ Checksum:    0x%04X\n", msg->checksum);
  Serial.println("  └──────────────────────────────────────────┘");
}

// ============= STATISTICS =============
void printStatistics() {
  unsigned long elapsed = (millis() - stats.startTime) / 1000;
  float throughput = calculateThroughput();
  float lossRate = stats.messagesSent > 0 ?
                   (float)(stats.simulatedDrops * 100) / stats.messagesSent : 0;
  float retxRate = stats.messagesSent > 0 ?
                   (float)(stats.retransmissions * 100) / stats.messagesSent : 0;

  Serial.println();
  Serial.println("╔══════════════════════════════════════════════════════════╗");
  Serial.println("║              TRANSPORT LAYER STATISTICS                  ║");
  Serial.println("╠══════════════════════════════════════════════════════════╣");
  Serial.printf("║  Messages Sent:       %-10lu\n",
                stats.messagesSent);
  Serial.printf("║  Messages Received:   %-10lu\n",
                stats.messagesReceived);
  Serial.printf("║  ACKs Received:       %-10lu\n",
                stats.acksReceived);
  Serial.printf("║  Timeouts:            %-10lu\n",
                stats.timeouts);
  Serial.printf("║  Retransmissions:     %-10lu\n",
                stats.retransmissions);
  Serial.printf("║  Simulated Drops:     %-10lu\n",
                stats.simulatedDrops);
  Serial.println("╠══════════════════════════════════════════════════════════╣");
  Serial.printf("║  Elapsed Time:        %-10lu seconds                 ║\n",
                elapsed);
  Serial.printf("║  Throughput:          %-10.2f bytes/sec              ║\n",
                throughput);
  Serial.printf("║  Loss Rate:           %-10.1f%%\n",
                lossRate);
  Serial.printf("║  Retransmission Rate: %-10.1f%%\n",
                retxRate);
  Serial.println("╚══════════════════════════════════════════════════════════╝");
}

float calculateThroughput() {
  unsigned long elapsed = millis() - stats.startTime;
  if (elapsed == 0) return 0;
  return (float)stats.bytesTransmitted / (elapsed / 1000.0);
}

17.2.5 Step-by-Step Instructions

Step 1: Set Up the Circuit

  1. Open the Wokwi simulator above (or visit wokwi.com/projects/new/esp32)
  2. Add four LEDs to the breadboard (green, red, yellow, blue)
  3. Connect each LED with a 220 ohm resistor
  4. Wire the connections as shown in the circuit diagram:
    • GPIO 4 to Green LED (ACK indicator)
    • GPIO 2 to Red LED (NACK/Timeout indicator)
    • GPIO 5 to Yellow LED (Transmitting indicator)
    • GPIO 18 to Blue LED (Received indicator)
    • All LED cathodes to GND

Step 2: Upload and Run the Code

  1. Copy the complete code into the Wokwi code editor
  2. Click “Start Simulation” to begin
  3. Open the Serial Monitor (set to 115200 baud) to see detailed output

Step 3: Observe Transport Behavior

Watch the LEDs and Serial Monitor to understand each phase:

LED Meaning Transport Layer Concept
Yellow ON Message being transmitted Data segment in transit
Blue ON Message arrived at receiver Successful delivery
Green ON ACK received by sender Acknowledgment mechanism
Red ON Timeout or NACK Retransmission trigger

Step 4: Analyze the Statistics

After 5 messages, the system prints statistics. Look for:

  • Retransmission Rate: Higher than packet loss rate? Why? (ACK loss doubles retransmissions)
  • Throughput: How does packet loss affect effective throughput?
  • Timeout Count: Each timeout represents wasted waiting time

17.2.6 Understanding the Code

Key Transport Layer Concepts in the Code:

1. Sequence Numbers (Lines 48-49)
uint16_t sequenceNum;    // Unique ID for each message
uint16_t ackNum;         // Which message is being acknowledged

Sequence numbers enable: - Ordering: Receiver knows correct order even if packets arrive shuffled - Duplicate detection: Same sequence number = retransmission, not new data - Loss detection: Gap in sequence numbers = missing packet

2. Acknowledgment System (Lines 212-229)
void createAckMessage(TransportMessage* ack, uint16_t ackNum) {
  ack->type = MSG_ACK;
  ack->ackNum = ackNum;  // "I received message #ackNum"
}

ACK/NACK enables: - Confirmation: Sender knows message arrived - Error feedback: NACK requests specific retransmission - Flow control: Receiver controls sender’s pace

3. Timeout and Retry with Exponential Backoff (Lines 251-295)
int currentTimeout = INITIAL_TIMEOUT_MS;  // Start at 500ms
// On timeout:
currentTimeout = min(currentTimeout * 2, MAX_TIMEOUT_MS);  // Double it

Timeouts handle: - Lost packets: No ACK after timeout triggers retry - Lost ACKs: Sender cannot distinguish from lost data - Exponential backoff: Prevents network congestion during problems

Exponential backoff doubles the timeout after each failed attempt, preventing network congestion. The timeout on retry \(n\) (zero-indexed) is:

\[T_n = \min\!\left(T_0 \times 2^n,\; T_{\max}\right)\]

Worked example: \(T_0 = 500\) ms, \(T_{\max} = 4{,}000\) ms, MAX_RETRIES = 3.

The code retries up to 3 times after the initial attempt fails, so the timeout sequence is:

Attempt Retry # Timeout
0 initial 500 ms
1 retry 1 1,000 ms
2 retry 2 2,000 ms
3 retry 3 4,000 ms (capped)

Total wait before giving up: \(500 + 1{,}000 + 2{,}000 + 4{,}000 = 7{,}500\) ms.

Compare to a fixed 500 ms timeout: \(4 \times 500 = 2{,}000\) ms. Exponential backoff waits 3.75x longer but prevents retry storms. With 100 devices all timing out simultaneously, fixed timeout produces 400 near-simultaneous retransmissions; exponential backoff spreads them over seconds, greatly reducing network congestion.

Try It: Exponential Backoff Explorer

Adjust the initial timeout, maximum timeout, and number of retries to see how exponential backoff spreads out retry attempts over time. Compare the total wait time with a fixed-timeout strategy.

4. Packet Loss Simulation (Lines 232-244)
bool simulatePacketLoss() {
  return (random(100) < PACKET_LOSS_PERCENT);  // 20% default
}

Simulating loss helps understand: - Why reliability matters: See real impact of unreliable networks - Overhead cost: Retransmissions consume bandwidth and time - Trade-offs: More aggressive timeouts vs. unnecessary retries

17.2.7 Experiments to Try

Experiment 1: Vary Packet Loss Rate

Change PACKET_LOSS_PERCENT to different values:

Loss Rate Expected Behavior
0% All messages succeed first try
20% Some retransmissions, most succeed
50% Many retries, some failures
80% Most messages fail after max retries
Try It: Packet Loss Impact Calculator

Explore how packet loss rate and maximum retries affect the probability of successful message delivery. The calculator shows the chance that at least one attempt (out of all retries) gets through.

Experiment 2: Adjust Timeout Values

Setting Effect
Lower timeout (100ms) Faster failure detection, but more false retries
Higher timeout (2000ms) Fewer false retries, but slower recovery
No backoff Network stays congested during problems
Try It: Timeout Strategy Simulator

Compare how different timeout strategies perform when multiple IoT devices experience simultaneous packet loss. See how exponential backoff prevents “retry storms” by spreading retransmissions over time.

Experiment 3: Remove Reliability Features

Comment out different sections to see what breaks: - Remove checksums: Corrupted data goes undetected - Remove sequence numbers: Duplicates and reordering cause chaos - Remove retries: Any packet loss means data loss

Scenario: A warehouse IoT system sends inventory updates every 10 seconds over Wi-Fi with 10% packet loss rate.

System Parameters:

  • Message payload: 50 bytes (item ID, location, quantity, timestamp)
  • Network MTU: 1500 bytes
  • Round-trip time (RTT): 20 ms
  • Packet loss rate: 10%
  • Initial timeout: 500 ms
  • Max retries: 3

Step 1: Calculate UDP Overhead

UDP packet structure:
- IPv4 header: 20 bytes
- UDP header: 8 bytes
- Payload: 50 bytes
Total: 78 bytes per message

Expected transmissions per delivery with 10% loss:
- 90% succeed on first try (1 transmission)
- 10% require at least one retry
Average transmissions: 1 / (1 - 0.10) ≈ 1.11 per delivery

Step 2: Calculate TCP Full-Connection Overhead

TCP new-connection overhead per message:
- SYN:         60 bytes (20 IP + 40 TCP with options)
- SYN-ACK:     60 bytes
- ACK:         40 bytes (connection established)
- Data:        90 bytes (20 IP + 20 TCP + 50 payload)
- ACK:         40 bytes (data acknowledged by server)
- FIN:         40 bytes (client initiates close)
- ACK:         40 bytes (server acknowledges FIN)
- FIN:         40 bytes (server sends its own FIN)
- ACK:         40 bytes (client acknowledges server FIN)
Total:        450 bytes per message exchange (9 segments)

Note: TCP uses a 4-way handshake for connection teardown -- each side
sends its own FIN and receives an ACK -- so a full open/transfer/close
cycle requires 9 segments, not 7.

With 10% loss, each segment has a 10% drop probability.
Probability that at least one of 9 segments is lost:
  1 - (0.9)^9 = 1 - 0.387 = 61.3%
Average bytes per exchange (including retransmissions):
  450 × 1 / (1 - 0.10) ≈ 500 bytes

Step 3: Compare Radio-On Time

At 54 Mbps Wi-Fi data rate (6.75 MB/s):

UDP:
- Transmission time: 78 bytes / 6.75 MB/s = 11.6 µs
- With ~1.11 average transmissions: 12.9 µs
- Plus radio startup overhead: ~100 µs
Total: ~113 µs per message

TCP (including 500 ms timeout probability):
- Base exchange: 450 bytes / 6.75 MB/s = 66.7 µs
- Average timeout waste: 61.3% × 500 ms = 306 ms
Total: ~306 ms per message (timeout dominates!)

Step 4: Calculate Battery Impact

Warehouse sensor sending 8,640 messages/day (every 10 seconds):

UDP:
- Active time: 8,640 × 0.113 ms = 977 ms/day ≈ 1 second
- With 50 mA TX current: 0.013 mAh/day

TCP with 500 ms timeouts:
- Active time: 8,640 × 306 ms = 2,644 seconds/day (44.1 min)
- With 50 mA TX current: 36.7 mAh/day

Battery life with 2,000 mAh battery:
- UDP: ~142,000 days (limited by self-discharge, ~389 years)
- TCP: 2,000 / 36.7 = 54.5 days ≈ 1.8 months

Key Insight: On lossy wireless links, TCP’s timeout-and-retry mechanism wastes far more energy than the retransmissions themselves. UDP with application-layer retry (such as CoAP CON with a 2-second timeout) provides a middle ground: roughly 5 ms total per message, 43 seconds of radio time per day, approximately 0.6 mAh per day, and a projected 9-year battery life.

Lesson Learned: TCP’s per-connection overhead and exponential backoff cascade make it unsuitable for battery-powered periodic telemetry on lossy links. Use UDP with selective application-layer retry instead.

17.2.8 Challenge Exercises

Challenge 1: Implement Sliding Window Protocol

Modify the code to send multiple messages before waiting for ACKs:

#define WINDOW_SIZE 4  // Send up to 4 messages before blocking

// Track which messages are awaiting ACK
bool awaitingAck[WINDOW_SIZE];
uint16_t windowBase = 0;  // First unacknowledged sequence number

// Send messages until window is full
while (nextSeq < windowBase + WINDOW_SIZE) {
  sendMessage(nextSeq++);
}

// On ACK, slide window forward
windowBase = ackNum + 1;

Goal: Improve throughput by not waiting for each ACK individually.

Challenge 2: Add Congestion Control

Implement basic congestion control that reduces send rate when losses occur:

int congestionWindow = 4;  // Start with window of 4

// On successful ACK
if (congestionWindow < MAX_WINDOW) {
  congestionWindow++;  // Additive increase
}

// On timeout (congestion signal)
congestionWindow = max(1, congestionWindow / 2);  // Multiplicative decrease

Goal: Prevent overwhelming the network when congestion is detected.

Challenge 3: Implement Selective Repeat

Instead of retransmitting from the lost packet onward (Go-Back-N), only retransmit the specific lost packets:

// Receiver tracks received packets with bitmap
uint32_t receivedBitmap = 0;

// On receiving packet with seq N
receivedBitmap |= (1 << (N - windowBase));

// Send selective NACK for missing packets
for (int i = 0; i < WINDOW_SIZE; i++) {
  if (!(receivedBitmap & (1 << i))) {
    sendNack(windowBase + i);
  }
}

Goal: Reduce retransmission overhead by only resending what was actually lost.

17.2.9 Key Transport Layer Concepts Summary

This lab demonstrates the fundamental mechanisms that make reliable transport possible:

Concept What It Solves How It Works
Sequence Numbers Ordering, duplicates, loss detection Each message gets unique incremental ID
Acknowledgments Delivery confirmation Receiver sends ACK/NACK for each message
Timeouts Lost packet detection Sender waits limited time for ACK
Retransmission Lost data recovery Resend unacknowledged messages
Checksums Data corruption Mathematical verification of integrity
Exponential Backoff Congestion avoidance Double timeout on each failure

Try It: Message Exchange Visualizer

Watch a step-by-step simulation of the reliable message delivery process. Adjust the loss rate to see how ACKs, timeouts, and retransmissions play out between sender and receiver.

Connection to Real Protocols

The mechanisms in this lab directly map to real transport protocols:

  • TCP uses all these mechanisms plus sliding windows, congestion control, and flow control
  • QUIC (used by HTTP/3) adds encryption and multiplexing on top of UDP
  • MQTT QoS 1 uses sequence numbers and ACKs similar to this lab
  • CoAP Confirmable messages use the same timeout/retry approach with exponential backoff

Understanding these fundamentals helps you make informed protocol choices for IoT applications.

17.3 Original Source Figures

Transport protocols diagram comparing TCP (connection-oriented, reliable, ordered delivery with acknowledgments) and UDP (connectionless, best-effort, no guarantees) with their key characteristics and use cases

Transport layer protocols showing TCP and UDP characteristics

Source: CP IoT System Design Guide, Chapter 4 - Networking Fundamentals

Data transportation diagram showing how application data flows through transport layer (TCP/UDP), network layer (IP), and data link layer with appropriate headers added at each stage

Data transportation through network layers

Source: CP IoT System Design Guide, Chapter 4 - Networking Fundamentals

UDP datagram structure showing the 8-byte header with source port (2 bytes), destination port (2 bytes), length (2 bytes), and checksum (2 bytes) fields followed by the payload data

UDP datagram structure and header fields

Source: CP IoT System Design Guide, Chapter 4 - Networking Fundamentals

Data link layer diagram showing its position between network and physical layers, with responsibilities including framing, addressing (MAC), error detection (CRC), and media access control

Data link layer functions and frame structure

Source: CP IoT System Design Guide, Chapter 4 - Networking Fundamentals

Concept Relationships

Depends on:

  • Plain English Guide - Understanding TCP/UDP analogies before building implementations
  • TCP Fundamentals - Sequence numbers, ACKs, timeouts are core mechanisms implemented in this lab

Enables:

  • Transport Optimizations - Understanding basic reliability enables advanced techniques like sliding windows
  • CoAP Protocol - CoAP Confirmable messages use the same ACK/timeout pattern built in this lab

Related concepts:

  • Exponential backoff prevents network congestion during packet loss (implemented in retry logic)
  • Sequence numbers enable duplicate detection and ordering (implemented in message structure)
  • Application-layer reliability on UDP provides TCP-like guarantees with lower overhead
See Also

Prerequisites:

Related labs:

  • CoAP Hands-On - See how CoAP uses these mechanisms over UDP
  • MQTT QoS Levels - Different reliability guarantees over TCP

External resources:

Common Pitfalls

TCP server labs that do not set SO_REUSEADDR before bind() fail to restart the server within 60–120 seconds of the previous run (TIME_WAIT state holds the port). Students restart the server immediately after stopping it and get “Address already in use” (EADDRINUSE), wasting lab time debugging what appears to be a bind error. Always include setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) = 1 before bind() in all TCP server lab code.

TCP lab exercises running client and server on the same machine via 127.0.0.1 do not test real network behavior: loopback has zero packet loss, zero congestion, and sub-millisecond latency. Lab results from loopback do not generalize to real IoT networks. Use two separate machines (or network namespaces: ip netns add client, ip netns add server) for realistic network simulation. If only one machine is available, use tc netem to add latency and loss to the loopback interface for more realistic testing.

TCP is a byte stream, not a message protocol. A TCP recv() call may return 1 byte, 50 bytes, or 500 bytes regardless of how many bytes were sent in a single write(). Lab code that expects recv() to return exactly one complete message per call fails randomly. Implement length-prefix framing: prepend each message with a 4-byte length, read the length first, then read exactly that many bytes. This is the fundamental TCP message framing pattern.

Python socket recv(1024) truncates any UDP datagram larger than 1024 bytes — the excess bytes are silently discarded. The recv() call succeeds without error but returns incomplete data. In lab exercises, always set the recv buffer to at least max_expected_datagram_size + 100 bytes. For CoAP datagrams, use recv(1500). A common symptom of this bug: short messages work correctly, long messages appear truncated, but no exception is raised.

17.4 What’s Next

Continue your transport protocol learning:

Topic Link Why Read It
Transport Selection and Scenarios transport-selection-and-scenarios.html Apply decision frameworks to choose between TCP, UDP, and QUIC for real IoT deployments
Transport Optimizations and Implementation transport-optimizations-and-implementation.html Implement sliding windows, congestion control, and lightweight stacks that build on this lab
CoAP Protocol Fundamentals coap-fundamentals.html Assess how CoAP applies ACK/timeout patterns over UDP with lower overhead than TCP
MQTT Protocol Fundamentals mqtt-fundamentals.html Compare TCP-based pub/sub QoS guarantees against the mechanisms built in this lab
Layered Network Models layered-network-models.html Distinguish where transport fits within the full OSI and TCP/IP stack
MQTT QoS and Session Management mqtt-qos-and-session.html Evaluate QoS 0, 1, and 2 trade-offs using the reliability intuition from this lab