Build the mental model
A computer network is a set of independent machines — hosts — connected by links so they can exchange data. The devices in between do two different jobs. A switch moves data between hosts inside one local network, using hardware addresses it has learned for itself. A router sits at the boundary between networks and decides which network a unit should travel toward next — hop by hop, with no device ever knowing the whole path.
How the data travels is the deeper question, and two designs competed. Circuit switching, which the old telephone system used, reserves a dedicated path end to end before you send anything. It is predictable but wasteful: the path stays reserved through every pause. Packet switching, which the internet uses, chops your data into small independent pieces called packets and sends each one separately. Each hop stores the whole packet, checks it, then forwards it — store-and-forward — so a single link can interleave packets belonging to many conversations. That is statistical multiplexing: because most senders are idle most of the time, a shared link carries far more conversations than dedicated circuits ever could. It also buys resilience — with no reserved path, a failed router just means later packets take a different route, whereas a broken circuit kills the call outright.
The cost is that packets can arrive late, out of order, or not at all; higher layers clean that up. Every packet has the same shape: a header carrying the control information a router needs — where from, where to, how long — followed by the payload, the bytes you actually wanted to send, which the network never looks inside.
TWO HOSTS, A SWITCH, A ROUTER, THE INTERNET
-------------------------------------------
Host A Host B
192.168.1.10 192.168.1.11
| |
+-------------+ +-------------+
| |
+---+---+-----+
| SWITCH | Layer 2: moves frames between
| (one LAN) | hosts inside this one network
+------+------+
|
+------+------+
| ROUTER | Layer 3: picks the next network
| 192.168.1.1 | for each packet, one hop at a time
+------+------+
|
( INTERNET ) many more routers, each one
| making the same local choice
+------+------+
| web server |
|198.51.100.10|
+-------------+
Every unit on those links has the same shape:
+----------------+--------------------------------+
| HEADER | PAYLOAD |
| from / to / len| the bytes you wanted to send |
+----------------+--------------------------------+
routers read this routers never look in hereConnect it to a real scenario
Picture a small office: two laptops and a printer plugged into one switch, the switch plugged into a router, the router facing the internet. Laptop A opens a page on a public web server. A sends the request as a packet whose header says "from 192.168.1.10, to 198.51.100.10". The switch looks only at hardware addresses, sees the destination is not local, and hands the frame to the router. The router reads the header's destination IP, consults its routing table, and forwards toward the next network — it never opens the payload. A dozen routers later the packet arrives, and the reply comes back the same way.
Now notice what the packet-switched design buys you. The printer can be spooling a large job at the same moment; its packets interleave with the web request on the same uplink, and neither one reserves the line. If an upstream router dies mid-transfer, the packets already in flight are lost, but the next ones are routed around the failure and TCP retransmits what was dropped — the user sees a pause, not a dead connection. Under circuit switching both of those would be different stories: the printer would need its own reserved capacity, and the router failure would drop the call entirely.
Try the working example
import struct
# A toy packet = fixed-size header + variable-length payload.
# Header layout, network byte order ("!" = big-endian):
# version 1 byte (B)
# ttl 1 byte (B)
# src_port 2 bytes (H)
# dst_port 2 bytes (H)
# total_len 2 bytes (H)
HEADER = "!BBHHH"
HDR_SIZE = struct.calcsize(HEADER)
def build_packet(payload):
total = HDR_SIZE + len(payload)
header = struct.pack(HEADER, 1, 64, 49152, 80, total)
return header + payload
def parse_packet(raw):
version, ttl, src, dst, total = struct.unpack(HEADER, raw[:HDR_SIZE])
return {
"version": version,
"ttl": ttl,
"src_port": src,
"dst_port": dst,
"total_len": total,
"payload": raw[HDR_SIZE:],
}
packet = build_packet(b"GET /index.html")
print("header size :", HDR_SIZE, "bytes")
print("packet size :", len(packet), "bytes")
print("on the wire :", packet.hex())
print()
fields = parse_packet(packet)
for key in ("version", "ttl", "src_port", "dst_port", "total_len"):
print("%-10s = %s" % (key, fields[key]))
print("%-10s = %s" % ("payload", fields["payload"].decode()))
print()
# A router forwards using the header alone; the payload stays opaque.
print("router reads:", packet[:HDR_SIZE].hex())
print("router skips:", len(packet) - HDR_SIZE, "payload bytes")
header size : 8 bytes
packet size : 23 bytes
on the wire : 0140c00000500017474554202f696e6465782e68746d6c
version = 1
ttl = 64
src_port = 49152
dst_port = 80
total_len = 23
payload = GET /index.html
router reads: 0140c00000500017
router skips: 15 payload bytes5-minute try-it
Change the HEADER format string to "!BBHHHI" to add a 4-byte sequence number field. Update both build_packet and parse_packet, then print how much the header grew and what the payload efficiency becomes for the same 15-byte payload.
One important caution
Omitting "!" (network byte order) in struct.pack — the default native order varies by machine, so a little-endian sender and a big-endian reader will disagree on every multi-byte number
Treating a switch and a router as interchangeable — a switch only moves data inside one network and cannot join two subnets on its own
Cloudflare Learning Center — What is packet switching? — Computer Networking