The Transport Layer
Ports, Sockets and Multiplexing
That machine is running a web server, an SSH
JrCodex·8 min read
Jr Codex Computer Networks Notes
Level: Intermediate Prerequisites: Module 3, Chapter 5: NAT, DHCP and ICMP Time to complete: ~20 minutes
Table of Contents
- The Gap IP Leaves
- Port Numbers
- The Connection 4-Tuple
- Sockets
- A Server and a Client
- Connection States and TIME_WAIT
- Summary & Next Steps
1. The Gap IP Leaves
What IP Delivers
─────────────────────────────────────────
A packet arrives at 203.0.113.5.
That machine is running a web server, an SSH
daemon, a database and a mail server.
WHICH ONE gets the packet?
IP has no answer — its address identifies a
MACHINE, not a PROGRAM.
─────────────────────────────────────────
The Transport Layer's Two Jobs
─────────────────────────────────────────
1. MULTIPLEXING / DEMULTIPLEXING
Deliver to the right PROCESS, using PORTS.
Both UDP and TCP do this.
2. RELIABILITY
Turn IP's lossy, unordered delivery into an
ordered, reliable byte stream.
TCP does this. UDP deliberately does not.
Job 1 is this chapter. Job 2 is Chapters 3 and 4.
─────────────────────────────────────────
2. Port Numbers
The Ranges
─────────────────────────────────────────
16 bits, so 0-65535.
0 - 1023 WELL KNOWN
Assigned by IANA. On Unix, binding
these requires root — a security
measure so an unprivileged user
cannot impersonate a system
service.
1024 - 49151 REGISTERED
Assigned to specific applications
on request. PostgreSQL 5432,
MySQL 3306.
49152 - 65535 DYNAMIC / EPHEMERAL
Assigned automatically to CLIENTS
for the duration of a connection.
─────────────────────────────────────────
Worth Memorising
─────────────────────────────────────────
20/21 FTP data / control
22 SSH
25 SMTP
53 DNS (both UDP and TCP)
67/68 DHCP server / client
80 HTTP
123 NTP
143 IMAP
443 HTTPS
3306 MySQL
5432 PostgreSQL
6379 Redis
8080 HTTP alternate
─────────────────────────────────────────
Client Ports Are Not Special
─────────────────────────────────────────
A server LISTENS on a known port so clients can
find it.
A client's port is chosen at random from the
ephemeral range, because nobody needs to find the
client — the server replies to whatever port the
request came from.
This asymmetry is the whole reason well-known
ports exist: discovery in one direction only.
─────────────────────────────────────────
3. The Connection 4-Tuple
What Identifies a Connection
─────────────────────────────────────────
(source IP, source port,
destination IP, destination port)
Plus the protocol, technically making it a
5-tuple.
Two connections may share three of the four and
still be distinct.
─────────────────────────────────────────
Why a Server Handles Thousands on One Port
─────────────────────────────────────────
A web server listens on port 443. Ten thousand
clients connect. All ten thousand connections
have the same destination IP and destination
port.
They are distinguished by the SOURCE side:
(203.0.113.9, 51234, 10.0.0.1, 443)
(203.0.113.9, 51235, 10.0.0.1, 443) ← same
client
(198.51.100.7, 51234, 10.0.0.1, 443) ← same
port,
other
client
All three are different tuples, so all three are
different connections.
This is why "one port can only handle one
connection" is wrong, and it is a common
misunderstanding.
─────────────────────────────────────────
# Demultiplexing, modelled: the OS looks up the tuple to find the socket.
connections = {} # 4-tuple -> socket
def demultiplex(packet):
key = (packet.src_ip, packet.src_port, packet.dst_ip, packet.dst_port)
if key in connections:
return connections[key] # an established connection
listening = (packet.dst_ip, packet.dst_port)
if listening in listeners and packet.syn:
sock = accept_new(packet) # a NEW connection
connections[key] = sock
return sock
return None # nothing listening ──► TCP sends RST, UDP sends ICMP4. Sockets
The Abstraction
─────────────────────────────────────────
A SOCKET is the operating system's handle for one
endpoint of a communication.
It is a FILE DESCRIPTOR — so read(), write() and
close() work on it, which is why network code
looks like file code on Unix.
Two main types:
SOCK_STREAM TCP — a reliable byte stream
SOCK_DGRAM UDP — independent messages
─────────────────────────────────────────
THE STREAM vs MESSAGE DISTINCTION
─────────────────────────────────────────
This causes more real bugs than anything else in
socket programming.
UDP PRESERVES MESSAGE BOUNDARIES.
Send 100 bytes, then 50 ──► the receiver gets
one 100-byte read and one 50-byte read.
TCP DOES NOT.
Send 100 bytes, then 50 ──► the receiver may
get 150 in one read, or 30 then 120, or any
other split.
TCP is a BYTE STREAM. It guarantees the bytes
arrive in order; it guarantees nothing about how
they are grouped into reads.
So EVERY TCP protocol must define its own
framing — a length prefix, a delimiter, or a
fixed size. This is Module 2, Chapter 2's framing
problem reappearing one layer up.
─────────────────────────────────────────
5. A Server and a Client
import socket
def server(host="0.0.0.0", port=9000):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # see Section 6
s.bind((host, port)) # claim the port
s.listen(128) # the ACCEPT QUEUE depth
print(f"listening on {host}:{port}")
while True:
conn, addr = s.accept() # blocks; returns a NEW socket per client
with conn:
print(f"connection from {addr}")
while True:
data = conn.recv(4096)
if not data: # empty ──► the peer closed
break
conn.sendall(data.upper())def client(host, port, message: bytes):
with socket.create_connection((host, port), timeout=5) as s:
s.sendall(message)
# TCP is a STREAM: one sendall may need several recv calls.
received = bytearray()
while len(received) < len(message):
chunk = s.recv(4096)
if not chunk:
raise ConnectionError("peer closed early")
received.extend(chunk)
return bytes(received)# Length-prefix framing — the standard fix for TCP's lack of boundaries.
import struct
def send_message(sock, payload: bytes):
sock.sendall(struct.pack("!I", len(payload)) + payload) # 4-byte length, then data
def recv_exactly(sock, n: int) -> bytes:
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("peer closed mid-message")
buf.extend(chunk)
return bytes(buf)
def recv_message(sock) -> bytes:
(length,) = struct.unpack("!I", recv_exactly(sock, 4)) # read the length first
return recv_exactly(sock, length) # then exactly that manyNote the listen() Backlog
─────────────────────────────────────────
listen(128) sets how many completed connections
may wait for accept().
If the application is slow to accept and the
queue fills, new connections are refused or
dropped — and the symptom is intermittent
connection failures under load, with the server
looking healthy.
─────────────────────────────────────────
6. Connection States and TIME_WAIT
The States You Will See
─────────────────────────────────────────
LISTEN a server waiting for connections
SYN_SENT a client has sent SYN
SYN_RECEIVED a server has received SYN, sent
SYN-ACK
ESTABLISHED the connection is open
FIN_WAIT_1/2 closing, initiated by this side
CLOSE_WAIT the peer closed; THIS side has not
TIME_WAIT closed, waiting before releasing
the tuple
CLOSED gone
─────────────────────────────────────────
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
# 4821 ESTAB
# 1203 TIME-WAIT
# 87 CLOSE-WAIT ← this one is a bug signalTWO STATES THAT DIAGNOSE REAL BUGS
─────────────────────────────────────────
MANY TIME_WAIT
NORMAL on a busy client or proxy. The side that
closes FIRST holds TIME_WAIT for 2×MSL
(typically 60s) so late-arriving packets from
the old connection cannot be mistaken for a new
one reusing the same tuple.
It is a correctness feature, not a leak.
Mitigate with connection reuse (keep-alive),
not by disabling it.
MANY CLOSE_WAIT
ALWAYS AN APPLICATION BUG. The peer closed, the
OS told your application, and your application
never called close().
The file descriptor leaks. Eventually you run
out and the process cannot open anything.
Fix the code — no kernel setting helps.
─────────────────────────────────────────
SO_REUSEADDR
─────────────────────────────────────────
Without it, restarting a server fails with
"address already in use" because old connections
sit in TIME_WAIT holding the port.
SO_REUSEADDR lets the new listener bind anyway.
It is safe and standard — which is why it appears
in essentially every server example, including
the one above.
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- IP addresses a machine; ports address a program, and the transport layer's first job is demultiplexing to the right process.
- A connection is identified by the 4-tuple, which is why one listening port serves thousands of simultaneous connections.
- TCP is a byte stream with no message boundaries, so every TCP protocol must define its own framing — usually a length prefix.
- Many TIME_WAIT sockets is normal and protective; many CLOSE_WAIT sockets is always an application that forgot to close, and leaks file descriptors.
Concept Check
- Ten thousand clients connect to one server on port 443. How does the OS tell the connections apart?
- Your code calls
sendtwice with 100 and 50 bytes, and the receiver's firstrecvreturns 150 bytes. Is this a bug? Explain. - A server accumulates CLOSE_WAIT sockets over days until it stops accepting connections. What is wrong and where is the fix?
Next Chapter
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to Computer Networks Index