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!