Part 3 - Deploy to the web - websockets 10.1 documentation
Description: In the first and second parts of the tutorial, for local development, you ran a HTTP server on http://localhost:8000/ with: and a WebSocket server on ws://localhost:8001/ with: Now you want to depl...

Hide navigation sidebar
Hide table of contents sidebar
Toggle table of contents sidebar

# Part 3 - Deploy to the web

This is the third part of the tutorial.

- In the first part, you created a server and connected one browser; you could play if you shared the same browser.

- In this second part, you connected a second browser; you could play from different browsers on a local network.

- In this third part, you will deploy the game to the web; you can play from any browser connected to the Internet.

In the first and second parts of the tutorial, for local development, you ran a HTTP server on `http://localhost:8000/` with:

    $ python -m http.server

and a WebSocket server on `ws://localhost:8001/` with:

    $ python app.py

Now you want to deploy these servers on the Internet. There’s a vast range of hosting providers to choose from. For the sake of simplicity, we’ll rely on:

- GitHub Pages for the HTTP server;

- Heroku for the WebSocket server.

## Commit project to git

Perhaps you committed your work to git while you were progressing through the tutorial. If you didn’t, now is a good time, because GitHub and Heroku offer git-based deployment workflows.

Initialize a git repository:

    $ git init -b main
    Initialized empty Git repository in websockets-tutorial/.git/
    $ git commit --allow-empty -m "Initial commit."
    [main (root-commit) ...] Initial commit.

Add all files and commit:

    $ git add .
    $ git commit -m "Initial implementation of Connect Four game."
    [main ...] Initial implementation of Connect Four game.
     6 files changed, 500 insertions(+)
     create mode 100644 app.py
     create mode 100644 connect4.css
     create mode 100644 connect4.js
     create mode 100644 connect4.py
     create mode 100644 index.html
     create mode 100644 main.js

## Prepare the WebSocket server

Before you deploy the server, you must adapt it to meet requirements of Heroku’s runtime. This involves two small changes:

1. Heroku expects the server to listen on a specific port, provided in the `$PORT` environment variable.

2. Heroku sends a `SIGTERM` signal when shutting down a dyno, which should trigger a clean exit.

Adapt the `main()` coroutine accordingly:

    import os
    import signal

    async def main():
        # Set the stop condition when receiving SIGTERM.
        loop = asyncio.get_running_loop()
        stop = loop.create_future()
        loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)

        port = int(os.environ.get("PORT", "8001"))
        async with websockets.serve(handler, "", port):
            await stop

To catch the `SIGTERM` signal, `main()` creates a `Future` called `stop` and registers a signal handler that sets the result of this future. The value of the future doesn’t matter; it’s only for waiting for `SIGTERM`.

Then, by using `serve()` as a context manager and exiting the context when `stop` has a result, `main()` ensures that the server closes connections cleanly and exits on `SIGTERM`.

The app is now fully compatible with Heroku.

## Deploy the WebSocket server

Create a `requirements.txt` file with this content to install `websockets` when building the image:

    websockets

Heroku treats `requirements.txt` as a signal to detect a Python app.

That’s why you don’t need to declare that you need a Python runtime.

Create a `Procfile` file with this content to configure the command for running the server:

    web: python app.py

Commit your changes:

    $ git add .
    $ git commit -m "Deploy to Heroku."
    [main ...] Deploy to Heroku.
     3 files changed, 12 insertions(+), 2 deletions(-)
     create mode 100644 Procfile
     create mode 100644 requirements.txt

Follow the set-up instructions to install the Heroku CLI and to log in, if you haven’t done that yet.

Create a Heroku app. You must choose a unique name and replace `websockets-tutorial` by this name in the following command:

    $ heroku create websockets-tutorial
    Creating ⬢ websockets-tutorial... done
    https://websockets-tutorial.herokuapp.com/ | https://git.heroku.com/websockets-tutorial.git

If you reuse a name that someone else already uses, you will receive this error; if this happens, try another name:

    $ heroku create websockets-tutorial
    Creating ⬢ websockets-tutorial... !
     ▸    Name websockets-tutorial is already taken

Deploy by pushing the code to Heroku:

    $ git push heroku

    ... lots of output...

    remote:        Released v1
    remote:        https://websockets-tutorial.herokuapp.com/ deployed to Heroku
    remote:
    remote: Verifying deploy... done.
    To https://git.heroku.com/websockets-tutorial.git
     * [new branch]      main -> main

You can test the WebSocket server with the interactive client exactly like you did in the first part of the tutorial. Replace `websockets-tutorial` by the name of your app in the following command:

    $ python -m websockets wss://websockets-tutorial.herokuapp.com/
    Connected to wss://websockets-tutorial.herokuapp.com/.
    > {"type": "init"}
    < {"type": "init", "join": "54ICxFae_Ip7TJE2", "watch": "634w44TblL5Dbd9a"}
    Connection closed: 1000 (OK).

It works!

## Prepare the web application

Before you deploy the web application, perhaps you’re wondering how it will locate the WebSocket server? Indeed, at this point, its address is hard-coded in `main.js`:

    const websocket = new WebSocket("ws://localhost:8001/");

You can take this strategy one step further by checking the address of the HTTP server and determining the address of the WebSocket server accordingly.

Add this function to `main.js`; replace `aaugustin` by your GitHub username and `websockets-tutorial` by the name of your app on Heroku:

    function getWebSocketServer() {
      if (window.location.host === "aaugustin.github.io") {
        return "wss://websockets-tutorial.herokuapp.com/";
      } else if (window.location.host === "localhost:8000") {
        return "ws://localhost:8001/";
      } else {
        throw new Error(`Unsupported host: ${window.location.host}`);
      }
    }

Then, update the initialization to connect to this address instead:

    const websocket = new WebSocket(getWebSocketServer());

Commit your changes:

    $ git add .
    $ git commit -m "Configure WebSocket server address."
    [main ...] Configure WebSocket server address.
     1 file changed, 11 insertions(+), 1 deletion(-)

## Deploy the web application

Go to GitHub and create a new repository called `websockets-tutorial`.

Push your code to this repository. You must replace `aaugustin` by your GitHub username in the following command:

    $ git remote add origin git@github.com:aaugustin/websockets-tutorial.git
    $ git push -u origin main
    Enumerating objects: 11, done.
    Counting objects: 100% (11/11), done.
    Delta compression using up to 8 threads
    Compressing objects: 100% (10/10), done.
    Writing objects: 100% (11/11), 5.90 KiB | 2.95 MiB/s, done.
    Total 11 (delta 0), reused 0 (delta 0), pack-reused 0
    To github.com:<username>/websockets-tutorial.git
     * [new branch]      main -> main
    Branch 'main' set up to track remote branch 'main' from 'origin'.

Go back to GitHub, open the Settings tab of the repository and select Pages in the menu. Select the main branch as source and click Save. GitHub tells you that your site is published.

Follow the link and start a game!

## Summary

In this third part of the tutorial, you learned how to deploy a WebSocket application with Heroku.

You can start a Connect Four game, send the JOIN link to a friend, and play over the Internet!

Congratulations for completing the tutorial. Enjoy building real-time web applications with websockets!

## Solution

app.py

      1#!/usr/bin/env python
      2
      3import asyncio
      4import json
      5import os
      6import secrets
      7import signal
      8
      9import websockets
     10
     11from connect4 import PLAYER1, PLAYER2, Connect4
     12
     13
     14JOIN = {}
     15
     16WATCH = {}
     17
     18
     19async def error(websocket, message):
     20    """
     21    Send an error message.
     22
     23    """
     24    event = {
     25        "type": "error",
     26        "message": message,
     27    }
     28    await websocket.send(json.dumps(event))
     29
     30
     31async def replay(websocket, game):
     32    """
     33    Send previous moves.
     34
     35    """
     36    # Make a copy to avoid an exception if game.moves changes while iteration
     37    # is in progress. If a move is played while replay is running, moves will
     38    # be sent out of order but each move will be sent once and eventually the
     39    # UI will be consistent.
     40    for player, column, row in game.moves.copy():
     41        event = {
     42            "type": "play",
     43            "player": player,
     44            "column": column,
     45            "row": row,
     46        }
     47        await websocket.send(json.dumps(event))
     48
     49
     50async def play(websocket, game, player, connected):
     51    """
     52    Receive and process moves from a player.
     53
     54    """
     55    async for message in websocket:
     56        # Parse a "play" event from the UI.
     57        event = json.loads(message)
     58        assert event["type"] == "play"
     59        column = event["column"]
     60
     61        try:
     62            # Play the move.
     63            row = game.play(player, column)
     64        except RuntimeError as exc:
     65            # Send an "error" event if the move was illegal.
     66            await error(websocket, str(exc))
     67            continue
     68
     69        # Send a "play" event to update the UI.
     70        event = {
     71            "type": "play",
     72            "player": player,
     73            "column": column,
     74            "row": row,
     75        }
     76        websockets.broadcast(connected, json.dumps(event))
     77
     78        # If move is winning, send a "win" event.
     79        if game.winner is not None:
     80            event = {
     81                "type": "win",
     82                "player": game.winner,
     83            }
     84            websockets.broadcast(connected, json.dumps(event))
     85
     86
     87async def start(websocket):
     88    """
     89    Handle a connection from the first player: start a new game.
     90
     91    """
     92    # Initialize a Connect Four game, the set of WebSocket connections
     93    # receiving moves from this game, and secret access tokens.
     94    game = Connect4()
     95    connected = {websocket}
     96
     97    join_key = secrets.token_urlsafe(12)
     98    JOIN[join_key] = game, connected
     99
    100    watch_key = secrets.token_urlsafe(12)
    101    WATCH[watch_key] = game, connected
    102
    103    try:
    104        # Send the secret access tokens to the browser of the first player,
    105        # where they'll be used for building "join" and "watch" links.
    106        event = {
    107            "type": "init",
    108            "join": join_key,
    109            "watch": watch_key,
    110        }
    111        await websocket.send(json.dumps(event))
    112        # Receive and process moves from the first player.
    113        await play(websocket, game, PLAYER1, connected)
    114    finally:
    115        del JOIN[join_key]
    116        del WATCH[watch_key]
    117
    118
    119async def join(websocket, join_key):
    120    """
    121    Handle a connection from the second player: join an existing game.
    122
    123    """
    124    # Find the Connect Four game.
    125    try:
    126        game, connected = JOIN[join_key]
    127    except KeyError:
    128        await error(websocket, "Game not found.")
    129        return
    130
    131    # Register to receive moves from this game.
    132    connected.add(websocket)
    133    try:
    134        # Send the first move, in case the first player already played it.
    135        await replay(websocket, game)
    136        # Receive and process moves from the second player.
    137        await play(websocket, game, PLAYER2, connected)
    138    finally:
    139        connected.remove(websocket)
    140
    141
    142async def watch(websocket, watch_key):
    143    """
    144    Handle a connection from a spectator: watch an existing game.
    145
    146    """
    147    # Find the Connect Four game.
    148    try:
    149        game, connected = WATCH[watch_key]
    150    except KeyError:
    151        await error(websocket, "Game not found.")
    152        return
    153
    154    # Register to receive moves from this game.
    155    connected.add(websocket)
    156    try:
    157        # Send previous moves, in case the game already started.
    158        await replay(websocket, game)
    159        # Keep the connection open, but don't receive any messages.
    160        await websocket.wait_closed()
    161    finally:
    162        connected.remove(websocket)
    163
    164
    165async def handler(websocket, path):
    166    """
    167    Handle a connection and dispatch it according to who is connecting.
    168
    169    """
    170    # Receive and parse the "init" event from the UI.
    171    message = await websocket.recv()
    172    event = json.loads(message)
    173    assert event["type"] == "init"
    174
    175    if "join" in event:
    176        # Second player joins an existing game.
    177        await join(websocket, event["join"])
    178    elif "watch" in event:
    179        # Spectator watches an existing game.
    180        await watch(websocket, event["watch"])
    181    else:
    182        # First player starts a new game.
    183        await start(websocket)
    184
    185
    186async def main():
    187    # Set the stop condition when receiving SIGTERM.
    188    loop = asyncio.get_running_loop()
    189    stop = loop.create_future()
    190    loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
    191
    192    port = int(os.environ.get("PORT", "8001"))
    193    async with websockets.serve(handler, "", port):
    194        await stop
    195
    196
    197if __name__ == "__main__":
    198    asyncio.run(main())

index.html

     1<!DOCTYPE html>
     2<html lang="en">
     3  <head>
     4    <title>Connect Four</title>
     5  </head>
     6  <body>
     7    <div class="actions">
     8      <a class="action new" href="/">New</a>
     9      <a class="action join" href="">Join</a>
    10      <a class="action watch" href="">Watch</a>
    11    </div>
    12    <div class="board"></div>
    13    <script src="main.js" type="module"></script>
    14  </body>
    15</html>

main.js

     1import { createBoard, playMove } from "./connect4.js";
     2
     3function getWebSocketServer() {
     4  if (window.location.host === "aaugustin.github.io") {
     5    return "wss://websockets-tutorial.herokuapp.com/";
     6  } else if (window.location.host === "localhost:8000") {
     7    return "ws://localhost:8001/";
     8  } else {
     9    throw new Error(`Unsupported host: ${window.location.host}`);
    10  }
    11}
    12
    13function initGame(websocket) {
    14  websocket.addEventListener("open", () => {
    15    // Send an "init" event according to who is connecting.
    16    const params = new URLSearchParams(window.location.search);
    17    let event = { type: "init" };
    18    if (params.has("join")) {
    19      // Second player joins an existing game.
    20      event.join = params.get("join");
    21    } else if (params.has("watch")) {
    22      // Spectator watches an existing game.
    23      event.watch = params.get("watch");
    24    } else {
    25      // First player starts a new game.
    26    }
    27    websocket.send(JSON.stringify(event));
    28  });
    29}
    30
    31function showMessage(message) {
    32  window.setTimeout(() => window.alert(message), 50);
    33}
    34
    35function receiveMoves(board, websocket) {
    36  websocket.addEventListener("message", ({ data }) => {
    37    const event = JSON.parse(data);
    38    switch (event.type) {
    39      case "init":
    40        // Create links for inviting the second player and spectators.
    41        document.querySelector(".join").href = "?join=" + event.join;
    42        document.querySelector(".watch").href = "?watch=" + event.watch;
    43        break;
    44      case "play":
    45        // Update the UI with the move.
    46        playMove(board, event.player, event.column, event.row);
    47        break;
    48      case "win":
    49        showMessage(`Player ${event.player} wins!`);
    50        // No further messages are expected; close the WebSocket connection.
    51        websocket.close(1000);
    52        break;
    53      case "error":
    54        showMessage(event.message);
    55        break;
    56      default:
    57        throw new Error(`Unsupported event type: ${event.type}.`);
    58    }
    59  });
    60}
    61
    62function sendMoves(board, websocket) {
    63  // Don't send moves for a spectator watching a game.
    64  const params = new URLSearchParams(window.location.search);
    65  if (params.has("watch")) {
    66    return;
    67  }
    68
    69  // When clicking a column, send a "play" event for a move in that column.
    70  board.addEventListener("click", ({ target }) => {
    71    const column = target.dataset.column;
    72    // Ignore clicks outside a column.
    73    if (column === undefined) {
    74      return;
    75    }
    76    const event = {
    77      type: "play",
    78      column: parseInt(column, 10),
    79    };
    80    websocket.send(JSON.stringify(event));
    81  });
    82}
    83
    84window.addEventListener("DOMContentLoaded", () => {
    85  // Initialize the UI.
    86  const board = document.querySelector(".board");
    87  createBoard(board);
    88  // Open the WebSocket connection and register event handlers.
    89  const websocket = new WebSocket(getWebSocketServer());
    90  initGame(websocket);
    91  receiveMoves(board, websocket);
    92  sendMoves(board, websocket);
    93});

Procfile

    1web: python app.py

requirements.txt

    1websockets