Python Socket Programming: TCP-, UDP-, Server- und Client-Kommunikation
Das socket-Modul von Python gehört zur Standardbibliothek und lässt sich für die Entwicklung netzwerkbasierter Anwendungen einsetzen. Dieses Tutorial vermittelt die Grundlagen der Socket-Programmierung mit Python. Dazu gehören der Aufbau einer einfachen Client-Server-Architektur, die Verarbeitung mehrerer Clients mithilfe von Threads sowie die Unterschiede zwischen TCP- und UDP-Sockets. Außerdem wird erläutert, wie Verbindungen hergestellt, Daten gesendet und empfangen sowie Netzwerkprogramme so aufgebaut werden, dass Verbindungen und Fehler korrekt behandelt werden. Für die Beispiele wird Python 3.10 oder neuer benötigt.
Die wichtigsten Erkenntnisse
- Das socket-Modul von Python stellt eine schlanke und plattformübergreifende Schnittstelle zu den Socket-APIs des Betriebssystems wie Berkeley Sockets und Winsock bereit. Viele Socket-Funktionen in Python entsprechen deshalb direkt den zugrunde liegenden Betriebssystemoperationen.
- TCP-Server verwenden bind(), listen() und accept(), während TCP-Clients connect() nutzen. Bei UDP kommen dagegen recvfrom() und sendto() zum Einsatz, ohne dass zuvor eine Verbindung aufgebaut werden muss.
- SO_REUSEADDR sollte vor bind() gesetzt werden, um während der Entwicklung Fehler wie Address already in use zu vermeiden.
- Für größere Datenmengen ist sendall() gegenüber send() vorzuziehen, damit unvollständige Übertragungen nicht zu abgeschnittenen Nachrichten führen.
- Ein eigener Thread pro Client ist einfach umzusetzen, wird jedoch bei mehreren Hundert gleichzeitigen Verbindungen zunehmend ineffizient. Für eine höhere Parallelität eignen sich asyncio oder selectors.
- recv()- und send()-Aufrufe sollten innerhalb von try/except-Blöcken ausgeführt werden, die ConnectionResetError, BrokenPipeError und socket.timeout behandeln. Sockets sollten außerdem immer über einen finally-Block geschlossen werden.
Voraussetzungen
- Python 3.10 oder neuer muss auf dem lokalen System installiert sein. Das Beispiel zur Nachrichtenrahmung nutzt die Syntax bytes | None für Union-Type-Hints, die ab Python 3.10 verfügbar ist.
- Grundkenntnisse in Python, darunter Variablen, Funktionen, Schleifen sowie try/except-Anweisungen.
- Zwei gleichzeitig geöffnete Terminalfenster oder Tabs, damit Server- und Client-Skript als getrennte Prozesse ausgeführt werden können.
Was ist ein Socket in Python?
Ein Socket ist ein Dateideskriptor beziehungsweise Handle, das einen Endpunkt einer Netzwerkverbindung repräsentiert. Das socket-Modul von Python bildet eine schlanke Abstraktionsschicht über den Socket-APIs des Betriebssystems, darunter Berkeley Sockets und Winsock. Es stellt unter anderem Funktionen wie bind(), listen(), accept(), connect(), send() und recv() bereit.
Wie das Python-socket-Modul mit dem Netzwerk-Stack des Betriebssystems zusammenarbeitet
Zwei typische Verhaltensweisen des Betriebssystems sind für viele Probleme bei der Socket-Programmierung verantwortlich. Zum einen liefert recv() lediglich die aktuell verfügbaren Bytes bis zur angegebenen Maximalgröße zurück und garantiert keine bestimmte Datenmenge. Zum anderen können Ports nach dem Beenden eines Servers noch ungefähr 60 Sekunden im Zustand TIME_WAIT reserviert bleiben. Fehler durch unvollständige Lesevorgänge und Meldungen wie „Address already in use“ lassen sich häufig auf diese beiden Mechanismen zurückführen.
Beim Aufruf von socket.socket(AF_INET, SOCK_STREAM) erzeugt das Betriebssystem einen Socket-Dateideskriptor für den IPv4-TCP-Stack. Mit bind() werden dem Socket eine lokale Adresse und ein Port zugeordnet. listen() versetzt den Socket in den passiven Lauschen-Modus. accept() wartet, bis der TCP-Drei-Wege-Handshake abgeschlossen ist, und gibt anschließend einen neuen verbundenen Socket zurück. sendall() schreibt Bytes in den Sendepuffer des Kernels. recv(n) liest maximal n Bytes aus dem Empfangspuffer. Befinden sich dort weniger Daten, werden entsprechend weniger Bytes zurückgegeben. Dieses Verhalten ist normal und erklärt, warum eine Nachrichtenrahmung erforderlich sein kann.
TCP oder UDP: Den passenden Socket-Typ auswählen
Die am häufigsten eingesetzten Socket-Typen sind SOCK_STREAM für TCP und SOCK_DGRAM für UDP. Für die Interprozesskommunikation auf demselben System können Unix-Domain-Sockets AF_UNIX mit SOCK_STREAM kombinieren, ohne dass die Kommunikation den regulären Netzwerk-Stack durchlaufen muss.
| Einsatzzweck | Zuverlässigkeit | Reihenfolge garantiert | Verbindung erforderlich | Python-Socket-Konstanten |
|---|---|---|---|---|
| TCP: Webverkehr, Dateiübertragungen und Protokolle mit garantierter Zustellung | Zuverlässig | Ja | Ja | socket.SOCK_STREAM |
| UDP: DNS, Videostreaming, Gaming und Telemetrie | Unzuverlässig | Nein | Nein | socket.SOCK_DGRAM |
| Unix Domain: lokale IPC zwischen Prozessen auf demselben System | Zuverlässig | Ja | Ja | socket.AF_UNIX mit socket.SOCK_STREAM |
Einen einfachen TCP-Server und TCP-Client einrichten
Ein TCP-Server bindet sich an eine Adresse, wartet auf eine Client-Verbindung und tauscht anschließend innerhalb einer Schleife Daten mit dem verbundenen Client aus. Die beiden folgenden Skripte bilden ein funktionsfähiges Server-Client-Paar, das auf demselben System ausgeführt werden kann.
Python Socket Server
Speichern Sie das folgende Programm als socket_server.py. Der Server bindet sich an 127.0.0.1, also die Loopback-Schnittstelle. Dadurch können ausschließlich Programme auf demselben System eine Verbindung herstellen. Soll der Server stattdessen auf allen verfügbaren Netzwerkschnittstellen lauschen, kann “ oder ‚0.0.0.0‘ verwendet werden.
socket_server.py
import socket
def server_program():
host = '127.0.0.1' # Loopback-Adresse für lokale Tests
port = 5000 # Portnummer oberhalb von 1024 verwenden
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4-TCP-Socket erstellen
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # sofortige Wiederverwendung des Ports ermöglichen
server_socket.bind((host, port)) # Socket an Host und Port binden
server_socket.listen(5) # bis zu 5 Verbindungsanfragen in die Warteschlange aufnehmen
conn, address = server_socket.accept() # auf eine Verbindung warten und diese annehmen
print("Connection from: " + str(address))
while True:
raw = conn.recv(1024) # maximal 1024 Bytes empfangen; größere Nachrichten benötigen mehrere recv()-Aufrufe
if not raw:
break
try:
data = raw.decode('utf-8')
except UnicodeDecodeError:
print(f"Received non-UTF-8 data from {address}, skipping")
continue
print("from connected user: " + str(data))
data = input(' -> ')
conn.sendall(data.encode()) # Antwort an den Client übertragen
conn.close() # Client-Verbindung schließen
server_socket.close() # lauschenden Server-Socket schließen
if __name__ == '__main__':
server_program()
Ein UnicodeDecodeError bei raw.decode(‚utf-8‘) kann zwei typische Ursachen haben. Entweder sind die empfangenen Daten tatsächlich nicht UTF-8-kodiert oder ein gültiges Mehrbyte-Zeichen wurde auf zwei recv()-Aufrufe verteilt. Der except-Block behandelt beide Fälle, indem die betreffenden Bytes verworfen werden. Wenn Nachrichtengrenzen oder Nicht-ASCII-Zeichen relevant sind, sollten die weiter unten beschriebenen Funktionen send_msg und recv_msg verwendet werden. Diese Hilfsfunktionen sammeln zunächst eine vollständige Nachricht, bevor sie dekodiert wird.
In diesem Beispiel existieren zwei unterschiedliche Socket-Objekte. server_socket ist der lauschende Socket. Er bleibt während der gesamten Laufzeit des Servers geöffnet und dient ausschließlich dazu, neue Verbindungen anzunehmen. conn ist der von accept() zurückgegebene verbundene Socket. Er repräsentiert genau einen Client und wird zum Lesen und Schreiben von Daten verwendet. Jeder Client erhält ein eigenes conn-Objekt. server_socket selbst wird dagegen nicht direkt mit send() oder recv() verwendet.
Der input()-Aufruf innerhalb der Schleife macht den Beispielserver interaktiv. In einer tatsächlichen Anwendung sollte input() durch die jeweilige Anwendungslogik ersetzt werden. Bleibt input() an dieser Stelle bestehen, kann der Server keine weitere Nachricht empfangen, bis eine Antwort im Terminal eingegeben wurde.
Python Socket Client
Speichern Sie das Client-Programm als socket_client.py. Der Client verbindet sich mit derselben Loopback-Adresse und demselben Port wie der Server. Ein bind()-Aufruf ist auf der Client-Seite nicht notwendig, da das Betriebssystem beim Aufruf von connect() automatisch einen temporären lokalen Port zuweist.
socket_client.py
import socket
def client_program():
host = '127.0.0.1' # Loopback-Adresse für lokale Tests
port = 5000 # vom Socket-Server verwendeter Port
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4-TCP-Socket erstellen
client_socket.connect((host, port)) # Verbindung zum Server herstellen
message = input(" -> ") # Benutzereingabe erfassen
while message.lower().strip() != 'bye':
client_socket.sendall(message.encode()) # Nachricht übertragen
raw = client_socket.recv(1024) # maximal 1024 Bytes empfangen; größere Nachrichten können mehrere recv()-Aufrufe erfordern
if not raw:
break
try:
data = raw.decode('utf-8')
except UnicodeDecodeError:
print("Received non-UTF-8 data from server, skipping")
continue
print('Received from server: ' + data) # Antwort im Terminal anzeigen
message = input(" -> ") # nächste Nachricht abfragen
client_socket.close() # Verbindung schließen
if __name__ == '__main__':
client_program()
Ausgabe der Python-Socket-Programme
Starten Sie zuerst das Server-Programm. Öffnen Sie anschließend ein weiteres Terminal und führen Sie dort das Client-Programm aus. Geben Sie im Client-Terminal eine Nachricht ein und drücken Sie Enter, um sie an den Server zu senden.
Das Server-Terminal zeigt:
Output:
python3 socket_server.py
Connection from: ('127.0.0.1', 57822)
from connected user: Hi
-> Hello
from connected user: How are you?
-> Good
from connected user: Awesome!
-> Ok then, bye!
Das Client-Terminal zeigt:
Output:
python3 socket_client.py
-> Hi
Received from server: Hello
-> How are you?
Received from server: Good
-> Awesome!
Received from server: Ok then, bye!
-> Bye
Der Server arbeitet auf Port 5000. Gleichzeitig erhält auch der Client während connect() eine eigene Portnummer vom Betriebssystem. In diesem Beispiel wurde auf der Client-Seite Port 57822 vergeben.
Vollständige Nachrichten senden und empfangen
TCP überträgt einen kontinuierlichen Bytestrom und keine voneinander abgegrenzten Nachrichten. Werden mit sendall() beispielsweise 500 Bytes übertragen, kann der erste recv(1024)-Aufruf auf der Empfängerseite 200 Bytes, alle 500 Bytes oder eine andere Menge zurückgeben. Das Ergebnis hängt unter anderem von Pufferung und Netzwerktiming ab. Deshalb entspricht ein einzelner sendall()-Aufruf nicht zwangsläufig genau einem recv()-Aufruf.
Eine verbreitete Lösung ist das sogenannte Length-Prefix-Framing. Vor der eigentlichen Nutzlast wird ein fester vier Byte großer Header übertragen, der die Größe der Nutzlast als Big-Endian-Integer ohne Vorzeichen enthält. Der Empfänger liest zunächst exakt diese vier Bytes, ermittelt daraus die Länge und ruft recv() anschließend so lange auf, bis die angegebene Anzahl an Nutzdaten empfangen wurde. Dieses Verfahren eignet sich sowohl für binäre als auch für textbasierte Protokolle und benötigt lediglich vier zusätzliche Bytes pro Nachricht.
Die folgenden Hilfsfunktionen setzen dieses Verfahren um. send_msg fügt den Header vor der Nutzlast ein und überträgt beides gemeinsam mit sendall(). recv_msg verwendet einen bytearray-Puffer und die interne Hilfsfunktion _recv_exactly, um Daten über beliebig viele recv()-Aufrufe hinweg vollständig einzulesen.
framing.py
import socket
import struct
def send_msg(sock, data: bytes) -> None:
header = struct.pack('>I', len(data)) # 4-Byte-Big-Endian-Längenheader erzeugen
sock.sendall(header + data)
def recv_msg(sock) -> bytes | None:
raw_len = _recv_exactly(sock, 4)
if raw_len is None:
return None # Verbindung wurde beendet, bevor der Header vollständig ankam
msg_len = struct.unpack('>I', raw_len)[0]
return _recv_exactly(sock, msg_len)
def _recv_exactly(sock, n: int) -> bytes | None:
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
return None # Gegenstelle hat die Verbindung geschlossen
buf.extend(chunk)
return bytes(buf)
send_msg und recv_msg sollten anstelle direkter sendall()- und recv()-Aufrufe eingesetzt werden, wenn eindeutige Nachrichtengrenzen erforderlich sind. Der zuvor gezeigte Echo-Server verwendet aus Gründen der Einfachheit direkt recv und sendall. Bei strukturierten Nachrichten können diese Aufrufe durch send_msg und recv_msg ersetzt werden.
Mehrere Clients mit Threading verarbeiten
Ein Server, der nur für einen Client ausgelegt ist, blockiert bei accept() und kann deshalb lediglich eine Verbindung gleichzeitig bedienen. Sollen mehrere Clients parallel verarbeitet werden, kann das threading-Modul von Python für jede angenommene Verbindung einen eigenen Thread erzeugen.
Für jede Client-Verbindung einen Thread starten
Der folgende vollständige Server verwendet handle_client(), um jeweils eine einzelne Verbindung zu verarbeiten. server_program() nimmt währenddessen fortlaufend neue Verbindungen an. Für jede akzeptierte Verbindung wird ein Daemon-Thread gestartet. Dadurch verhindern die Worker-Threads nicht, dass der Python-Interpreter beendet wird, sobald die Hauptschleife endet.
threaded_server.py
import socket
import threading
def handle_client(conn, addr):
print(f"Connection from: {addr}")
try:
while True:
data = conn.recv(1024)
if not data: # leere Bytes bedeuten, dass der Client die Verbindung geschlossen hat
break
conn.sendall(data) # empfangene Daten an den Client zurücksenden
except (ConnectionResetError, BrokenPipeError) as e:
print(f"Connection error with {addr}: {e}")
finally:
conn.close()
def server_program():
host = '127.0.0.1' # Loopback-Adresse für lokale Tests
port = 5000
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4-TCP-Socket erstellen
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # sofortige Wiederverwendung nach dem Beenden erlauben
server_socket.bind((host, port))
server_socket.listen(5) # bis zu 5 ausstehende Verbindungen in die Warteschlange aufnehmen
print(f"Server listening on {host}:{port}")
try:
while True:
conn, addr = server_socket.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
thread.start()
print(f"Active connections: {threading.active_count() - 1}")
except KeyboardInterrupt:
print("Server shutting down")
finally:
server_socket.close()
if __name__ == '__main__':
server_program()
threading.active_count() liefert die Anzahl aller momentan aktiven Threads einschließlich des Hauptthreads zurück. Durch das Subtrahieren von 1 erhält man daher die Zahl der aktuell aktiven Client-Threads.
Durch Drücken von Strg+C wird ein KeyboardInterrupt ausgelöst. Der äußere try/except-Block fängt diese Ausnahme ab, sodass der lauschende Server-Socket ordnungsgemäß geschlossen werden kann, bevor das Programm beendet wird.
Thread-Sicherheit bei gemeinsam genutzten Daten
Greifen mehrere Threads gleichzeitig auf gemeinsame Daten zu und verändern diese, beispielsweise eine Liste verbundener Clients, ein Nachrichtenprotokoll oder einen Zähler, sollte dieser Zustand mit threading.Lock() geschützt werden. Ohne Lock können sich Lese- und Schreiboperationen verschiedener Threads überschneiden und dadurch inkonsistente Daten erzeugen.
import threading
client_list = []
lock = threading.Lock()
def handle_client(conn, addr):
with lock:
client_list.append(addr)
# Verbindung verarbeiten...
Die Schreibweise with lock: ist dem manuellen Aufruf von lock.acquire() und lock.release() vorzuziehen. Die with-Anweisung stellt sicher, dass das Lock auch dann wieder freigegeben wird, wenn innerhalb des geschützten Blocks eine Ausnahme auftritt.
Grenzen des Thread-pro-Client-Modells
Unter Linux kann jeder Thread bis zu 8 MB virtuellen Adressraum für seinen Stack reservieren, auch wenn ein inaktiver Thread in der Regel weniger als 100 KB tatsächlichen Arbeitsspeicher belegt. Bei einer hohen Anzahl an Verbindungen spielen jedoch Scheduler-Overhead und Kontextwechsel häufig eine größere Rolle als der reine Speicherbedarf. Sobald die Anzahl gleichzeitiger Verbindungen in den Bereich mehrerer Hundert steigt, nehmen diese Kosten deutlich zu. Server, die Hunderte oder Tausende Verbindungen gleichzeitig verarbeiten müssen, können stattdessen selectors.DefaultSelector für I/O-Multiplexing oder asyncio.start_server() für Coroutine-basierte Nebenläufigkeit einsetzen. Beide Ansätze werden weiter unten im Abschnitt über Python-Socket-Bibliotheken behandelt.
Fehlerbehandlung und Socket-Ausnahmen
Bei Socket-Anwendungen treten bestimmte Fehler besonders häufig auf. ConnectionResetError kann entstehen, wenn ein entfernter Prozess unerwartet beendet wird. BrokenPipeError kann auftreten, wenn Daten an einen Socket gesendet werden, den die Gegenstelle bereits geschlossen hat. socket.timeout wird ausgelöst, wenn eine blockierende Operation länger als das festgelegte Zeitlimit benötigt. Die folgenden Muster behandeln alle drei Situationen, ohne dass der Server dadurch abstürzt.
Häufige Ausnahmen: ConnectionResetError, BrokenPipeError und socket.timeout
- ConnectionResetError: tritt auf, wenn die entfernte Gegenstelle die Verbindung unerwartet beendet, beispielsweise weil der Client-Prozess während einer laufenden Sitzung beendet wird.
- BrokenPipeError: kann bei send() oder sendall() entstehen, wenn die empfangende Seite ihren Socket bereits geschlossen hat.
- socket.timeout: wird ausgelöst, wenn eine blockierende Operation das mit settimeout() definierte Zeitlimit überschreitet.
try/except-Blöcke für recv und send verwenden
Die Schleife für Empfang und Versand sollte in einen try/except-Block eingebettet werden, der alle drei Fehlerfälle behandelt:
while True:
try:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
except ConnectionResetError:
print("Client disconnected unexpectedly")
break
except BrokenPipeError:
print("Send failed: client has closed the connection")
break
except socket.timeout:
print("Connection timed out")
break
Die Prüfung auf leere Bytes mit if not data: break ist von der eigentlichen Ausnahmebehandlung zu unterscheiden. Gibt recv() b“ zurück, bedeutet das normalerweise, dass die Gegenstelle die Verbindung regulär beendet hat. Dabei handelt es sich nicht um einen Fehler.
Sockets mit einem finally-Block sicher schließen
conn.close() sollte innerhalb eines finally-Blocks stehen. Dadurch wird der Socket unabhängig davon geschlossen, ob die Schleife regulär endet oder durch eine Ausnahme verlassen wird:
conn, addr = server_socket.accept()
try:
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
finally:
conn.close()
Nicht korrekt geschlossene Sockets belegen weiterhin Dateideskriptoren. Unter Linux liegt das Standardlimit häufig bei 1024 geöffneten Dateideskriptoren pro Prozess. Ein Server, der im Laufe der Zeit viele Verbindungen verarbeitet, kann dieses Limit erreichen, wenn einzelne Verbindungen nicht ordnungsgemäß geschlossen werden.
Socket-Konfiguration und wichtige Optionen
Drei Einstellungen sind für viele Server besonders relevant. SO_REUSEADDR hilft dabei, Fehler durch bereits belegte Adressen nach einem Neustart zu vermeiden. settimeout() verhindert, dass inaktive Verbindungen dauerhaft blockieren. setblocking(False) bildet die Grundlage für nicht blockierende Ein- und Ausgabe.
SO_REUSEADDR verwenden, um „Address already in use“ zu vermeiden
Nachdem ein Server beendet wurde, kann das Betriebssystem den verwendeten Port für ungefähr 60 Sekunden im Zustand TIME_WAIT behalten. Auf diese Weise können verspätete Pakete der vorherigen Verbindung auslaufen. Wird während dieser Zeit erneut versucht, denselben Port zu binden, kann folgender Fehler auftreten:
OSError: [Errno 98] Address already in use
Das Problem lässt sich durch eine einzelne Zeile beheben, die nach der Erstellung des Sockets und vor bind() ausgeführt wird:
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Damit wird dem Betriebssystem erlaubt, dieselbe Kombination aus Adresse und Port unmittelbar erneut zu verwenden. Diese Einstellung ist insbesondere bei Servern üblich, die während der Entwicklung oder Bereitstellung regelmäßig neu gestartet werden.
Socket-Timeouts mit settimeout konfigurieren
socket.settimeout(seconds) gilt für alle blockierenden Operationen auf dem jeweiligen Socket. Wird eine Operation nicht innerhalb der angegebenen Zeit abgeschlossen, löst Python socket.timeout aus. Mit dem Wert None kann wieder in den normalen blockierenden Modus ohne Zeitlimit gewechselt werden.
Das folgende Beispiel setzt vor dem Verbindungsaufbau ein Timeout auf der Client-Seite:
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4-TCP-Socket erstellen
client_socket.settimeout(5)
try:
client_socket.connect(('127.0.0.1', 5000))
except socket.timeout:
print("Connection timed out after 5 seconds")
Nicht blockierende Sockets mit setblocking(False)
Durch sock.setblocking(False) verändern recv(), send(), accept() und connect() ihr Verhalten. Würde eine dieser Operationen normalerweise blockieren, wird stattdessen sofort BlockingIOError ausgelöst. Dieses Prinzip bildet die Grundlage für I/O-Multiplexing. Ein oder mehrere nicht blockierende Sockets können bei selectors.DefaultSelector registriert werden. Der Selector meldet anschließend, welche Sockets zum Lesen oder Schreiben bereit sind, ohne dass die übrigen blockiert werden.
import selectors
import socket
sel = selectors.DefaultSelector()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IPv4-TCP-Socket erstellen
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, data=None)
Ein vollständiger Server auf Basis dieses Multiplexing-Verfahrens ist in der Dokumentation zu Python selectors beschrieben.
Einen UDP-Server und UDP-Client in Python erstellen
UDP-Sockets können Nachrichten austauschen, ohne zuvor eine Verbindung aufzubauen. Der folgende Echo-Server und Client kommunizieren über UDP und verwenden Port 5001, damit kein Konflikt mit den zuvor gezeigten TCP-Beispielen entsteht.
Die wichtigsten Unterschiede zur TCP-Socket-Konfiguration
Bei UDP wird recvfrom() anstelle von recv() verwendet. recvfrom() gibt sowohl die empfangenen Daten als auch die Adresse des Absenders zurück. sendto() erhält neben den Daten auch die Zieladresse als Argument. Ein UDP-Server benötigt weder listen() noch accept() oder connect().
UDP-Server: Codebeispiel
udp_server.py
import socket
def udp_server():
host = '127.0.0.1' # Loopback-Adresse für lokale Tests
port = 5001
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # IPv4-UDP-Socket erstellen
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # unmittelbare Wiederverwendung nach dem Beenden erlauben
server_socket.bind((host, port))
print(f"UDP server listening on {host}:{port}")
while True:
data, addr = server_socket.recvfrom(4096)
print(f"Received from {addr}: {data.decode()}")
server_socket.sendto(data, addr) # empfangene Daten an den Absender zurücksenden
if __name__ == '__main__':
udp_server()
UDP-Client: Codebeispiel
udp_client.py
import socket
def udp_client():
host = '127.0.0.1' # Loopback-Adresse für lokale Tests
port = 5001
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # IPv4-UDP-Socket erstellen
client_socket.sendto(b"Hello, UDP server!", (host, port))
data, addr = client_socket.recvfrom(4096)
print(f"Response from server: {data.decode()}")
client_socket.close()
if __name__ == '__main__':
udp_client()
Starten Sie zuerst python3 udp_server.py und anschließend in einem separaten Terminal python3 udp_client.py. Da UDP verbindungslos arbeitet, kann der Client unmittelbar ein Datagramm versenden, ohne zuvor einen Handshake auszuführen. Die Echo-Antwort wird anschließend mit einem einzelnen recvfrom()-Aufruf empfangen.
Python-Socket-Bibliotheken und wann sie sinnvoller sind
Direkte Socket-Aufrufe bieten maximale Kontrolle, erfordern allerdings auch, dass viele grundlegende Funktionen selbst implementiert werden. Bevor rohe Socket-Operationen direkt eingesetzt werden, sollte daher geprüft werden, ob eine der folgenden Abstraktionen bereits für den jeweiligen Einsatzzweck geeignet ist.
Das socketserver-Modul für weniger Server-Boilerplate
socketserver.TCPServer übernimmt bind(), listen() und accept() intern. Dazu kann BaseRequestHandler erweitert und eine handle()-Methode definiert werden, die die Logik für eine einzelne Verbindung enthält:
import socketserver
class EchoHandler(socketserver.BaseRequestHandler):
def handle(self):
data = self.request.recv(1024)
self.request.sendall(data)
if __name__ == '__main__':
with socketserver.TCPServer(('127.0.0.1', 5002), EchoHandler) as server:
server.serve_forever()
socketserver eignet sich gut für einfache Request-Response-Server, bei denen keine detaillierte Kontrolle einzelner Socket-Optionen erforderlich ist. Für die Verarbeitung mehrerer Verbindungen mithilfe von Threads kann socketserver.ThreadingMixIn ergänzt werden.
asyncio für hohe Parallelität
asyncio.start_server() erzeugt einen nicht blockierenden TCP-Server, der über den Event Loop arbeitet. Anstatt für jede Verbindung einen eigenen Betriebssystem-Thread anzulegen, verwendet asyncio Coroutinen. Dadurch können Tausende gleichzeitige Verbindungen innerhalb eines einzelnen Threads verarbeitet werden. Asynchrone Socket-Muster sind in Python 3.11 und neueren Versionen ein gängiger Ansatz für Anwendungen mit hoher Parallelität:
import asyncio
async def handle_client(reader, writer):
data = await reader.read(1024)
writer.write(data)
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(
handle_client, '127.0.0.1', 5003
)
async with server:
await server.serve_forever()
if __name__ == '__main__':
asyncio.run(main())
Jede Verbindung wird von einer Coroutine verarbeitet. Dadurch können Tausende Coroutinen gleichzeitig ausgeführt werden, ohne den Speicherbedarf und den Scheduler-Overhead zu verursachen, der bei einem eigenen Thread für jeden Client entsteht. Eine vollständige Referenz bietet die Python-asyncio-Dokumentation.
Wann direkte Sockets weiterhin die richtige Wahl sind
Das socket-Modul sollte direkt verwendet werden, wenn:
- ein eigenes Protokoll über TCP oder UDP entwickelt werden soll und keine höher abstrahierte Bibliothek das benötigte Nachrichtenformat abbildet.
- Netzwerkprogrammierung auf Betriebssystemebene erlernt werden soll, da rohe Sockets die Berkeley-Sockets-API mit nur geringer Abstraktion zugänglich machen.
- Socket-Optionen wie SO_RCVBUF, SO_SNDBUF oder TCP_NODELAY direkt gesteuert werden müssen und eine höher abstrahierte Bibliothek diese Möglichkeiten nicht bereitstellt.
Vergleich der Nebenläufigkeitsmodelle:
| Modell | Ungefähre maximale Client-Anzahl | Komplexität | Blockierend | Empfohlener Einsatzzweck |
|---|---|---|---|---|
| Ein Thread pro Client | Hunderte | Niedrig | Ja | Entwicklung und Dienste mit geringem Datenverkehr |
| select/selectors | Tausende | Mittel | Nein | I/O-lastige Server innerhalb eines einzelnen Threads |
| asyncio | Zehntausende | Mittel bis hoch | Nein | Python-3.x-Dienste mit hoher Parallelität |
FAQ zu Python Socket Programming
1. Wie groß ist der Standardpuffer von socket.recv() in Python und welche Größe sollte verwendet werden?
recv() besitzt keine standardmäßig festgelegte Puffergröße. Das size-Argument ist verpflichtend und legt fest, wie viele Bytes ein einzelner Aufruf maximal lesen darf. Häufig verwendete Werte sind 1024 und 4096 Bytes. Für interaktive oder zeilenbasierte Protokolle ist ein Puffer von 4096 Bytes eine verbreitete Wahl. Bei größeren Datenübertragungen wie Datei-Downloads kann ein Wert von beispielsweise 65536 Bytes verwendet werden. Dabei muss weiterhin so lange gelesen werden, bis sämtliche erwarteten Daten eingetroffen sind. Ein größerer Puffer reduziert die Anzahl der Systemaufrufe, erhöht jedoch gleichzeitig den Speicherverbrauch pro Verbindung.
2. Warum erscheint beim Neustart eines Servers die Meldung „Address already in use“?
Nachdem ein Server-Prozess beendet wurde, kann das Betriebssystem den zuvor verwendeten Port ungefähr 60 Sekunden lang im Zustand TIME_WAIT halten. Dadurch können noch vorhandene Pakete der früheren Sitzung auslaufen. Ein erneuter bind()-Aufruf innerhalb dieses Zeitraums kann OSError: [Errno 98] Address already in use verursachen. SO_REUSEADDR sollte direkt nach der Erstellung des Sockets und vor bind() gesetzt werden:
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Damit darf das Betriebssystem die Adresse erneut verwenden, selbst wenn sie kurz zuvor bereits von einem anderen Socket genutzt wurde.
3. Was ist der Unterschied zwischen socket.send() und socket.sendall() in Python?
send() kann weniger Bytes übertragen, als ursprünglich übergeben wurden, und gibt die tatsächlich gesendete Anzahl zurück. Bei größeren Datenmengen muss send() deshalb wiederholt aufgerufen und die Position innerhalb des Puffers entsprechend weitergeschoben werden, bis sämtliche Bytes versendet wurden. sendall() übernimmt diese Schleife intern und löst nur dann eine Ausnahme aus, wenn die Übertragung fehlschlägt. Für gewöhnliche Anwendungen ist sendall() daher meistens die bessere Wahl, weil Teilübertragungen nicht unbemerkt zu unvollständigen Nachrichten führen.
4. Wie lassen sich mehrere Clients gleichzeitig mit einem Python Socket Server bedienen?
Grundsätzlich stehen zwei verbreitete Ansätze zur Verfügung. Für jede akzeptierte Verbindung kann ein eigener threading.Thread gestartet werden. Alternativ ermöglicht selectors.DefaultSelector nicht blockierendes I/O-Multiplexing. Threading ist einfacher umzusetzen und eignet sich für geringe bis mittlere Last. Bei sehr vielen gleichzeitig verbundenen Clients ist asyncio mit asyncio.start_server(), wie im vorherigen Abschnitt gezeigt, eine besser skalierbare Möglichkeit.
5. Was bedeutet der Backlog-Parameter bei socket.listen()?
Das an listen() übergebene backlog-Argument bestimmt, wie viele ausstehende Verbindungsversuche das Betriebssystem maximal in einer Warteschlange halten kann, bevor neue Verbindungen abgewiesen werden. Der Wert begrenzt nicht die Gesamtzahl bereits bestehender Verbindungen, sondern ausschließlich die Anzahl noch nicht akzeptierter Verbindungen in der Warteschlange. Für die Entwicklung eignet sich beispielsweise ein Wert von 5. In produktiven Umgebungen können Werte wie 128 oder socket.SOMAXCONN verwendet werden.
6. Wann sollte UDP statt TCP für Python Socket Programming verwendet werden?
UDP ist geeignet, wenn eine möglichst geringe Latenz wichtiger ist als eine garantierte Zustellung. Typische Beispiele sind DNS-Anfragen, Videostreaming, Online-Spiele und Telemetriesysteme. UDP garantiert weder, dass Pakete überhaupt ankommen, noch dass sie in der ursprünglichen Reihenfolge eintreffen. Benötigt eine Anwendung eine Empfangsbestätigung oder eine zuverlässige und geordnete Übertragung, sollte TCP eingesetzt werden.
7. Wie wird ein Timeout für eine Python-Socket-Verbindung gesetzt?
Vor connect() oder recv() kann socket.settimeout(seconds) auf dem jeweiligen Socket aufgerufen werden:
client_socket.settimeout(5)
Wird die jeweilige Operation nicht innerhalb von fünf Sekunden abgeschlossen, löst Python socket.timeout aus. Mit None kann anschließend wieder in den blockierenden Modus ohne Zeitlimit gewechselt werden.
8. Was passiert, wenn der Server recv() aufruft, nachdem der Client die Verbindung bereits geschlossen hat?
Wird eine Verbindung von der Gegenstelle ordnungsgemäß geschlossen, gibt recv() ein leeres Bytes-Objekt b“ zurück. Dabei handelt es sich nicht um eine Ausnahme, sondern um das normale Signal für eine saubere Beendigung der Verbindung. Innerhalb der Empfangsschleife sollte deshalb der Rückgabewert geprüft werden. Wird b“ empfangen, kann die Schleife beendet und anschließend der serverseitige Socket geschlossen werden. Wird die Verbindung dagegen abrupt unterbrochen, beispielsweise durch das Beenden eines Prozesses oder einen Netzwerkfehler, kann recv() stattdessen ConnectionResetError auslösen.
Fazit
Dieses Tutorial hat die Python-Socket-Programmierung von den grundlegenden Konzepten bis zu fortgeschrittenen Varianten behandelt. Dazu gehörten die Verbindung des socket-Moduls mit der Berkeley-Sockets-API des Betriebssystems, der Aufbau eines TCP-Servers und TCP-Clients für einzelne Verbindungen, die Verarbeitung mehrerer Clients mithilfe von Threads, die Konfiguration von Optionen wie SO_REUSEADDR und Timeouts, nicht blockierende Ein- und Ausgabe, ein UDP-Echo-Server sowie die Entscheidung zwischen direkten Sockets und höher abstrahierten Lösungen wie socketserver und asyncio.
Mit diesen Grundlagen lassen sich eigene TCP- und UDP-Protokolle entwickeln, typische Socket-Probleme wie ConnectionResetError und BrokenPipeError diagnostizieren und geeignete Nebenläufigkeitsmodelle für unterschiedliche Anforderungen auswählen. Die funktionsfähigen Beispiele können direkt mit Python 3.x ausgeführt werden und bilden eine Grundlage für Erweiterungen wie Authentifizierung, TLS-Verschlüsselung über ssl.SSLContext.wrap_socket() oder eine eigene Nachrichtenrahmung.


