The echo server in the sockets article handles one caller at a time, then loops back for the next. That is fine for learning. But the moment a second caller arrives while the first is still being served, the second has to wait. This article fixes that, and in doing so gives you a design you will recognise in almost every real network server.
The problem: one caller at a time
Take the single-loop echo server and make each caller need two seconds of "work". Three clients arrive together:
sequential_slow.py
# the single-loop echo server, made slow: one caller at a time
import socket, time
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 9998))
server.listen(5)
while True:
client_sock, client_addr = server.accept()
data = client_sock.recv(1024)
time.sleep(2) # two seconds of "work" per caller
client_sock.sendall(data)
client_sock.close()
Three clients arrive at the same moment
client 1: got b'hello 1' after 2.0s
client 2: got b'hello 2' after 4.0s
client 3: got b'hello 3' after 6.0s
all three finished in 6.0s
The third caller waited six seconds for a two-second job. A slow or silent caller at the front of the queue stalls everyone behind it.
The idea: separate accepting from handling
A more reusable design separates the part that accepts connections from the part that decides what to do with each one. Then the same accepting loop can serve a banner-collecting listener, a small honeypot, or a logging service without the networking code being rewritten each time.
Analogy: The front desk and the bellboys
Picture a small hotel. If the receptionist had to carry each guest's bags up to the room before greeting the next guest, a queue would build at the desk. So she does not. She greets the guest, hands the bags to a bellboy, and turns straight back to the next arrival.
The receptionist is the accept loop. Each bellboy is a thread. What the bellboy actually does with the guest, the room, the tea, the luggage, is the handler. Change the handler and the same hotel runs a different service.
The framework
server_framework.py
# server_framework.py: a reusable threaded socket server
import socket
import threading
class SocketServerFramework:
def __init__(self, host, port, handler):
self.host = host
self.port = port
self.handler = handler # a function that takes one connected socket
def start(self):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((self.host, self.port))
server.listen(5)
print(f"Framework listening on {self.host}:{self.port}")
while True:
client_sock, client_addr = server.accept()
thread = threading.Thread(
target=self._handle_client, args=(client_sock, client_addr)
)
thread.daemon = True
thread.start()
def _handle_client(self, client_sock, client_addr):
try:
self.handler(client_sock, client_addr)
finally:
client_sock.close()
def log_and_echo(client_sock, client_addr):
print(f"New connection from {client_addr}")
data = client_sock.recv(1024)
print(f"Received: {data!r}")
client_sock.sendall(data)
if __name__ == "__main__":
server = SocketServerFramework("0.0.0.0", 9999, log_and_echo)
server.start()
| Piece | What it does |
|---|---|
class SocketServerFramework | A class (you met the rubber stamp earlier) that bundles a host, a port and a handler |
handler | A function passed in from outside. The framework never knows what it does; it only calls it |
server.accept() | Wait for the next caller. The receptionist's job |
threading.Thread(target=..., args=...) | Create a bellboy: a separate line of execution that will run _handle_client for this one caller |
thread.daemon = True | A daemon thread will not keep the program alive on its own. When you stop the server, stray threads do not hold it open |
thread.start() | Send the bellboy off. accept() is free again immediately |
try: ... finally: client_sock.close() | Whatever the handler does, including crashing, the connection is closed. Cleanup always lives in finally |
log_and_echo | The simplest handler: log, read, echo |
Python concept: Threads
Handing each connection to its own threading.Thread lets the server go straight back to accept() instead of waiting for one slow caller to finish before serving the next. For a lab server handling a few connections at once, threads are enough.
A production listener expecting thousands of simultaneous callers would move to Python's asyncio framework, which is outside this course's scope.
Run the framework in one terminal and send it two messages from another:
Server terminal
Framework listening on 0.0.0.0:9999
New connection from ('127.0.0.1', 64328)
Received: b'hello'
New connection from ('127.0.0.1', 64329)
Received: b'second client'
Change the handler, change the service
The handler is passed in from outside, so the same framework becomes a different service simply by supplying a different function. This pattern, a fixed accepting loop plus an interchangeable handler, is how most real network servers are structured, and you will recognise it again whenever you read the source of one.
Handler 1: a slow echo, to prove the threads work
Run the same three clients against the framework with a handler that also takes two seconds:
handlers.py
# handlers.py: different services built on the same framework
import time
from server_framework import SocketServerFramework
def slow_echo(client_sock, client_addr):
# pretend each caller needs two seconds of work
data = client_sock.recv(1024)
time.sleep(2)
client_sock.sendall(data)
The same three clients, now served in parallel
client 3: got b'hello 3' after 2.0s
client 1: got b'hello 1' after 2.0s
client 2: got b'hello 2' after 2.0s
all three finished in 2.0s
Six seconds became two. All three callers were served at once, and they finished in whatever order the threads happened to complete. That is the whole point of the bellboys.
Handler 2: a small honeypot
A honeypot is a service whose only job is to be knocked on. This handler announces a believable banner, listens briefly, and logs who came by and what they sent. It uses the banner from the previous article, from the other side of the conversation:
honeypot handler
def banner_honeypot(client_sock, client_addr):
# announce a banner, listen briefly, and log who knocked
client_sock.sendall(b"SSH-2.0-OpenSSH_7.4\r\n")
client_sock.settimeout(1.0)
try:
first = client_sock.recv(1024)
except OSError:
first = b""
print(f"HONEYPOT: {client_addr[0]} knocked, sent {first!r}")
SocketServerFramework("127.0.0.1", 9997, banner_honeypot).start()
Point grab_banner() from the previous article at port 9997, then knock once more with a client that introduces itself. The honeypot's terminal shows both visitors:
Honeypot terminal
Framework listening on 127.0.0.1:9997
HONEYPOT: 127.0.0.1 knocked, sent b''
HONEYPOT: 127.0.0.1 knocked, sent b'SSH-2.0-scanner_1.0\r\n'
The first line is grab_banner(): it connected, read the banner, and hung up without sending a word. The second is a client that answered with its own greeting. A scanner and a real SSH client leave different footprints, and a honeypot's log is how a defender tells them apart.
Handler 3: never wait forever
Real callers are slow, confused or hostile. A caller that connects and says nothing would hold its thread open indefinitely. A handler should always set a timeout.
hardened.py
import socket
from server_framework import SocketServerFramework
def safe_echo(client_sock, client_addr):
client_sock.settimeout(2.0) # never wait forever for a caller
try:
data = client_sock.recv(1024)
except socket.timeout:
print(f"{client_addr[0]} connected but said nothing; hanging up")
return
client_sock.sendall(data)
SocketServerFramework("127.0.0.1", 9996, safe_echo).start()
A caller that connects and stays silent
silent caller was disconnected after 2.0 s; recv gave b''
Framework listening on 127.0.0.1:9996
127.0.0.1 connected but said nothing; hanging up
After two seconds the server gave up on the silent caller, closed the connection through the finally block, and freed the thread. Without that timeout, a few hundred silent callers could tie up every thread the server had.
Close in finally
Every connection is closed whatever the handler did, so a crash never leaks a socket.
Set a timeout
A caller who says nothing cannot hold a thread forever.
Limit what you read
recv(1024) takes at most 1024 bytes at once. Never assume what arrives is small, well-formed or polite.
Log who knocked
The address and the first bytes are the evidence. Without a log, an attack leaves nothing to study.
Caution: Lab use only
A listener is a door. Run these servers on 127.0.0.1 for practice, and only ever on 0.0.0.0 inside an isolated lab. A honeypot exposed to the open internet will attract real attackers, and operating one on a network you do not control can breach your institution's policy.
A case from 1988
Real incident: The Morris Worm, 1988: one unchecked service, ten percent of the internet
In November 1988, a program written by Robert Tappan Morris, then a graduate student at Cornell University, spread across the early internet by exploiting weaknesses in a few standard network services, among them a buffer overflow in the fingerd daemon and guessable passwords tried over the rsh and rexec services.
The worm was not built to destroy data, but a flaw in its own spreading logic let it reinfect a machine repeatedly, and roughly ten percent of the internet's machines at the time, an estimated six thousand systems, slowed to a crawl or stopped responding. Morris became the first person convicted under the United States' then-new Computer Fraud and Abuse Act.
What this teaches: every weakness the worm used travelled over an ordinary socket to an ordinary network service, the same TCP building block this unit has just covered. Four decades later the lesson has not changed: a network service that accepts a connection without carefully checking what arrives on it remains one of the oldest and most reliable ways into a system. That is precisely why the framework above closes every connection in a finally block, why a good handler sets timeouts and limits what it reads, and why Unit 2 treats every byte a service returns as untrusted.
Check your understanding
Question 1: What are the two parts of the framework, and why is it useful to keep them separate?
The accept loop (waiting for callers and handing each to a thread) and the handler (the function that decides what to do with one connection). Keeping them separate means the same networking code can serve a completely different service just by passing in a different handler.
Question 2: Why does each connection get its own thread?
So the server can go straight back to accept() instead of waiting for one slow caller. With a single loop, three two-second callers took six seconds in total; with a thread each, all three finished in two.
Question 3: Why does _handle_client close the socket in a finally block?
Because finally runs whether the handler finished normally or crashed. Closing there guarantees the connection is always released, so a faulty handler cannot leak sockets.
Question 4: A caller connects to your handler and never sends anything. What happens without a timeout, and what is the fix?
The handler waits on recv() forever, holding its thread. Enough silent callers would exhaust the server. The fix is client_sock.settimeout(...) and a try/except socket.timeout that hangs up.
Key takeaway: Key takeaway
A fixed accepting loop plus an interchangeable handler is how most real network servers are built. Give each caller its own thread, close every connection in finally, set timeouts, and treat everything that arrives as untrusted. The Morris Worm showed in 1988 what happens when a service does not.

