Computer networks
Every distributed system in this track talks over a network, and the network has its own laws. This lesson builds the layered model, TCP vs UDP, the handshake, IP/DNS, and HTTPS/TLS — then walks the full path of typing a URL. Runnable models of a checksum and a sliding-window sender make the reliability machinery concrete.
Learning objectives
- Map the OSI and TCP/IP layer models and say what each layer does.
- Choose TCP vs UDP for a workload and explain the 3-way handshake.
- Explain IP addressing, routing, and how DNS resolves a name.
- Describe the HTTPS/TLS handshake and what it guarantees.
- Trace end-to-end what happens when you type a URL and press enter.
1 · Layering — OSI vs TCP/IP
Networks are built as a stack of layers. Each layer uses the layer below and serves the layer above, so you can reason about (say) HTTP without thinking about voltage on a wire. The OSI model has seven layers (a teaching reference); the TCP/IP model that the internet actually runs has four. Data moving down the stack is encapsulated — each layer wraps the layer above in its own header.
| TCP/IP layer | Job | Examples | ≈ OSI layers |
|---|---|---|---|
| Application | app-level messages | HTTP, DNS, TLS | 5–7 |
| Transport | end-to-end delivery | TCP, UDP | 4 |
| Internet | addressing & routing | IP, ICMP | 3 |
| Link | one physical hop | Ethernet, Wi-Fi | 1–2 |
2 · TCP vs UDP & the 3-way handshake
TCP gives a reliable, ordered byte stream: it retransmits lost data, reorders what arrives out of order, and controls its sending rate. UDP gives none of that — it just fires datagrams and hopes. You pay for TCP's guarantees with latency (handshakes, acknowledgements); UDP trades reliability for speed, which is why live video, games, and DNS often use it. A TCP connection opens with a 3-way handshake: SYN → SYN-ACK → ACK, which synchronises sequence numbers on both sides before any data flows.
| TCP | UDP | |
|---|---|---|
| Delivery | reliable, ordered | best-effort |
| Connection | yes (handshake) | no |
| Overhead | higher (acks, retransmit) | minimal |
| Use for | web, APIs, file transfer | video, games, DNS, VoIP |
handshake.py# MODEL: the client/server sequence-number exchange of a TCP open.
def three_way_handshake(client_isn, server_isn):
log = []
# 1. client -> server: SYN, seq = client_isn
log.append(("C->S", "SYN", client_isn))
# 2. server -> client: SYN-ACK, seq = server_isn, ack = client_isn + 1
log.append(("S->C", "SYN-ACK", server_isn, client_isn + 1))
# 3. client -> server: ACK, ack = server_isn + 1
log.append(("C->S", "ACK", server_isn + 1))
established = True
return established, log
ok, log = three_way_handshake(client_isn=1000, server_isn=5000)
for step in log:
print(step)
print("connection established:", ok)
('C->S', 'SYN', 1000)
('S->C', 'SYN-ACK', 5000, 1001)
('C->S', 'ACK', 5001)
connection established: True
3 · IP addressing, routing & DNS
An IP address identifies an interface on the network; a packet carries a source and destination IP. Routing is how packets hop from router to router toward the destination network — each router consults a table and forwards to the best next hop. But humans use names, not numbers, so DNS (the Domain Name System) resolves a name like example.com to an IP, walking from the root servers down to the authoritative server, with caching at every level to keep it fast.
dns.py# MODEL: a resolver that walks root -> TLD -> authoritative, caching answers.
ROOT = {"com": "tld-com-server"}
TLD = {"example.com": "ns.example.com"}
AUTH = {"example.com": "93.184.216.34"}
class Resolver:
def __init__(self):
self.cache = {}
def resolve(self, name):
if name in self.cache:
return ("cache", self.cache[name])
tld = name.split(".")[-1]
ROOT[tld] # ask root for the TLD server
TLD[name] # ask TLD for the authoritative server
ip = AUTH[name] # ask authoritative for the A record
self.cache[name] = ip
return ("resolved", ip)
r = Resolver()
print(r.resolve("example.com")) # walks the chain
print(r.resolve("example.com")) # served from cache
('resolved', '93.184.216.34')
('cache', '93.184.216.34')
4 · HTTP, HTTPS & the TLS handshake
HTTP is the application-layer request/response protocol of the web: a method (GET, POST), a path, headers, and an optional body; the server replies with a status code and body. HTTPS is HTTP running inside a TLS tunnel. The TLS handshake (after the TCP handshake) authenticates the server via its certificate and negotiates a shared symmetric key, so everything afterward is encrypted and tamper-evident. TLS gives three guarantees: confidentiality (eavesdroppers see ciphertext), integrity (tampering is detected), and authentication (you're really talking to that server).
tls_model.py# MODEL: a key exchange producing a shared secret, then an integrity check.
# This illustrates WHAT TLS guarantees, not the real cryptographic protocol.
import hashlib, hmac
def derive_shared_key(client_random, server_random, premaster):
seed = f"{client_random}:{server_random}:{premaster}".encode()
return hashlib.sha256(seed).hexdigest() # stand-in for the TLS PRF
def mac(key, message): # integrity tag
return hmac.new(key.encode(), message.encode(), hashlib.sha256).hexdigest()
key = derive_shared_key("cr-123", "sr-456", "premaster-secret")
msg = "GET /account HTTP/1.1"
tag = mac(key, msg)
print("shared key (first 16):", key[:16])
# receiver recomputes the tag; a tampered message would not match
print("integrity ok:", hmac.compare_digest(tag, mac(key, msg)))
print("tamper detected:", not hmac.compare_digest(tag, mac(key, "GET /admin HTTP/1.1")))
shared key (first 16): fb61405c41b1c1df
integrity ok: True
tamper detected: True
5 · Reliability — checksums, windows, congestion & flow control
TCP turns an unreliable IP layer into a reliable stream with a few mechanisms. A checksum detects corrupted packets. Sequence numbers + acknowledgements detect loss and reordering. A sliding window lets the sender have several packets in flight at once (not stop-and-wait) while bounding how far ahead it may run. Two feedback loops shape the rate: flow control stops a fast sender from overwhelming a slow receiver (the receiver advertises its window), and congestion control stops senders from overwhelming the network (slow start, then back off on loss).
reliability.py# MODEL: 16-bit ones-complement-style checksum + a sliding window sender.
def checksum(data: bytes) -> int:
total = 0
for b in data:
total = (total + b) & 0xFFFF # keep it 16-bit
return (~total) & 0xFFFF # ones-complement
def corrupted(data, sent_ck):
return checksum(data) != sent_ck # receiver recomputes and compares
msg = b"hello-network"
ck = checksum(msg)
print("checksum ok:", not corrupted(msg, ck))
print("corruption caught:", corrupted(b"hallo-network", ck))
# Sliding window: at most `window` unacked packets in flight at once.
def send_with_window(packets, window):
base, next_seq, acked = 0, 0, []
timeline = []
while base < len(packets):
# send everything the window allows
while next_seq < len(packets) and next_seq < base + window:
timeline.append(("send", next_seq)); next_seq += 1
# receiver acks the oldest in order; window slides forward
timeline.append(("ack", base)); acked.append(base); base += 1
return timeline, acked
tl, acked = send_with_window(list(range(6)), window=3)
print("in-flight cap = 3; acked in order:", acked)
print("first 5 events:", tl[:5])
checksum ok: True
corruption caught: True
in-flight cap = 3; acked in order: [0, 1, 2, 3, 4, 5]
first 5 events: [('send', 0), ('send', 1), ('send', 2), ('ack', 0), ('send', 3)]
6 · What happens when you type a URL
This is the classic interview question — and it ties every section together. Follow one request from keystroke to rendered page:
From https://studybydoing.in to a rendered page:
- DNS resolution — the browser resolves
example.comto an IP (§3), hitting caches first. - TCP handshake — a 3-way SYN/SYN-ACK/ACK opens a connection to that IP on port 443 (§2).
- TLS handshake — client and server authenticate and agree a symmetric key; the channel is now encrypted (§4).
- HTTP request — the browser sends
GET /with headers, inside the TLS tunnel, carried by TCP over IP (§1 encapsulation). - Server processing — a load balancer picks an app server (SD5), which may hit a cache or database and returns an HTTP response.
- Render — the browser parses HTML, and fetches CSS/JS/images (often reusing the same connection), then paints the page.
✓ Checkpoint — you can move on when you can…
- Map the four TCP/IP layers and give an example protocol at each.
- Choose TCP vs UDP for a workload and justify it.
- Explain the 3-way handshake and why sequence numbers matter.
- Say what TLS guarantees and where its handshake sits relative to TCP.
- Walk the full URL-to-page path without skipping a layer.
Knowledge check
check yourselfA live multiplayer game sends 60 position updates per second. Would you build it on TCP or UDP, and why does the "wrong" choice actually hurt here?
Show answer
HTTPS runs HTTP inside TLS. Name the three guarantees TLS provides, and state which one stops an attacker who can read and modify traffic from silently changing your POST body.
Show answer
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Knowing which layer a protocol lives in is the first step to reasoning about a network problem — it tells you whose job a given failure is.
Your task: Given a list of protocols (HTTP, TCP, IP, Ethernet, DNS, UDP, TLS), map each to its TCP/IP layer and print the grouping.
Requirements:
- Use the four TCP/IP layers: Application, Transport, Internet, Link
- Place TCP and UDP at Transport, IP at Internet, Ethernet at Link
- Place HTTP, DNS and TLS at Application
- Print each layer with the protocols assigned to it
💡 Hint: A dict from protocol to layer, then invert it to group by layer.
Show solution
Map each protocol to its layer, then group:
layer_of = {
"HTTP": "Application", "DNS": "Application", "TLS": "Application",
"TCP": "Transport", "UDP": "Transport",
"IP": "Internet",
"Ethernet": "Link",
}
from collections import defaultdict
grouped = defaultdict(list)
for proto, layer in layer_of.items():
grouped[layer].append(proto)
for layer in ["Application", "Transport", "Internet", "Link"]:
print(f"{layer:12s}: {grouped[layer]}")TLS sits at the application layer (it runs over TCP); this is why a TLS problem is a different diagnosis from an IP-routing problem — layering localises the fault.
Context: The handshake is where TCP establishes the shared sequence-number state that reliable delivery depends on; modelling it makes the SYN/ACK numbers concrete.
Your task: Model the client/server message exchange of a TCP open, tracking sequence and acknowledgement numbers, and confirm the connection ends ESTABLISHED.
Requirements:
- Message 1: client SYN with its initial sequence number (ISN)
- Message 2: server SYN-ACK with its ISN and ack = client ISN + 1
- Message 3: client ACK with ack = server ISN + 1
- Return the message log and an established flag
💡 Hint: Each ACK number is the next sequence byte the sender expects, i.e. the peer's ISN + 1.
Show solution
Three messages, each carrying the sequence/ack numbers:
def handshake(client_isn, server_isn):
log = []
log.append(("C->S", "SYN", {"seq": client_isn}))
log.append(("S->C", "SYN-ACK", {"seq": server_isn, "ack": client_isn + 1}))
log.append(("C->S", "ACK", {"ack": server_isn + 1}))
return True, log
established, log = handshake(1000, 5000)
for m in log: print(m)
print("ESTABLISHED" if established else "FAILED")The ack is always "the next byte I expect from you," i.e. your ISN + 1. After the third message both sides agree on each other's starting sequence number, so ordered, reliable delivery can begin.
Context: Corruption detection is the most basic reliability guarantee; a checksum lets a receiver reject a damaged packet instead of acting on garbage.
Your task: Implement a 16-bit ones-complement-style checksum, and show it flags a single-bit/byte change in the payload.
Requirements:
- Sum the bytes modulo 2^16, then take the ones-complement
- A receiver recomputes the checksum and compares it to the sent value
- Show an intact message verifies and a modified one fails
- Note this catches many but not all corruptions (it is not cryptographic)
💡 Hint: Mask with & 0xFFFF to stay 16-bit; the final complement is ~total & 0xFFFF.
Show solution
Sum-and-complement, kept to 16 bits:
def checksum(data: bytes) -> int:
total = 0
for b in data:
total = (total + b) & 0xFFFF
return (~total) & 0xFFFF
def verify(data, sent_ck):
return checksum(data) == sent_ck
msg = b"reliable-transport"
ck = checksum(msg)
print("intact verifies:", verify(msg, ck)) # True
print("one byte changed:", verify(b"reliqble-transport", ck)) # FalseThe intact message verifies; flipping one byte changes the sum and the check fails, so the receiver rejects it. A simple additive checksum catches many errors but not all (e.g. some transpositions) and provides no protection against a deliberate attacker — that is TLS's job, not the checksum's.
Context: The sliding window is why TCP can fill a fast, high-latency link; comparing it to stop-and-wait quantifies the win.
Your task: Model the time to send N packets over a link with a given round-trip time under stop-and-wait (window 1) versus a window of W, and show the speedup.
Requirements:
- Stop-and-wait sends one packet per RTT
- A window of W sends up to W packets per RTT
- Model total time as roughly ceil(N / window) × RTT
- Report the time for window 1 vs window W and the speedup factor
- Relate the ideal window to bandwidth × RTT
💡 Hint: The number of round-trips is ceil(N / window); throughput scales with the window until it hits the bandwidth-delay product.
Show solution
Model the number of round-trips each strategy needs:
import math
def send_time_ms(n_packets, window, rtt_ms=50):
round_trips = math.ceil(n_packets / window)
return round_trips * rtt_ms
N = 100
saw = send_time_ms(N, window=1) # stop-and-wait
win = send_time_ms(N, window=10)
print(f"stop-and-wait: {saw} ms") # 5000
print(f"window=10: {win} ms") # 500
print(f"speedup: {saw / win:.0f}x") # 10x
# ideal window ~ bandwidth-delay product
bandwidth_bps, rtt_s, pkt_bits = 10_000_000, 0.05, 12_000
bdp_packets = (bandwidth_bps * rtt_s) / pkt_bits
print(f"ideal window ~ {bdp_packets:.0f} packets")Stop-and-wait pays one full RTT per packet; a window of 10 sends ten per RTT, a 10× speedup here. The window should be about the bandwidth-delay product — enough packets in flight to keep the pipe full — after which more window buys nothing and only risks congestion.
Context: A resolver with TTL-aware caching is the real workhorse of DNS; expiring stale records correctly is what keeps names both fast and fresh.
Your task: Build a DNS resolver that walks a mock hierarchy on a miss and caches answers with a TTL, treating expired entries as misses so they are re-resolved.
Requirements:
- Cache each answer with an expiry time (now + TTL)
- On lookup, serve from cache only if the entry has not expired
- On a miss or expiry, walk the mock root→TLD→authoritative chain
- Show a cached hit, then an expired entry causing a fresh resolve
- Label which path each lookup took
💡 Hint: Store (ip, expires_at) and compare a monotonic clock; a tiny sleep or a manual clock lets you demonstrate expiry deterministically.
Show solution
Cache the answer with an expiry and treat stale entries as misses:
import time
ROOT = {"com": "tld-com"}
TLD = {"example.com": "ns.example.com"}
AUTH = {"example.com": "93.184.216.34"}
class Resolver:
def __init__(self): self.cache = {} # name -> (ip, expires_at)
def resolve(self, name, ttl=1.0):
now = time.monotonic()
hit = self.cache.get(name)
if hit and hit[1] > now:
return ("cache", hit[0])
_ = ROOT[name.split(".")[-1]] # walk root
_ = TLD[name] # walk TLD
ip = AUTH[name] # authoritative answer
self.cache[name] = (ip, now + ttl)
return ("resolved", ip)
r = Resolver()
print(r.resolve("example.com", ttl=0.05)) # resolved
print(r.resolve("example.com", ttl=0.05)) # cache
time.sleep(0.06)
print(r.resolve("example.com", ttl=0.05)) # resolved again (expired)Prints resolved, then cache, then resolved again after the TTL lapses. TTL-aware caching is the exact mechanism that makes DNS both fast (almost always a cache hit) and eventually consistent (records refresh when they expire).
Context: Diagnosing a slow page load means attributing time to the right phase — DNS, connect, TLS, server, transfer — because each has a different fix.
Your task: Model the end-to-end latency of loading a URL as the sum of its phases (DNS, TCP, TLS, server, transfer), then identify the dominant phase and name the fix for it.
Requirements:
- Represent each phase with a modelled millisecond cost
- Sum them for the total page-load time
- Identify the single largest phase
- Map that phase to a concrete remedy (e.g. DNS caching, keep-alive, CDN, cache)
- Label all numbers as a MODEL, not a measurement
💡 Hint: Reuse the connection (keep-alive / session resumption) to remove repeat DNS/TCP/TLS costs on the second request — that is often the biggest real-world win.
Show solution
Attribute the modelled latency to phases and read off the fix:
def load_url_model(phases_ms):
"""MODEL: phases_ms maps phase -> modelled milliseconds."""
total = sum(phases_ms.values())
worst = max(phases_ms, key=phases_ms.get)
fixes = {
"dns": "cache DNS / raise TTL",
"tcp": "connection keep-alive / reuse",
"tls": "session resumption / keep-alive",
"server": "cache or speed up the app tier (SD5)",
"transfer": "compression / CDN edge",
}
return total, worst, fixes[worst]
first_load = {"dns": 40, "tcp": 30, "tls": 50, "server": 120, "transfer": 60}
total, worst, fix = load_url_model(first_load)
print(f"total {total} ms; dominant phase: {worst} -> {fix}")
# second load reuses the connection: DNS/TCP/TLS drop out
second_load = {"dns": 0, "tcp": 0, "tls": 0, "server": 120, "transfer": 60}
print("second load total:", sum(second_load.values()), "ms")On the first load the server phase dominates (120 ms), so the fix is app-tier caching (SD5). On the second load, keep-alive/session-resumption removes the DNS, TCP and TLS phases entirely — the single biggest real-world win for repeat requests. All figures here are modelled, not measured on your network.