Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.
Beide Seiten, vorherige Überarbeitung Vorherige Überarbeitung Nächste Überarbeitung | Vorherige Überarbeitung | ||
talit:web:webapps:server [2025-04-10 05:12] – [Wer ist dran?] hof | talit:web:webapps:server [2025-04-13 10:59] (aktuell) – hof | ||
---|---|---|---|
Zeile 55: | Zeile 55: | ||
Mit dem Kommandozeilenaufruf `node app.js` wird der Node-Server gestartet. Auf der Adresse http:// | Mit dem Kommandozeilenaufruf `node app.js` wird der Node-Server gestartet. Auf der Adresse http:// | ||
- | Der Code bedeutet: Wann immer eine Anfrage | + | Der Code bedeutet: Wann immer eine _Request_ (_res_) |
- | Es ist auch möglich, Teile der im Request | + | Es ist auch möglich, Teile des im Request verlangten |
<code javascript> | <code javascript> | ||
Zeile 80: | Zeile 80: | ||
### Server-Side Vier-Gewinnt | ### Server-Side Vier-Gewinnt | ||
- | Erstelle eine Kopie deiner `connect4.js` Datei, um auf dem Server ein Spiel laufen zu lassen. | + | Erstelle eine Kopie deiner `connect4.js` Datei als `connect4_server.js`, um auf dem Server ein Spiel laufen zu lassen. |
Was muss sich ändern? | Was muss sich ändern? | ||
Zeile 87: | Zeile 87: | ||
- | In `app.js` erstellen wir ein Spiel, z.B. mit `let game = newGame()`. Damit das funktioniert, | + | In `app.js` erstellen wir ein Spiel, z.B. mit `let game = newGame()`. Damit das funktioniert, |
<code javascript connect4_server.js> | <code javascript connect4_server.js> | ||
+ | /* Returns a fresh game. */ | ||
export function newGame() { | export function newGame() { | ||
- | | + | |
+ | " | ||
+ | " | ||
+ | 0, 0, 0, 0, 0, 0, 0, | ||
+ | 0, 0, 0, 0, 0, 0, 0, | ||
+ | 0, 0, 0, 0, 0, 0, 0, | ||
+ | 0, 0, 0, 0, 0, 0, 0, | ||
+ | 0, 0, 0, 0, 0, 0, 0, | ||
+ | 0, 0, 0, 0, 0, 0, 0, | ||
+ | ], | ||
+ | " | ||
+ | " | ||
+ | " | ||
+ | } | ||
} | } | ||
- | export function dropPiece() { | + | |
+ | /* Drops a piece into given column. */ | ||
+ | export function dropPiece(game, column) { | ||
... | ... | ||
} | } | ||
Zeile 229: | Zeile 245: | ||
Falls ein Spieler direkt auf einen Spiel-URL geht (z.B. weil der erste Spieler den URL mit ihm geteilt hat), so muss das Spiel noch gejoint werden. Die Zustandsabfrage ändert sich also: | Falls ein Spieler direkt auf einen Spiel-URL geht (z.B. weil der erste Spieler den URL mit ihm geteilt hat), so muss das Spiel noch gejoint werden. Die Zustandsabfrage ändert sich also: | ||
- | <code javascript> | + | <code javascript |
/** Serve the game state of any valid game id. */ | /** Serve the game state of any valid game id. */ | ||
app.get('/: | app.get('/: | ||
Zeile 276: | Zeile 292: | ||
return game; | return game; | ||
} | } | ||
- | |||
- | |||
- | |||
</ | </ | ||
+ | \\ | ||
### Sicherheit | ### Sicherheit | ||
Nun können wir auch sicherstellen, | Nun können wir auch sicherstellen, | ||
- | <code javascript> | + | <code javascript |
/** Make a play. Only allows the joined players to */ | /** Make a play. Only allows the joined players to */ | ||
app.get('/: | app.get('/: | ||
Zeile 303: | Zeile 317: | ||
}) | }) | ||
</ | </ | ||
+ | |||
+ | <code javascript connect4_server.js> | ||
+ | /* Returns the playerid whose turn it is. */ | ||
+ | export function getCurrentPlayer(game) { | ||
+ | return game.next == 1 ? game.player1 : game.player2; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | \\ | ||
### Wer ist dran? | ### Wer ist dran? | ||
Zeile 322: | Zeile 345: | ||
} | } | ||
</ | </ | ||
- | |||
### Polling | ### Polling | ||
Zeile 329: | Zeile 351: | ||
Damit wir alle 500ms oder jede Sekunde den Serverzustand abfragen, bietet es sich an, wieder mit `await` zu arbeiten. Mit Promises können wir eine `async` Funktion bauen, die nebenläufig eine bestimme Zeitdauer wartet. Am Ende der `fetchUrl` Funktion rufen wir diese auf, bevor wir den Gamezustand mit einem Selbstaufruf pollen: | Damit wir alle 500ms oder jede Sekunde den Serverzustand abfragen, bietet es sich an, wieder mit `await` zu arbeiten. Mit Promises können wir eine `async` Funktion bauen, die nebenläufig eine bestimme Zeitdauer wartet. Am Ende der `fetchUrl` Funktion rufen wir diese auf, bevor wir den Gamezustand mit einem Selbstaufruf pollen: | ||
- | <code javascript> | + | <code javascript |
async function wait(ms) { | async function wait(ms) { | ||
// Schläft für eine Anzahl Millsekunden, | // Schläft für eine Anzahl Millsekunden, | ||
Zeile 353: | Zeile 375: | ||
} | } | ||
</ | </ | ||
+ | |||
+ | ### Long-Polling | ||
+ | Mit der obigen Lösung fragt jeder Client zweimal pro Sekunde nach einem Update. Polling hat zwei Nachteile: | ||
+ | * es wird relative viel Traffic erzeugt, insbesondere, | ||
+ | * trotzdem ist die Responsivität tief - im Mittel wartet jeder Client die Hälfte des eingestellten Delays, also 250 Millisekunden. Das ist für Vier-Gewinnt zwar ausreichend tief, aber nicht genug für Spiele mit Interaktivität. | ||
+ | |||
+ | Um das zu beheben gibt es zwei Lösungsmöglichkeiten: | ||
+ | - **Long-Polling**: | ||
+ | - **Web-Sockets**: | ||
+ | |||
+ | Hier werden wir Long-Polling anschauen. Als erstes benötigen wir eine Datenstruktur, | ||
+ | <code javascript app.js> | ||
+ | /** All long-poll requests by gameid. For each gameid, a list of | ||
+ | * [response, userid] entries is stored. | ||
+ | let longpolls = {}; | ||
+ | </ | ||
+ | |||
+ | Dazu definieren wir eine neue Route `/< | ||
+ | |||
+ | <code javascript app.js> | ||
+ | /** Serve the game state of any valid game id, but block until | ||
+ | * the client is no longer blocked (long-polling). */ | ||
+ | app.get('/: | ||
+ | const userid = getUserId(req, | ||
+ | const game = games[parseInt(req.params[' | ||
+ | if (game == undefined) { | ||
+ | res.status(404).json(" | ||
+ | } else { | ||
+ | if (isWaiting(game, | ||
+ | console.log(`Game ${game.id} directly joined by ${userid}`) | ||
+ | join(game, userid) | ||
+ | res.json(toJson(game, | ||
+ | res.end() | ||
+ | return | ||
+ | } | ||
+ | // Check if userid needs to wait, then either: | ||
+ | // - stash the [response, userid] pair for later | ||
+ | // - instantly return actionable state | ||
+ | if (shouldBlockRequest(game, | ||
+ | console.log(`Stash long-poll request for ${game.id} by ${userid}`) | ||
+ | longpolls[game.id] ??= [] | ||
+ | longpolls[game.id].push([res, | ||
+ | // do not end here but keep request hanging. | ||
+ | } else { | ||
+ | // User can act on the state, no point in waiting. | ||
+ | res.json(toJson(game, | ||
+ | res.end() | ||
+ | } | ||
+ | } | ||
+ | }) | ||
+ | </ | ||
+ | |||
+ | Im der server-side Game-Logik implementieren wir eine neue Funktion, um zu wissen, ob ein Request blockiert werden soll oder nicht - genau dann, wenn der Zustand `" | ||
+ | |||
+ | <code javascript connect4_server.js> | ||
+ | export function shouldBlockRequest(game, | ||
+ | return game.state == " | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Nun benötigen wir noch die Logik, um die Long-Poll-Requests zu beantworten, | ||
+ | |||
+ | <code javascript app.js> | ||
+ | /** Sends any pending long-polling responses after a state change. */ | ||
+ | function sendLongPollResponses(game) { | ||
+ | let polls = longpolls[game.id] | ||
+ | delete longpolls[game.id] | ||
+ | if (polls) { | ||
+ | for (let response of polls) { | ||
+ | // a response object is a list with the actual HTTP response and the userid. | ||
+ | let res = response[0] | ||
+ | let userid = response[1] | ||
+ | console.log(`End long-poll request for ${game.id} by ${userid}`) | ||
+ | res.json(toJson(game, | ||
+ | res.end() | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Was bleibt zu tun? Der Client soll nicht mehr warten, sondern bei Bedarf das `longpoll` API benützen: | ||
+ | |||
+ | <code javascript connect4_client.js> | ||
+ | async function handleFetch(grid, | ||
+ | let response = await fetch(url); | ||
+ | if (response.ok) { | ||
+ | game = await response.json(); | ||
+ | // Update the HTML view. | ||
+ | updateHtml(grid, | ||
+ | // Update game status area to reflect winner / tie | ||
+ | updateStatus(status, | ||
+ | } | ||
+ | if (!response.ok) { | ||
+ | await wait(500) // wait a little if there is connection trouble | ||
+ | } | ||
+ | if (!response.ok || game.state == " | ||
+ | await handleFetch(grid, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Nun haben wir viel weniger Datenverkehr, | ||
+ | |||
### Hinweise | ### Hinweise | ||