A socket is an endpoint for network communication. It’s the programming interface that lets your code send and receive data over a network. When you open a TCP connection, send a UDP datagram, or even ping a host, sockets are doing the work under the hood.
Sockets operate at the Transport layer (Layer 4). They abstract away everything below — Ethernet framing, IP routing, physical transmission — and give you a simple API: create a socket, connect it, send data, receive data, close it.
Every networked application you use — web browsers, email clients, chat apps, game servers — is built on sockets.
Python’s socket module provides a thin wrapper over the BSD socket API, which is the standard across all operating systems. The core operations are:
| Operation | TCP | UDP |
|---|---|---|
| Create | socket() |
socket() |
| Bind to address | bind() |
bind() |
| Listen for connections | listen() |
— |
| Accept a connection | accept() |
— |
| Connect to server | connect() |
optional |
| Send data | send() / sendall() |
sendto() |
| Receive data | recv() |
recvfrom() |
| Close | close() |
close() |
TCP provides reliable, ordered, byte-stream delivery. Before exchanging data, client and server establish a connection via the three-way handshake.
import socket
# Create a TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("example.com", 80))
s.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
response = s.recv(4096)
print(response.decode())
Let’s break down the arguments:
AF_INET — IPv4 address family (use AF_INET6 for IPv6)SOCK_STREAM — TCP (stream-oriented)connect((host, port)) — initiates the three-way handshakesendall() — sends all data, retrying until complete (unlike send(), which may send partial data)recv(bufsize) — receives up to bufsize bytesimport socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", 9000))
s.listen(5)
print("Server listening on port 9000...")
while True:
conn, addr = s.accept()
with conn:
print(f"Connection from {addr}")
data = conn.recv(1024)
if data:
conn.sendall(data.upper())
Key details:
SO_REUSEADDR — allows reusing the port immediately after the server stops (avoids “Address already in use” errors)bind(("0.0.0.0", port)) — listen on all interfaces; use "127.0.0.1" for localhost onlylisten(backlog) — start listening; backlog is the queue size for pending connectionsaccept() — blocks until a client connects; returns a new socket for that client and the client’s addressFull example: code/tcp_echo_server.py
UDP provides fast, best-effort delivery. There’s no connection setup, no reliability guarantees, and no ordering. Each sendto() call is an independent datagram.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto(b"Hello, UDP!", ("127.0.0.1", 9001))
data, addr = s.recvfrom(1024)
print(f"Received from {addr}: {data.decode()}")
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(("0.0.0.0", 9001))
print("UDP server listening on port 9001...")
while True:
data, addr = s.recvfrom(1024)
print(f"From {addr}: {data.decode()}")
s.sendto(data.upper(), addr)
Note the differences from TCP:
SOCK_DGRAM instead of SOCK_STREAMlisten() or accept() — UDP has no connectionssendto(data, address) — each send specifies the destinationrecvfrom(bufsize) — returns data and the sender’s addressFull example: code/udp_echo_server.py
| Feature | TCP | UDP |
|---|---|---|
| Connection | Required (3-way handshake) | None |
| Reliability | Guaranteed delivery, retransmission | Best-effort, may lose packets |
| Ordering | Guaranteed in-order delivery | No ordering guarantees |
| Flow control | Yes (sliding window) | None |
| Overhead | Higher (headers, handshake, ACKs) | Lower (minimal header) |
| Use cases | HTTP, SSH, email, file transfer | DNS, gaming, video streaming, VoIP |
Use TCP when you need every byte to arrive correctly and in order. Use UDP when speed matters more than reliability, or when the application handles its own reliability.
Supporting IPv6 requires minimal changes:
import socket
# IPv6 TCP client
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
s.connect(("::1", 9000)) # Connect to localhost via IPv6
s.sendall(b"Hello, IPv6!")
print(s.recv(1024).decode())
A dual-stack socket handles both IPv4 and IPv6 connections:
import socket
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("::", 9000)) # Bind to all IPv4 and IPv6 addresses
s.listen(5)
print("Dual-stack server on port 9000")
# Accepts both IPv4 and IPv6 connections
Note: On Linux, dual-stack is the default behavior for AF_INET6 sockets. On some systems, you need to explicitly set IPV6_V6ONLY to 0.
By default, socket operations block — they pause your program until the operation completes. This is simple but doesn’t scale to multiple clients.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5.0) # 5-second timeout
try:
s.connect(("example.com", 80))
s.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
data = s.recv(4096)
except socket.timeout:
print("Connection timed out!")
import socket
import select
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setblocking(False)
try:
s.connect(("example.com", 80))
except BlockingIOError:
pass # Connection in progress
# Wait until the socket is writable (connected)
_, writable, _ = select.select([], [s], [], 5.0)
if writable:
s.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
readable, _, _ = select.select([s], [], [], 5.0)
if readable:
print(s.recv(4096).decode())
The select module monitors multiple sockets and tells you which ones are ready for reading or writing. This is the foundation of event-driven networking (covered in depth in Chapter 9).
Socket options fine-tune socket behavior:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Reuse address — essential for servers
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Reuse port — allows multiple processes to bind to the same port
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
# TCP keepalive — detect dead connections
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# TCP_NODELAY — disable Nagle's algorithm for low-latency
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# Set send/receive buffer sizes
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
| Option | Purpose |
|---|---|
SO_REUSEADDR |
Allow binding to a port in TIME_WAIT state |
SO_REUSEPORT |
Allow multiple sockets to bind to the same port |
SO_KEEPALIVE |
Send periodic probes to detect dead connections |
TCP_NODELAY |
Send data immediately (disable buffering) |
SO_SNDBUF / SO_RCVBUF |
Set kernel buffer sizes |
SO_LINGER |
Control behavior on close() with unsent data |
A critical concept: recv() may return fewer bytes than you expect. TCP is a byte stream, not a message stream — the kernel can split or merge your sends.
def recv_exactly(sock: socket.socket, n: int) -> bytes:
"""Receive exactly n bytes from a socket."""
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise ConnectionError("Connection closed")
data += chunk
return data
For message-based protocols, you need a framing mechanism:
\n)import struct
def send_message(sock, msg: bytes) -> None:
"""Send a length-prefixed message."""
length = struct.pack("!I", len(msg))
sock.sendall(length + msg)
def recv_message(sock) -> bytes:
"""Receive a length-prefixed message."""
raw_len = recv_exactly(sock, 4)
msg_len = struct.unpack("!I", raw_len)[0]
return recv_exactly(sock, msg_len)
SOCK_STREAM) provide reliable, ordered, connection-oriented communicationSOCK_DGRAM) provide fast, connectionless, best-effort communicationAF_INET for IPv4 and AF_INET6 for IPv6; dual-stack sockets handle bothrecv() may return partial data — always implement proper framing for message boundariesSO_REUSEADDR, TCP_NODELAY, and SO_KEEPALIVE are essential for production useselect() are the foundation for scalable networking| ← Previous: TLS, Encryption, and Certificates | Table of Contents | Next: Building TCP and UDP Servers → |