# WebSockets

Twisted Web provides support for the WebSocket protocol for clients (such as web browsers) to connect to resources and receive bi-directional communication with the server.

For the purposes of our example here, you will need to have some familiarity with the concepts covered in serving static content and rpy scripts, because we will be using those to construct our resource hierarchy.

Note

In order to use the websocket support in Twisted, you will need the `websocket` optional dependency, so install Twisted with either `pip install twisted[websocket]` or install one that includes it, such as `twisted[all_non_platform]` or `twisted[all]` .

## WebSocket Server

Let’s use Twisted to create a simple websocket server, and then build a web-browser based client to communicate with it.

To begin with, we will need a folder with 3 files in it. First, let’s do the Twisted part. We need a `twisted.web.websocket.WebSocketResource` to be served at a known URL, so let’s put one into a `.rpy` file called `websocket-server.rpy` :

`websocket-server.rpy`

    from __future__ import annotations
    from twisted.python.failure import Failure
    from twisted.web.iweb import IRequest
    from twisted.web.resource import Resource
    from twisted.web.static import File
    from twisted.web.websocket import WebSocketResource, WebSocketTransport
    from twisted.internet.task import LoopingCall

    class WebSocketDemo:
        loop: LoopingCall | None = None

        @classmethod
        def buildProtocol(cls, request: IRequest) -> WebSocketDemo:
            return cls()

        def negotiationStarted(self, transport: WebSocketTransport) -> None:
            self.transport = transport
            self.counter = 0

        def negotiationFinished(self) -> None:
            def heartbeat() -> None:
                self.counter += 1
                self.transport.sendTextMessage(f"heartbeat {self.counter}")

            self.loop = LoopingCall(heartbeat)
            self.loop.start(1.0)

        def textMessageReceived(self, data: str) -> None:
            self.transport.sendTextMessage(f"reply to {data}")

        def connectionLost(self, reason: Failure) -> None:
            if self.loop is not None:
                self.loop.stop()

        # Since WebSocketProtocol is a typing.Protocol and not a class, we must
        # provide implementations for all events, even those we don't care about.
        def bytesMessageReceived(self, data: bytes) -> None: ...
        def pongReceived(self, payload: bytes) -> None: ...

    resource = WebSocketResource(WebSocketDemo)

Note that by using a `@classmethod` for `buildProtocol`, the type of `WebSocketDemo` complies with the `twisted.web.websocket.WebSocketServerFactory` `protocol`, returning a `WebSocketDemo` that complies with `twisted.web.websocket.WebSocketProtocol` ; we can then pass the type of `WebSocketDemo` itself, without instantiating it, to `twisted.web.websocket.WebSocketResource`. We implement `negotiationFinished`, the method called once the websocket connection is fully set up, to begin sending a text message to our peer once per second.

Then, we will need an index page for our live websocket site, with a button on it that hooks up a JavaScript function to connect to `/websocket-server.rpy`:

`index.html`

    <!DOCTYPE html>
    <html>
     <head>
      <title>
       web sockets
      </title>
      <script src="websocket-browser-client.js" charset="utf-8">
      </script>
      <style>
      #console {
       border: 1px solid red;
       padding: 1em;
      }
      </style>
     </head>
     <body>
      <button onclick="doConnect()">connect websocket</button>
      <div id="console">
      </div>
     </body>
    </html>

Finally, we need our JavaScript source code that actually does the connecting of various events. Learning how to program in JavaScript, or even the entire JavaScript WebSocket API, is a bit outside the scope of this tutorial. You can read more about WebSocket JavaScript API at the MDN WebSocket documentation. Hopefully this minimal example is clear:

`websocket-browser-client.js`

    function recordEvent(evtType, evt) {
     const console = document.getElementById("console");
     const div = document.createElement("div");
     console.append(div);
     div.append(document.createTextNode(evtType + ": «" + evt + "»"));
    }

    function doConnect() {
     webSocket = new WebSocket("ws://localhost:8080/websocket-server.rpy");
     webSocket.onopen = (event) => {
      console.log("opened");
      webSocket.send("hello world");
      recordEvent("socket opened", JSON.stringify(event));
     };
     webSocket.onmessage = (event) => {
      recordEvent("message received", event.data)
     };
     webSocket.onerror = (event) => {
      recordEvent("error", JSON.stringify(event));
     };
     webSocket.onclose = (event) => {
      recordEvent("close", JSON.stringify(event));
     };
    }

We define a function, `doConnect`, that the button in our HTML example above will call, which:

1. creates a new websocket that will connect to the URL `ws://localhost:8080/websocket-server.rpy` .

2. adds an event handler to that websocket for when it connects, to send a message to the server,

3. and also adds event handlers for when the connection receives a text message, an error, or a closure from the peer, to display those events to the user.

If you were to put these all into a folder called `my-websocket-demo` , you can use `twist` to serve them, via `twist web --path ./my-websocket-demo`. Then, you can open up a web browser, pointed at `http://localhost:8080/` and see a “connect websocket” button. Click the button, and you should see the web page populate with text like this:

    socket opened: «{"isTrusted":true}»
    message received: «heartbeat 1»
    message received: «reply to hello world»
    message received: «heartbeat 2»
    message received: «heartbeat 3»
    message received: «heartbeat 4»

And that’s all you need to implement a websocket server with Twisted!

Since `twisted.web.websocket.WebSocketResource` is a standard Twisted `resource`, you can integrate it with things like authentication or sessions, just as you would any other resource.

## WebSocket Client

Of course, if we have a server, we may also want to talk to it from our Twisted applications. To do that, let’s build a simple websocket client, with `twisted.web.websocket.WebSocketClientEndpoint`. This client could talk to any WebSocket server, regardless of how it was implemented, but since we just built one with Twisted, we’ll use that one. It looks much the same as the server, but, we will just print out each data message we receive.

Note

In this example, we use the `ws://` protocol which indicates a plain-text websocket connection. Obtaining a valid HTTPS certificate for your local example goes a bit beyond the scope of this tutorial, but you can test using the `wss://` protocol, as well as testing against a non-Twisted server, by substituting the URL `"wss://echo.websocket.org/"` in the client demo. Make sure to also install the `[tls]` optional dependency group to install the required dependencies for Twisted’s HTTPS client.

`websocket-client.py`

    from __future__ import annotations

    from typing import Any

    from twisted.internet.defer import Failure
    from twisted.internet.task import LoopingCall, deferLater, react
    from twisted.web.websocket import WebSocketClientEndpoint, WebSocketTransport

    class WebSocketClientDemo:
        @classmethod
        def buildProtocol(cls, uri: str) -> WebSocketClientDemo:
            return cls()

        def textMessageReceived(self, data: str) -> None:
            print(f"received text: {data!r}")

        def negotiationStarted(self, transport: WebSocketTransport) -> None:
            self.transport = transport

        def negotiationFinished(self) -> None:
            self.transport.sendTextMessage("hello, world!")

        def bytesMessageReceived(self, data: bytes) -> None:
            ...

        def connectionLost(self, reason: Failure) -> None:
            ...

        def pongReceived(self, payload: bytes) -> None:
            ...

    async def main(reactor: Any) -> None:
        endpoint = WebSocketClientEndpoint.new(
            reactor, "ws://localhost:8080/websocket-server.rpy"
        )
        print("connecting...")
        await endpoint.connect(WebSocketClientDemo)
        print("connected!")
        await deferLater(reactor, 10)

    react(main)

If you leave the same server running, the one you just tested with your browser, and then run this example, you should see something like this:

    connecting...
    connected!
    received text: 'heartbeat 1'
    received text: 'reply to hello, world!'
    received text: 'heartbeat 2'
    received text: 'heartbeat 3'
    received text: 'heartbeat 4'
    received text: 'heartbeat 5'
    received text: 'heartbeat 6'
    received text: 'heartbeat 7'
    received text: 'heartbeat 8'
    received text: 'heartbeat 9'
    received text: 'heartbeat 10'
    received text: 'heartbeat 11'

And now you have a functioning WebSocket server and client using Twisted!