CY350

On this page

  • Objective
  • Steps
  • Levels
    • Level 1: UDP is Unreliable
    • Level 2: Timeout and Retransmission
    • Level 3: Stop-and-Wait with ACKs
  • Server Code
  • Completion

In-Class Lab 4 (10 points)

Implementing a reliable data transfer protocol over an unreliable transport layer

Stop-and-Wait Reliable Transfer over UDP
Published

February 23, 2026

Objective

Experience the unreliability of UDP through incremental challenges. Implement strategies to overcome the madness.

Steps

  1. Run the provided server. There is only one server program that will work with all levels of the client. Scroll to the bottom of this page to find the server code.
  2. Implement the client for each level, starting with the simplest and working your way up.
  3. Use Wireshark to capture and analyze the packets for each level.
  4. Discuss these packet captures with your instructor for the first three levels for full credit.

Levels

  1. UDP is unreliable. Watch your packets get lost in despair.
  2. Retransmit lost packets. Maybe duplicates will arrive, but who cares? Just get the data there.
  3. Use an alternating bit to detect duplicates. Slow but reliable.

Level 1: UDP is Unreliable

udp_client_1.py
import socket
import time

TYPE_DATA = 0xD1  # level 1 DATA
TYPE_ACK = 0xA1  # level 1 ACK
START_TIME = time.time()


def print_with_time(msg: str):
    elapsed = time.time() - START_TIME
    print(f"[{elapsed:7.3f}s] {msg}", flush=True)


def build_packet(seq: int, msg: str, pkt_type: int = TYPE_DATA) -> bytes:
    return bytes([pkt_type, seq]) + msg.encode()


def get_payload(data: bytes):
    if len(data) < 2:
        return False
    payload = data[2:]
    return payload.decode()


def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(1.0)

    server = ("127.0.0.1", 5005)

    for i in range(20):
        msg = f"packet {i}"
        pkt = build_packet(i, msg)
        sock.sendto(pkt, server)
        print_with_time(f"Sent: packet {i}")
        try:
            data, addr = sock.recvfrom(4096)
            payload = get_payload(data)
            print_with_time(f"Received: {payload}")
        except socket.timeout:
            print_with_time(f"Packet {i} was lost. No ACK.")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print_with_time("Client interrupted. Exiting.")

Level 2: Timeout and Retransmission

udp_client_2.py
import socket
import time

TYPE_DATA = 0xD2  # level 2 DATA
TYPE_ACK = 0xA2  # level 2 ACK
START_TIME = time.time()


def print_with_time(msg: str):
    elapsed = time.time() - START_TIME
    print(f"[{elapsed:7.3f}s] {msg}", flush=True)


def build_packet(seq: int, msg: str, pkt_type: int = TYPE_DATA) -> bytes:
    return bytes([pkt_type, seq]) + msg.encode()


def get_payload(data: bytes):
    if len(data) < 2:
        return False
    payload = data[2:]
    return payload.decode()


def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(1.0)

    server = ("127.0.0.1", 5005)

    for i in range(20):
        msg = f"packet {i}"
        pkt = build_packet(i, msg, TYPE_DATA)
        acked = False

        while not acked:
            sock.sendto(pkt, server)
            print_with_time(f"Sent: packet {i}")
            try:
                data, addr = sock.recvfrom(4096)
                payload = get_payload(data)
                print_with_time(f"Received ACK")
                acked = True
            except socket.timeout:
                print_with_time(f"Packet {i} was lost. No ACK.")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print_with_time("Client interrupted. Exiting.")

Level 3: Stop-and-Wait with ACKs

udp_client_3.py
import socket
import time

TYPE_DATA = 0xD3  # level 3 DATA
TYPE_ACK = 0xA3  # level 3 ACK
START_TIME = time.time()


def print_with_time(msg: str):
    elapsed = time.time() - START_TIME
    print(f"[{elapsed:7.3f}s] {msg}", flush=True)


def build_packet(seq: int, msg: str, pkt_type: int = TYPE_DATA) -> bytes:
    return bytes([pkt_type, seq]) + msg.encode()


def parse_response(data: bytes):
    if len(data) < 2:
        return None
    pkt_type = data[0]
    seq = data[1]
    payload = data[2:]
    return pkt_type, seq, payload.decode()


def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(1.0)

    server = ("127.0.0.1", 5005)

    seq = 0

    for i in range(20):
        msg = f"packet {i}"
        pkt = build_packet(seq, msg, TYPE_DATA)
        acked = False

        while not acked:
            sock.sendto(pkt, server)
            print(f"Sent: packet {i}")

            try:
                data, addr = sock.recvfrom(4096)
                if data[0] == TYPE_ACK:
                    ack_seq = data[1]
                    if ack_seq == seq:
                        acked = True
                        print_with_time(f"Received ACK for seq {ack_seq}")
                        seq = (seq + 1) % 2  # toggle between 0 and 1
                    else:
                        print_with_time(
                            f"Received ACK with wrong seq {ack_seq}, expected {seq}")
                        print_with_time(f'Retransmitting packet {i} with seq {seq}')

            except socket.timeout:
                print_with_time(f"Packet {i} was lost. No ACK.")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print_with_time("Client interrupted. Exiting.")

Server Code

Run the following server code in a separate terminal. It simulates an unreliable network by randomly dropping packets and introducing delays. You can adjust the loss rate and delay parameters to see how your client handles different conditions.

udp_server.py
#!/usr/bin/env python3
import socket
import random
import argparse
import logging
import time

# Client types
D1 = 0xD1  # Stage 1
D2 = 0xD2  # Stage 2
D3 = 0xD3  # Stage 3

# Server types
A1 = 0xA1
A2 = 0xA2
A3 = 0xA3

def build_packet(ptype, seq, payload=b""):
    return bytes([ptype, seq]) + payload

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", default="0.0.0.0")
    parser.add_argument("--port", type=int, default=5005)
    parser.add_argument("--loss", type=float, default=0.3)
    parser.add_argument("--target", type=int, default=20)
    parser.add_argument("--max-delay", type=float, default=0.2, help="max delay in seconds for simulating network latency")
    parser.add_argument("--debug", action="store_true")
    args = parser.parse_args()

    logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO,
                        format="%(asctime)s %(levelname)s: %(message)s")

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((args.host, args.port))

    logging.info("Server listening on %s:%d", args.host, args.port)

    # Per-client state for Stage 3
    client_state = {}  # addr -> {expected_seq, count}

    while True:
        data, addr = sock.recvfrom(4096)

        if len(data) < 2:
            continue

        # Simulated loss
        if random.random() < args.loss:
            logging.debug("Dropped packet from %s", addr)
            continue

        # Simulated delay
        delay = random.random() * args.max_delay
        time.sleep(delay)

        ptype = data[0]
        seq = data[1]
        payload = data[2:]

        # -----------------------
        # Stage 1: Simple Echo
        # -----------------------
        if ptype == D1:
            logging.debug("Stage 1 packet from %s", addr)
            response = build_packet(A1, seq, payload.upper())
            sock.sendto(response, addr)

        # -----------------------
        # Stage 2: Echo with loss (students retransmit)
        # -----------------------
        elif ptype == D2:
            logging.debug("Stage 2 packet from %s", addr)
            response = build_packet(A2, seq)
            sock.sendto(response, addr)

        # -----------------------
        # Stage 3: Stop-and-Wait
        # -----------------------
        elif ptype == D3:
            state = client_state.get(addr)
            if state is None:
                state = {"expected_seq": 0, "count": 0}
                client_state[addr] = state

            expected = state["expected_seq"]

            if seq == expected:
                # Valid in-order packet
                ack = build_packet(A3, seq)
                sock.sendto(ack, addr)

                state["expected_seq"] ^= 1
                state["count"] += 1

                logging.debug("Accepted seq %d from %s (count=%d)",
                              seq, addr, state["count"])

                if state["count"] >= args.target:
                    flag_msg = b"FLAG{STOP_AND_WAIT_SUCCESS}"
                    sock.sendto(build_packet(A3, seq, flag_msg), addr)
                    state["count"] = 0  # reset for replay

            else:
                # Duplicate — resend last ACK
                last_ack_seq = 1 - expected
                ack = build_packet(A3, last_ack_seq)
                sock.sendto(ack, addr)

        else:
            logging.debug("Unknown packet type from %s", addr)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("Server interrupted. Exiting.")

Completion

Be prepared to discuss these ideas.

  • Explain the retransmission strategy of each level.
  • Identify the packet header. What are the first two bytes?
  • Is there a sequence number present? How does it increment or change?
  • Does the client detect duplicates in level 3?
  • What is unique about level 3 compared to the previous levels?

Reuse

CC BY-NC-SA 4.0
 

© 2026 United States Military Academy