A real server must frame messages out of the byte stream, handle partial reads, and serve many clients. Build a robust TCP server loop — the foundation of a matchmaking or lobby service.
The most common TCP bug: assuming one send equals one recv. TCP is a byte stream with no message boundaries, so a single recv might return half a message, or two messages concatenated, or one message split across several reads. You must frame — typically by prefixing each message with its length, then reading exactly that many bytes. Getting framing right is essential for any TCP protocol.
Always frame TCP required — One send() does not equal one recv(). TCP is a byte stream — you must delimit messages yourself (length prefix or delimiter). Assuming message boundaries is a classic bug.import struct
# length-prefixed framing: send a 4-byte length, then the message bytes
def send_msg(sock, payload: bytes):
sock.sendall(struct.pack("!I", len(payload)) + payload)
def recv_exactly(sock, n: int) -> bytes:
buf = b""
while len(buf) < n: # loop until we have all n bytes
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("closed")
buf += chunk
return buf
def recv_msg(sock) -> bytes:
(length,) = struct.unpack("!I", recv_exactly(sock, 4)) # read the 4-byte length
return recv_exactly(sock, length) # then exactly that many
# TCP is a STREAM: frame every message, or reads will split/merge them.A blocking accept-then-handle loop serves one client at a time, which is useless for a game lobby with hundreds of players. The three approaches are: a thread per connection (simple, limited scale), an event loop with non-blocking sockets (scales to many thousands), or async/await (the modern ergonomic version of an event loop). The Server Concurrency course goes deep; here is the thread-per-client shape.
import socket, threading
def handle(conn, addr):
with conn:
while True:
data = conn.recv(1024)
if not data:
break # client disconnected
conn.sendall(b"echo: " + data)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 7777)); server.listen()
while True:
conn, addr = server.accept()
threading.Thread(target=handle, args=(conn, addr), daemon=True).start()
# a thread per client: simple, but memory/context-switch cost limits scale.
# high-concurrency servers use an EVENT LOOP instead (Server Concurrency course).Networks fail: clients crash, Wi-Fi drops, connections reset. A recv returning empty bytes means a clean close; exceptions mean an abrupt one. A robust server treats every socket operation as fallible, cleans up the client’s state on any failure, and never lets one client’s error take down the server. For games this also means detecting silent disconnects with timeouts or heartbeats, since a dead client may never send a proper close.
import socket
def handle(conn, addr, game):
conn.settimeout(30) # detect silent disconnects (no data)
try:
while True:
data = conn.recv(1024)
if not data: # empty -> clean disconnect
break
process(data)
except (ConnectionError, socket.timeout, OSError) as e:
print(f"{addr} dropped: {e}") # abrupt disconnect / timeout
finally:
game.remove_player(addr) # ALWAYS clean up this client's state
conn.close()
# assume any socket call can fail; isolate per-client so one drop can't crash
# the server. Games add HEARTBEATS/pings to spot dead-but-not-closed clients.By default TCP buffers small writes to send them together (Nagle’s algorithm), improving throughput but adding delay — bad for interactive traffic. Real-time servers disable it (TCP_NODELAY) so small messages go out immediately. This is a small setting with a real latency impact, and one of several reasons games lean on UDP where they cannot afford any buffering delay at all.
import socket
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # disable Nagle
# Nagle's algorithm BUFFERS small writes to batch them -> better throughput,
# but adds up to ~40ms of delay. For interactive/real-time TCP traffic, turn it
# OFF so small messages (a move, a chat line) send immediately.
# this is one reason games prefer UDP for real-time data: TCP has buffering +
# retransmit delays you can't fully remove. Use TCP_NODELAY when you must use TCP.