AI EngineeringZero to ProductionHome·About·Contact
System Design · Chapter SD10

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.

⏱️ ~3.5 hours🧪 5 labs🎯 Beginner→Expert
🌱 Start here — from zeroComputer networks, from scratch. A network moves bytes between machines that may be on the other side of the planet, over links that lose, reorder, and delay packets. The genius of the design is layering: each layer solves one problem and hands a clean abstraction up. This chapter walks the layers, contrasts TCP and UDP, and traces exactly what happens when you type a URL. The reliability sims (checksum, sliding window) are models of the algorithms, not a real network stack.

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.
▶ Runnable companionThe checksum and sliding-window labs are plain-Python models of the reliability mechanisms TCP uses — they reproduce the logic (detect corruption, keep a window of unacked packets) without touching a real socket.

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 layerJobExamples≈ OSI layers
Applicationapp-level messagesHTTP, DNS, TLS5–7
Transportend-to-end deliveryTCP, UDP4
Internetaddressing & routingIP, ICMP3
Linkone physical hopEthernet, Wi-Fi1–2
Application HTTP/DNS/TLS Transport TCP/UDP Internet IP Link Ethernet/Wi-Fi
Encapsulation in one sentenceYour HTTP request is put inside a TCP segment, which is put inside an IP packet, which is put inside a link-layer frame — like nested envelopes, each added by one layer and stripped by the same layer on the far side.

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.

SYN SYN-ACK ACK data
TCPUDP
Deliveryreliable, orderedbest-effort
Connectionyes (handshake)no
Overheadhigher (acks, retransmit)minimal
Use forweb, APIs, file transfervideo, games, DNS, VoIP
Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Step 1 · The handshake as a state machine (model)
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.

example.com Resolver Root/TLD/Auth 93.184.x.x
Step 2 · A recursive DNS resolver with caching (model)
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')
DNS caching is why the web is fastThe first lookup walks the hierarchy; every later one hits a cache (in your OS, your resolver, or the browser) until the record's TTL expires. This is the SD5 cache pattern again — recompute rarely, serve from memory usually.

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).

TCP open ClientHello Cert + key exchange Encrypted HTTP
Step 3 · Modelling the TLS guarantees
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
This is a model, not real TLSReal TLS uses asymmetric crypto (certificates, ephemeral Diffie–Hellman) to agree the key and negotiate cipher suites and versions. The lab shows the outcome — a shared symmetric key plus a MAC for integrity — so you can reason about the guarantees without reimplementing the protocol.

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).

Step 4 · A checksum + sliding-window sender (model)
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)]
Window size is the throughput knobStop-and-wait (window = 1) wastes the link waiting for each ack; a larger window keeps more data in flight and fills the pipe. The right window is roughly the bandwidth × round-trip-time product — a direct tie to the latency thinking in SD5/SD6.

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:

  1. DNS resolution — the browser resolves example.com to an IP (§3), hitting caches first.
  2. TCP handshake — a 3-way SYN/SYN-ACK/ACK opens a connection to that IP on port 443 (§2).
  3. TLS handshake — client and server authenticate and agree a symmetric key; the channel is now encrypted (§4).
  4. HTTP request — the browser sends GET / with headers, inside the TLS tunnel, carried by TCP over IP (§1 encapsulation).
  5. Server processing — a load balancer picks an app server (SD5), which may hit a cache or database and returns an HTTP response.
  6. Render — the browser parses HTML, and fetches CSS/JS/images (often reusing the same connection), then paints the page.
Every box you built shows up hereDNS caching, the handshake, TLS, load balancing, and app caching are all on this single path. When an interviewer asks "what happens when you type a URL," they are checking whether you can connect all of these layers end to end.

✓ 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 yourself
✓ Knowledge check

A 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
Use UDP. A position update that arrives late is worthless — the game has already moved on — so TCP's guarantee of in-order, retransmitted delivery is exactly the wrong thing: a single lost packet would stall the stream (head-of-line blocking) while TCP retransmits stale data. UDP lets the game drop the old update and use the next fresh one, trading reliability for the low latency the workload actually needs.
✓ Knowledge check

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
TLS provides confidentiality (encryption), integrity (tamper detection via a MAC), and authentication (the certificate proves the server's identity). Integrity is what stops silent modification: any change to the ciphertext makes the MAC fail to verify, so the tampered message is rejected rather than acted on.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Classify protocols into layersBeginner

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.

Exercise 2 · Model the 3-way handshakeIntermediate

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.

Exercise 3 · A checksum that catches corruptionAdvanced

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))  # False

The 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.

Exercise 4 · Sliding window vs stop-and-wait throughputExpert

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.

Exercise 5 · A recursive resolver with TTL cachingProfessional

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).

Exercise 6 · Trace a URL load and attribute the latencyIndustry scenario

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.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in