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, Array list, String str, Zahl int oder float, true/false werden True/False, null wird None. In die Gegenrichtung werden Tupel zu Arrays.
  • Funktionen: dumps(obj) Objekt zu String, loads(s) String zu Objekt, dump(obj, f) und load(f) arbeiten mit Dateien (Textmodus).
  • Optionen: indent=2, sort_keys=True, ensure_ascii=False für Umlaute im Klartext.
  • Fehler: json.JSONDecodeError (Unterklasse von ValueError) bei ungültigem JSON, TypeError bei 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

  1. send mit einem String statt bytes: TypeError. Umwandeln mit .encode("utf-8").
  2. recv(n) liefert bis zu n Bytes, nicht genau n. Leere Bytes bedeuten: Verbindung geschlossen. Wer ohne Schleife liest, bekommt halbe Nachrichten.
  3. send muss nicht alles senden, sendall schon.
  4. json.loads auf ein dict ist ein TypeError, json.dumps auf einen String erzeugt einen String mit Anführungszeichen.
  5. 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):

  • status ist der Statuscode als int.
  • header ist ein dict. Die Namen sind klein geschrieben, die Werte ohne umgebende Leerzeichen.
  • body ist der Rest nach der ersten Leerzeile als str (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.py

Solange 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.