w5isp.com/static/js/tcpudp.js

683 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// TCP / UDP explainer. Vanilla JS + 2D canvas.
// Scoped to a single post via <link>/<script> in the markdown.
(() => {
'use strict';
const C = {
bg: '#1f232b',
panel: '#242932',
fg: '#d4d8df',
dim: '#5c6370',
grid: '#323844',
blue: '#61afef',
cyan: '#56b6c2',
green: '#98c379',
orange: '#d19a66',
red: '#e06c75',
yellow: '#e5c07b',
magenta: '#c678dd',
};
// ----- framework (same shape as transistor/nlos scenes) -----
function setupCanvas(canvas) {
const ctx = canvas.getContext('2d');
const dpr = Math.max(1, window.devicePixelRatio || 1);
const fit = () => {
const rect = canvas.getBoundingClientRect();
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
fit();
new ResizeObserver(fit).observe(canvas);
return { ctx, getSize: () => ({ w: canvas.clientWidth, h: canvas.clientHeight }) };
}
function scene(id, { height = 360, controls = [], readout = [], caption = '', threeCol = false } = {}) {
const root = document.getElementById(id);
if (!root) return null;
root.classList.add('scene-box');
const canvas = document.createElement('canvas');
canvas.style.width = '100%';
canvas.style.height = `${height}px`;
root.appendChild(canvas);
const readoutEl = document.createElement('div');
readoutEl.className = 'scene-readout';
const readoutSpans = {};
readout.forEach(r => {
const item = document.createElement('span');
item.className = 'scene-readout-item';
const label = document.createElement('span');
label.textContent = r.label + ':';
const val = document.createElement('span');
val.textContent = r.init ?? '';
item.appendChild(label); item.appendChild(val);
readoutEl.appendChild(item);
readoutSpans[r.key] = val;
});
if (readout.length) root.appendChild(readoutEl);
const controlsEl = document.createElement('div');
controlsEl.className = 'scene-controls' + (threeCol ? ' scene-controls--three' : '');
const values = {};
controls.forEach(c => {
const wrap = document.createElement('div');
wrap.className = 'scene-control';
const label = document.createElement('label');
const name = document.createElement('span'); name.textContent = c.label;
const valSpan = document.createElement('span'); valSpan.className = 'scene-value';
label.appendChild(name); label.appendChild(valSpan);
const input = document.createElement('input');
input.type = 'range';
input.min = c.min; input.max = c.max; input.step = c.step ?? 'any';
input.value = c.value;
values[c.key] = parseFloat(input.value);
const fmt = c.format || (v => v.toFixed(2));
valSpan.textContent = fmt(values[c.key]);
input.addEventListener('input', () => {
values[c.key] = parseFloat(input.value);
valSpan.textContent = fmt(values[c.key]);
});
wrap.appendChild(label); wrap.appendChild(input);
controlsEl.appendChild(wrap);
});
if (controls.length) root.appendChild(controlsEl);
if (caption) {
const cap = document.createElement('div');
cap.className = 'scene-caption';
cap.textContent = caption;
root.appendChild(cap);
}
const { ctx, getSize } = setupCanvas(canvas);
const setReadout = (key, text) => {
if (readoutSpans[key]) readoutSpans[key].textContent = text;
};
return { canvas, ctx, getSize, values, setReadout };
}
function clear(ctx, w, h) { ctx.clearRect(0, 0, w, h); }
function text(ctx, str, x, y, color = C.fg, align = 'left', baseline = 'alphabetic', size = 12) {
ctx.save();
ctx.fillStyle = color;
ctx.font = `${size}px system-ui, sans-serif`;
ctx.textAlign = align;
ctx.textBaseline = baseline;
ctx.fillText(str, x, y);
ctx.restore();
}
// ----- sequence-diagram primitives -----
// Draw a packet as a slanted arrow from (fromX, yStart) to (toX, yEnd).
// If lost === true, dash the line and draw an X near the midpoint where it gets dropped.
// dropFrac (0..1) controls how far along the loss happens.
function drawPacket(ctx, fromX, toX, yStart, yEnd, label, color, lost = false, dropFrac = 0.5) {
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = 1.8;
if (lost) {
ctx.setLineDash([4, 4]);
const mx = fromX + (toX - fromX) * dropFrac;
const my = yStart + (yEnd - yStart) * dropFrac;
ctx.beginPath(); ctx.moveTo(fromX, yStart); ctx.lineTo(mx, my); ctx.stroke();
ctx.setLineDash([]);
// X at drop point
ctx.strokeStyle = C.red;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(mx - 6, my - 6); ctx.lineTo(mx + 6, my + 6);
ctx.moveTo(mx - 6, my + 6); ctx.lineTo(mx + 6, my - 6);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(fromX, yStart);
ctx.lineTo(toX, yEnd);
ctx.stroke();
// arrowhead
const dx = toX - fromX, dy = yEnd - yStart;
const L = Math.hypot(dx, dy);
const ux = dx / L, uy = dy / L;
const ax = toX - ux * 8;
const ay = yEnd - uy * 8;
const px = -uy, py = ux;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(toX, yEnd);
ctx.lineTo(ax + px * 4, ay + py * 4);
ctx.lineTo(ax - px * 4, ay - py * 4);
ctx.closePath();
ctx.fill();
}
// label, place near the midpoint, slightly above the line
const lx = (fromX + toX) / 2;
const ly = (yStart + yEnd) / 2 - 4;
text(ctx, label, lx, ly, color, 'center');
ctx.restore();
}
function drawTimelines(ctx, leftX, rightX, topY, botY, clientLabel = 'Client', serverLabel = 'Server') {
ctx.save();
ctx.strokeStyle = C.dim;
ctx.lineWidth = 1;
ctx.setLineDash([2, 4]);
ctx.beginPath();
ctx.moveTo(leftX, topY); ctx.lineTo(leftX, botY);
ctx.moveTo(rightX, topY); ctx.lineTo(rightX, botY);
ctx.stroke();
ctx.setLineDash([]);
// boxes at the top
ctx.fillStyle = C.panel;
ctx.strokeStyle = C.cyan;
ctx.fillRect(leftX - 40, topY - 30, 80, 24);
ctx.strokeRect(leftX - 40, topY - 30, 80, 24);
ctx.fillRect(rightX - 40, topY - 30, 80, 24);
ctx.strokeRect(rightX - 40, topY - 30, 80, 24);
text(ctx, clientLabel, leftX, topY - 14, C.fg, 'center');
text(ctx, serverLabel, rightX, topY - 14, C.fg, 'center');
// "time" arrow on the far left
ctx.strokeStyle = C.dim;
ctx.beginPath();
ctx.moveTo(20, topY); ctx.lineTo(20, botY - 8);
ctx.lineTo(16, botY - 14);
ctx.moveTo(20, botY - 8); ctx.lineTo(24, botY - 14);
ctx.stroke();
text(ctx, 'time', 26, botY - 4, C.dim);
ctx.restore();
}
// ---------- Scene 1: TCP three-way handshake ----------
function sceneHandshake() {
const s = scene('scene-handshake', {
height: 360,
readout: [
{ key: 'cseq', label: 'Client seq' },
{ key: 'sseq', label: 'Server seq' },
{ key: 'state', label: 'Connection state' },
],
caption: 'Every TCP connection opens with the same short ritual. The client announces a starting sequence number, the server acknowledges it and announces its own, and the client acknowledges that. After those three packets the connection is open. TCP wants you to have made eye contact before exchanging any data.',
});
if (!s) return;
const CLIENT_SEQ = 1000;
const SERVER_SEQ = 5000;
// Animated cycle schedule (seconds).
const CYCLE = 7.0;
const schedule = [
{ name: 'SYN', t0: 0.3, t1: 1.5, color: C.yellow, dir: 'right', label: `SYN seq=${CLIENT_SEQ}` },
{ name: 'SYN-ACK', t0: 2.0, t1: 3.2, color: C.orange, dir: 'left', label: `SYN-ACK seq=${SERVER_SEQ} ack=${CLIENT_SEQ + 1}` },
{ name: 'ACK', t0: 3.7, t1: 4.9, color: C.green, dir: 'right', label: `ACK seq=${CLIENT_SEQ + 1} ack=${SERVER_SEQ + 1}` },
];
const startWall = performance.now();
function drawPartial(ctx, fromX, toX, yStart, yEnd, frac, label, color) {
// Draw a forward-progress arrow whose head has advanced `frac` along the
// full line from (fromX, yStart) to (toX, yEnd). 1.0 = fully arrived.
frac = Math.max(0, Math.min(1, frac));
if (frac <= 0) return;
const endX = fromX + (toX - fromX) * frac;
const endY = yStart + (yEnd - yStart) * frac;
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = 1.8;
ctx.beginPath();
ctx.moveTo(fromX, yStart);
ctx.lineTo(endX, endY);
ctx.stroke();
// Arrowhead
const dx = toX - fromX, dy = yEnd - yStart;
const L = Math.hypot(dx, dy);
const ux = dx / L, uy = dy / L;
const ax = endX - ux * 8;
const ay = endY - uy * 8;
const px = -uy, py = ux;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(endX, endY);
ctx.lineTo(ax + px * 4, ay + py * 4);
ctx.lineTo(ax - px * 4, ay - py * 4);
ctx.closePath();
ctx.fill();
// Label at midpoint of the traveled portion
const lx = (fromX + endX) / 2;
const ly = (yStart + endY) / 2 - 4;
text(ctx, label, lx, ly, color, 'center');
ctx.restore();
}
function draw() {
const { w, h } = s.getSize();
const ctx = s.ctx;
clear(ctx, w, h);
const topY = 60, botY = h - 40;
const leftX = 150, rightX = w - 60;
drawTimelines(ctx, leftX, rightX, topY, botY);
const slant = 24;
const rowH = (botY - topY - slant) / 3.5;
const y1a = topY + rowH * 0.2, y1b = y1a + slant;
const y2a = y1b + rowH * 0.5, y2b = y2a + slant;
const y3a = y2b + rowH * 0.5, y3b = y3a + slant;
const rowsY = [
{ a: y1a, b: y1b },
{ a: y2a, b: y2b },
{ a: y3a, b: y3b },
];
const now = ((performance.now() - startWall) / 1000) % CYCLE;
schedule.forEach((pkt, i) => {
const { a, b } = rowsY[i];
const fromX = pkt.dir === 'right' ? leftX : rightX;
const toX = pkt.dir === 'right' ? rightX : leftX;
const frac = (now - pkt.t0) / (pkt.t1 - pkt.t0);
if (frac <= 0) return;
drawPartial(ctx, fromX, toX, a, b, frac, pkt.label, pkt.color);
});
// Progressive state labels on the client timeline. Place them in the
// vertical gaps between the three packet rows so they never collide with
// each other or with the packet arrows.
const gutterRight = leftX - 10;
const state =
now < schedule[0].t0 ? 'CLOSED' :
now < schedule[1].t1 ? 'SYN-SENT' :
'ESTABLISHED';
const yClosed = topY + 10;
const ySynSent = (y1b + y2a) / 2;
const yEstablished = (y2b + y3a) / 2;
const yOpen = y3b + 22;
text(ctx, 'CLOSED', gutterRight, yClosed, state === 'CLOSED' ? C.fg : C.dim, 'right', 'middle');
text(ctx, 'SYN-SENT', gutterRight, ySynSent, state === 'SYN-SENT' ? C.yellow : C.dim, 'right', 'middle');
text(ctx, 'ESTABLISHED', gutterRight, yEstablished, state === 'ESTABLISHED' ? C.green : C.dim, 'right', 'middle');
if (state === 'ESTABLISHED') {
text(ctx, 'connection open', gutterRight, yOpen, C.green, 'right', 'middle');
}
// Time progress bar along the very top, subtle.
const barY = 18, barX0 = 20, barX1 = w - 20;
ctx.strokeStyle = C.grid;
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(barX0, barY); ctx.lineTo(barX1, barY); ctx.stroke();
ctx.fillStyle = C.cyan;
ctx.beginPath();
ctx.arc(barX0 + (barX1 - barX0) * (now / CYCLE), barY, 3, 0, Math.PI * 2);
ctx.fill();
s.setReadout('cseq', `${CLIENT_SEQ + 1}`);
s.setReadout('sseq', `${SERVER_SEQ + 1}`);
s.setReadout('state', state);
}
function loop() { draw(); requestAnimationFrame(loop); }
loop();
}
// ---------- Scene 2: Data with sequence numbers, ACKs, and loss ----------
function sceneData() {
const s = scene('scene-data', {
height: 460,
controls: [
{ key: 'loss', label: 'Packet loss', min: 0, max: 4, value: 1, step: 1,
format: v => ['none', '1 packet', '2 packets', '3 packets', 'burst'][Math.floor(v)] },
],
readout: [
{ key: 'sent', label: 'Segments sent' },
{ key: 'retx', label: 'Retransmissions' },
],
caption: 'Once the connection is open, each byte has a sequence number. The receiver acknowledges the next byte it expects, not the one it just got. If a segment goes missing, the receiver keeps asking for the same byte (duplicate ACKs) and the sender, eventually noticing, hands it over again. This is TCP being stubborn about delivery.',
});
if (!s) return;
function draw() {
const { w, h } = s.getSize();
const ctx = s.ctx;
clear(ctx, w, h);
const topY = 50, botY = h - 30;
const leftX = 90, rightX = w - 90;
drawTimelines(ctx, leftX, rightX, topY, botY);
// Scenario data: sequence of sends and the receiver response.
// Each packet occupies one "row". Lost packets don't arrive; ACKs keep
// asking for the lost byte.
const scenarios = [
// 0: none lost
[
{ kind: 'data', seq: 1, lost: false },
{ kind: 'data', seq: 2, lost: false },
{ kind: 'data', seq: 3, lost: false },
{ kind: 'data', seq: 4, lost: false },
],
// 1: one lost (seq 2)
[
{ kind: 'data', seq: 1, lost: false },
{ kind: 'data', seq: 2, lost: true },
{ kind: 'data', seq: 3, lost: false },
{ kind: 'data', seq: 4, lost: false },
{ kind: 'retx', seq: 2, lost: false },
],
// 2: two lost (seq 2 and 4)
[
{ kind: 'data', seq: 1, lost: false },
{ kind: 'data', seq: 2, lost: true },
{ kind: 'data', seq: 3, lost: false },
{ kind: 'data', seq: 4, lost: true },
{ kind: 'retx', seq: 2, lost: false },
{ kind: 'retx', seq: 4, lost: false },
],
// 3: three lost
[
{ kind: 'data', seq: 1, lost: true },
{ kind: 'data', seq: 2, lost: true },
{ kind: 'data', seq: 3, lost: false },
{ kind: 'retx', seq: 1, lost: false },
{ kind: 'retx', seq: 2, lost: false },
],
// 4: burst loss
[
{ kind: 'data', seq: 1, lost: false },
{ kind: 'data', seq: 2, lost: true },
{ kind: 'data', seq: 3, lost: true },
{ kind: 'data', seq: 4, lost: true },
{ kind: 'retx', seq: 2, lost: false },
{ kind: 'retx', seq: 3, lost: false },
{ kind: 'retx', seq: 4, lost: false },
],
];
const scenario = scenarios[Math.floor(s.values.loss)];
const slant = 22;
// Compute rows: each data/retx gets a row; if not lost, the ACK comes
// back just below on the next slant.
const rows = [];
let nextExpected = 1;
scenario.forEach(pkt => {
rows.push({ type: 'send', pkt });
if (!pkt.lost) {
if (pkt.seq === nextExpected) nextExpected++;
rows.push({ type: 'ack', expect: nextExpected });
} else {
// duplicate ACK still goes out for the last in-order byte
rows.push({ type: 'ack', expect: nextExpected, dup: true });
}
});
const rowH = Math.min(34, (botY - topY - slant - 10) / rows.length);
let sent = 0, retx = 0;
rows.forEach((r, i) => {
const y = topY + 10 + i * rowH;
if (r.type === 'send') {
const color = r.pkt.kind === 'retx' ? C.orange : C.cyan;
const label = r.pkt.kind === 'retx' ? `retx seq=${r.pkt.seq}` : `DATA seq=${r.pkt.seq}`;
drawPacket(ctx, leftX, rightX, y, y + slant, label, color, r.pkt.lost, 0.5);
sent++;
if (r.pkt.kind === 'retx') retx++;
} else {
const label = r.dup ? `ACK ack=${r.expect} (dup)` : `ACK ack=${r.expect}`;
const color = r.dup ? C.red : C.green;
drawPacket(ctx, rightX, leftX, y, y + slant, label, color, false);
}
});
s.setReadout('sent', sent);
s.setReadout('retx', retx);
requestAnimationFrame(() => {});
}
// Re-render whenever slider changes: attach input listeners via rAF loop.
function loop() {
draw();
requestAnimationFrame(loop);
}
loop();
}
// ---------- Scene 3: Sliding window ----------
function sceneWindow() {
const s = scene('scene-window', {
height: 520,
controls: [
{ key: 'win', label: 'Window size (packets)', min: 1, max: 8, value: 4, step: 1, format: v => `${v}` },
],
readout: [
{ key: 'rtts', label: 'RTTs to send 8 packets' },
{ key: 'throughput', label: 'Throughput vs stop-and-wait' },
],
caption: 'Sending one packet and then waiting for its ACK wastes nearly the entire round-trip time. TCP instead keeps a pile of packets in flight at once, up to the window size, and only waits when that pile is full. Doubling the window roughly doubles the throughput, up to the point where something else in the network becomes the bottleneck.',
});
if (!s) return;
function draw() {
const { w, h } = s.getSize();
const ctx = s.ctx;
clear(ctx, w, h);
const topY = 50, botY = h - 30;
const leftX = 90, rightX = w - 90;
drawTimelines(ctx, leftX, rightX, topY, botY);
const win = Math.floor(s.values.win);
const totalPackets = 8;
const rtts = Math.ceil(totalPackets / win);
const slant = 20;
// Compute vertical layout adaptively so every seq/ack label gets
// enough space to be legible regardless of window size.
const avail = botY - topY - 20;
const rowsCount = totalPackets * 2; // send rows + ack rows
const gapCount = rtts - 1;
const unit = avail / (rowsCount + gapCount * 0.7);
const rowStep = Math.max(slant + 8, Math.min(32, unit));
const rttGap = Math.min(rowStep * 0.7, 24);
let y = topY + 12;
let packetN = 1;
for (let batch = 0; batch < rtts; batch++) {
const batchSize = Math.min(win, totalPackets - batch * win);
// Send batch
for (let i = 0; i < batchSize; i++) {
drawPacket(ctx, leftX, rightX, y, y + slant, `seq=${packetN}`, C.cyan);
packetN++;
y += rowStep;
}
// ACKs for the batch
for (let i = 0; i < batchSize; i++) {
const ackNum = batch * win + i + 2;
drawPacket(ctx, rightX, leftX, y, y + slant, `ack=${ackNum}`, C.green);
y += rowStep;
}
// Extra gap between RTTs so each round-trip is visually distinct.
if (batch < rtts - 1) y += rttGap;
}
s.setReadout('rtts', `${rtts}`);
const speedup = Math.ceil(totalPackets / rtts);
s.setReadout('throughput', `${speedup}× faster`);
}
function loop() { draw(); requestAnimationFrame(loop); }
loop();
}
// ---------- Scene 4: Congestion control ----------
function sceneCongestion() {
const s = scene('scene-congestion', {
height: 380,
controls: [
{ key: 'lossAt', label: 'First loss at time', min: 3, max: 20, value: 8, step: 0.5, format: v => `RTT ${v.toFixed(1)}` },
{ key: 'ssthresh0', label: 'Initial ssthresh', min: 8, max: 64, value: 32, step: 1, format: v => `${v}` },
],
readout: [
{ key: 'peak', label: 'Peak cwnd' },
{ key: 'final', label: 'Final cwnd' },
],
caption: 'TCP has no idea how much bandwidth is available. It finds out by sending more and more until something breaks. Slow start ramps the congestion window up exponentially until it hits the slow-start threshold, then tiptoes up by one packet per RTT. A packet loss halves the window and the whole thing starts over. This is a protocol that lives its entire life anxious about whether it should be going faster.',
});
if (!s) return;
function draw() {
const { w, h } = s.getSize();
const ctx = s.ctx;
clear(ctx, w, h);
const margin = { l: 50, r: 20, t: 25, b: 40 };
const pW = w - margin.l - margin.r;
const pH = h - margin.t - margin.b;
const tMax = 40; // RTTs
const cwndMax = 80;
const xOf = t => margin.l + (t / tMax) * pW;
const yOf = c => margin.t + (1 - c / cwndMax) * pH;
ctx.strokeStyle = C.grid;
ctx.strokeRect(margin.l, margin.t, pW, pH);
for (let t = 0; t <= tMax; t += 5) {
ctx.beginPath(); ctx.moveTo(xOf(t), margin.t); ctx.lineTo(xOf(t), margin.t + pH); ctx.stroke();
text(ctx, `${t}`, xOf(t), margin.t + pH + 14, C.dim, 'center');
}
for (let c = 0; c <= cwndMax; c += 20) {
ctx.beginPath(); ctx.moveTo(margin.l, yOf(c)); ctx.lineTo(margin.l + pW, yOf(c)); ctx.stroke();
text(ctx, `${c}`, margin.l - 5, yOf(c), C.dim, 'right', 'middle');
}
text(ctx, 'time (RTTs)', margin.l + pW / 2, margin.t + pH + 28, C.dim, 'center');
ctx.save();
ctx.translate(margin.l - 40, margin.t + pH / 2);
ctx.rotate(-Math.PI / 2);
text(ctx, 'cwnd (segments)', 0, 0, C.dim, 'center');
ctx.restore();
// Run the classic Reno state machine
let cwnd = 1;
let ssthresh = s.values.ssthresh0;
const firstLoss = s.values.lossAt;
const lossInterval = 12; // RTTs between simulated losses
const points = [{ t: 0, cwnd, phase: 'slow' }];
let nextLoss = firstLoss;
let peak = cwnd;
for (let t = 1; t <= tMax; t += 1) {
if (t >= nextLoss) {
// Multiplicative decrease: ssthresh = cwnd / 2, cwnd resumes at ssthresh (fast recovery)
ssthresh = Math.max(2, Math.floor(cwnd / 2));
cwnd = ssthresh;
nextLoss += lossInterval;
points.push({ t, cwnd, phase: 'loss' });
continue;
}
if (cwnd < ssthresh) {
cwnd = cwnd * 2; // slow start: exponential
} else {
cwnd = cwnd + 1; // congestion avoidance: linear
}
if (cwnd > cwndMax) cwnd = cwndMax;
peak = Math.max(peak, cwnd);
points.push({ t, cwnd, phase: cwnd < ssthresh ? 'slow' : 'avoid' });
}
// Plot cwnd
ctx.strokeStyle = C.cyan;
ctx.lineWidth = 2;
ctx.beginPath();
points.forEach((p, i) => {
const x = xOf(p.t), y = yOf(p.cwnd);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
// Mark losses
points.filter(p => p.phase === 'loss').forEach(p => {
ctx.fillStyle = C.red;
ctx.beginPath();
ctx.arc(xOf(p.t), yOf(p.cwnd), 4, 0, Math.PI * 2);
ctx.fill();
text(ctx, 'loss', xOf(p.t) + 6, yOf(p.cwnd) - 4, C.red);
});
// Phase legend
text(ctx, 'slow start → congestion avoidance → loss → repeat', margin.l + 6, margin.t + 14, C.dim);
s.setReadout('peak', `${peak.toFixed(0)} segments`);
s.setReadout('final', `${points[points.length - 1].cwnd.toFixed(0)} segments`);
requestAnimationFrame(() => {});
}
function loop() { draw(); requestAnimationFrame(loop); }
loop();
}
// ---------- Scene 5: UDP: send and forget ----------
function sceneUDP() {
const s = scene('scene-udp', {
height: 400,
controls: [
{ key: 'loss', label: 'Network loss rate', min: 0, max: 60, value: 15, step: 1, format: v => v.toFixed(0) + '%' },
],
readout: [
{ key: 'sent', label: 'Sent' },
{ key: 'rcvd', label: 'Received' },
{ key: 'recovery', label: 'Recovery' },
],
caption: 'UDP lives in the same IP packets that TCP does but deliberately refuses almost all of TCP\'s help. There is no handshake, no sequence tracking, no retransmission, no flow control, no congestion control. Packets either arrive or they don\'t, and if they don\'t, UDP would like you to deal with it. This sounds bad until you need to send voice, video, DNS queries, or game state, at which point it sounds wonderful.',
});
if (!s) return;
// Deterministic pseudo-random so the scene is reproducible.
function hash(seed, n) {
let x = Math.sin(seed * 9301 + n * 49297) * 233280;
return x - Math.floor(x);
}
function draw() {
const { w, h } = s.getSize();
const ctx = s.ctx;
clear(ctx, w, h);
const topY = 50, botY = h - 30;
const leftX = 90, rightX = w - 90;
drawTimelines(ctx, leftX, rightX, topY, botY, 'Client', 'Server');
const totalPackets = 10;
const lossPct = s.values.loss / 100;
const slant = 18;
const rowH = Math.min(28, (botY - topY - slant - 10) / totalPackets);
let received = 0;
for (let i = 0; i < totalPackets; i++) {
const y = topY + 10 + i * rowH;
const lost = hash(1, i) < lossPct;
drawPacket(ctx, leftX, rightX, y, y + slant, `UDP seq=${i + 1}`, C.magenta, lost, 0.5 + (hash(2, i) - 0.5) * 0.3);
if (!lost) received++;
}
s.setReadout('sent', `${totalPackets}`);
s.setReadout('rcvd', `${received}`);
s.setReadout('recovery', received === totalPackets ? 'none needed' : 'none (UDP does not care)');
requestAnimationFrame(() => {});
}
function loop() { draw(); requestAnimationFrame(loop); }
loop();
}
function boot() {
const host = document.querySelector('.scene-host, [data-scene-host]');
if (host) host.classList.add('scene-host');
sceneHandshake();
sceneData();
sceneWindow();
sceneCongestion();
sceneUDP();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();