PCPP1 Block 4 (Teil a): Sockets, HTTP und JSON
Track Python · PCPP1 Block 4 (4.1 bis 4.4) · ca. 50 Min.
Worum es geht
Netzwerkprogrammierung (network programming) ist laut Quelle PCPP1 Block 4 mit 18 % der Prüfung und 8 Fragen (Stand der Quelle: 1. Oktober 2026, bitte vor der Buchung prüfen). Als Web-Entwickler kennst du HTTP und JSON schon. Neu ist die Sicht von unten (Sockets, rohe Bytes). Block 4 ist auf zwei Lektionen verteilt. Diese hier (Teil a) deckt 4.1 bis 4.4 ab: Grundbegriffe, socket, HTTP und json. Teil b folgt mit XML (ElementTree) und dem REST-Client mit requests.
Nicht behandelt: UDP-Beispiele mit sendto/recvfrom im Detail, mehrere Clients gleichzeitig am Server, HTTPS und Zertifikate, Authentifizierung.
Ablauf: Tabelle JS/TS gegen Python, vier kurze Schritte, danach drei Übungen. Zwei laufen im Browser, eine läuft lokal (ein echter Socket braucht deinen Rechner). Im Browser gibt es kein Netzwerk wie auf deinem Rechner, deshalb arbeiten wir dort mit festen Antwort-Strings.
Von JS/TS her gedacht
| Thema | JS/TS | Python |
|---|---|---|
| TCP-Socket | net.Socket und net.createServer (Node) |
socket.socket(AF_INET, SOCK_STREAM) |
| Daten über den Socket | Buffer |
bytes, Strings mit .encode("utf-8") und .decode("utf-8") |
| Objekt zu JSON | JSON.stringify(o) |
json.dumps(o) |
| JSON zu Objekt | JSON.parse(s) (Fehler: SyntaxError) |
json.loads(s) (Fehler: json.JSONDecodeError) |
Zwei Dinge sind in beiden Welten gleich: Über das Netzwerk laufen Bytes, und aus Text wird erst durch Parsen ein Objekt (JSON.parse gegen json.loads). Eine rohe HTTP-Antwort ist in Python nur ein bytes-Objekt mit \r\n.
Konzept in kleinen Schritten
Schritt 1: Grundbegriffe (4.1)
- Client und Server: Der Client schickt eine Anfrage (request), der Server antwortet (response). Derselbe Rechner kann beides sein.
- IP-Adresse identifiziert einen Rechner (IPv4 wie
127.0.0.1, IPv6). Eine Domain ist ein lesbarer Name, den DNS in eine IP-Adresse auflöst (socket.gethostbyname("example.com")). - Port: Zahl von 0 bis 65535, die einen Dienst auf dem Rechner adressiert. Ports 0 bis 1023 sind bekannte Dienste: HTTP 80, HTTPS 443, FTP 21, SSH 22, SMTP 25, DNS 53.
- TCP ist verbindungsorientiert (connection-oriented): Verbindung wird aufgebaut, Daten kommen zuverlässig und in Reihenfolge an. UDP ist verbindungslos (connectionless): einzelne Pakete ohne Verbindung und ohne Garantie.
- Ein Socket ist der Endpunkt einer Verbindung, bestehend aus Adresse und Port.
- REST (Representational State Transfer) ist ein Architekturstil für Web-Schnittstellen: Ressourcen haben URLs, der Server ist zustandslos, die Operationen sind die HTTP-Methoden, die Daten meist JSON.
Schritt 2: Das Modul socket (4.2)
Sockets übertragen Bytes, keine Strings. Umwandeln geht mit .encode("utf-8") und .decode("utf-8"):
Die wichtigsten Aufrufe:
| Aufruf | Bedeutung |
|---|---|
socket.AF_INET, socket.AF_INET6 |
Adressfamilie: IPv4 oder IPv6 |
socket.SOCK_STREAM, socket.SOCK_DGRAM |
Typ: TCP oder UDP |
connect((host, port)) |
Client verbindet sich mit dem Server |
bind((host, port)), listen(), accept() |
Server: Adresse binden, auf Verbindungen warten, eine annehmen |
send(b), sendall(b) |
Bytes senden. send kann weniger senden als verlangt, sendall sendet alles |
recv(n) |
Bis zu n Bytes empfangen. Leere Bytes bedeuten: Gegenseite hat geschlossen |
sendto(b, addr), recvfrom(n) |
UDP senden und empfangen |
settimeout(s), close() |
Zeitlimit in Sekunden, Verbindung beenden (mit with automatisch) |
Ein Echo-Server und ein Client in einem Skript (Server in einem Thread, damit beides in einer Datei läuft). Das läuft nur auf deinem Rechner, nicht im Browser. Die Ausgabe unten stammt aus einem echten Lauf mit Python 3.13:
import socket, threading
def server(bereit):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 50718))
srv.listen()
bereit.set() # Client darf jetzt verbinden
conn, addr = srv.accept()
with conn:
data = conn.recv(1024)
conn.sendall(data.upper())
bereit = threading.Event()
threading.Thread(target=server, args=(bereit,), daemon=True).start()
bereit.wait()
with socket.socket() as c: # Standard: IPv4, TCP
c.connect(("127.0.0.1", 50718))
c.sendall(b"hallo")
print(c.recv(1024)) # b'HALLO'
try:
c.sendall("text") # str statt bytes
except TypeError as e:
print(e) # a bytes-like object is required, not 'str'Fehlerklassen (alle Unterklassen von OSError, socket.error ist nur ein Alias dafür): socket.timeout (Zeitlimit), socket.gaierror (Name nicht auflösbar), ConnectionRefusedError (niemand lauscht am Port). Wer except OSError schreibt, fängt alle drei.
Warum recv(n) die Hauptfalle ist: TCP ist ein Strom (stream) von Bytes ohne Nachrichtengrenzen. Ein sendall(b"hallo\n") kann beim Empfänger als b"hal" und b"lo\n" ankommen. Wer mit einem einzigen recv rechnet, liest halbe Nachrichten (oder halbe Umlaute). Die Lösung ist ein Protokoll mit Ende-Markierung (hier \n) und eine Schleife, die so lange liest, bis die Markierung da ist. Das ist die lokale Übung 3.
Schritt 3: HTTP-Grundlagen (4.3)
Eine Anfrage besteht aus Startzeile (GET /pfad HTTP/1.1), Headern, einer Leerzeile und optional einem Body. Die Antwort hat Statuszeile, Header, Leerzeile, Body. Das Zeilenende in HTTP ist \r\n. Genau das parst du in Übung 1.
| Methode | CRUD | Zweck | Idempotent (idempotent) |
|---|---|---|---|
GET |
Read | Ressource lesen | ja |
POST |
Create | Neue Ressource anlegen | nein |
PUT |
Update | Ressource ersetzen | ja |
PATCH |
Update | Ressource teilweise ändern | nicht garantiert |
DELETE |
Delete | Ressource löschen | ja |
Idempotent heißt: Zweimal dieselbe Anfrage hat denselben Effekt wie einmal. Ein doppeltes POST kann zwei Ressourcen anlegen, ein doppeltes DELETE löscht nichts Zusätzliches.
| Statuscode | Bedeutung |
|---|---|
| 200 | OK |
| 201 | Created, Ressource wurde angelegt |
| 204 | No Content, Erfolg ohne Antwortinhalt |
| 301, 302 | Weiterleitung |
| 400 | Bad Request, fehlerhafte Anfrage |
| 401 | nicht authentifiziert |
| 403 | verboten |
| 404 | nicht gefunden |
| 500 | Serverfehler |
| 503 | Dienst nicht verfügbar |
Bereiche: 1xx Information, 2xx Erfolg, 3xx Umleitung, 4xx Clientfehler, 5xx Serverfehler. Eine rohe Antwort ist in Python nur bytes mit \r\n:
partition zerlegt am ersten Treffer in drei Teile (davor, Trenner, danach). Das ist hier sicherer als split, weil der Body selbst auch Leerzeilen oder Doppelpunkte enthalten darf.
Schritt 4: JSON mit json (4.4)
- Syntax: Objekte
{"key": value}, Arrays[...], Strings nur in doppelten Anführungszeichen, Zahlen,true,false,null. Keine Kommentare, kein nachgestelltes Komma. - Typabbildung (type mapping): Objekt wird
dict, Arraylist, Stringstr, Zahlintoderfloat,true/falsewerdenTrue/False,nullwirdNone. In die Gegenrichtung werden Tupel zu Arrays. - Funktionen:
dumps(obj)Objekt zu String,loads(s)String zu Objekt,dump(obj, f)undload(f)arbeiten mit Dateien (Textmodus). - Optionen:
indent=2,sort_keys=True,ensure_ascii=Falsefür Umlaute im Klartext. - Fehler:
json.JSONDecodeError(Unterklasse vonValueError) bei ungültigem JSON,TypeErrorbei nicht serialisierbaren Objekten (z. B.set).
Die Fehler, die in Prüfungen vorkommen, siehst du am besten selbst:
Merke: Ein Tupel kommt als Liste zurück, ein int-Schlüssel als String, und dumps auf einen String gibt wieder einen String (mit Anführungszeichen). Das ist Übung 2.
Falle: die Prüfungs- und Praxisklassiker
sendmit einem String stattbytes:TypeError. Umwandeln mit.encode("utf-8").recv(n)liefert bis zunBytes, nicht genaun. Leere Bytes bedeuten: Verbindung geschlossen. Wer ohne Schleife liest, bekommt halbe Nachrichten.sendmuss nicht alles senden,sendallschon.json.loadsauf eindictist einTypeError,json.dumpsauf einen String erzeugt einen String mit Anführungszeichen.- JSON verändert Typen: Tupel kommen als Listen zurück,
int-Schlüssel als Strings.
Übungen
Übung 1: Rohe HTTP-Antwort zerlegen
Schreibe parse_antwort(raw). raw ist eine rohe HTTP-Antwort als bytes. Rückgabe: ein Tupel (status, header, body):
statusist der Statuscode alsint.headerist ein dict. Die Namen sind klein geschrieben, die Werte ohne umgebende Leerzeichen.bodyist der Rest nach der ersten Leerzeile alsstr(UTF-8). Er kann leer sein, Umlaute enthalten und selbst Leerzeilen haben.
Die Zeilen sind durch \r\n getrennt. Die Statuszeile hat die Form HTTP/1.1 404 Not Found (der Text hinter dem Code besteht aus mehreren Wörtern).
Zuerst Kopf und Body trennen: Wo darf der Schnitt sein, wenn der Body selbst Leerzeilen enthält? Dann die Statuszeile: Wie viele Teile willst du abspalten, damit “Not Found” nicht zerfällt? Bei den Headern: Ein Wert kann selbst einen Doppelpunkt enthalten (z. B. eine URL mit Port).
def parse_antwort(raw):
kopf, _, body = raw.partition(b"\r\n\r\n")
zeilen = kopf.decode("utf-8").split("\r\n")
status = int(zeilen[0].split(" ", 2)[1])
header = {}
for zeile in zeilen[1:]:
name, _, wert = zeile.partition(":")
header[name.strip().lower()] = wert.strip()
return status, header, body.decode("utf-8")
parse_antwortÜbung 2: JSON-Rundreise vorhersagen
Der Code steht fest. Du sagst vorher, welche Werte a, b und c danach haben:
import json
daten = {1: "a", "t": (1, 2), "n": None, "ok": True, "s": {"x": 1.0}}
a = json.loads(json.dumps(daten))
b = json.dumps("hi")
c = json.loads('{"w": [true, null, 2]}')Trage das Tupel (a, b, c) als Python-Wert ein.
Gehe die Schlüssel und Werte von daten einzeln durch: Was ändert sich auf dem Weg durch JSON bei dem int-Schlüssel, beim Tupel, bei None und True? Bei b: dumps bekommt einen String, was für ein Typ kommt heraus, und wie sieht er aus? Bei c ist der Text schon JSON, du gehst nur in die Python-Richtung.
antwort = (
{"1": "a", "t": [1, 2], "n": None, "ok": True, "s": {"x": 1.0}},
'"hi"',
{"w": [True, None, 2]},
)
antwortÜbung 3 (lokal): Zeilen über einen echten Socket lesen
Das läuft nicht im Browser. Die Aufgabe liegt in lernlabor/uebung/python/python_16_socket_zeilen.py, die Anleitung steht im Kopf der Datei. Der Server und ein Test-Client laufen in diesem einen Skript (Thread, 127.0.0.1, Port 50716). Du schreibst recv_zeile und bediene. Der Test-Client sendet absichtlich in Stücken, auch mitten in einem Umlaut. Starten:
cd lernlabor && uv run python uebung/python/python_16_socket_zeilen.pySolange die Funktionen nicht geschrieben sind, meldet das Skript “Noch nicht erfüllt”. Bei Erfolg steht “Aufgabe erfüllt.” in der Ausgabe.
Ein recv liefert, was gerade da ist, nicht eine fertige Nachricht. Wann weißt du, dass eine Zeile komplett ist, und was machst du mit Bytes, die schon zur nächsten Zeile gehören? Wann ist ein Umlaut decodierbar? Und woran erkennst du, dass die Gegenseite geschlossen hat?
def recv_zeile(conn, puffer):
while b"\n" not in puffer:
stueck = conn.recv(1024)
if not stueck: # Gegenseite hat geschlossen
return None
puffer.extend(stueck)
zeile, _, rest = bytes(puffer).partition(b"\n")
puffer[:] = rest # Überschuss für die nächste Zeile behalten
return zeile.decode("utf-8") # erst jetzt decodieren
def bediene(conn):
puffer = bytearray()
while True:
zeile = recv_zeile(conn, puffer)
if zeile is None:
break
conn.sendall(zeile.upper().encode("utf-8") + b"\n")Merksatz und Prüfstein
Merksatz: Sockets übertragen Bytes in einem Strom, recv(n) liefert bis zu n Bytes (leere Bytes heißen: geschlossen), eine HTTP-Antwort trennt Kopf und Body an der ersten Leerzeile (partition), und JSON ändert Typen (Tupel werden Listen, int-Schlüssel werden Strings).
Prüfstein (offene Frage): Ein Client liest eine HTTP-Antwort mit einem einzigen recv(4096). Manchmal fehlt hinten ein Stück vom Body, und bei Umlauten steht Unsinn im Text. Erkläre die Ursache, nenne zwei Änderungen an der Leseschleife und sage, woran der Client erkennt, dass der Kopf zu Ende ist.
Weiter mit Teil b: XML und REST-Client mit requests.
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 4: Netzwerkprogrammierung”, 4.1 bis 4.4 und “Typische Fallen in Block 4”. Blockgewicht und Fragenzahl laut Quelle (Stand 1. Oktober 2026, bitte prüfen). Die Beispiele mit partition für rohe Antworten und die lokalen Socket-Übung sind Übungskonstruktionen, nicht Prüfungsstoff der Quelle.