Einführung
Eine gewöhnliche HTTP-Anfrage wird geöffnet, empfängt eine Antwort und endet. Ein WebSocket wandelt diese erste HTTP-Anfrage in eine bidirektionale Verbindung um, die geöffnet bleibt. Dadurch kann ein Server eine Aktualisierung sofort senden, sobald sich etwas ändert. Chatnachrichten, kollaborative Cursor und Live-Bestellübersichten profitieren von diesem Echtzeitkanal.
Ein Durable Object stellt für jeden Raum einen eigenen Koordinationspunkt bereit. Der vorgeschaltete Worker wandelt einen validierten Raumnamen wie planning in eine stabile Objektidentität um. Das ausgewählte Objekt akzeptiert die WebSocket-Verbindungen dieses Raums, validiert jede eingehende Nachricht und überträgt nur genehmigte Aktualisierungen an die verbundenen Clients desselben Raums. Ein anderer Name wählt ein anderes Objekt aus. Daher kann support keinen Datenverkehr von planning empfangen.
In diesem Lab verwenden Sie bewusst die standardmäßige WebSocket-API und speichern die aktiven Sockets im Arbeitsspeicher. So wird das Verhalten von Verbindungen und Broadcasts sichtbar, bevor O06 WebSocket Hibernation und Connection Attachments einführt. SQLite speichert einen kleinen Nachrichtenverlauf, damit Sie nachweisen können, dass fehlerhafte Eingaben den persistenten Zustand nicht verändert haben. Der offene Socket selbst wird dadurch nicht persistent.
Sie implementieren das Protokoll, verbinden zwei bereitgestellte Clients mit einem Raum und einen dritten Client mit einem anderen Raum, beobachten einen gültigen Broadcast, weisen fehlerhafte Eingaben zurück, wiederholen den Test auf Cloudflare, untersuchen den Browser-Client und das Dashboard und entfernen anschließend die genau festgelegten temporären Ressourcen.
Jede neue VM benötigt eine eigene Wrangler-Autorisierung. Sie sollten stabile Durable-Object-Namen, Bindings, RPC und SQLite-basierten Zustand aus O01–O04 bereits verstehen. Das Setup installiert Node.js 22.22.0, das projektlokale Wrangler 4.132.0 und den ws-Testclient in /home/labex/project/room-broadcast. Es stellt den Browser- und die Testclients bereit, schreibt jedoch weder Ihren Worker, autorisiert Cloudflare noch führt es einen Deploy durch.
Die VM autorisieren und den Raumnamensraum deklarieren
In diesem Schritt autorisieren Sie die neue VM und deklarieren eine SQLite-gestützte Durable-Object-Klasse für Echtzeiträume.
Wechseln Sie in das vorbereitete Projekt, prüfen Sie die festgelegte Wrangler-Version und autorisieren Sie diese VM:
cd /home/labex/project/room-broadcast
npx wrangler --version
npx wrangler login --device --browser=false
Erwartet wird Wrangler 4.132.0. Öffnen Sie die angezeigte Cloudflare-URL im Browser, geben Sie den Kurzcode ein, bestätigen Sie das vorgesehene Lernkonto und autorisieren Sie es. Der Browser gewährt Wrangler Zugriff. Ihr Passwort wird dabei niemals an die VM gesendet.
Lesen Sie nur sichere Identitätsfelder aus, wählen Sie das bestätigte Konto aus und erzeugen Sie einen eindeutigen Namen für den temporären Worker:
WHOAMI="$(npx wrangler whoami --json)"
printf '%s\n' "$WHOAMI" | jq '{loggedIn, authType, accounts: [.accounts[] | {name}]}'
ACCOUNT_ID="$(printf '%s\n' "$WHOAMI" | jq -r '.accounts[] | select(.name == "LabEx Learning") | .id')"
test -n "$ACCOUNT_ID"
RUN="labex-c10-o05-$(openssl rand -hex 6)"
printf '%s\n' "$RUN" | tee .labex/run-name
Wenn Ihr dediziertes Lernkonto einen anderen Anzeigenamen hat, ersetzen Sie den Namen durch den von Ihnen bestätigten Namen. Schreiben Sie nun die Konfiguration:
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/index.js",
"compatibility_date": "2026-09-18",
"workers_dev": true,
"preview_urls": false,
"observability": { "enabled": true, "head_sampling_rate": 1 },
"durable_objects": { "bindings": [
{ "name": "ROOMS", "class_name": "RoomBroadcast" }
] },
"exports": {
"RoomBroadcast": { "type": "durable-object", "storage": "sqlite" }
}
}
JSON
Das Binding ROOMS ist der Zugriffspunkt des Workers auf den Klassennamensraum. getByName("planning") wählt immer denselben logischen Raum aus, während getByName("support") ein unabhängiges Objekt auswählt. Der Export gibt jedem ausgewählten Raum einen eigenen privaten SQLite-Speicher. Bis zum Deploy existiert keine Cloud-Ressource.
Das validierte WebSocket-Protokoll implementieren
In diesem Schritt definieren Sie einen kleinen Nachrichtenvertrag und implementieren das Raumobjekt, das WebSocket-Nachrichten akzeptiert und überträgt.
Die erste Anfrage muss Upgrade: websocket enthalten. Nach dem Upgrade werden Nachrichten als Frames und nicht als neue HTTP-Anfragen übertragen. Ein Client kann beliebigen Text innerhalb eines Frames senden. Daher ist das Parsen von JSON nur die erste Prüfung. Vor einer Änderung des persistenten Zustands muss die Validierung außerdem den erwarteten type, genau ein nichtleeres und begrenztes Feld text sowie keine unerwarteten Felder verlangen.
Erstellen Sie die gemeinsamen Protokoll-Hilfsfunktionen:
cat > src/protocol.js <<'JS'
const ROOM_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
export function parseRoomPath(pathname) {
const match = pathname.match(/^\/rooms\/([^/]+)\/(connect|state)$/);
if (!match || !ROOM_PATTERN.test(match[1])) return null;
return { room: match[1], action: match[2] };
}
export function parseClientMessage(raw) {
if (typeof raw !== "string" || raw.length > 512) return { ok: false };
let value;
try { value = JSON.parse(raw); } catch { return { ok: false }; }
if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false };
const keys = Object.keys(value).sort();
if (keys.length !== 2 || keys[0] !== "text" || keys[1] !== "type") return { ok: false };
if (value.type !== "update" || typeof value.text !== "string") return { ok: false };
const text = value.text.trim();
if (text.length < 1 || text.length > 80) return { ok: false };
return { ok: true, text };
}
JS
Erstellen Sie den vorgeschalteten Worker und die Durable-Object-Klasse:
cat > src/index.js <<'JS'
import { DurableObject } from "cloudflare:workers";
import { CLIENT_HTML } from "./client-html.js";
import { parseClientMessage, parseRoomPath } from "./protocol.js";
const json = (body, status = 200) => Response.json(body, { status });
export class RoomBroadcast extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
this.sessions = new Set();
this.ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`);
});
}
async fetch(request) {
if ((request.headers.get("Upgrade") || "").toLowerCase() !== "websocket") {
return json({ error: "websocket_upgrade_required" }, 426);
}
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
this.sessions.add(server);
server.addEventListener("message", event => this.receive(server, event.data));
const forget = () => this.sessions.delete(server);
server.addEventListener("close", forget);
server.addEventListener("error", forget);
server.send(JSON.stringify({ type: "ready" }));
return new Response(null, { status: 101, webSocket: client });
}
receive(sender, raw) {
const message = parseClientMessage(raw);
if (!message.ok) {
sender.send(JSON.stringify({
type: "error",
code: "invalid_message",
detail: "Send only {type: update, text: 1-80 characters}."
}));
return;
}
const createdAt = Date.now();
const row = this.ctx.storage.sql.exec(`
INSERT INTO messages (text, created_at)
VALUES (?, ?)
RETURNING sequence
`, message.text, createdAt).one();
const update = JSON.stringify({
type: "update",
sequence: row.sequence,
text: message.text,
createdAt
});
for (const socket of this.sessions) {
try { socket.send(update); } catch { this.sessions.delete(socket); }
}
console.log(JSON.stringify({ event: "room_update", sequence: row.sequence, connected: this.sessions.size }));
}
async getState() {
const messages = this.ctx.storage.sql.exec(`
SELECT sequence, text, created_at AS createdAt
FROM messages ORDER BY sequence
`).toArray();
return {
messageCount: messages.length,
latestSequence: messages.at(-1)?.sequence ?? 0,
messages
};
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/" && request.method === "GET") {
return new Response(CLIENT_HTML, { headers: { "content-type": "text/html; charset=utf-8" } });
}
const route = parseRoomPath(url.pathname);
if (!route) return json({ error: "not_found" }, 404);
if (route.action === "connect") {
if (request.method !== "GET" || (request.headers.get("Upgrade") || "").toLowerCase() !== "websocket") {
return json({ error: "websocket_upgrade_required" }, 426);
}
return env.ROOMS.getByName(route.room).fetch(request);
}
if (request.method !== "GET") return json({ error: "method_not_allowed" }, 405);
const state = await env.ROOMS.getByName(route.room).getState();
return json({ room: route.room, ...state });
}
};
JS
WebSocketPair erstellt die Client- und Server-Seite einer Verbindung. Wenn Sie die Client-Seite mit HTTP 101 zurückgeben, ist das Upgrade abgeschlossen. server.accept() startet den serverseitigen Standard-Socket. Das Set sessions im Arbeitsspeicher gehört absichtlich nur zu einer Instanz des Objekts. Der stabile Raumname verhindert, dass dieses Set raumübergreifend global wird.
Führen Sie die deterministischen Protokolltests aus und lassen Sie Wrangler einen Build ohne Deploy durchführen:
npm test
npx wrangler deploy --dry-run
Erwartet werden vier erfolgreiche Tests. Der Dry Run prüft das Worker-Modul und die Binding-Konfiguration. Die späteren Live-Schritte weisen das tatsächliche Socket-Verhalten nach.
Eine Aktualisierung innerhalb eines Raums übertragen
In diesem Schritt starten Sie den Worker lokal und weisen nach, dass eine Aktualisierung zwei Clients im selben Raum erreicht, aber keinen Client in einem anderen Raum.
Starten Sie Wrangler als Hintergrundprozess. Durch die Umleitung der Ausgabe bleibt das Terminal übersichtlich. Die gespeicherte Prozess-ID ermöglicht es Ihnen später, genau diesen Prozess zu beenden:
mkdir -p .labex/local-state
npx wrangler dev --local --ip 127.0.0.1 --port 8787 --persist-to .labex/local-state > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
for attempt in $(seq 1 30); do
curl --silent --fail http://127.0.0.1:8787/ >/dev/null && break
sleep 1
done
curl --silent --fail http://127.0.0.1:8787/ | grep -o '<title>[^<]*</title>'
Das bereitgestellte Clientprogramm öffnet drei echte WebSocket-Verbindungen: zwei für planning und eine für support. Es sendet eine Aktualisierung vom ersten planning-Client und wartet bei allen drei Clients auf begrenzte Nachweise:
node tools/room-clients.mjs http://127.0.0.1:8787 planning support broadcast | tee .labex/local-broadcast.json
Die Objekte sender und peer sollten dieselbe sequence: 1 und denselben Text enthalten. otherUpdates muss 0 sein. Der Abschnitt state zeigt unabhängig davon eine persistente Nachricht im Raum planning und keine Nachricht im Raum support. Damit werden beide Teile des Designs sichtbar: Der gemeinsame stabile Name verbindet die ersten beiden Clients, während der andere Name den dritten Client außerhalb der Broadcast-Grenze hält.
Eine fehlerhafte Nachricht vor einer Zustandsänderung zurückweisen
In diesem Schritt senden Sie einen Frame, der gültiges JSON, aber eine ungültige Anwendungseingabe enthält. Anschließend vergleichen Sie den persistenten Zustand vor und nach der Eingabe.
Das leere Feld text ist der entscheidende Unterschied: Das Parsen des JSON ist erfolgreich, aber das Raumprotokoll weist die Nachricht zurück. Führen Sie die zweite bereitgestellte Phase gegen dieselben lokalen Objekte aus:
node tools/room-clients.mjs http://127.0.0.1:8787 planning support invalid | tee .labex/local-invalid.json
Nur der sendende Client empfängt einen Fehler mit dem Code invalid_message; peerErrors bleibt 0. Die Verläufe before und after sind mit einer Nachricht identisch. Eine ungültige Nachricht kann daher weder eine Zeile hinzufügen, die Sequenz erhöhen noch einen Fehler in einen raumweiten Broadcast umwandeln.
Lesen Sie den Zustand beider Räume direkt aus:
curl --silent --fail http://127.0.0.1:8787/rooms/planning/state | jq
curl --silent --fail http://127.0.0.1:8787/rooms/support/state | jq
Die erste Antwort meldet eine Nachricht, die zweite keine. HTTP-Zustandsabfragen bleiben maßgeblich, auch wenn ein Client nach dem Test die Verbindung trennt.
WebSocket-Clients in der Cloud deployen und testen
In diesem Schritt beenden Sie die lokale Laufzeit, deployen denselben Code und wiederholen den Vertrag mit drei Clients über Cloudflare.
Beenden Sie nur den zuvor gespeicherten lokalen Prozess und führen Sie anschließend den Deploy durch:
kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
npx wrangler deploy | tee .labex/deploy.log
APP_URL="$(grep -Eo 'https://[^ ]+\.workers\.dev' .labex/deploy.log | tail -1)"
test -n "$APP_URL"
printf '%s\n' "$APP_URL" | tee .labex/app-url
Beim Deploy wird zunächst der Worker erstellt und der Namensraum RoomBroadcast abgeglichen. Eine erfolgreich geladene Startseite beweist allein noch nicht, dass die zustandsbehaftete Route bereit ist. Fragen Sie daher den Zustand eines harmlosen leeren Raums anhand des genauen JSON-Vertrags ab und warten Sie anschließend kurz, bis sich die Umgebung stabilisiert hat:
for attempt in $(seq 1 30); do
READY="$(curl --silent --show-error "$APP_URL/rooms/cloud-observer/state" || true)"
test "$(printf '%s' "$READY" | jq -r '.messageCount // -1' 2>/dev/null)" = 0 && break
sleep 2
done
test "$(printf '%s' "$READY" | jq -r .messageCount)" = 0
sleep 5
Führen Sie denselben echten WebSocket-Client gegen eindeutige Cloud-Räume aus:
node tools/room-clients.mjs "$APP_URL" cloud-planning cloud-support broadcast | tee .labex/cloud-broadcast.json
node tools/room-clients.mjs "$APP_URL" cloud-planning cloud-support invalid | tee .labex/cloud-invalid.json
Die Cloud-Ausgabe muss dasselbe Verhalten wie die lokale Entwicklung zeigen: Zwei planning-Clients empfangen die Sequenz 1, der support-Client empfängt keine Aktualisierung, und eine fehlerhafte Eingabe lässt den Verlauf unverändert.
Öffnen Sie die ausgegebene APP_URL im Browser. Wählen Sie Connect three clients und anschließend Send planning update. Die Clients A und B sollten dieselbe neue update anzeigen, während Client C nur seine ready-Nachricht anzeigt. Wählen Sie Send malformed update und prüfen Sie, dass der Fehler nur bei Client A erscheint. Wenn Sie die Ergebnisse ausreichend beobachtet haben, wählen Sie Disconnect clients und warten Sie, bis alle drei Karten Closed melden. Damit wird der WebSocket-Schließungs-Handshake abgeschlossen, bevor Sie die Seite verlassen. Diese Seite ist ein bereitgestellter Beobachtungsclient. Der Node-Probe und die Backend-Prüfungen bleiben der maßgebliche Nachweis für die Abnahme.
Den Browser-Client und das Durable Object untersuchen
In diesem Schritt verbinden Sie die Laufzeitnachweise mit dem Cloudflare-Dashboard und weisen nach, dass der persistente Raumverlauf nach einem unveränderten Redeploy erhalten bleibt.
Lassen Sie die Browser-Demo lange genug verbunden, um ihre drei Karten zu untersuchen. Die beiden Karten für planning zeigen sichtbar, dass ein Broadcast auf den Raum begrenzt ist. Die ruhige Karte für support ist ebenso wichtig, weil sie zeigt, was die Identitätsgrenze nicht überschritten hat.

Öffnen Sie im Cloudflare-Dashboard Workers & Pages, wählen Sie den exakten Namen aus .labex/run-name aus und untersuchen Sie die Bindings. ROOMS sollte auf RoomBroadcast verweisen. Öffnen Sie anschließend Durable Objects, wählen Sie den Namensraum <your-worker>_RoomBroadcast aus und bestätigen Sie unter Overview Storage: SQL.


Öffnen Sie im Namensraum die Registerkarte Logs. Wählen Sie eine aktuelle erfolgreiche Zeile aus, die zum Browser- oder Node-Probe gehört. Eine strukturierte Anwendungsmeldung room_update meldet die Sequenz und die aktuelle Anzahl verbundener Clients, speichert aber nicht den Nachrichtentext. Das Dashboard kann Logs verzögert bereitstellen. Laufzeitantworten und die unabhängigen Prüfungen bleiben maßgeblich.
Deployen Sie den unveränderten Code erneut. Offene WebSocket-Verbindungen sind ein aktiver Transport und müssen einen Deploy nicht überleben. Der SQLite-Verlauf gehört jedoch zum benannten Objekt und sollte erhalten bleiben:
npx wrangler deploy
APP_URL="$(cat .labex/app-url)"
curl --silent --fail "$APP_URL/rooms/cloud-planning/state" | jq
curl --silent --fail "$APP_URL/rooms/cloud-support/state" | jq
Der Raum planning meldet weiterhin eine Nachricht mit der Sequenz 1; support bleibt leer. Ihr generiertes Suffix, die Zeitstempel und die im Dashboard angezeigten Verkehrsstatistiken weichen von den getesteten Beispielen ab.
Den Raumnamensraum löschen
In diesem Schritt löschen Sie den exakt festgelegten temporären Durable-Object-Namensraum und den Worker. Anschließend bleibt die VM autorisiert, damit LabEx am Ende dieses Schritts prüfen kann, dass beide Ressourcen nicht mehr vorhanden sind.
Bestätigen Sie, dass der gespeicherte Name mit labex-c10-o05- beginnt. Erstellen Sie einen zustandslosen Cleanup-Einstiegspunkt:
RUN="$(cat .labex/run-name)"
case "$RUN" in labex-c10-o05-*) ;; *) echo "Unexpected Worker name" >&2; exit 1;; esac
cat > src/cleanup.js <<'JS'
export default {
fetch() {
return Response.json({ status: "cleanup" }, { status: 410 });
}
};
JS
Erstellen Sie eine Cleanup-Konfiguration für denselben Worker und dasselbe Konto. Der Tombstone state: "deleted" entfernt nur den Klassennamensraum dieses Labs einschließlich seiner temporären Verläufe:
ACCOUNT_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).account_id)')"
cat > wrangler.cleanup.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/cleanup.js",
"compatibility_date": "2026-09-18",
"workers_dev": true,
"preview_urls": false,
"exports": {
"RoomBroadcast": { "type": "durable-object", "state": "deleted" }
}
}
JSON
npx wrangler deploy --config wrangler.cleanup.jsonc
Die Reconciliation-Ausgabe sollte Deleted: RoomBroadcast melden. Löschen Sie den verbleibenden zustandslosen Worker. Wrangler bittet um Bestätigung, weil das Löschen nicht rückgängig gemacht werden kann. Bestätigen Sie erst, nachdem der angezeigte Name exakt mit Ihrem Wert von $RUN übereinstimmt:
npx wrangler delete --config wrangler.cleanup.jsonc
Geben Sie an der Eingabeaufforderung y ein und drücken Sie Enter. Der Befehl sollte mit Successfully deleted und anschließend dem generierten Worker-Namen enden.
Lassen Sie diese VM für die Prüfung am Ende dieses Schritts autorisiert. Bestätigen Sie, dass Wrangler weiterhin eine authentifizierte Sitzung meldet:
npx wrangler whoami --json | jq '{loggedIn, authType}'
Das JSON muss "loggedIn": true enthalten. LabEx kann nun das ausgewählte Konto abfragen und nachweisen, dass sowohl der Worker als auch sein Durable-Object-Namensraum nicht mehr vorhanden sind. Ein Netzwerk- oder Authentifizierungsfehler ist kein Nachweis für eine erfolgreiche Bereinigung.
Die Wrangler-Autorisierung dieser VM widerrufen
In diesem Schritt widerrufen Sie die OAuth-Autorisierung, die nach der Prüfung der Cloud-Ressourcen nur auf dieser neuen VM gespeichert ist.
wrangler logout entfernt die lokale Autorisierung. Die strukturierte Prüfung mit whoami --json ist wichtig, weil die gewöhnliche menschenlesbare Ausgabe mehrdeutig sein kann. Das Feld loggedIn ist das maßgebliche Ergebnis:
npx wrangler logout
npx wrangler whoami --json
Das abschließende JSON muss "loggedIn": false enthalten. Dadurch wird Ihr Cloudflare-Lernkonto im Browser weder gelöscht noch abgemeldet. Es verhindert lediglich, dass diese VM weitere authentifizierte Wrangler-Anfragen ausführt.
Zusammenfassung
Sie haben HTTP-Anfragen in WebSockets umgewandelt, validierte Raumnamen an unabhängige Durable Objects weitergeleitet, eine genehmigte Aktualisierung an zwei Clients desselben Raums übertragen und einen anderen Raum isoliert gehalten. Sie haben das JSON-Parsen von der Anwendungsvalidierung getrennt, nachgewiesen, dass fehlerhafte Eingaben weder den Broadcast-Zustand noch den SQLite-Verlauf verändern, das Verhalten auf Cloudflare wiederholt, die Browser- und Dashboard-Ansichten untersucht, den Verlauf nach einem Redeploy überprüft und den genau festgelegten temporären Namensraum entfernt.
Die wiederverwendbare Designregel lautet: Validieren Sie Eingaben, bevor Sie Zustand auswählen oder ändern, koordinieren Sie jede Echtzeitgruppe über eine eigene stabile Objektidentität und behandeln Sie aktive Verbindungen getrennt vom persistenten Anwendungsverlauf.



