Python Socket Programming: TCP Server, Client and UDP
How Programs Talk Over a Network
Every time you open a website, send a WhatsApp message or stream a video, two programs on different computers exchange bytes over a network. Underneath HTTP, email and chat protocols is a simple building block: the socket, one end of a two-way communication channel. This Python socket programming tutorial explains IP addresses, ports, TCP and UDP, then builds a TCP echo server and client, handles several clients with threads, deals with the fact that TCP is a byte stream (not a message stream) using simple message framing, and finishes with a quick look at UDP. All examples run on your own computer using the loopback address 127.0.0.1, and the outputs shown are real.
Key Networking Ideas
- IP address: identifies a computer on a network, such as 192.168.1.10. The special address 127.0.0.1 ("localhost") always means "this computer", which is perfect for testing.
- Port: a number from 0 to 65535 that identifies a particular program on that computer. Web servers usually listen on port 80 (HTTP) or 443 (HTTPS). An address plus a port, such as 127.0.0.1:5050, identifies one endpoint.
- TCP (Transmission Control Protocol): connection-based, reliable and ordered. Lost data is resent automatically and bytes arrive in the order sent. Used for the web, email and file transfer.
- UDP (User Datagram Protocol): connectionless, sends individual packets (datagrams) with no guarantee of delivery or order, but with less overhead. Used for DNS lookups, online games and live voice and video, where speed matters more than perfection.
The TCP Server and Client Life Cycle
A TCP server:
- creates a socket:
socket.socket(socket.AF_INET, socket.SOCK_STREAM)(IPv4, TCP); binds it to an address and port;listens for connections;accepts a connection, which returns a new socket for talking to that client;recvs andsendalls bytes, then closes the connection.
A TCP client creates a socket, connects to the server's address and port, then sends and receives. Sockets send and receive bytes, not strings, so text must be encoded ("hi".encode()) and decoded.
Python Socket Programming: An Echo Server and Client
To keep the example in one runnable program, the server runs in a background thread and the client in the main thread. In real use they would be separate programs, often on separate machines. Binding to port 0 asks the operating system for any free port.
import socket
import threading
def start_echo_server():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0)) # port 0: let the OS choose a free port
srv.listen()
port = srv.getsockname()[1]
def serve():
conn, addr = srv.accept() # waits for one client
with conn:
while True:
data = conn.recv(1024)
if not data: # empty bytes: the client closed the connection
break
conn.sendall(b"echo: " + data)
srv.close()
t = threading.Thread(target=serve)
t.start()
return port, t
port, server_thread = start_echo_server()
with socket.create_connection(("127.0.0.1", port), timeout=5) as client:
for msg in ["Hello", "How far?"]:
client.sendall(msg.encode("utf-8"))
reply = client.recv(1024).decode("utf-8")
print("client got:", reply)
server_thread.join()
print("server finished")
Output:
client got: echo: Hello client got: echo: How far? server finished
Points to notice:
recv(1024)returns up to 1,024 bytes, whatever has arrived so far. It returns an empty bytes object when the other side closes the connection, which is how the server knows to stop.sendallkeeps sending until every byte has gone; plainsendmay send only part.SO_REUSEADDRlets you restart a server immediately on the same port while you are developing.- The
timeoutstops the client from hanging forever if the server doesn't respond.
TCP Is a Byte Stream: Framing Messages
In the example above, each recv happened to return exactly one message. That is not guaranteed. TCP delivers a continuous stream of bytes: two quick sends may arrive in one recv, and one large send may arrive in several pieces. Real protocols therefore define where each message ends. Two common methods:
- Delimiters: end each message with a special character such as a newline, and split the stream on it (HTTP headers and many chat protocols do this).
- Length prefixes: send the length of each message first, in a fixed number of bytes, then the message itself.
Here the client sends three messages back to back with length prefixes, and the server reconstructs them correctly however the bytes happen to arrive:
import struct
def send_msg(sock, text):
data = text.encode("utf-8")
sock.sendall(struct.pack("!I", len(data)) + data) # 4-byte big-endian length, then data
def recv_exact(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("connection closed early")
buf += chunk
return buf
def recv_msg(sock):
header = sock.recv(4, socket.MSG_PEEK)
if not header:
return None # clean close
(length,) = struct.unpack("!I", recv_exact(sock, 4))
return recv_exact(sock, length).decode("utf-8")
srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen()
port = srv.getsockname()[1]
received = []
def serve():
conn, _ = srv.accept()
with conn:
while (msg := recv_msg(conn)) is not None:
received.append(msg)
srv.close()
t = threading.Thread(target=serve)
t.start()
with socket.create_connection(("127.0.0.1", port)) as c:
for m in ["MTH101: 72", "PHY101: 65", "A longer message with ₦ and emoji 🎓"]:
send_msg(c, m)
t.join()
print(received)
Output:
['MTH101: 72', 'PHY101: 65', 'A longer message with ₦ and emoji 🎓']
The length counts bytes, not characters: "₦" takes 3 bytes in UTF-8 and the emoji takes 4, which is why we encode before measuring.
Serving Several Clients
The servers above handle one client and stop. A real server loops forever on accept and handles each client separately, for example in its own thread. Python's socketserver module does the looping and threading for you:
import socketserver
class UpperHandler(socketserver.StreamRequestHandler):
def handle(self):
line = self.rfile.readline().decode("utf-8").strip() # newline-framed message
self.wfile.write((line.upper() + "\n").encode("utf-8"))
server = socketserver.ThreadingTCPServer(("127.0.0.1", 0), UpperHandler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
replies = []
for word in ["lagos", "kano", "enugu"]:
with socket.create_connection(("127.0.0.1", port)) as c:
c.sendall((word + "\n").encode("utf-8"))
replies.append(c.makefile("r", encoding="utf-8").readline().strip())
server.shutdown()
server.server_close()
print(replies)
Output:
['LAGOS', 'KANO', 'ENUGU']
Threads are simple and fine for modest numbers of clients. For thousands of simultaneous connections, servers use asynchronous I/O instead (Python's asyncio has asyncio.start_server), which handles many sockets in one thread.
A Quick Look at UDP
UDP has no connection: each sendto sends one self-contained datagram to an address, and recvfrom returns one datagram plus the sender's address. Message boundaries are preserved, but delivery and order are not guaranteed (on the loopback interface, losses are very rare).
receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
receiver.bind(("127.0.0.1", 0))
receiver.settimeout(2)
addr = receiver.getsockname()
sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sender.sendto(b"ping 1", addr)
sender.sendto(b"ping 2", addr)
for _ in range(2):
data, from_addr = receiver.recvfrom(1024)
print(data.decode(), "from localhost:", from_addr[0] == "127.0.0.1")
sender.close(); receiver.close()
Output:
ping 1 from localhost: True ping 2 from localhost: True
Practical Tips
- To accept connections from other computers on your network, bind to
"0.0.0.0"instead of 127.0.0.1, and allow the port through your firewall. Only do this on networks you trust. - Never trust data received from the network: validate lengths and contents before using them.
- For anything sensitive, use encryption: Python's
sslmodule wraps a socket in TLS, the same security that HTTPS uses. - For web work you will usually use higher-level libraries (requests, Flask, FastAPI), but they are all built on these socket operations.
Summary
- In Python socket programming, a socket is one end of a network connection, identified by an IP address and a port.
- TCP is reliable and ordered; UDP is connectionless and faster but unreliable.
- TCP servers bind, listen, accept and then use the new connection socket; clients connect and send.
- Sockets carry bytes, and TCP is a stream, so messages need framing with delimiters or length prefixes.
socketserverandasynciohelp serve many clients;ssladds encryption.