Sockets #

Almost all network communication — HTTP, databases, email, chat — is built on top of sockets. A socket is an abstraction representing the endpoint of a network connection: you can open a socket, send data through it, and read incoming data from it, just like reading and writing to a file. Python’s socket module gives you direct access to the operating system’s socket API, letting you build network servers and clients from the most basic level.

How Sockets Work #

Before writing code, it’s important to understand the socket lifecycle and the different roles of servers and clients.

flowchart LR
    subgraph Server["TCP LIFECYCLE (Server)"]
        S1["socket() (create socket)"] --> S2["bind() (bind to IP:port)"]
        S2 --> S3["listen() (start listening)"]
        S3 --> S4["accept() (accept client connection)"]
        S4 --> S5["recv() (read data from client)"]
        S5 --> S6["send() (send response)"]
        S6 --> S7["close() (close client connection)"]
    end

    subgraph Client["TCP LIFECYCLE (Client)"]
        C1["socket() (create socket)"] --> C2["connect() (connect to server)"]
        C2 --> C3["send() (send data)"]
        C3 --> C4["recv() (receive data)"]
        C4 --> C5["close() (close connection)"]
    end

    C2 -. "connect" .-> S4
    C3 -. "send data" .-> S5
    S6 -. "send response" .-> C4

The two main protocols running on top of sockets:

FeatureTCP (SOCK_STREAM)UDP (SOCK_DGRAM)
ConnectionPersistentConnectionless
Data OrderGuaranteedNot guaranteed
DeliveryAcknowledgedNo acknowledgment
SpeedSlower (handshake overhead)Faster
CharacteristicReliableBest-effort
Use CasesHTTP, databases, file transferVideo streaming, online gaming, DNS

TCP Sockets #

TCP guarantees data arrives in the correct order. A TCP server waits for client connections, then both sides can exchange data until one of them closes the connection.

Basic TCP Server #

import socket

def run_server(host="127.0.0.1", port=65432):
    # AF_INET = IPv4, SOCK_STREAM = TCP
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        # SO_REUSEADDR: allow reusing the port after the server restarts
        # without this, a restart fails with "Address already in use"
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((host, port))
        server.listen(5)  # maximum queue of 5 connections
        print(f"TCP server running at {host}:{port}")

        while True:
            conn, addr = server.accept()
            print(f"Connection from: {addr}")
            handle_client(conn, addr)

def handle_client(conn, addr):
    with conn:
        while True:
            data = conn.recv(1024)  # read at most 1024 bytes
            if not data:
                break  # the client closed the connection
            message = data.decode("utf-8")
            print(f"[{addr}] Received: {message}")
            reply = f"Echo: {message}"
            conn.sendall(reply.encode("utf-8"))
        print(f"Connection from {addr} closed.")

if __name__ == "__main__":
    run_server()

TCP Client #

import socket

def run_client(host="127.0.0.1", port=65432):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
        client.connect((host, port))
        print(f"Connected to {host}:{port}")

        message_list = ["Hello server!", "This is the second message", "Last message"]
        for message in message_list:
            client.sendall(message.encode("utf-8"))
            data = client.recv(1024)
            print(f"Server response: {data.decode('utf-8')}")

if __name__ == "__main__":
    run_client()
SO_REUSEADDR is mandatory on the server. Without this option, after the server is stopped, the port stays in TIME_WAIT state for several minutes. When you try to run the server again, you get OSError: [Errno 98] Address already in use. Always add setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) before bind().

Multi-Client TCP Server #

The server above can only serve one client at a time — the second client has to wait until the first one finishes. This is the most common anti-pattern in socket programming.

# ANTI-PATTERN: single-threaded server
# the second client is blocked until the first one finishes
while True:
    conn, addr = server.accept()
    handle_client(conn, addr)  # blocks here until finished

# CORRECT: spawn a new thread for each client
import threading

while True:
    conn, addr = server.accept()
    thread = threading.Thread(target=handle_client, args=(conn, addr))
    thread.daemon = True  # thread dies when the main program exits
    thread.start()

Complete multi-client TCP server implementation:

import socket
import threading

active_clients = {}
lock = threading.Lock()

def handle_client(conn, addr):
    with lock:
        active_clients[addr] = conn
    print(f"[+] New client: {addr} | Total: {len(active_clients)}")

    try:
        with conn:
            while True:
                data = conn.recv(1024)
                if not data:
                    break
                message = data.decode("utf-8")
                print(f"[{addr}] {message}")
                conn.sendall(f"Echo: {message}".encode("utf-8"))
    except ConnectionResetError:
        print(f"[!] Client {addr} forcibly disconnected.")
    finally:
        with lock:
            active_clients.pop(addr, None)
        print(f"[-] Client {addr} left | Remaining: {len(active_clients)}")

def run_server(host="127.0.0.1", port=65432):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((host, port))
        server.listen()
        print(f"Multi-client server running at {host}:{port}")

        while True:
            conn, addr = server.accept()
            thread = threading.Thread(target=handle_client, args=(conn, addr))
            thread.daemon = True
            thread.start()

if __name__ == "__main__":
    run_server()

Timeouts and Error Handling #

Network connections can hang indefinitely — a client crashes, the network drops, or the server doesn’t respond. Without a timeout, a program can hang forever.

import socket

def client_with_timeout(host="127.0.0.1", port=65432):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.settimeout(5.0)  # 5-second timeout for connect, recv, send operations

    try:
        client.connect((host, port))
        client.sendall("Hello!".encode("utf-8"))

        data = client.recv(1024)
        print(f"Response: {data.decode('utf-8')}")

    except socket.timeout:
        print("Connection timed out — the server didn't respond within 5 seconds.")
    except ConnectionRefusedError:
        print("Connection refused — the server isn't running or the port is wrong.")
    except OSError as e:
        print(f"Network error: {e}")
    finally:
        client.close()

client_with_timeout()

A timeout can also be set on the server side so idle client connections don’t hang onto resources:

def handle_client_with_timeout(conn, addr):
    conn.settimeout(30.0)  # the client must send data within 30 seconds
    with conn:
        try:
            while True:
                data = conn.recv(1024)
                if not data:
                    break
                conn.sendall(data)
        except socket.timeout:
            print(f"[!] Client {addr} idle too long, closing the connection.")

UDP Sockets #

UDP doesn’t establish a connection — the server directly receives packets from anyone, and the client sends directly without a handshake first. Faster, but with no guarantee the data arrives.

UDP Server #

import socket

def udp_server(host="127.0.0.1", port=65433):
    # SOCK_DGRAM = UDP
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
        server.bind((host, port))
        print(f"UDP server running at {host}:{port}")

        while True:
            # recvfrom: receive data + sender address at once
            data, sender_addr = server.recvfrom(1024)
            message = data.decode("utf-8")
            print(f"[UDP] From {sender_addr}: {message}")

            # sendto: send to a specific address (no connection needed)
            reply = f"Received: {message}"
            server.sendto(reply.encode("utf-8"), sender_addr)

if __name__ == "__main__":
    udp_server()

UDP Client #

import socket

def udp_client(host="127.0.0.1", port=65433):
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
        client.settimeout(3.0)
        server_addr = (host, port)

        message_list = ["Packet 1", "Packet 2", "Packet 3"]
        for message in message_list:
            client.sendto(message.encode("utf-8"), server_addr)
            try:
                data, _ = client.recvfrom(1024)
                print(f"Response: {data.decode('utf-8')}")
            except socket.timeout:
                print(f"Timeout — packet '{message}' may have been lost.")

if __name__ == "__main__":
    udp_client()

When to Choose TCP vs UDP #

Use TCP when:
  ✓ Data must arrive complete and in order
  ✓ Implementing application protocols: HTTP, FTP, SSH, databases
  ✓ Transferring files or documents
  ✓ Chat or API communication

Use UDP when:
  ✓ Speed matters more than reliability
  ✓ Losing a few packets is tolerable
  ✓ Real-time audio/video streaming
  ✓ Online games (player positions, frame states)
  ✓ DNS queries (small packets, send once)
  ✓ Broadcasting to many receivers at once

Summary #

  • A socket is an abstraction of a network connection endpoint — you open, send, receive, and close it like a file.
  • Always add SO_REUSEADDR before bind() on the server so the port can be reused immediately after a restart.
  • A single-threaded server is an anti-pattern — the second client gets blocked; use threading.Thread or ThreadPoolExecutor to serve many clients concurrently.
  • Always set a timeout with socket.settimeout() — without it, a program can hang forever when the network misbehaves.
  • TCP for communication needing reliability and data ordering; UDP for speed when losing a few packets is tolerable.
  • Use with socket.socket(...) as s: so the socket is always closed automatically even when an error occurs.
  • Socket data is bytes, not strings — always .encode() when sending and .decode() when receiving.

← Previous: Decorators   Next: WebSockets →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact