Building a TCP Server and Client in Python
TCP socket programming is one of the foundational skills for any developer working with networked applications. Python’s standard library makes it straightforward to create both servers and clients that communicate over TCP. Whether you are building a chat application, a custom protocol, or just learning how networks work under the hood, understanding sockets gives you direct control over network communication.
The socket Module
Python’s built-in socket module provides a low-level interface to the BSD socket API. It supports TCP (stream sockets) and UDP (datagram sockets), along with IPv4 and IPv6 addressing. The module wraps the operating system’s native socket calls, so the concepts transfer directly to other languages and platforms.
The core workflow for TCP is: create a socket object, bind it to an address (server side), listen for connections, and then send and receive data through the connection.
import socket
# Create a TCP/IPv4 socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
AF_INET specifies IPv4, and SOCK_STREAM specifies TCP. These two constants are the ones you will use most often.
Building a TCP Server
A TCP server follows a predictable sequence: bind to an address, listen for incoming connections, accept a connection, exchange data, and close the connection.
import socket
HOST = "127.0.0.1"
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_sock:
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind((HOST, PORT))
server_sock.listen(5)
print(f"Server listening on {HOST}:{PORT}")
conn, addr = server_sock.accept()
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
print(f"Received: {data.decode()}")
conn.sendall(data) # Echo back
Key steps explained:
bind((host, port))associates the socket with a specific network interface and port number. Use"0.0.0.0"to listen on all interfaces.listen(backlog)marks the socket as a passive socket that will accept incoming connections. The backlog argument sets the maximum number of queued connections.accept()blocks until a client connects. It returns a new socket object for the connection and the client’s address.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)allows the port to be reused immediately after the server stops, avoiding “Address already in use” errors during development.
Building a TCP Client
The client side is simpler. You create a socket, connect to the server, and exchange data.
import socket
HOST = "127.0.0.1"
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_sock:
client_sock.connect((HOST, PORT))
message = "Hello, server!"
client_sock.sendall(message.encode())
data = client_sock.recv(1024)
print(f"Received from server: {data.decode()}")
connect((host, port))initiates the TCP three-way handshake with the server.sendall(data)sends the entire byte string, handling partial sends internally. Always prefersendalloversendunless you need fine-grained control.recv(bufsize)reads up tobufsizebytes from the connection. It returns an empty bytes object when the remote side has closed the connection.
Handling Multiple Clients
The basic server above handles only one client at a time. For concurrent connections, you can use threading or the selectors module.
Using Threading
import socket
import threading
HOST = "127.0.0.1"
PORT = 65432
def handle_client(conn, addr):
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
print(f"Disconnected: {addr}")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_sock:
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind((HOST, PORT))
server_sock.listen(5)
print(f"Server listening on {HOST}:{PORT}")
while True:
conn, addr = server_sock.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr))
thread.daemon = True
thread.start()
Each incoming connection is handed off to a new thread, allowing the main loop to continue accepting connections. Setting daemon = True ensures threads do not prevent the program from exiting.
Using selectors
For higher concurrency without the overhead of threads, the selectors module provides an event-driven approach built on top of select, poll, or epoll depending on the platform.
import socket
import selectors
sel = selectors.DefaultSelector()
def accept(server_sock):
conn, addr = server_sock.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, data=addr)
def read(conn, addr):
data = conn.recv(1024)
if data:
conn.sendall(data)
else:
sel.unregister(conn)
conn.close()
HOST, PORT = "127.0.0.1", 65432
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))
server_sock.listen(5)
server_sock.setblocking(False)
sel.register(server_sock, selectors.EVENT_READ, data=None)
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None:
accept(key.fileobj)
else:
read(key.fileobj, key.data)
Error Handling
Socket operations can fail in several ways. Wrapping calls in try/except blocks makes your code resilient.
import socket
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5.0)
s.connect(("192.0.2.1", 65432))
s.sendall(b"ping")
data = s.recv(1024)
except socket.timeout:
print("Connection timed out")
except ConnectionRefusedError:
print("Server is not running")
except OSError as e:
print(f"Socket error: {e}")
Common exceptions to handle:
socket.timeout— raised when a socket operation exceeds the timeout set bysettimeout().ConnectionRefusedError— the server is not listening on the target address.ConnectionResetError— the remote side unexpectedly closed the connection.OSError— a catch-all for lower-level socket errors, such as “Address already in use.”
When to Use Raw Sockets vs Higher-Level Libraries
The socket module is ideal when you need full control over the protocol or are implementing a custom binary protocol. However, for many common tasks, higher-level alternatives save significant effort:
asyncio— provides async TCP servers and clients withasyncio.start_server()andasyncio.open_connection(), well suited for high-concurrency applications.http.server— a quick way to serve HTTP without external dependencies.socketserver— part of the standard library, it abstracts the boilerplate of threaded and forking servers.aiohttp,Flask,FastAPI— full-featured web frameworks when you need HTTP/WebSocket support.
Start with raw sockets to understand the fundamentals, then move to these libraries when building production systems. The concepts of binding, listening, connecting, and managing data streams remain the same regardless of the abstraction layer.