Network Models & Characterization
The Two Layering Models
OSI Reference Model (7 layers)
| # | Layer | PDU | Role | Examples |
|---|---|---|---|---|
| 7 | Application | Message | User-facing services | HTTP, FTP, SMTP, DNS |
| 6 | Presentation | — | Data format conversion (endianness, encoding) | — |
| 5 | Session | — | Access mgmt, synchronization | — |
| 4 | Transport | Segment | End-to-end delivery | TCP, UDP |
| 3 | Network | Packet | Global routing | IP, IPv6 |
| 2 | Data Link | Frame | Local hop delivery, MAC | Ethernet, WiFi |
| 1 | Physical | Bit | Transfer bits over medium | Copper, Fiber, Radio |
TCP/IP Hybrid Model (5 layers)
| Layer | Protocols | Device |
|---|---|---|
| Application | HTTP, SMTP, FTP, SIP | End host |
| Transport | TCP, UDP | End host |
| Network | IP (the glue!) | Router (L3 gateway) |
| Link | Ethernet, WiFi, ADSL | Switch (L2 gateway) |
| Physical | Twisted pair, fiber, coax | NIC, cable |
IP is the "narrow waist" — the single protocol that unifies all networks above and below.
Encapsulation
Each layer wraps the layer above with its own header:
Application: [HTTP header | data]
Transport: [TCP header | HTTP header | data]
Network: [IP header | TCP header | HTTP header | data]
Link: [Eth header | IP header | TCP header | HTTP header | data | Eth trailer]
Routers strip/read up to L3; switches only L2; end hosts read all layers.
Circuit Switching vs. Packet Switching
| Feature | Circuit Switching | Packet Switching |
|---|---|---|
| Resource | Reserved in advance | On-demand |
| Multiplexing | Flow-level | Packet-level |
| Efficiency | Low (idle circuits waste bandwidth) | High |
| Predictability | High (guaranteed bandwidth) | Low (variable delay) |
| Example | Landline phone (PSTN) | Internet |
Key insight: P/A = peak-to-average ratio.
- Voice: P/A ≈ 3 → reservation is OK
- Data: P/A > 100 → reservation wastes resources → use packet switching
Circuit switching problems: - Bursty traffic leaves circuits idle most of the time - Short flows: setup overhead (T1 + T3) can exceed transfer time (T2) - Failures require re-establishing a new circuit
Network Characterization: Delay, Loss, Throughput
Types of Delay
Total delay = Transmission + Propagation + Processing + Queueing
| Type | Formula | Depends on |
|---|---|---|
| Transmission | packet size (bits) / link bandwidth (bps) | Link speed, packet size |
| Propagation | link length (m) / signal speed (m/s) | Distance (speed of light ≈ 2×10⁸ m/s in fiber) |
| Processing | ~ ns | Router hardware |
| Queueing | statistical — varies per packet | Traffic intensity La/R |
Example: 100B packet, 1 Mbps link, 1 ms propagation: - Transmission: 800 bits / 10⁶ = 800 μs - Propagation: 1 ms - Total ≈ 1.8 ms
Same packet on 1 Gbps link: - Transmission: 800 ns → propagation dominates → ≈ 1.0008 ms
Queueing Delay & Traffic Intensity
Traffic intensity = La/R
L = packet size (bits)
a = arrival rate (packets/sec)
R = link transmission rate (bps)
La/R < 1→ queue is manageableLa/R > 1→ queue grows without bound → infinite delay
Golden rule: Design so traffic intensity stays well below 1.
Loss
Queues are finite. When persistently overloaded, packets are dropped (lost).
Throughput
Throughput = data size / transfer time [bits/sec]
Determined by the bottleneck link — the slowest link along the path.
CDNs move content closer to users to reduce propagation delay.
ISP Hierarchy
| Tier | Role |
|---|---|
| Tier-1 | Global backbone; no upstream provider (~few dozen) |
| Tier-2 | National; buys transit from Tier-1 (~thousands) |
| Tier-3 | Local/access ISP; buys from Tier-2 (~85-90% of all ISPs) |
Peering: Two ISPs connect directly to avoid paying their common provider.
IXP (Internet Exchange Point): Physical location where many ISPs peer to reduce costs (e.g. DE-CIX Frankfurt).
Internet History Timeline
| Year | Event |
|---|---|
| 1969 | ARPANET — first packet-switched network |
| 1971 | NCP (predecessor of TCP/IP) |
| 1973 | Ethernet invented |
| 1974 | TCP/IP paper (Cerf & Kahn) |
| 1983 | NCP → TCP/IP switch; DNS introduced |
| 1989 | WWW (Tim Berners-Lee, CERN) |
| 1998 | IPv6 standardized; Google |
| 2007 | Netflix streaming; first iPhone |
| 2009 | Bitcoin genesis; 4G/LTE |
Physical Layer — Cheatsheet
Role
Move bits across a physical medium. The key challenge: converting digital 0/1 to analog signals and back without desynchronization or data corruption.
Line Encoding Schemes
| Scheme | Rule | Problem |
|---|---|---|
| NRZ | 1 = high, 0 = low | Long runs of 0 or 1 → clock drift |
| NRZI | 1 = transition, 0 = stay | Solves runs of 1s, not 0s |
| Manchester | 1 = high→low, 0 = low→high | Every bit has a transition (self-clocking); halves throughput |
| 4B/5B | Map every 4-bit group to a 5-bit code with ≤ 1 leading 0 | 80% efficiency; used with NRZI at 100 Mbps Ethernet |
Clock drift problem: Two independent clocks drift apart. Long runs of same bit → receiver loses track of where bit boundaries are.
4B/5B encoding example:
0000 → 11110
0001 → 01001
...
1111 → 11101
Manchester used by 10BASE-TX. 4B/5B + NRZI used by 100BASE-TX.
Baseband vs. Broadband
| Baseband | Broadband | |
|---|---|---|
| Signal | Digital levels (DC) directly on wire | Analog sine wave carrier, modulated |
| Spectrum | Wide (many frequencies) | Narrow (around carrier frequency) |
| Problem | Attenuation varies by frequency | Requires modulation/demodulation |
| Use | Ethernet | Cable TV, DSL, wireless |
Modulation Techniques (Broadband)
Three properties of a sine wave can carry information:
| Technique | What changes | Use |
|---|---|---|
| AM (Amplitude Modulation) | Signal amplitude | AM radio |
| FM (Frequency Modulation) | Carrier frequency | FM radio, modem |
| PM (Phase Modulation) | Phase of the wave | WiFi, LTE |
Symbols vs. bits:
Using more than 2 symbols increases data rate without increasing baud rate.
4 symbols (A, B, C, D) = 2 bits/symbol
600 baud × 4 symbols → 1200 bps
Multiplexing
Allowing multiple signals to share the same medium simultaneously.
| Type | How | Example |
|---|---|---|
| FDM (Frequency-Division) | Each signal uses different frequency band | Cable TV, AM/FM radio |
| TDM (Time-Division) | Each signal gets a dedicated time slot | Phone networks, SONET |
| WDM (Wavelength-Division) | Different wavelengths of light | Optical fiber backbone |
| CDMA (Code-Division) | Signals encoded with orthogonal chip vectors | 3G/4G cellular |
| SDM (Space-Division) | Physically separate wires/antennas | Simple wired LANs, MIMO |
CDMA (Code Division Multiple Access)
Each station gets an orthogonal chip vector v of length m.
- Send bit 1 → transmit v
- Send bit 0 → transmit −v
- Simultaneous signals add (linear combination)
Decoding: Take the dot product of the received signal with the sender's chip code: - dot product > 0 → bit 1 - dot product < 0 → bit 0 - dot product = 0 → station was silent
Works because orthogonal vectors don't interfere: v·w = 0 for different stations.
Transmission Pipeline (Baseband)
Source bits
→ Source encoding (compress: MP3, MPEG4, Huffman)
→ Channel encoding (add redundancy for error correction)
→ Physical transmission (turn symbols into voltage/light/radio)
Physical Media
| Medium | Speed | Distance | Notes |
|---|---|---|---|
| Twisted pair (UTP) | up to 10 Gbps | ~100 m | Ethernet, phone |
| Coax | hundreds of Mbps | km | Cable TV, legacy Ethernet |
| Optical fiber | up to Tbps | thousands of km | Internet backbone |
| Wireless (radio) | varies | varies | WiFi, cellular |
| Microwave | Gbps | line-of-sight | Backhaul |
Bandwidth Formulas
Transmission delay = packet_size_bits / bandwidth_bps
Propagation delay = distance_m / signal_speed_mps
(signal speed ≈ 2×10⁸ m/s in fiber)
Bandwidth units (SI):
1 kbps = 10³ bps
1 Mbps = 10⁶ bps
1 Gbps = 10⁹ bps
1 Tbps = 10¹² bps
1 Byte = 8 bits
Data Link Layer — Cheatsheet
Role
Transmit frames between nodes on the same physical link. Handles: 1. Framing — delineating packet boundaries 2. Error detection/correction — detecting bit errors 3. Media Access Control (MAC) — who gets to transmit when
Framing Methods
| Method | How | Protocol |
|---|---|---|
| Byte stuffing | FLAG byte marks start/end; DLE escape if FLAG appears in data | PPP (modem, DSL) |
| Bit stuffing | Sentinel bits (e.g. 01111110); insert 0 after every five 1s in data |
HDLC |
| Byte counting | Length field at start of frame | Legacy |
| Clock-based (SONET) | Fixed frame size; scramble payload to avoid long runs | STS-1 = 810 bytes |
Bit stuffing receiver logic (after seeing 11111):
- Next bit = 0 → remove it (was stuffed)
- Next bit = 10 → end of frame
- Next bit = 11 → error
Error Detection
Parity Bit
- 1 extra bit to make the count of 1s even (or odd)
- Detects single-bit errors only
- Code distance = 2
Checksum
- Sum all bytes in the frame (ones-complement arithmetic)
- 16-bit overhead; used in UDP, TCP, IP
- Not resilient to correlated errors
CRC (Cyclic Redundancy Check) — Exam Favourite
Concept: Treat the message as a polynomial M(x).
Choose a generator polynomial G(x) of degree k.
Transmit P(x) = M(x) × xᵏ + remainder such that P(x) is divisible by G(x).
Receiver checks: if P(x) + E(x) mod G(x) ≠ 0, there's an error.
Algorithm:
1. Append k zero bits to M → M(x)·xᵏ
2. Divide by G(x) using mod-2 arithmetic (XOR, no carries)
3. Remainder R = M(x)·xᵏ mod G(x)
4. Transmit M(x)·xᵏ XOR R (this is divisible by G(x))
Mod-2 arithmetic:
1111 11001
+ 1010 × 101
------ ------
0101 11001
+11001
-------
1111101
Example: MSG = 10011010, G(x) = 1101 (x³+x²+1, degree k=3)
- Append 3 zeros: 10011010000
- Divide by 1101 → remainder 101
- Transmit: 10011010101
Common CRC polynomials:
| Name | Polynomial | Used in |
|---|---|---|
| CRC-8 | x⁸+x²+x+1 | |
| CRC-16 | x¹⁶+x¹⁵+x²+1 | |
| CRC-CCITT | x¹⁶+x¹²+x⁵+1 | HDLC |
| CRC-32 | x³²+x²⁶+…+x+1 | Ethernet |
CRC detects: all single-bit errors, all burst errors shorter than k bits, all odd-number errors (if G has factor x+1).
Hamming Distance
The number of bit positions where two codewords differ.
- To detect d errors: need code distance ≥ d+1
- To correct d errors: need code distance ≥ 2d+1
Example: S = {00000000, 00001111, 11110000, 11111111}
Min Hamming distance = 4 → can detect 3-bit errors or correct 1-bit errors.
Error Correction Protocols (ARQ)
Stop-and-Wait
Sender sends one frame; waits for ACK before sending next. - Utilization: η = T_frame / (T_frame + 2·T_propagation + T_ack) - Simple but inefficient over long-latency links
Alternating Bit Protocol (ABP)
1-bit sequence number (0 or 1). Sender retransmits until ACK with matching number. - Solves the "lost ACK" problem - Still sends only one frame at a time
Sliding Window
Sender may have up to W frames outstanding (unACKed).
Receiver buffers up to W frames.
Sequence numbers: 0 to 2ⁿ−1.
Go-Back-N: Receiver window = 1. On error, discard all subsequent frames; sender retransmits from bad frame onward.
Selective Repeat: Receiver buffers correct frames after a bad one. Sender retransmits only the bad frame.
| Protocol | Sender Window | Receiver Window | Retransmit |
|---|---|---|---|
| Stop-and-Wait | 1 | 1 | Whole frame |
| Go-Back-N | N | 1 | Frame + all successors |
| Selective Repeat | N | N | Bad frame only |
Media Access Control (MAC)
Needed on shared media (Ethernet, WiFi) where simultaneous transmissions cause collisions.
Strategies
| Category | Approach | Example |
|---|---|---|
| Channel partitioning | Fixed allocation of time/frequency | TDMA, FDMA |
| Taking turns | Coordinated access | Token Ring |
| Contention | Transmit and deal with collisions | Ethernet (CSMA/CD) |
Problem with static allocation: Bursty traffic (peak/mean ratio up to 1000:1) makes static channels inefficient — either wasted capacity or excessive queueing delay.
ALOHA → CSMA/CD Evolution
Pure ALOHA
- Send whenever you have data
- Collision → wait random time → retransmit
- Max throughput: 18% (vulnerable window = 2·T_frame)
Slotted ALOHA
- Transmit only at slot boundaries
- Vulnerable window reduced to T_frame
- Max throughput: 37% (S = G·e⁻ᴳ, max at G=1)
CSMA (Carrier Sense Multiple Access)
- Sense the channel before transmitting → don't collide with active transmission
- 1-persistent: retry immediately if idle
- Non-persistent: wait random time if busy
- p-persistent: with probability p, transmit; else delay one slot
CSMA/CD (Collision Detection)
Used by Ethernet.
- Sense channel — if busy, wait
- Transmit frame, keep sensing
- If collision detected → send jam signal (32 bits)
- Exponential backoff: after n-th collision, wait k random slots where k ∈ [0, 2ⁿ−1] (n capped at 10; drop after 16 collisions)
- Retry
Minimum frame size: The transmitter must still be on air when the collision signal returns. With propagation delay d:
T_frame ≥ 2·d → min_frame_size = rate × 2d
For 10 Mbps Ethernet:
(64 bytes × 8 bits) × 2.5×10⁸ m/s / (2 × 10⁷ bps) = 6400 m max cable
MTU (Maximum Transmission Unit): 1500 bytes for standard Ethernet; 9000 bytes for Jumbo Frames.
802.3 Ethernet Frame
| Preamble (7B) | SF (1B) | Dst MAC (6B) | Src MAC (6B) | Length (2B) | Data (0–1500B) | Pad (0–46B) | CRC (4B) |
- Preamble:
10101010× 7 (synchronization) - Start Frame:
10101011 - MAC address: 6 bytes, e.g.
00:45:A5:F3:25:0C - Broadcast:
FF:FF:FF:FF:FF:FF - Min total frame: 64 bytes (necessary for CSMA/CD collision detection)
Struct & Binary I/O — Cheatsheet
Format Specifiers
| Code | Type | Size (bytes) |
|---|---|---|
i / I |
signed / unsigned int | 4 |
f |
float | 4 |
? |
bool | 1 |
c |
char (single byte) | 1 |
Xs |
byte string of length X (e.g. 9s) |
X |
Padding gotcha:
struct.Struct('i 2s f')is 12 bytes, not 10.
Thefloatis aligned to a 4-byte boundary → 2 padding bytes are inserted after2s.
Usestruct.calcsize('i2sf')to verify actual size.
Pack & Unpack — Quick Reference
import struct
# --- PACK (Python → bytes) ---
packed = struct.pack('i 2s f', 42, b'ab', 3.14)
# --- UNPACK (bytes → Python) ---
unpacked = struct.unpack('i 2s f', packed) # returns tuple
# --- Reusable packer (preferred) ---
packer = struct.Struct('i 2s f')
packed = packer.pack(42, b'ab', 3.14)
result = packer.unpack(packed)
# Size
print(packer.size) # total bytes
print(struct.calcsize('i2sf')) # same, ad-hoc
Strings must be bytes: use
b'abc'or'abc'.encode().
'abc'(str) →struct.error: argument for 's' must be a bytes object
Writing Binary Files
import struct
packer = struct.Struct('i 3s i') # e.g. year, month, day
with open('dates.bin', 'wb') as f:
for i in range(5):
values = (2020 + i, b'Sep', 15 + i)
f.write(packer.pack(*values))
Reading Binary Files
with open('dates.bin', 'rb') as f:
# Read all records sequentially
for i in range(5):
data = f.read(packer.size)
print(packer.unpack(data))
# Jump to a specific record with seek
f.seek(packer.size * 4) # 5th record (0-indexed)
data = f.read(packer.size)
print(packer.unpack(data)) # last entry
seek(offset, whence):
- whence=0 — absolute position (default)
- whence=1 — relative to current position
- whence=2 — relative to end of file
String ↔ Bytes Conversion
s = "hello"
b = s.encode() # b'hello'
s2 = b.decode() # 'hello'
# Fixed-width string (padded with \x00)
packed = struct.pack('8s', s.encode()) # b'hello\x00\x00\x00'
unpacked = packed.decode().strip('\x00') # 'hello'
Endianness
import socket
# Convert host byte order → network (big-endian)
socket.htons(x) # host→network short (16-bit)
socket.htonl(x) # host→network long (32-bit)
# Convert network → host byte order
socket.ntohs(x)
socket.ntohl(x)
Struct format prefix:
- > — big-endian (network order)
- < — little-endian
- = — native order
Assignment 2 Pattern
The exam gives you binary files with unknown formats and asks you to read them using the correct struct, then pack new values.
import struct, sys
params = [
struct.Struct('c9si'), # file 1 format
struct.Struct('if?'), # file 2 format
struct.Struct('?9sc'), # file 3 format
struct.Struct('icf') # file 4 format
]
for idx, param in enumerate(params):
with open(sys.argv[idx + 1], 'rb') as f:
data = f.read(param.size)
print(param.unpack(data))
# Pack new values
s1 = struct.Struct('11si?')
print(s1.pack(b'elso', 89, True))
The format string for each file is given per-student on TMS.
Always useparam.size— never hard-code byte counts.
TCP Sockets — Cheatsheet
Key Concepts
- Connection-oriented — three-way handshake before data transfer
- Reliable — guaranteed delivery and ordering
- Used by HTTP, SMTP, POP3, FTP
- Socket = IP address + port
Minimal TCP Server
import socket
server_addr = ('', 10000) # '' = bind to all interfaces
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(server_addr)
server.listen(1) # backlog: max queued connections
conn, client_addr = server.accept() # blocks until client connects
with conn:
data = conn.recv(1024) # receive bytes
print("Got:", data.decode())
conn.sendall("Hello client!".encode())
Minimal TCP Client
import socket
server_addr = ('localhost', 10000)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.connect(server_addr)
client.sendall("Hello server!".encode())
data = client.recv(1024)
print("Got:", data.decode())
API Reference
| Method | Side | Description |
|---|---|---|
socket(AF_INET, SOCK_STREAM) |
both | Create TCP socket |
setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) |
server | Allow address reuse |
bind((host, port)) |
server | Attach to address |
listen(backlog) |
server | Start accepting (backlog = queue length) |
accept() |
server | Block until client; returns (conn, addr) |
connect((host, port)) |
client | Connect to server |
sendall(data) |
both | Send all bytes (loops internally) |
send(data) |
both | Send bytes; may not send all — check return |
recv(bufsize) |
both | Receive up to bufsize bytes |
close() |
both | Close socket |
Struct Messages (Calculator Pattern)
# Server side
import struct
unpacker = struct.Struct('I I 1s') # uint, uint, char
data = conn.recv(unpacker.size)
num1, num2, op = unpacker.unpack(data)
op_str = op.decode()
result = eval(f"{num1} {op_str} {num2}") # use eval() for operator dispatch
conn.sendall(str(result).encode())
# Client side
packer = struct.Struct('I I 1s')
values = (10, 3, b'+')
client.sendall(packer.pack(*values))
answer = client.recv(64).decode()
Socket Timeout
sock.settimeout(1.0) # raises socket.timeout after 1 s
sock.setblocking(False) # non-blocking (settimeout(0))
sock.setblocking(True) # blocking (settimeout(None))
try:
conn, addr = server.accept()
except socket.timeout:
pass # no client connected within timeout
Command-Line Port (sys.argv pattern)
import sys
port = int(sys.argv[1])
server_addr = ('localhost', port)
Common Exam Patterns
Server handles 1 client at a time (simple)
Use the template above — accept() → serve → close → loop.
Struct-based protocol
Define a struct.Struct for both client and server.
Server unpacks with .size as recv argument.
Multiple simultaneous clients
→ See the select() cheatsheet.
UDP Sockets — Cheatsheet
Key Concepts
- Connectionless — no handshake; just send datagrams
- Unreliable — no guaranteed delivery, no ordering
- Used by DNS, DHCP, RIP, video/audio streaming
- Server does not call
connect()/accept()/listen()
Minimal UDP Server
from socket import socket, AF_INET, SOCK_DGRAM
server_address = ('', 10000)
with socket(AF_INET, SOCK_DGRAM) as server:
server.bind(server_address)
data, client_addr = server.recvfrom(4096) # receive + get sender address
print("Got:", data.decode(), "from:", client_addr)
server.sendto("Hello client!".encode(), client_addr) # send reply to sender
Minimal UDP Client
from socket import socket, AF_INET, SOCK_DGRAM
server_addr = ('localhost', 10000)
with socket(AF_INET, SOCK_DGRAM) as client:
client.sendto("Hello Server!".encode(), server_addr)
data, _ = client.recvfrom(4096)
print("Got:", data.decode())
API Differences vs TCP
| Feature | TCP | UDP |
|---|---|---|
| Socket type | SOCK_STREAM |
SOCK_DGRAM |
| Connection | connect() / accept() |
✗ |
| Send | sendall(data) |
sendto(data, addr) |
| Receive | recv(n) |
recvfrom(n) → (data, addr) |
| Multiple clients | Needs select or threads |
Built-in (addr per packet) |
UDP servers handle multiple clients naturally — each
recvfrom()returns the sender's address.
Multi-Client Loop (UDP)
with socket(AF_INET, SOCK_DGRAM) as server:
server.bind(('', 10000))
while True:
data, addr = server.recvfrom(4096)
# handle data...
server.sendto(response, addr)
File Transfer over UDP (chunked)
# Client: send file in 200-byte chunks
CHUNK = 200
with open('photo.jpg', 'rb') as f:
while True:
chunk = f.read(CHUNK)
client.sendto(chunk if chunk else b'', server_addr)
ack, _ = client.recvfrom(16) # wait for 'ok'
if not chunk:
break
# Server: receive chunks
with socket(AF_INET, SOCK_DGRAM) as server:
server.bind(('', 10000))
with open('received.jpg', 'wb') as out:
while True:
data, addr = server.recvfrom(256)
server.sendto(b'ok', addr)
if not data:
break
out.write(data)
Calculator over UDP
# Server
import struct
packer = struct.Struct('I I 1s')
with socket(AF_INET, SOCK_DGRAM) as server:
server.bind(('', 10000))
while True:
data, addr = server.recvfrom(packer.size)
n1, n2, op = packer.unpack(data)
result = eval(f"{n1} {op.decode()} {n2}")
server.sendto(str(result).encode(), addr)
Multicast
import socket, struct
MULTICAST_GROUP = '224.3.29.71'
PORT = 10000
# --- Sender ---
ttl = struct.pack('b', 1)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sender:
sender.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
sender.sendto(b'Hello multicast!', (MULTICAST_GROUP, PORT))
# --- Receiver ---
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as recv:
recv.bind(('', PORT))
group = socket.inet_aton(MULTICAST_GROUP)
mreq = struct.pack('4sL', group, socket.INADDR_ANY)
recv.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
data, addr = recv.recvfrom(1024)
Common Exam Patterns
- Same calculator / lottery task as TCP but with
SOCK_DGRAM - Use
sendto(data, addr)/recvfrom(bufsize)throughout - No
connect/accept/listen - Byte format: same struct patterns as TCP
select() — Multi-Client TCP Server
Why select()?
A simple accept() + recv() server can only serve one client at a time.
select() lets you monitor multiple sockets and act on whichever is ready — all in one thread.
How select() Works
from select import select
readable, writable, exceptional = select(inputs, outputs, inputs, timeout)
| Argument | Contains | Result |
|---|---|---|
inputs |
sockets to watch for reading | readable = sockets with data (or new connections) |
outputs |
sockets to watch for writing | writable = sockets ready to send |
inputs (3rd arg) |
watch for errors | exceptional = sockets with errors |
timeout |
seconds to wait | returns empty lists if nothing happens |
Full Multi-Client Select Server
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
from select import select
server_addr = ('', 10000)
with socket(AF_INET, SOCK_STREAM) as server:
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(server_addr)
server.listen(5)
inputs = [server] # start with just the server socket
while True:
r, w, e = select(inputs, [], inputs, timeout=1)
if not (r or w or e):
continue # timeout, nothing happened
for s in r:
if s is server:
# New connection
client, addr = s.accept()
inputs.append(client)
print("Connected:", addr)
else:
# Existing client sent data
data = s.recv(1024)
if not data:
# Client disconnected
print("Disconnected:", s.getpeername())
inputs.remove(s)
s.close()
else:
print("Received:", data.decode())
s.sendall("OK".encode())
for s in e:
inputs.remove(s)
s.close()
Select Client (sends periodically)
from socket import socket, AF_INET, SOCK_STREAM
from time import sleep
server_addr = ('localhost', 10000)
with socket(AF_INET, SOCK_STREAM) as client:
client.connect(server_addr)
for _ in range(5):
client.sendall("Hello Server!".encode())
data = client.recv(200)
print("Received:", data.decode())
sleep(2)
Chat Server Pattern
Broadcast received message to all other clients:
for s in r:
if s is server:
client, addr = s.accept()
inputs.append(client)
name = client.recv(64).decode()
client_names[client] = name
else:
data = s.recv(1024)
if not data:
inputs.remove(s)
s.close()
else:
name = client_names.get(s, '?')
msg = f"[{name}] {data.decode()}".encode()
for other in inputs:
if other is not server and other is not s:
other.sendall(msg)
Guessing Game Server Pattern (Assignment III)
Key points from the task:
- Server picks a number 1–100
- Client sends (operator_char, integer) as a struct
- Server replies (result_char, dummy_int) as a struct
- After a win: send V (End of game) to all other clients
import struct, random
from select import select
from socket import *
MSG = struct.Struct('c i') # 1 char + 4 byte int
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('', int(sys.argv[2])))
server.listen(5)
inputs = [server]
target = random.randint(1, 100)
while True:
r, _, e = select(inputs, [], inputs, 1)
for s in r:
if s is server:
client, _ = s.accept(); inputs.append(client)
else:
raw = s.recv(MSG.size)
if not raw:
inputs.remove(s); s.close(); continue
op, num = MSG.unpack(raw)
op = op.decode()
if op == '<':
reply = b'I' if target < num else b'N'
elif op == '>':
reply = b'I' if target > num else b'N'
elif op == '=':
if num == target:
s.sendall(MSG.pack(b'Y', 0))
for other in inputs:
if other is not server and other is not s:
other.sendall(MSG.pack(b'V', 0))
inputs = [server]
target = random.randint(1, 100)
continue
else:
reply = b'K'
s.sendall(MSG.pack(reply, 0))
Common Gotchas
| Issue | Fix |
|---|---|
select not in scope |
from select import select |
| Server socket in inputs | Always add server to inputs first |
| Forgot to remove closed socket | inputs.remove(s) before s.close() |
Iterating inputs while removing |
Iterate over a copy: for s in list(r) |
IP Addressing & Subnets — Cheatsheet
IP Address Classes
| Class | Range | Default Mask | Use |
|---|---|---|---|
| A | 0.0.0.0 – 127.255.255.255 | /8 | Unicast (large networks) |
| B | 128.0.0.0 – 191.255.255.255 | /16 | Unicast (medium networks) |
| C | 192.0.0.0 – 223.255.255.255 | /24 | Unicast (small networks) |
| D | 224.0.0.0 – 239.255.255.255 | — | Multicast |
| E | 240.0.0.0 – 255.255.255.255 | — | Reserved |
Subnet Calculation — Step by Step
Example: 192.168.123.132 /24
- CIDR
/24→ mask255.255.255.0
(11111111.11111111.11111111.00000000) - IP in binary:
11000000.10101000.01111011.10000100 - AND with mask → Network ID:
192.168.123.0 - Host part (non-masked bits) → Host:
132 - All host bits = 1 → Broadcast:
192.168.123.255 - Usable:
.1to.254
Key Formulas
Number of subnets = 2^(subnet bits)
= 2^(CIDR_prefix - class_default_prefix)
Number of hosts = 2^(host bits) - 2
= 2^(32 - CIDR_prefix) - 2
Subtract 2: network address + broadcast address are reserved.
Example: 172.10.85.60 /22 (Class B)
Step 1 — CIDR mask:
/22 → 11111111.11111111.11111100.00000000 → 255.255.252.0
Step 2 — Subnet bits:
Class B default = /16, so subnet bits = 22 − 16 = 6 → 2⁶ = 64 subnets
Step 3 — Host bits:
32 − 22 = 10 → 2¹⁰ − 2 = 1022 hosts per subnet
Step 4 — Find the network (3rd octet = 85):
3rd octet in mask = 11111100 → last 1-bit = value 4 (the "magic number")
85 / 4 = 21 → 21 × 4 = 84 → Network ID = 172.10.84.0
Next network: 84 + 4 = 88 → Broadcast = 172.10.87.255
Result:
- Network: 172.10.84.0
- Broadcast: 172.10.87.255
- Usable: 172.10.84.1 – 172.10.87.254
Example: 188.100.22.12 /20
/20 mask: 255.255.240.0 (3rd octet = 11110000 = 240)
Magic number = 16 (lowest set bit in 3rd octet)
3rd octet 22 → floor(22/16) × 16 = 16
- Network:
188.100.16.0 - Broadcast:
188.100.31.255(16 + 16 − 1 = 31) - Usable:
188.100.16.1–188.100.31.254 - Hosts: 2¹² − 2 = 4094
Worked: /32 and /10
188.100.22.12 /32:
One address only — host route.
Network = Broadcast = 188.100.22.12, 0 usable hosts.
188.100.22.12 /10:
Mask 255.192.0.0 (2nd octet = 11000000 = 192)
Magic = 64. 100 / 64 = 1 → 1 × 64 = 64
Network: 188.64.0.0, Broadcast: 188.127.255.255
Hosts: 2²² − 2 = 4,194,302
Well-Known Ports
| Port | Protocol |
|---|---|
| 22 | SSH |
| 25 | SMTP |
| 53 | DNS |
| 67/68 | DHCP |
| 80 | HTTP |
| 443 | HTTPS |
| 110 | POP3 |
Port range: 0 – 65535 (16-bit field)
Well-known ports (IANA): 0 – 1023
Python Hostname / Port Utilities
import socket
socket.gethostname() # local hostname
socket.gethostbyname('www.example.com') # → '93.184.216.34'
socket.gethostbyname_ex('www.example.com') # → (hostname, aliases, [ips])
socket.gethostbyaddr('157.181.161.79') # reverse lookup
# List services on ports 1-100
for port in range(1, 101):
for proto in ('tcp', 'udp'):
try:
print(f"{port}/{proto} → {socket.getservbyport(port, proto)}")
except OSError:
pass
Exam Task Guide
The midterm is structured in 5 tasks (I–V). Below is each task's pattern and a reference implementation skeleton.
Task I — Transport Protocol
"Use TCP" or "Use UDP"
Pick the right socket type:
# TCP
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# UDP
socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
See TCP Sockets and UDP Sockets cheatsheets for full templates.
Task II — Message Format
IIa — Byte string with : separator
# Client packs: "5:10:15:20:25:50"
nums = [5, 10, 15, 20, 25]
bet = 50
msg = ':'.join(map(str, nums)) + ':' + str(bet)
sock.sendall(msg.encode())
# Server unpacks
raw = conn.recv(1024).decode()
parts = raw.split(':')
nums = list(map(int, parts[:5]))
bet = int(parts[5])
IIb — struct with ≥2 members
import struct
# Example: 5 shorts + 1 int (numbers + bet)
packer = struct.Struct('5H I') # 5 unsigned short + 1 unsigned int
packed = packer.pack(5, 10, 15, 20, 25, 50)
conn.sendall(packed)
# Server
data = conn.recv(packer.size)
nums_and_bet = packer.unpack(data)
nums = list(nums_and_bet[:5])
bet = nums_and_bet[5]
IIc — Checksum (MD5/CRC)
import hashlib, zlib
# MD5 checksum
payload = b"5:10:15:20:25:50"
checksum = hashlib.md5(payload).digest() # 16 bytes
conn.sendall(payload + checksum)
# Verify on server
raw = conn.recv(1024)
data, chk_recv = raw[:-16], raw[-16:]
assert hashlib.md5(data).digest() == chk_recv, "Checksum mismatch!"
# CRC32 (4 bytes)
crc = struct.pack('I', zlib.crc32(payload) & 0xFFFFFFFF)
conn.sendall(payload + crc)
Task III — Multiple Simultaneous Connections
Use select for TCP:
from select import select
inputs = [server]
while True:
r, _, e = select(inputs, [], inputs, 1)
for s in r:
if s is server:
client, _ = s.accept()
inputs.append(client)
else:
data = s.recv(1024)
if not data:
inputs.remove(s); s.close()
else:
# handle client
s.sendall(response)
UDP handles multiple clients naturally — no
selectneeded.
Task IV — Client Input Source
IVa — Random numbers
import random
nums = random.sample(range(1, 21), 5) # 5 unique from 1..20
bet = random.randint(1, 100)
IVb — Standard input
raw = input("Enter 5 numbers (space-separated): ")
nums = list(map(int, raw.split()))
bet = int(input("Enter bet: "))
IVc — JSON file
import json, sys
with open(sys.argv[1]) as f:
data = json.load(f)
nums = data['numbers']
bet = data['bet']
IVd — Command-line arguments
import sys
nums = list(map(int, sys.argv[1:6]))
bet = int(sys.argv[6])
Task V — History / Logging Server
Three architectures tested in exams:
Va — History as Proxy (client talks to history, not lottery)
Client → History Server → Lottery Server
Client ← History Server ← Lottery Server
(history logs here)
# History server receives from client
data, addr = history_sock.recvfrom(1024) # UDP example
# Forward to lottery server
lottery_addr = ('localhost', 20000)
history_sock.sendto(data, lottery_addr)
# Wait for lottery reply
result, _ = history_sock.recvfrom(1024)
# Log it
log_to_json(data, result)
# Forward back to client
history_sock.sendto(result, addr)
Vb — Lottery pushes to History after each request
Client → Lottery Server
Lottery Server → History Server (async notification)
Client ← Lottery Server
Vc — Client logs its own result
Client → Lottery Server → Client
Client → History Server (after getting result)
Use a different protocol for the logging channel (e.g. lottery uses TCP → history uses UDP).
# Client logs via UDP to history
hist_addr = ('localhost', 30000)
hist_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
log = json.dumps({'picks': nums, 'drawn': drawn, 'win': winnings}).encode()
hist_sock.sendto(log, hist_addr)
Persisting logs (JSON append)
import json, os
LOG_FILE = 'history.json'
def log_to_json(entry: dict):
records = []
if os.path.exists(LOG_FILE):
with open(LOG_FILE) as f:
records = json.load(f)
records.append(entry)
with open(LOG_FILE, 'w') as f:
json.dump(records, f, indent=2)
Lottery Example — Full Server
import socket, random, struct
from select import select
NUMBERS_COUNT = 5
MAX_NUMBER = 20
packer = struct.Struct('5H I') # 5 picks + bet
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('', 10000))
server.listen(5)
inputs = [server]
drawn = random.sample(range(1, MAX_NUMBER + 1), NUMBERS_COUNT)
print("Drawn:", drawn)
while True:
r, _, _ = select(inputs, [], inputs, 1)
for s in r:
if s is server:
client, _ = s.accept(); inputs.append(client)
else:
raw = s.recv(packer.size)
if not raw:
inputs.remove(s); s.close(); continue
*picks, bet = packer.unpack(raw)
hits = len(set(picks) & set(drawn))
win = bet * hits
# Reply: drawn numbers + winnings
resp_fmt = struct.Struct('5H I I') # drawn + hits + win
s.sendall(resp_fmt.pack(*drawn, hits, win))
Quick Checklist per Task
- [ ] I: Correct socket type (
STREAMorDGRAM) - [ ] II: Consistent format on both ends; struct size matches recv buffer
- [ ] III:
selectloop with server ininputs; handle disconnect - [ ] IV: Input source reads correct data types
- [ ] V: Different protocol for history;
json.dumpappends correctly
Assignments — Walkthrough
Assignment 1 — Circuit-Switched Network Simulation
Task
Simulate resource allocation/deallocation in a circuit-switched network from a JSON topology file.
python3 client.py cs1.json
Output format:
1. demand allocation: A<->C st:1 - successful
2. demand allocation: B<->C st:2 - successful
3. demand deallocation: A<->C st:5
4. demand allocation: D<->C st:6 - successful
5. demand allocation: A<->C st:7 - unsuccessful
JSON Structure
{
"links": [
{ "points": ["A", "B"], "capacity": 10.0 }
],
"possible-circuits": [
["A", "B", "C"],
["A", "C"]
],
"simulation": {
"duration": 20,
"demands": [
{ "end-points": ["A", "C"], "demand": 3.0, "start-time": 1, "end-time": 5 }
]
}
}
Algorithm
- Build capacity map from
links(use sorted tuple as key for bidirectional lookup) - Generate events — each demand creates an
allocatstart-timeand adeallocatend-time - Sort events by time; break ties so
dealloc(priority 0) comes beforealloc(priority 1) - Allocate: iterate
possible-circuits, check all links have ≥ demand capacity → reduce capacities - Deallocate: restore capacities of the circuit that was allocated
Reference Implementation
import json, sys
def get_link_key(a, b):
return tuple(sorted([a, b]))
def simulate(filename):
with open(filename) as f:
data = json.load(f)
# Build capacity map
cap = {}
for link in data['links']:
key = get_link_key(link['points'][0], link['points'][1])
cap[key] = link['capacity']
# Create and sort events
events = []
for d in data['simulation']['demands']:
events.append((d['start-time'], 1, 'alloc', d))
events.append((d['end-time'], 0, 'dealloc', d))
events.sort(key=lambda e: (e[0], e[1]))
allocated = {} # demand id → circuit used
n = 1
for time, _, etype, demand in events:
n1, n2 = demand['end-points']
if etype == 'alloc':
chosen = None
for circuit in data['possible-circuits']:
# Check endpoints match
if not ((circuit[0] == n1 and circuit[-1] == n2) or
(circuit[0] == n2 and circuit[-1] == n1)):
continue
# Check capacity on every link
if all(cap.get(get_link_key(circuit[i], circuit[i+1]), 0) >= demand['demand']
for i in range(len(circuit) - 1)):
chosen = circuit
break
if chosen:
for i in range(len(chosen) - 1):
cap[get_link_key(chosen[i], chosen[i+1])] -= demand['demand']
allocated[id(demand)] = chosen
status = "successful"
else:
status = "unsuccessful"
print(f"{n}. demand allocation: {n1}<->{n2} st:{time} - {status}")
else: # dealloc
did = id(demand)
if did in allocated:
circuit = allocated.pop(did)
for i in range(len(circuit) - 1):
cap[get_link_key(circuit[i], circuit[i+1])] += demand['demand']
print(f"{n}. demand deallocation: {n1}<->{n2} st:{time}")
else:
continue
n += 1
if __name__ == '__main__':
simulate(sys.argv[1])
Common Mistakes
| Mistake | Fix |
|---|---|
Using string "A-B" as link key |
Use tuple(sorted([a,b])) — bidirectional |
| Allocating same circuit twice | Track allocated circuits by id(demand) |
| Dealloc events before alloc at same time | Sort by (time, priority) where dealloc = 0, alloc = 1 |
| Skipping dealloc if not allocated | continue without incrementing n |
Assignment 2 — Binary File Reading & Struct Packing
Task
- Read 4 binary files passed as arguments, each with a different struct format
- Print the first record unpacked to stdout
- Pack and print 4 hardcoded values in given struct formats
The exact struct formats are per-student (check TMS).
Template
import struct, sys
# Part 1: read binary files
# Replace these formats with your actual ones from TMS
params = [
struct.Struct('c9si'), # file 1
struct.Struct('if?'), # file 2
struct.Struct('?9sc'), # file 3
struct.Struct('icf') # file 4
]
for idx, param in enumerate(params):
with open(sys.argv[idx + 1], 'rb') as f:
data = f.read(param.size)
print(param.unpack(data))
# Part 2: pack values
# Replace formats and values with your actual ones from TMS
s1 = struct.Struct('11si?')
print(s1.pack(b'elso', 89, True))
s2 = struct.Struct('f?c')
print(s2.pack(92.5, False, b'X'))
s3 = struct.Struct('i9sf')
print(s3.pack(80, b'masodik', 99.9))
s4 = struct.Struct('ci12s')
print(s4.pack(b'Z', 111, b'harmadik'))
Run it
python3 client.py database1.bin database2.bin database3.bin database4.bin
Checklist
- [ ] Read exactly
param.sizebytes from each file - [ ] String literals are
bytes(b'...'), notstr - [ ]
Xsformat requires a bytes object of exactly X chars (pad if needed) - [ ] Submit as a single
client.pyin a.zip
General Submission Rules
- Submit via TMS (
tms.inf.elte.hu) - Format:
.zipcontaining one file (client.py) - File must be named exactly
client.py - Test locally before submitting:
python3 client.py <args>