Build the mental model
Layering exists because a network stack is far too large to design, build, or replace as one piece. If the code that puts bits on a wire were tangled with the code that formats an HTTP request, then switching from Wi-Fi to Ethernet would mean rewriting your web browser. Layering draws the lines deliberately: each layer offers one narrow service upward and depends on one narrow service below. So Wi-Fi can be reinvented without HTTP noticing, and HTTP/3 can appear without touching a single cable.
The OSI model names seven such layers — physical, data link, network, transport, session, presentation, application — and is best treated as vocabulary, not a blueprint. Nobody ships an OSI stack. The TCP/IP model, which real systems actually implement, uses four: link, internet, transport, application. The mapping is loose but useful. OSI's physical and data link collapse into TCP/IP's link; network becomes internet; transport stays transport; and OSI's top three fold into a single application layer — which is why people say "Layer 7" for anything above transport.
The mechanism that makes layering real is encapsulation. As data travels down the stack, each layer prepends its own header and treats what it received as an opaque payload. Your bytes become a segment, the segment becomes a packet, the packet becomes a frame. On the receiving side each layer strips exactly its own header and hands the rest upward, so peers at each level act as if talking directly. That is why a router can forward your traffic while being indifferent to whether it carries HTTP, video, or a game — and why adding encryption at one layer changes nothing elsewhere.
OSI VS TCP/IP, AND WHAT ENCAPSULATION LOOKS LIKE
------------------------------------------------
OSI (7 layers) TCP/IP (4 layers)
+------------------+ +--------------------+
| 7 Application | | |
| 6 Presentation |---------->| Application |
| 5 Session | | |
+------------------+ +--------------------+
| 4 Transport |---------->| Transport |
+------------------+ +--------------------+
| 3 Network |---------->| Internet |
+------------------+ +--------------------+
| 2 Data Link | | |
| 1 Physical |---------->| Link |
+------------------+ +--------------------+
Going DOWN the stack, each layer wraps whatever it was handed:
L7 [ data ] your bytes
L4 [ TCP | data ] = segment
L3 [ IP | TCP | data ] = packet
L2 [ETH| IP | TCP | data |FCS] = frame
\____________________________/
this is what the cable carries
Going UP, each layer removes exactly its own header and no more.Connect it to a real scenario
Suppose you are debugging why a request never arrives, and you have a packet capture open. Layering tells you exactly where to look, and in what order. Expand one captured frame and you will see the same nesting the model describes: an Ethernet header with two MAC addresses, inside it an IP header with two IP addresses, inside that a TCP header with two port numbers, and only then your HTTP request text.
Read it as a checklist. If there is no frame at all, the problem is below the network layer — cable, Wi-Fi association, switch port. If frames appear but the IP header shows a destination you did not expect, your routing or DNS resolution is wrong, not your application code. If IP looks right but TCP shows repeated SYNs with no reply, something is dropping the connection attempt — a firewall, or a service that is not listening — and reading the HTTP payload would be pointless because there is no HTTP yet.
Encapsulation also explains a very real performance concern: every layer adds header bytes. A tiny 5-byte message ends up inside a 36-byte frame, as the code below shows. That overhead is negligible for a file transfer and dominant for chatty small messages, which is exactly why protocols batch.
Try the working example
import struct
# Start at the top of the stack: the data the application wants to send.
data = b"hello"
# L4 transport: prepend src port, dst port, segment length.
l4_header = struct.pack("!HHH", 51000, 443, 6 + len(data))
segment = l4_header + data
# L3 network: prepend version, ttl, protocol, then src and dst IPv4.
l3_header = (struct.pack("!BBB", 4, 64, 6)
+ bytes([192, 0, 2, 15])
+ bytes([198, 51, 100, 34]))
packet = l3_header + segment
# L2 link: prepend dst MAC, src MAC, ethertype (0x0800 = IPv4).
l2_header = (bytes.fromhex("aabbccddeeff")
+ bytes.fromhex("112233445566")
+ struct.pack("!H", 0x0800))
frame = l2_header + packet
units = [
("L7 data", data, 0),
("L4 segment", segment, len(l4_header)),
("L3 packet", packet, len(l3_header)),
("L2 frame", frame, len(l2_header)),
]
print("%-11s %6s %7s %8s" % ("unit", "total", "header", "payload"))
print("-" * 36)
for name, unit, hdr in units:
print("%-11s %6d %7d %8d" % (name, len(unit), hdr, len(unit) - hdr))
print()
print("the frame is one flat byte string, built from these pieces:")
print(" L2 header:", l2_header.hex())
print(" L3 header:", l3_header.hex())
print(" L4 header:", l4_header.hex())
print(" L7 data :", data.hex())
print()
hex_frame = frame.hex()
print("full frame (%d bytes):" % len(frame))
for i in range(0, len(hex_frame), 56):
print(" ", hex_frame[i:i + 56])
print()
print("total header overhead :", len(frame) - len(data), "bytes")
print("payload efficiency :", round(100 * len(data) / len(frame), 1), "%")
# Decapsulation: the receiver strips the same headers in reverse order.
recovered = frame[len(l2_header):][len(l3_header):][len(l4_header):]
print("recovered payload :", recovered.decode())
unit total header payload
------------------------------------
L7 data 5 0 5
L4 segment 11 6 5
L3 packet 22 11 11
L2 frame 36 14 22
the frame is one flat byte string, built from these pieces:
L2 header: aabbccddeeff1122334455660800
L3 header: 044006c000020fc6336422
L4 header: c73801bb000b
L7 data : 68656c6c6f
full frame (36 bytes):
aabbccddeeff1122334455660800044006c000020fc6336422c73801
bb000b68656c6c6f
total header overhead : 31 bytes
payload efficiency : 13.9 %
recovered payload : hello5-minute try-it
Change data from b"hello" to a 1400-byte payload (for example b"x" * 1400) and see how payload efficiency changes. Then loop over payload sizes 1, 10, 100, and 1000, printing the efficiency for each, and decide at what size the header overhead stops mattering.
One important caution
Thinking encapsulation transforms the data — each layer only prepends a header; the original bytes survive untouched inside the frame, which is why you can read them straight out of a capture
Hunting for OSI layers 5 (session) and 6 (presentation) in a real stack — TCP/IP has no separate layers for them; they are folded into the application layer
RFC 1122 — Requirements for Internet Hosts (Communication Layers) — Computer Networking