140 lines
5.4 KiB
Python
140 lines
5.4 KiB
Python
"""Measure the three protocols over a real lossy link. Given; do not change it.
|
|
|
|
Run it inside the course container (see README.md), as root:
|
|
|
|
python3 experiments.py # every experiment, about four minutes
|
|
python3 experiments.py loss # goodput against loss rate only
|
|
python3 experiments.py window # goodput against window size only
|
|
|
|
It builds the smallest possible network: two network namespaces, h1 and h2,
|
|
joined by one virtual cable (a veth pair), with netem on each end adding the
|
|
delay and the loss. That is exactly what Mininet builds for two hosts and a
|
|
link. h1 sends a file to h2 with transfer.py, and the run counts only if the
|
|
file arrives byte for byte. Loss is random, so every setting runs three times
|
|
and the tables show the median run. Results print as tables for FINDINGS.md
|
|
and go to results.json with every run kept.
|
|
|
|
netem's loss applies in both directions: at 5%, data packets and ACKs are
|
|
each lost 5% of the time.
|
|
|
|
References:
|
|
https://man7.org/linux/man-pages/man8/ip-netns.8.html
|
|
https://man7.org/linux/man-pages/man8/tc-netem.8.html
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
FILE_BYTES = 100_000
|
|
DELAY = "10ms" # each way, so the round trip is about 20 ms
|
|
TIMEOUT = 0.1
|
|
TRIALS = 3
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ADDR = {"h1": "10.0.0.1", "h2": "10.0.0.2"}
|
|
|
|
|
|
def sh(*cmd):
|
|
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
|
|
|
|
def build(loss):
|
|
"""Two namespaces joined by a veth pair, with netem delay and loss on both ends."""
|
|
teardown()
|
|
for host in ADDR:
|
|
sh("ip", "netns", "add", host)
|
|
sh("ip", "link", "add", "h1-eth0", "netns", "h1", "type", "veth", "peer", "name", "h2-eth0", "netns", "h2")
|
|
for host, addr in ADDR.items():
|
|
dev = f"{host}-eth0"
|
|
sh("ip", "-n", host, "addr", "add", f"{addr}/24", "dev", dev)
|
|
sh("ip", "-n", host, "link", "set", dev, "up")
|
|
sh("ip", "-n", host, "link", "set", "lo", "up")
|
|
sh("ip", "netns", "exec", host, "tc", "qdisc", "add", "dev", dev, "root",
|
|
"netem", "delay", DELAY, "loss", f"{loss}%")
|
|
|
|
|
|
def teardown():
|
|
for host in ADDR:
|
|
subprocess.run(["ip", "netns", "del", host], capture_output=True)
|
|
|
|
|
|
def run(proto, loss, window):
|
|
build(loss)
|
|
src, dst = "/tmp/p3_send.bin", "/tmp/p3_recv.bin"
|
|
with open(src, "wb") as f:
|
|
f.write(os.urandom(FILE_BYTES))
|
|
if os.path.exists(dst):
|
|
os.remove(dst)
|
|
receiver = subprocess.Popen(["ip", "netns", "exec", "h2", sys.executable, "transfer.py", "recv",
|
|
"--proto", proto, "--window", str(window), "--out", dst], cwd=HERE)
|
|
time.sleep(0.3) # let the receiver bind its port first
|
|
try:
|
|
sent = subprocess.run(["ip", "netns", "exec", "h1", sys.executable, "transfer.py", "send",
|
|
"--proto", proto, "--window", str(window), "--timeout", str(TIMEOUT),
|
|
ADDR["h2"], "5000", src], cwd=HERE, capture_output=True, text=True, timeout=300)
|
|
out = sent.stdout + sent.stderr
|
|
except subprocess.TimeoutExpired:
|
|
out = "the sender ran for 300 s without finishing"
|
|
try:
|
|
receiver.wait(5) # 5 s to finish after the sender is done
|
|
except subprocess.TimeoutExpired:
|
|
receiver.kill()
|
|
receiver.wait()
|
|
teardown()
|
|
try:
|
|
stats = json.loads(out.strip().splitlines()[-1])
|
|
except (ValueError, IndexError):
|
|
return {"proto": proto, "loss": loss, "window": window, "ok": False, "error": out.strip()[-300:]}
|
|
same = os.path.exists(dst) and _digest(src) == _digest(dst)
|
|
stats.update(proto=proto, loss=loss, window=window, ok=same,
|
|
goodput_kbps=round(FILE_BYTES * 8 / 1000 / stats["seconds"], 1))
|
|
return stats
|
|
|
|
|
|
def median_run(proto, loss, window):
|
|
runs = [run(proto, loss, window) for _ in range(TRIALS)]
|
|
good = sorted((r for r in runs if r["ok"]), key=lambda r: r["goodput_kbps"])
|
|
best = dict(good[len(good) // 2]) if len(good) == TRIALS else dict(runs[0], ok=False)
|
|
best["runs"] = runs
|
|
return best
|
|
|
|
|
|
def _digest(path):
|
|
with open(path, "rb") as f:
|
|
return hashlib.sha256(f.read()).hexdigest()
|
|
|
|
|
|
def table(rows, key):
|
|
print(f"\n| protocol | {key} | goodput (kbit/s) | transmissions | retransmissions | intact |")
|
|
print("| :--- | --: | --: | --: | --: | :--- |")
|
|
for r in rows:
|
|
if not r["ok"]:
|
|
print(f"| {r['proto']} | {r[key]} | failed | | | no |")
|
|
continue
|
|
print(f"| {r['proto']} | {r[key]} | {r['goodput_kbps']} | {r['transmissions']} "
|
|
f"| {r['retransmissions']} | yes |")
|
|
|
|
|
|
def main():
|
|
if os.geteuid() != 0:
|
|
sys.exit("run this as root inside the course container: creating namespaces needs it")
|
|
wanted = sys.argv[1:] or ["loss", "window"]
|
|
results = {}
|
|
if "loss" in wanted:
|
|
rows = [median_run(p, loss, 8) for p in ("sw", "gbn", "sr") for loss in (0, 1, 5, 10, 20)]
|
|
table(rows, "loss")
|
|
results["loss"] = rows
|
|
if "window" in wanted:
|
|
rows = [median_run(p, 5, w) for p in ("gbn", "sr") for w in (1, 2, 4, 8, 16, 32)]
|
|
table(rows, "window")
|
|
results["window"] = rows
|
|
with open(os.path.join(HERE, "results.json"), "w") as f:
|
|
json.dump(results, f, indent=1)
|
|
print("\nwrote results.json")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|