Socket Servers#

Building network servers is one of the most common applications of socket programming. A server listens for incoming connections on a specific port, accepts client connections, and processes requests. Python’s socket module provides all the primitives needed to build robust TCP and UDP servers, from simple single-threaded echo servers to complex multi-client applications.

This section covers building TCP and UDP servers in Python using the socket module, including simple echo servers for learning the basics, IPv6 and dual-stack servers for modern network compatibility, Unix domain sockets for high-performance local IPC, the SocketServer module for rapid development, threaded servers for handling multiple clients, and zero-copy file transfer with sendfile. These patterns form the foundation for building production-ready network services.

Simple TCP Echo Server#

A basic TCP echo server demonstrates the fundamental server pattern: create a socket, bind to an address, listen for connections, accept clients, and process data. This example uses Python’s context manager protocol for proper resource cleanup, ensuring the socket is closed even if an exception occurs. The SO_REUSEADDR option allows the server to restart immediately without waiting for the TIME_WAIT state to expire.

import socket

class Server:
    def __init__(self, host, port):
        self._host = host
        self._port = port

    def __enter__(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind((self._host, self._port))
        sock.listen(10)
        self._sock = sock
        return self._sock

    def __exit__(self, *exc_info):
        if exc_info[0]:
            import traceback
            traceback.print_exception(*exc_info)
        self._sock.close()

if __name__ == '__main__':
    with Server('localhost', 5566) as s:
        while True:
            conn, addr = s.accept()
            msg = conn.recv(1024)
            conn.send(msg)
            conn.close()

Output:

$ nc localhost 5566
Hello World
Hello World

TCP Echo Server via IPv6#

IPv6 server using AF_INET6 address family. Note the different address tuple format which includes flow info and scope ID.

import contextlib
import socket

host = "::1"
port = 5566

@contextlib.contextmanager
def server(host, port):
    s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
    try:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((host, port))
        s.listen(10)
        yield s
    finally:
        s.close()

with server(host, port) as s:
    try:
        while True:
            conn, addr = s.accept()
            msg = conn.recv(1024)
            if msg:
                conn.send(msg)
            conn.close()
    except KeyboardInterrupt:
        pass

Output:

$ python3 ipv6.py &
$ nc -6 ::1 5566
Hello IPv6
Hello IPv6

Dual-Stack Server (IPv4 and IPv6)#

A server that accepts both IPv4 and IPv6 connections by binding to :: and disabling IPV6_V6ONLY. IPv4 clients appear as IPv4-mapped IPv6 addresses.

import contextlib
import socket

host = "::"
port = 5566

@contextlib.contextmanager
def server(host, port):
    s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0)
    try:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
        s.bind((host, port))
        s.listen(10)
        yield s
    finally:
        s.close()

with server(host, port) as s:
    try:
        while True:
            conn, addr = s.accept()
            print(f"Connection from: {conn.getpeername()}")
            msg = conn.recv(1024)
            if msg:
                conn.send(msg)
            conn.close()
    except KeyboardInterrupt:
        pass

Output:

$ python3 dual_stack.py &
$ nc -4 127.0.0.1 5566
Connection from: ('::ffff:127.0.0.1', 42604, 0, 0)
Hello IPv4
Hello IPv4
$ nc -6 ::1 5566
Connection from: ('::1', 50882, 0, 0)
Hello IPv6
Hello IPv6

TCP Server via SocketServer#

The socketserver module provides a higher-level interface for building servers. It handles the socket setup and connection management automatically.

import socketserver

class EchoHandler(socketserver.BaseRequestHandler):
    def handle(self):
        data = self.request.recv(1024)
        print(f"Client: {self.client_address}")
        self.request.sendall(data)

if __name__ == '__main__':
    with socketserver.TCPServer(('localhost', 5566), EchoHandler) as server:
        server.serve_forever()

Simple UDP Echo Server#

UDP servers use SOCK_DGRAM and don’t require connection handling. Each recvfrom() returns the data and sender address.

import socket

class UDPServer:
    def __init__(self, host, port):
        self._host = host
        self._port = port

    def __enter__(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.bind((self._host, self._port))
        self._sock = sock
        return sock

    def __exit__(self, *exc_info):
        if exc_info[0]:
            import traceback
            traceback.print_exception(*exc_info)
        self._sock.close()

if __name__ == '__main__':
    with UDPServer('localhost', 5566) as s:
        while True:
            msg, addr = s.recvfrom(1024)
            s.sendto(msg, addr)

Output:

$ nc -u localhost 5566
Hello World
Hello World

UDP Server via SocketServer#

import socketserver

class UDPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        data, sock = self.request
        print(f"Client: {self.client_address}")
        sock.sendto(data, self.client_address)

if __name__ == '__main__':
    with socketserver.UDPServer(('localhost', 5566), UDPHandler) as server:
        server.serve_forever()

UDP Client - Sender#

Simple UDP client that sends periodic messages.

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
host = ('localhost', 5566)

while True:
    sock.sendto(b"Hello\n", host)
    time.sleep(5)

Broadcast UDP Packets#

Send UDP packets to all hosts on the local network using broadcast address.

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', 0))
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

while True:
    msg = f'{time.time()}\n'.encode()
    sock.sendto(msg, ('<broadcast>', 5566))
    time.sleep(5)

Output:

$ nc -k -w 1 -ul 5566
1431473025.72

Unix Domain Socket#

Unix domain sockets provide inter-process communication on the same machine, faster than TCP/IP loopback as they bypass the network stack.

import socket
import contextlib
import os

@contextlib.contextmanager
def domain_server(addr):
    try:
        if os.path.exists(addr):
            os.unlink(addr)
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock.bind(addr)
        sock.listen(10)
        yield sock
    finally:
        sock.close()
        if os.path.exists(addr):
            os.unlink(addr)

addr = "./domain.sock"
with domain_server(addr) as sock:
    while True:
        conn, _ = sock.accept()
        msg = conn.recv(1024)
        conn.send(msg)
        conn.close()

Output:

$ nc -U ./domain.sock
Hello
Hello

Socket Pair for IPC#

socketpair() creates a pair of connected sockets, useful for bidirectional communication between parent and child processes.

import os
import socket

child_sock, parent_sock = socket.socketpair()
pid = os.fork()

try:
    if pid == 0:
        # Child process
        parent_sock.close()
        child_sock.send(b'Hello Parent!')
        msg = child_sock.recv(1024)
        print(f'Parent says: {msg}')
    else:
        # Parent process
        child_sock.close()
        msg = parent_sock.recv(1024)
        print(f'Child says: {msg}')
        parent_sock.send(b'Hello Child!')
        os.wait()
finally:
    child_sock.close()
    parent_sock.close()

Output:

$ python socketpair_demo.py
Child says: b'Hello Parent!'
Parent says: b'Hello Child!'

Threaded TCP Server#

Handle multiple clients concurrently using threads.

from threading import Thread
import socket

def handle_client(conn):
    while True:
        msg = conn.recv(1024)
        if not msg:
            break
        conn.send(msg)
    conn.close()

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('localhost', 5566))
sock.listen(5)

while True:
    conn, addr = sock.accept()
    t = Thread(target=handle_client, args=(conn,))
    t.daemon = True
    t.start()

Using sendfile for Zero-Copy Transfer#

os.sendfile() efficiently transfers file data to a socket without copying through user space (zero-copy).

import os
import socket
import contextlib

@contextlib.contextmanager
def server(host, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((host, port))
    s.listen(10)
    try:
        yield s
    finally:
        s.close()

def send_file(conn, filepath):
    with open(filepath, 'rb') as f:
        fd = f.fileno()
        size = os.fstat(fd).st_size
        offset = 0
        while size > 0:
            sent = os.sendfile(conn.fileno(), fd, offset, min(size, 65536))
            offset += sent
            size -= sent

# Server that sends a file to each client
with server('localhost', 5566) as s:
    while True:
        conn, addr = s.accept()
        send_file(conn, 'large_file.bin')
        conn.close()