UDP is the backbone of real-time games. Learn to send and receive datagrams, why there is no connection, and how to structure packets and detect loss on top of an unreliable channel.
UDP has no connection, so you do not connect/accept — you just send a datagram to an address and receive datagrams from anyone. The API is sendto (with the destination) and recvfrom (which tells you the sender). Each call sends or receives exactly one packet, so unlike TCP there is no framing problem — a datagram is a message. This is the whole UDP model.
import socket
# UDP server: no listen/accept — just bind and receive datagrams
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # SOCK_DGRAM = UDP
server.bind(("0.0.0.0", 7777))
while True:
data, addr = server.recvfrom(2048) # one datagram + who sent it
server.sendto(b"pong", addr) # reply to that sender
# UDP client:
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b"ping", ("127.0.0.1", 7777)) # fire a datagram, no connection
reply, _ = client.recvfrom(2048)
# one sendto/recvfrom = one packet -> NO framing needed (a datagram IS a message).Because a datagram is a self-contained message, you design a compact binary layout: a small header (a message type, a sequence number, maybe flags) followed by the payload. Keeping packets small matters — they must fit within the network’s MTU (~1200–1500 bytes) to avoid fragmentation, which increases loss. Designing tight packet formats is a defining part of game networking, expanded in the serialization lesson of the Game Networking course.
import struct
# design a compact binary packet: [type:1][seq:4][payload...]
MSG_MOVE = 1
def make_move_packet(seq: int, x: float, y: float) -> bytes:
header = struct.pack("!BI", MSG_MOVE, seq) # 1-byte type + 4-byte seq
body = struct.pack("!ff", x, y) # two floats
return header + body # 13 bytes total
def parse(packet: bytes):
msg_type, seq = struct.unpack("!BI", packet[:5])
x, y = struct.unpack("!ff", packet[5:13])
return msg_type, seq, x, y
# keep packets SMALL: fit under the network MTU (~1200-1500 bytes) or they
# fragment (worse loss). Tight binary layouts + bit-packing = the Game Networking course.UDP will silently lose, duplicate, and reorder packets, so you add a sequence number to every packet and track what you have seen. Gaps reveal loss; out-of-order sequence numbers reveal reordering; a repeated number reveals a duplicate. With this you can drop stale packets (ignore a movement update older than the newest you have) and measure the loss rate — the minimum bookkeeping every UDP game protocol needs.
# receiver-side: use per-packet sequence numbers to spot loss/reorder/dupes
class Receiver:
def __init__(self):
self.latest_seq = -1
self.received = 0
def on_packet(self, seq: int):
if seq <= self.latest_seq:
return "stale/duplicate — drop it" # older than what we have
lost = seq - self.latest_seq - 1 # gap = packets missed
self.latest_seq = seq
self.received += 1
return f"ok (missed {lost} before this)"
r = Receiver()
for s in [0, 1, 3, 2, 3]: # note: 2 arrives late, 3 dup
print(s, "->", r.on_packet(s))
# for real-time state you DROP stale packets (keep only the newest). track the
# gap count to estimate packet loss. this is the core UDP bookkeeping.A practical hurdle: most players are behind NAT (their router shares one public IP among many devices), so a server cannot simply send an unsolicited packet to a player. The client must send first so the NAT creates a mapping the reply can use, and peer-to-peer connections need "NAT punchthrough" coordinated by a server. This is why even "peer-to-peer" games use a server for matchmaking and connection setup — a reality every server-side game dev meets.
NAT (Network Address Translation): a home router shares ONE public IP among
many devices, rewriting ports. Consequence for games:
- a server can't send an UNSOLICITED packet to a player behind NAT —
there's no mapping yet. The CLIENT must send first; the NAT then creates a
mapping that the server's reply can come back through.
- peer-to-peer? two players both behind NAT can't directly reach each other.
"NAT punchthrough" has both send to each other simultaneously (coordinated
by a shared server) to open mappings. Sometimes it fails -> fall back to a
relay server.
This is WHY even "P2P" games need a server: matchmaking + connection brokering.
Dedicated servers sidestep it (the server has a reachable public IP).