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:12] – [Polling] 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 351: Zeile 374:
   }   }
 } }
-</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.1744261971.txt.gz
  • Zuletzt geändert: 2025-04-10 05:12
  • von hof