reliable-transport/channel.py
2026-10-19 13:24:25 -06:00

89 lines
3.6 KiB
Python

"""A simulated unreliable network for testing without Mininet. Given; do not change it.
a, b = link(loss=0.1, delay=0.005, seed=1)
gives two endpoints that behave like UDP sockets connected to each other:
sendto(), recvfrom(), and settimeout(), where recvfrom raises socket.timeout
when nothing arrives in time. The link can drop, delay, reorder, duplicate, or
corrupt packets, each with its own probability, and the same seed always
gives the same run. Your sender and receiver only ever call those three
methods, so the same code runs over this link and over a real UDP socket.
Reference: https://docs.python.org/3/library/socket.html
"""
import heapq
import itertools
import random
import socket
import threading
import time
class _Endpoint:
def __init__(self, name, link):
self.name = name
self._link = link
self._queue = [] # (arrival time, order, bytes, from)
self._timeout = None
def settimeout(self, seconds):
self._timeout = seconds
def sendto(self, data, addr):
self._link._carry(self, bytes(data))
return len(data)
def recvfrom(self, bufsize):
deadline = None if self._timeout is None else time.monotonic() + self._timeout
with self._link._cond:
while True:
now = time.monotonic()
if self._queue and self._queue[0][0] <= now:
_, _, data, sender = heapq.heappop(self._queue)
return data[:bufsize], sender
waits = [t for t in (deadline, self._queue[0][0] if self._queue else None) if t is not None]
if deadline is not None and now >= deadline:
raise socket.timeout("timed out")
self._link._cond.wait(None if not waits else max(0.0, min(waits) - now))
class _Link:
def __init__(self, loss, delay, jitter, duplicate, corrupt, seed, watch, drop):
self.loss, self.delay, self.jitter = loss, delay, jitter
self.duplicate, self.corrupt = duplicate, corrupt
self._rng = random.Random(seed)
self._order = itertools.count()
self._cond = threading.Condition()
self._watch = watch
self._drop = drop
self.a = _Endpoint("A", self)
self.b = _Endpoint("B", self)
def _carry(self, source, data):
dest = self.b if source is self.a else self.a
with self._cond:
if self._watch is not None:
self._watch(source.name, data)
if self._drop is not None and self._drop(source.name, data):
self._cond.notify_all()
return
copies = 2 if self._rng.random() < self.duplicate else 1
for _ in range(copies):
if self._rng.random() < self.loss:
continue
packet = data
if packet and self._rng.random() < self.corrupt:
i = self._rng.randrange(len(packet))
packet = packet[:i] + bytes([packet[i] ^ 0xFF]) + packet[i + 1:]
arrive = time.monotonic() + self.delay + self._rng.uniform(0, self.jitter)
heapq.heappush(dest._queue, (arrive, next(self._order), packet, source.name))
self._cond.notify_all()
def link(loss=0.0, delay=0.005, jitter=0.0, duplicate=0.0, corrupt=0.0, seed=0, watch=None, drop=None):
"""Two connected endpoints (A, B). A jitter larger than the gap between
packets reorders them. `watch(sender_name, bytes)` sees every packet sent;
`drop(sender_name, bytes)` returning True loses that one packet."""
made = _Link(loss, delay, jitter, duplicate, corrupt, seed, watch, drop)
return made.a, made.b