Skip to content
Codeloom
Python

Python Socket Programming: Build a TCP Server and Client

Learn Python socket programming from scratch. Build TCP and UDP servers and clients, handle multiple connections, and implement a simple chat application.

·9 min read · By Codeloom
Intermediate 13 min read

What you'll learn

  • Create TCP servers and clients with the socket module
  • Handle multiple connections with threading and selectors
  • Build a working multi-client chat server
  • Understand UDP sockets and when to use them

Prerequisites

  • Python functions and classes
  • Basic networking concepts (IP, ports, TCP)

What Are Sockets?

A socket is an endpoint for sending or receiving data across a network. When you visit a website, your browser creates a socket to connect to the web server. When you send an API request, a socket handles the underlying communication.

Python’s socket module provides a low-level interface to the BSD socket API. While most developers use higher-level libraries like requests or aiohttp, understanding sockets teaches you what those libraries do under the hood and lets you build custom network protocols.

A Minimal TCP Server and Client

TCP (Transmission Control Protocol) provides reliable, ordered delivery of data. Most network applications use TCP.

The Server

import socket

def start_server(host='127.0.0.1', port=65432):
    # Create a TCP socket
    # AF_INET = IPv4, SOCK_STREAM = TCP
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        # Allow reusing the address immediately after the server stops
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # Bind to the address and port
        server.bind((host, port))

        # Start listening for connections (backlog of 5)
        server.listen(5)
        print(f"Server listening on {host}:{port}")

        while True:
            # Accept a new connection (blocks until a client connects)
            client_socket, client_address = server.accept()
            with client_socket:
                print(f"Connected by {client_address}")

                # Receive data (up to 1024 bytes)
                data = client_socket.recv(1024)
                if data:
                    print(f"Received: {data.decode()}")
                    # Send a response
                    response = f"Echo: {data.decode()}"
                    client_socket.sendall(response.encode())

start_server()

The Client

import socket

def start_client(host='127.0.0.1', port=65432):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
        # Connect to the server
        client.connect((host, port))

        # Send a message
        message = "Hello, Server!"
        client.sendall(message.encode())

        # Receive the response
        data = client.recv(1024)
        print(f"Received from server: {data.decode()}")

start_client()

Run the server in one terminal and the client in another. The client sends “Hello, Server!” and receives “Echo: Hello, Server!” back.

Understanding the Socket Lifecycle

A TCP connection follows this flow:

# Server side:
# 1. socket()    - Create the socket
# 2. bind()      - Attach to an address and port
# 3. listen()    - Start accepting connections
# 4. accept()    - Wait for a client (returns a new socket)
# 5. recv/send   - Exchange data
# 6. close()     - Shut down

# Client side:
# 1. socket()    - Create the socket
# 2. connect()   - Connect to the server
# 3. send/recv   - Exchange data
# 4. close()     - Shut down

Important Details

import socket

# sendall vs send
# send() may not send all bytes in one call
# sendall() keeps sending until everything is delivered
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# sock.send(data)      # Might send partial data
# sock.sendall(data)   # Guarantees all data is sent

# recv returns empty bytes when the connection is closed
# Always check for this
data = b""
while True:
    chunk = sock.recv(4096)
    if not chunk:
        break  # Connection closed
    data += chunk

Handling Multiple Clients with Threading

The basic server above handles one client at a time. To serve multiple clients simultaneously, use threads:

import socket
import threading

def handle_client(client_socket, address):
    """Handle a single client connection."""
    print(f"[+] New connection from {address}")
    try:
        while True:
            data = client_socket.recv(1024)
            if not data:
                break
            message = data.decode()
            print(f"[{address}] {message}")

            response = f"Server received: {message}"
            client_socket.sendall(response.encode())
    except ConnectionResetError:
        print(f"[-] Connection reset by {address}")
    finally:
        client_socket.close()
        print(f"[-] Connection closed: {address}")

def start_threaded_server(host='127.0.0.1', port=65432):
    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)
    print(f"Server listening on {host}:{port}")

    try:
        while True:
            client_socket, address = server.accept()
            # Spawn a new thread for each client
            thread = threading.Thread(
                target=handle_client,
                args=(client_socket, address),
                daemon=True  # Thread dies when main thread exits
            )
            thread.start()
            print(f"Active connections: {threading.active_count() - 1}")
    except KeyboardInterrupt:
        print("\nShutting down server")
    finally:
        server.close()

start_threaded_server()

Interactive Client

import socket

def interactive_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}. Type 'quit' to exit.")

        while True:
            message = input("You: ")
            if message.lower() == 'quit':
                break

            client.sendall(message.encode())
            response = client.recv(1024).decode()
            print(f"Server: {response}")

interactive_client()

Building a Chat Server

Here is a more complete example: a multi-client chat room where messages from one client are broadcast to all others.

Chat Server

import socket
import threading

class ChatServer:
    def __init__(self, host='127.0.0.1', port=65432):
        self.host = host
        self.port = port
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.clients: dict[socket.socket, str] = {}  # socket -> nickname
        self.lock = threading.Lock()

    def broadcast(self, message, sender_socket=None):
        """Send a message to all connected clients except the sender."""
        with self.lock:
            for client_socket in list(self.clients):
                if client_socket != sender_socket:
                    try:
                        client_socket.sendall(message.encode())
                    except (BrokenPipeError, ConnectionResetError):
                        self.remove_client(client_socket)

    def remove_client(self, client_socket):
        """Remove a client from the server."""
        if client_socket in self.clients:
            nickname = self.clients.pop(client_socket)
            client_socket.close()
            self.broadcast(f"[Server] {nickname} has left the chat.")
            print(f"[-] {nickname} disconnected")

    def handle_client(self, client_socket, address):
        """Handle messages from a single client."""
        try:
            # First message is the nickname
            client_socket.sendall("Enter your nickname: ".encode())
            nickname = client_socket.recv(1024).decode().strip()

            with self.lock:
                self.clients[client_socket] = nickname

            welcome = f"[Server] {nickname} has joined the chat!"
            print(f"[+] {nickname} connected from {address}")
            self.broadcast(welcome, sender_socket=client_socket)
            client_socket.sendall("[Server] Welcome to the chat!\n".encode())

            while True:
                data = client_socket.recv(1024)
                if not data:
                    break
                message = data.decode().strip()
                if message:
                    formatted = f"[{nickname}] {message}"
                    print(formatted)
                    self.broadcast(formatted, sender_socket=client_socket)

        except (ConnectionResetError, BrokenPipeError):
            pass
        finally:
            self.remove_client(client_socket)

    def start(self):
        self.server.bind((self.host, self.port))
        self.server.listen(10)
        print(f"Chat server running on {self.host}:{self.port}")

        try:
            while True:
                client_socket, address = self.server.accept()
                thread = threading.Thread(
                    target=self.handle_client,
                    args=(client_socket, address),
                    daemon=True
                )
                thread.start()
        except KeyboardInterrupt:
            print("\nShutting down chat server")
        finally:
            with self.lock:
                for client_socket in list(self.clients):
                    client_socket.close()
            self.server.close()

if __name__ == '__main__':
    ChatServer().start()

Chat Client

import socket
import threading

def receive_messages(sock):
    """Continuously receive and print messages from the server."""
    while True:
        try:
            message = sock.recv(1024).decode()
            if not message:
                print("\nDisconnected from server.")
                break
            print(f"\r{message}\nYou: ", end='', flush=True)
        except (ConnectionResetError, OSError):
            print("\nConnection lost.")
            break

def chat_client(host='127.0.0.1', port=65432):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
        client.connect((host, port))

        # Receive the nickname prompt
        prompt = client.recv(1024).decode()
        nickname = input(prompt)
        client.sendall(nickname.encode())

        # Start receiving messages in a background thread
        receiver = threading.Thread(
            target=receive_messages,
            args=(client,),
            daemon=True
        )
        receiver.start()

        # Send messages from the main thread
        while True:
            message = input("You: ")
            if message.lower() == '/quit':
                break
            client.sendall(message.encode())

if __name__ == '__main__':
    chat_client()

Using selectors for Scalable I/O

Threading works fine for moderate numbers of clients, but for high concurrency, the selectors module provides event-driven I/O multiplexing:

import socket
import selectors
import types

sel = selectors.DefaultSelector()

def accept_connection(server_socket):
    client_socket, address = server_socket.accept()
    print(f"Connected: {address}")
    client_socket.setblocking(False)

    # Store per-connection state
    data = types.SimpleNamespace(addr=address, inb=b'', outb=b'')
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(client_socket, events, data=data)

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data

    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)
        if recv_data:
            data.outb += recv_data  # Echo back
        else:
            print(f"Closing connection to {data.addr}")
            sel.unregister(sock)
            sock.close()

    if mask & selectors.EVENT_WRITE:
        if data.outb:
            sent = sock.send(data.outb)
            data.outb = data.outb[sent:]

def start_selector_server(host='127.0.0.1', port=65432):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((host, port))
    server.listen()
    server.setblocking(False)

    sel.register(server, selectors.EVENT_READ, data=None)
    print(f"Selector server listening on {host}:{port}")

    try:
        while True:
            events = sel.select(timeout=None)
            for key, mask in events:
                if key.data is None:
                    # This is the server socket -- accept new connection
                    accept_connection(key.fileobj)
                else:
                    # This is a client socket -- handle data
                    service_connection(key, mask)
    except KeyboardInterrupt:
        print("\nShutting down")
    finally:
        sel.close()

start_selector_server()

UDP Sockets

UDP (User Datagram Protocol) is connectionless and does not guarantee delivery or order. It is faster than TCP and useful for real-time applications like gaming, DNS, and streaming.

import socket

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

        while True:
            data, client_address = server.recvfrom(1024)
            message = data.decode()
            print(f"Received from {client_address}: {message}")

            response = f"Echo: {message}"
            server.sendto(response.encode(), client_address)

# UDP Client
def udp_client(host='127.0.0.1', port=65433):
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
        message = "Hello, UDP!"
        client.sendto(message.encode(), (host, port))

        data, server_address = client.recvfrom(1024)
        print(f"Received: {data.decode()}")

# Note: No connect(), listen(), or accept() needed for UDP

Key Differences Between TCP and UDP

# TCP (SOCK_STREAM):
# - Connection-oriented (connect -> send/recv -> close)
# - Reliable, ordered delivery
# - Stream-based (no message boundaries)
# - Use for: HTTP, FTP, SSH, database connections

# UDP (SOCK_DGRAM):
# - Connectionless (just send/recv)
# - No guaranteed delivery or ordering
# - Message-based (each send is one datagram)
# - Use for: DNS, gaming, video streaming, IoT

Sending Structured Data

Raw sockets deal in bytes. To send structured messages, you need a protocol for framing:

import socket
import json
import struct

def send_message(sock, data):
    """Send a JSON message with a length prefix."""
    message = json.dumps(data).encode()
    # Pack the length as a 4-byte unsigned integer
    length_prefix = struct.pack('>I', len(message))
    sock.sendall(length_prefix + message)

def recv_message(sock):
    """Receive a length-prefixed JSON message."""
    # Read the 4-byte length prefix
    raw_length = recv_exact(sock, 4)
    if not raw_length:
        return None
    message_length = struct.unpack('>I', raw_length)[0]

    # Read exactly that many bytes
    raw_message = recv_exact(sock, message_length)
    if not raw_message:
        return None
    return json.loads(raw_message.decode())

def recv_exact(sock, num_bytes):
    """Receive exactly num_bytes from the socket."""
    data = b''
    while len(data) < num_bytes:
        chunk = sock.recv(num_bytes - len(data))
        if not chunk:
            return None
        data += chunk
    return data

# Usage in a server handler:
# send_message(client_socket, {"status": "ok", "result": 42})
# data = recv_message(client_socket)
# print(data)  # {'status': 'ok', 'result': 42}

Socket Options and Timeouts

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Set a timeout (seconds) -- applies to connect, recv, send
sock.settimeout(10.0)

# Non-blocking mode
sock.setblocking(False)

# Reuse address (avoids "Address already in use" errors)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Keep-alive (detect dead connections)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

# Get the socket's own address after binding
sock.bind(('0.0.0.0', 0))  # Port 0 = OS picks a free port
print(f"Bound to: {sock.getsockname()}")

Wrapping Up

Socket programming is the foundation of all network communication in Python. Start with socket.socket() and subprocess.run()-style simplicity: create, connect, send, receive, close. Use threading or selectors to handle multiple clients. For most real applications, you will use libraries built on top of sockets (like asyncio, aiohttp, or requests), but understanding the underlying socket layer makes debugging network issues and building custom protocols much easier.