Skip to content

Chapter 2: Socket Programming Basics


Metadata Card

DimensionValue
Difficulty
PrerequisitesChapter 1 (Layering Model), basic Python syntax
KeywordsSocket, TCP, UDP, bind/listen/accept, select/epoll, non-blocking
Code LanguagePython (primary) / Java (diff window)

Your Progress

"The post road is ready (the layered beacon tower system), but you need to learn how to speak the transmission incantation. The Socket is the magical interface through which the mage talks to the post road — you use it to send spell messages, and other mages send to you."

In the previous chapter you learned how spell messages leave the beacon tower through multi-layer encapsulation. But now you stand at the entrance with a practical problem: Who is the mage on the other end? Which tower? How do I deliver the spell message to them?

You can't call result = receive("192.168.1.20", 8080) — the post road has no return values, only spell messages passing back and forth.

You need a pipe extending from your array space, connecting to another mage's tower.

This pipe is called a Socket.

Chapter Layering

  • Must Read: What is a Socket (city gate analogy), TCP client/server API order, implementing echo server/client, TCP vs UDP API differences
  • Optional: Non-blocking I/O concepts, basic select/epoll understanding
  • Advanced: Multi-threaded TCP server, message boundary handling

This chapter will NOT require you to master: epoll tuning, Reactor/Proactor patterns, Unix Domain Socket

Your Task

Understand what a Socket is, what APIs the TCP client and server call, where UDP differs from TCP, and — most importantly — when you write your first while True: sock.recv(1024), what your program is really waiting for.

You'll realize: the core of Socket programming isn't the API — it's waiting. And the cost of "waiting" is much larger than you think.

TCP Socket Complete Lifecycle

A TCP echo server is the "Hello World" of network programming. The code below implements the eight steps: create, address reuse, bind, listen, accept, receive, send, close.

python
import socket

def echo_server(host='0.0.0.0', port=8888):
    """TCP echo server: returns whatever it receives"""
    server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_sock.bind((host, port))
    print(f"[Server] Bound to {host}:{port}")

    server_sock.listen(5)
    print(f"[Server] Listening...")

    while True:
        client_sock, client_addr = server_sock.accept()
        print(f"[Server] Accepted: {client_addr}")
        with client_sock:
            while True:
                data = client_sock.recv(1024)
                if not data:
                    print(f"[Server] {client_addr} disconnected")
                    break
                print(f"[Server] Received {len(data)} bytes: {data[:50]!r}")
                client_sock.send(data)  # echo back
                print(f"[Server] Echoed {len(data)} bytes")

The client is simpler — create socket, connect, send, receive, close:

python
def echo_client(message="Hello, Network!", host='127.0.0.1', port=8888):
    client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_sock.connect((host, port))
    print(f"[Client] Connected to {host}:{port}")
    client_sock.send(message.encode('utf-8'))
    reply = client_sock.recv(1024)
    print(f"[Client] Received: {reply.decode('utf-8')!r}")
    client_sock.close()

TCP API Flowchart

[Server]                              [Client]
─────────                            ─────────
socket(AF_INET, SOCK_STREAM)         socket(AF_INET, SOCK_STREAM)
    │                                     │
bind(("0.0.0.0", port))                   │
    │                                     │
listen(backlog)                            │
    │                                     │
accept()  ←── Three-Way Handshake ──→  connect()
    │                                     │
    ├─ recv() ←─────────────── send() ──┤
    ├─ send() ───────────────→ recv() ──┤
    │     ... more data exchange ...       │
close()                                close()

TCP vs UDP Key Differences

FeatureTCPUDP
Connection stateYes (3-way handshake)No (send and forget)
ReliabilityReliable (retransmission, ACK, SEQ)Unreliable
OrderingGuaranteedMay be out of order
Message boundaryByte stream (no boundaries)Preserves message boundaries
Use casesWeb, file transfer, emailVideo streaming, DNS, game state
recv APIrecv(size)recvfrom(size)
send APIsend(data)sendto(data, addr)

Blocking Problem & Solutions

python
# WRONG: sequentially blocking
data1 = sock1.recv(1024)    # waits for sock1 — if sock1 never gets data
data2 = sock2.recv(1024)    # never reaches sock2!

Three solutions, from simple to efficient:

SolutionComplexitySuitable Connections
Multi-thread/processLow~100
select/pollMedium~1000
epoll (Linux) / kqueue (macOS)High~100,000+

select — Waiting on Multiple City Gates

python
import select

def echo_server_select(host='0.0.0.0', port=8888):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((host, port))
    server.listen(5)
    server.setblocking(False)

    inputs = [server]
    outputs = []

    while inputs:
        readable, writable, _ = select.select(inputs, outputs, inputs, 1.0)
        for s in readable:
            if s is server:
                client, addr = s.accept()
                client.setblocking(False)
                inputs.append(client)
            else:
                data = s.recv(1024)
                if data:
                    outputs.append(s)
                else:
                    inputs.remove(s)
                    s.close()
        for s in writable:
            s.send(b"echo: got it")
            outputs.remove(s)

epoll — Streetlight System

python
import selectors

def echo_server_epoll(host='0.0.0.0', port=8888):
    sel = selectors.DefaultSelector()
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((host, port))
    server.listen(5)
    server.setblocking(False)
    sel.register(server, selectors.EVENT_READ, data=None)

    while True:
        events = sel.select()
        for key, mask in events:
            if key.data is None:
                client, addr = server.accept()
                client.setblocking(False)
                sel.register(client, selectors.EVENT_READ, data=addr)
            else:
                data = key.fileobj.recv(1024)
                if data:
                    key.fileobj.send(data)
                else:
                    sel.unregister(key.fileobj)
                    key.fileobj.close()

Core Intuition: select = every morning knock on every door: "Any letters?" epoll = install a letter bell on every door; when someone drops a letter, go to that door. The more connections, the bigger epoll's advantage.

TCP Byte Stream Trap

TCP is a byte stream, not a message stream. send("hello") may be split into multiple segments, and recv(1024) may only get "hel". You must handle message boundaries — that's why HTTP has Content-Length.

python
def recv_exact(sock, n: int) -> bytes:
    """Receive exactly n bytes"""
    buf = []
    total = 0
    while total < n:
        chunk = sock.recv(n - total)
        if not chunk:
            raise ConnectionError("Connection closed unexpectedly")
        buf.append(chunk)
        total += len(chunk)
    return b''.join(buf)

Acceptance Checklist

  • [ ] What is a Socket? Relationship with file descriptors?
  • [ ] TCP client/server API call sequence?
  • [ ] Three key API differences between TCP and UDP?
  • [ ] Does recv(1024) always return 1024 bytes? Why?
  • [ ] Problem with blocking I/O for multiple connections? How do select/epoll help?

Traveler's Notes

When I wrote my first echo server, I spent 40 minutes debugging because I mixed up send and sendto. Later in production, I saw the exact same problem — a reminder that Socket's silence is worse than an error message.

Later, I debugged a gigabit Python server where single-threaded recv/send only reached 200 Mbps. Switching to epoll + non-blocking pushed it to 800+ Mbps. The extra 600 Mbps wasn't magic — just replacing an empty-waiting loop with an event-driven handler.

The essence of network programming isn't the API — it's where to wait, what to wait for, and how not to wait.

Next: TCP Deep Dive

You can now establish pipes between machines. Next: what happens inside TCP — three-way handshake, sliding window, congestion control.

Built with VitePress | Software Systems Atlas