In-Class Lab 4 (10 points)
Implementing a reliable data transfer protocol over an unreliable transport layer
Stop-and-Wait Reliable Transfer over UDP
Objective
Experience the unreliability of UDP through incremental challenges. Implement strategies to overcome the madness.
Steps
- 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.
- Implement the client for each level, starting with the simplest and working your way up.
- Use Wireshark to capture and analyze the packets for each level.
- Discuss these packet captures with your instructor for the first three levels for full credit.
Levels
- UDP is unreliable. Watch your packets get lost in despair.
- Retransmit lost packets. Maybe duplicates will arrive, but who cares? Just get the data there.
- 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?