Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.

Link zu der Vergleichsansicht

Beide Seiten, vorherige Überarbeitung Vorherige Überarbeitung
Nächste Überarbeitung
Vorherige Überarbeitung
talit:web:webapps:server [2025-04-10 05:13] hoftalit: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://localhost:3001/hello können wir auf die oben definierte API zugreifen. Mit dem Kommandozeilenaufruf `node app.js` wird der Node-Server gestartet. Auf der Adresse http://localhost:3001/hello können wir auf die oben definierte API zugreifen.
  
-Der Code bedeutet: Wann immer eine Anfrage für den Pfad `/hello` ankommt, so sende als _Response_ (_res_) den String `Hello World` zurück.+Der Code bedeutet: Wann immer eine _Request_ (_res_) für den Pfad `/hello` ankommt, so sende als _Response_ (_res_) den String `Hello World` zurück.
  
-Es ist auch möglich, Teile der im Request (_req_) verlangten Adresse als Parameter zu erhalten:+Es ist auch möglich, Teile des im Request verlangten Pfades als Parameter zu erhalten:
  
 <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, müssen wir alle Funktionen oder Klassen in `connect4.js` mit `export` markieren und in `app.js` importieren:+In `app.js` erstellen wir ein Spiel, z.B. mit `let game = newGame()`. Damit das funktioniert, müssen wir alle Funktionen oder Klassen in `connect4_server.js` mit `export` markieren und in `app.js` importieren:
  
 <code javascript connect4_server.js> <code javascript connect4_server.js>
 +/* Returns a fresh game. */
 export function newGame() { export function newGame() {
-  ...+  return { 
 +    "state": "waiting",  // or "waiting" or "won" or "tie" 
 +    "board":
 +      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, 
 +    ], 
 +    "next": 1,  // 1 or 2, the player whose turn it is, the winner if state is "won" 
 +    "player1": undefined, 
 +    "player2": undefined, 
 +  }
 } }
-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 app.js>
 /** Serve the game state of any valid game id. */ /** Serve the game state of any valid game id. */
 app.get('/:gameid/game', (req, res) => { app.get('/:gameid/game', (req, res) => {
Zeile 276: Zeile 292:
   return game;   return game;
 } }
- 
- 
- 
 </code> </code>
 +\\
  
 ### Sicherheit ### Sicherheit
 Nun können wir auch sicherstellen, dass nur ein berechtigter Spieler einen Stein setzen kann. In der `/:gameid/set/` Schnittstelle wird die Benutzer-Id ausgelesen und überprüft, ob der Spieler überhaupt im Spiel mitspielt, und ob er an der Reihe ist. Andernfalls wird ein HTTP Status-Code aus dem [[wp>List_of_HTTP_status_codes#4xx_client_errors|400er-Bereich]] zurückgegeben, z.B. `403` (_Forbidden_). Nun können wir auch sicherstellen, dass nur ein berechtigter Spieler einen Stein setzen kann. In der `/:gameid/set/` Schnittstelle wird die Benutzer-Id ausgelesen und überprüft, ob der Spieler überhaupt im Spiel mitspielt, und ob er an der Reihe ist. Andernfalls wird ein HTTP Status-Code aus dem [[wp>List_of_HTTP_status_codes#4xx_client_errors|400er-Bereich]] zurückgegeben, z.B. `403` (_Forbidden_).
  
-<code javascript>+<code javascript app.js>
 /** Make a play. Only allows the joined players to  */ /** Make a play. Only allows the joined players to  */
 app.get('/:gameid/set/:column', (req, res) => { app.get('/:gameid/set/:column', (req, res) => {
Zeile 303: Zeile 317:
 }) })
 </code> </code>
 +
 +<code javascript connect4_server.js>
 +/* Returns the playerid whose turn it is. */
 +export function getCurrentPlayer(game) {
 +  return game.next == 1 ? game.player1 : game.player2;
 +}
 +</code>
 +
 +\\
 ### Wer ist dran? ### Wer ist dran?
  
Zeile 322: Zeile 345:
 } }
 </code> </code>
- 
 ### Polling ### Polling
  
Zeile 354: Zeile 376:
 </code> </code>
  
 +### 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, wenn ausser der zwei Spieler noch zahlreiche Beobachter auf Änderungen warten.
 +  * 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**: Die Requests für den Spielzustand werden vom Server nicht sofort beantwortet, sondern erst, wenn sich der Zustand geändert hat.
 +  - **Web-Sockets**: Das Websockets-Protokoll erlaubt bidirektionale Verbindungen zwischen Client und Server. Über eine solche Verbindung werden Nachrichten über den Spielzustand ausgetauscht.
 +
 +Hier werden wir Long-Polling anschauen. Als erstes benötigen wir eine Datenstruktur, um die Requests für später zu speichern.
 +<code javascript app.js>
 +/** All long-poll requests by gameid. For each gameid, a list of 
 +  * [response, userid] entries is stored.  */
 +let longpolls = {};
 +</code>
 +
 +Dazu definieren wir eine neue Route `/<gameid>/longpoll`, die ganz ähnlich wie `/<gameid>/game` funktioniert:
 +
 +<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('/:gameid/longpoll', (req, res) => {
 +    const userid = getUserId(req, res);
 +    const game = games[parseInt(req.params['gameid'])]
 +    if (game == undefined) {
 +        res.status(404).json("no such game");
 +    } else {
 +        if (isWaiting(game, userid)) {
 +            console.log(`Game ${game.id} directly joined by ${userid}`)
 +            join(game, userid)
 +            res.json(toJson(game, userid))
 +            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, userid)) {
 +            console.log(`Stash long-poll request for ${game.id} by ${userid}`)
 +            longpolls[game.id] ??= []
 +            longpolls[game.id].push([res, userid])
 +            // do not end here but keep request hanging.
 +        } else {
 +            // User can act on the state, no point in waiting.
 +            res.json(toJson(game, userid))
 +            res.end()
 +        }
 +    }
 +})
 +</code>
 +
 +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 `"waiting"` ist oder wenn der User gerade nicht am Zug ist (User hat als letztes gespielt, oder User ist ein Beobachter):
 +
 +<code javascript connect4_server.js>
 +export function shouldBlockRequest(game, userid) {
 +  return game.state == "waiting" || game.state == "playing" && getCurrentPlayer(game) != userid;
 +}
 +</code>
 +
 +Nun benötigen wir noch die Logik, um die Long-Poll-Requests zu beantworten, sobald sich der Spielzustand geändert hat. Die Funktion wird aufgerufen, wenn ein Spiel gejoint wird, oder wenn ein Spielzug gemacht wird.
 +
 +<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]  // remove
 +    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, userid))
 +            res.end()
 +        }
 +    }
 +}
 +</code>
 +
 +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, status, url) {
 +  let response = await fetch(url);
 +  if (response.ok) {
 +    game = await response.json();
 +    // Update the HTML view.
 +    updateHtml(grid, game);
 +    // Update game status area to reflect winner / tie
 +    updateStatus(status, game);
 +  }
 +  if (!response.ok) {
 +    await wait(500) // wait a little if there is connection trouble
 +  }
 +  if (!response.ok || game.state == "waiting" || game.state == "playing" && !game.myturn) {
 +    await handleFetch(grid, status, `longpoll`);  // longpoll to wait for state change
 +  }
 +}
 +</code>
 +
 +Nun haben wir viel weniger Datenverkehr, der Server wird entlastet - dafür muss er sich mehr Informationen speichern. Ein Vorteil ist auch, dass Änderungen schneller beim long-polling Client ankommen.
  
 ### Hinweise ### Hinweise
  • talit/web/webapps/server.1744261985.txt.gz
  • Zuletzt geändert: 2025-04-10 05:13
  • von hof