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:19] 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 345: Zeile 345:
 } }
 </code> </code>
- 
 ### Polling ### Polling
  
Zeile 377: 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.1744262399.txt.gz
  • Zuletzt geändert: 2025-04-10 05:19
  • von hof