computer-science4 min read

Networking and Data Communication Tutorial from Scratch (2026)

Networking and Data Communication Tutorial from Scratch (2026)

Published:  |  Category: Computer Science  |  Reading time: ~15 min
Networking and Data Communication Tutorial from Scratch (2026)

Computer networking is the backbone of modern distributed systems, connecting billions of devices across the globe. This tutorial takes you from the physics of signal transmission at Layer 1 up through application-layer protocols like HTTP/3. With hands-on experience building network tools and analyzing packet flows, I will show you how data travels across the internet: from your keyboard through the OS kernel, across routers and switches, to servers potentially thousands of kilometers away.

We will implement a TCP handshake, a simple HTTP server, and a network congestion control algorithm in Python.

OSI Model and TCP/IP Stack

The OSI model has seven layers: Physical, Data Link, Network, Transport, Session, Presentation, Application. TCP/IP collapses these into four: Link, Internet, Transport, Application. Each layer provides abstractions: the transport layer (TCP/UDP) provides reliable or unreliable end-to-end delivery; the internet layer (IP) handles routing and addressing.

import socket, struct
def checksum(data):
    if len(data)%2: data+=b'\x00'
    s=sum(struct.unpack('!%dH'%(len(data)//2),data)); s=(s>>16)+(s&0xFFFF); return ~s&0xFFFF
# construct IP header
ip_hdr = struct.pack('!BBHHHBBH4s4s',0x45,0,28+8,0x4000,64,6,0,socket.inet_aton('10.0.0.1'),socket.inet_aton('10.0.0.2'))
ip_hdr = ip_hdr[:10] + struct.pack('!H',checksum(ip_hdr)) + ip_hdr[12:]
print(f'IP header ({len(ip_hdr)} bytes): {ip_hdr.hex()}')

Ethernet and MAC Layer

Ethernet frames contain a preamble, destination MAC (6 bytes), source MAC (6 bytes), EtherType (2 bytes), payload (46-1500 bytes), and CRC (4 bytes). The MTU for typical Ethernet is 1500 bytes. MAC addresses are globally unique, burned into NICs. Switches learn MAC addresses by source address flooding, maintaining a CAM table.

from dataclasses import dataclass
@dataclass
class EthFrame:
    dst: str; src: str; etype: int; payload: bytes
    def enc(self) -> bytes:
        d=bytes.fromhex(self.dst.replace(':',''))
        s=bytes.fromhex(self.src.replace(':',''))
        return d+s+self.etype.to_bytes(2,'big')+self.payload+self._crc(d+s+self.payload.to_bytes(2,'big')+self.payload)
    def _crc(self,b):
        poly=0xEDB88320; tbl=[0]*256
        for i in range(256): v=i; [v:=((v>>1)^(poly&-(v&1))) for _ in range(8)]; tbl[i]=v
        c=0xFFFFFFFF; [c:=tbl[(c^b)&0xFF]^(c>>8) for b in b]; return (c^0xFFFFFFFF).to_bytes(4,'little')

IP Addressing and Subnetting

IPv4 addresses are 32 bits, typically written in dotted-decimal notation. Subnetting divides a network into smaller subnets using a subnet mask. CIDR notation combines prefix length (e.g., 192.168.1.0/24). A host network address is derived by ANDing the IP address with the subnet mask. Special addresses: network address (all zeros), broadcast (all ones).

import ipaddress
net = ipaddress.ip_network('10.0.0.0/24')
print(f'Network: {net}; Mask: {net.netmask}; Broadcast: {net.broadcast_address}')
for i,sub in enumerate(net.subnets(new_prefix=28)):
    print(f'Sub{i}: {sub} ({sub.netmask}) usable: {list(sub.hosts())[0]}-{list(sub.hosts())[-1]}')
    if i>=3: break
# calculate subnet manually
def subnet(ip, mask):
    ipn=int(ipaddress.IPv4Address(ip)); maskn=int(ipaddress.IPv4Address(mask))
    net=ipaddress.IPv4Address(ipn & maskn); bc=ipaddress.IPv4Address(ipn | (~maskn & 0xFFFFFFFF))
    return net, bc
print(subnet('192.168.1.37','255.255.255.0'))

TCP: Reliable Transport Protocol

TCP provides connection-oriented, reliable, ordered byte-stream delivery. The three-way handshake (SYN, SYN-ACK, ACK) establishes a connection. Sequence numbers track bytes. Flow control uses a sliding window; congestion control uses AIMD with slow start, congestion avoidance, fast retransmit, and fast recovery.

import random
class TCPConn:
    def __init__(self): self.state='CLOSED'; self.seq=random.randint(0,2**32); self.ack=0; self.cwnd=10; self.ssthresh=65535; self.wq=[]
    def syn(self):
        self.state='SYN_SENT'; self.seq+=1
    def synack(self, ack):
        self.state='ESTABLISHED'; self.ack=ack+1; self.cwnd=10
    def send(self,data):
        self.wq.append((self.seq,data)); self.seq+=len(data)
    def ack_recv(self,ack):
        self.wq=[(s,d) for s,d in self.wq if s+len(d)>ack]; self.ack=ack
    def cwnd_update(self, acked, loss=False):
        if loss: self.ssthresh=max(self.cwnd//2,2); self.cwnd=self.ssthresh
        elif self.cwnd

Routing Algorithms: OSPF and BGP

Routing protocols exchange network topology information. OSPF is an interior gateway protocol using link-state routing: routers flood LSAs to build a complete topology, then run Dijkstra's algorithm to compute shortest paths. BGP is the exterior gateway protocol of the internet, using path-vector routing: each route carries the AS_PATH attribute to detect loops and enforce policy.

import heapq
def dijkstra(adj, src):
    dist={v:float('inf') for v in adj}; dist[src]=0; pq=[(0,src)]; prev={}
    while pq:
        d,u=heapq.heappop(pq)
        if d>dist[u]: continue
        for v,w in adj[u].items():
            if d+w".join(path(prev,"D"))}')

HTTP/3 and QUIC Protocol

QUIC is a transport protocol built on UDP, combining encryption, multiplexed streams, and connection migration. HTTP/3 maps HTTP semantics over QUIC. QUIC eliminates TCP head-of-line blocking by allowing independent streams. It uses TLS 1.3 for encryption, provides 0-RTT connection establishment, and supports connection migration for mobile clients.

import asyncio, struct
class QUICPacket:
    def __init__(self, dst, conn_id, pn, payload):
        self.dst=dst; self.conn_id=conn_id; self.pn=pn; self.payload=payload
    def enc(self):
        fl=0xC0|0b01; pkt=struct.pack('!BB',fl,self.conn_id)+self.pn.to_bytes(4,'big')+self.payload
        return pkt
class QUICStream:
    def __init__(self, sid): self.sid=sid; self.recv=bytearray(); self.send=bytearray()
    def read(self,n):
        if len(self.recv)>=n: d=bytes(self.recv[:n]); self.recv=self.recv[n:]; return d
        raise BlockingIOError
    def write(self,d): self.send.extend(d); return len(d)
# HTTP/3 frame encoding
def h3_frame(typ,data): return struct.pack('!BH',typ,len(data))+data
print(h3_frame(0, b'Hello QUIC world!').hex())

Frequently Asked Questions

What is the difference between TCP and UDP?

TCP is connection-oriented with reliability, ordering, flow control, and congestion control. UDP is connectionless, best-effort delivery without guarantees, suitable for real-time applications.

What is CIDR notation?

CIDR (Classless Inter-Domain Routing) notation appends a prefix length to an IP address (e.g., 10.0.0.0/8). The prefix length indicates how many bits are the network portion.

How does NAT work?

NAT translates private IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) to a public IP. It maintains a mapping table of (private IP, private port) to public port.

What is the difference between OSPF and BGP?

OSPF is an IGP that runs inside an autonomous system using link-state routing. BGP is an EGP between ASes using path-vector routing with policy-based path selection.

Originally published on Ayodhyyya. Last updated June 1, 2026.