Introduction

Real-time web applications are web applications that use technologies that provide instant or near-instant communication between the client, typically a browser, and the web server. Typical examples of real-time web applications include collaborative editing tools, chat applications, stock trading platforms, and online gaming platforms.

HTML5 introduced two major technologies that allow real-time communication on the web: Server-Sent Events and Web Sockets. In this article we cover Web Sockets.

Overview

Web sockets allow the client and the server to asynchronously, arbitrarily and in real-time exchange messages. Such exchange dynamic is in stark contrast to classic synchronous HTTP request-response cycle.

However, the web socket connection still starts with client sending a typical HTTP request to which the server responds with an HTTP response using code 101 Switching Protocols. On the same undelying TCP connection, the HTTP then changes to a bidirectional and binary protocol in which either side can send messages at any time.

In the browser, we handle the web socket connection in JavaScript using a WebSocket object which fires events when a connection is established or closed, a message is received or when an error occurs. The same object is also used to send messages to the server or close the connection.

The server-side depends on the programming language and the framework that are used. In the example below, we use Python, FastAPI framework and uvicorn application server. But to achieve any semblance of efficiency and support more than a few concurrently connected clients, the server has to support long-lived connections: those that are kept open all the time the client is connected. Typically these are best achieved with asynchronous web frameworks since they can handle multiple I/O operations simultaneously.

What is a Web Socket

Message format

Once the web socket connection is established, messages can flow in any direction at any time and we are no longer bound to the HTTP request-response cycle. Consequently, we need additional mechanisms to support such exchanges.

Web sockets achieve this through a message-oriented API where the sender provides a payload in either text or binary, and the receiver is notified only after the entire message has been received. This is achieved with a custom binary framing format that divides each application message into one or more frames, which are then transported to the destination, reassembled, and ultimately delivered to the receiver as a complete message; the exact framing rules and the rest of the Web Socket protocol specification can be accessed in the RFC 6455.

Developers do not have to manage the framing on their own since the Websocket object, or a dedicated client library, if used, handles the framing – and the same is true on the server-side. All we have to do is define application-level messages and react appropriately to them.

Web Sockets on the client-side

If the client is a web browser, we use JavaScript to connect to the server, send and receive messages and get notified about relevant events. The following example shows how to connect to the web socket server, and define handlers that fire when the connection is established or closed, when an error occurs, or when a message is received.

// connect to server on given address
const ws = new WebSocket(`ws://example.com/web-socket-stream`);

// invoked after successfully established connection
ws.onopen = function(event) {
    console.log(`Successfully connected to server.`);
};

// invoked after the connection is closed
ws.onclose = function(event) {
    console.log(`Connection to server was closed.`);
};

// invoked if something goes wrong
ws.onerror = function(event) {
    console.log(`Oh no! Something went wrong.`);
};

// invoked after receiving a message from the server
ws.onmessage = function(event) {
    console.log(`Received message ${event.data}`);
};

Web sockets identify end-points with uniform resource identifiers (URI) that begin either with ws:// or wss://. Like http:// and https:// in HTTP, the first denotes an insecure and the other the secure variant, that is, a web socket connection secured with TLS.

To send a message using the WebSocket object, all we need to do is call the send() method. And similarly, to disconnect we call close().

// send a message
ws.send("Hello World!");

// disconnect from server
ws.close();

Web Sockets on the server-side

The server-side code will obviously depend on the choice of the programming language and the web application framework. Here we are using Python with FastAPI and uvicorn to implement a simple real-time browser-based chat application.

The first end-point, mapped to /, serves some simple HTML and JavaScript that implement a simple browser-based WebSocket client. The other end-point, mapped to /ws, implements the server-side of the web socket connection.

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
import time


html = """
<!DOCTYPE html>
<html lang="en">
<title>Chatting with WebSockets</title>
<style>
    input, textarea, button {
        width: 100%;
        max-width: 300px;
        box-sizing: border-box;
    }
</style>
<h1>Chat</h1>
<input type="text" placeholder="Your name" value="Anonymous" id="name" /><br>
<textarea id="message" autocomplete="off" autofocus rows="4"></textarea><br>
<button id="button">Send</button>
<ul id='messages'></ul>
<script>
    document.addEventListener("DOMContentLoaded", initialize);
    function initialize() {
        const ws = new WebSocket(`ws://localhost:8000/ws`);
        ws.onmessage = function(event) {
            const messages = document.getElementById('messages');
            const message = document.createElement('li');
            const msg = JSON.parse(event.data);
            message.innerText = `[${msg.time}] ${msg.name}: ${msg.message}`;
            messages.appendChild(message)
        };
        document.querySelector("#button").onclick = event => {
            const nameInput = document.querySelector("#name");
            const messageInput = document.querySelector("#message");
            ws.send(JSON.stringify({
                name: nameInput.value,
                message: messageInput.value
            }));
            messageInput.value = ""
            messageInput.focus();
        };
    }
</script>
"""

app = FastAPI()
app.clients = [] # list of connected websocket clients


@app.get("/")
async def root():
    return HTMLResponse(html)


@app.websocket("/ws")
async def chat(websocket: WebSocket):
    await websocket.accept() # accept client connection
    app.clients.append(websocket) # add the client to the list of clients
    try:
        while True:
            message = await websocket.receive_json() # receive a message
            message["time"] = time.strftime("%H:%M:%S", time.localtime()) # add server's time
            for client in app.clients: # forward the message to all connected clients
                await client.send_json(message)
    except WebSocketDisconnect:
        app.clients.remove(websocket) # when disconnected, remove the client from the list

The essence of the chat(websocket) handler is as follows. When a client connects, the HTTP connection is automatically upgraded to a web socket connection. Next we add the reference to the client’s web socket (variable websocket) to a global list of all such connections, app.clients, that is, a list of currently connected clients.

Then we wait for the client to send a message. When it arrives, we add a timestamp and forward the message to all connected clients, including the one that sent it. Then we repeat the wait-for-message-and-resend cycle until the client remains connected. When the client disconnects, we remove the socket from the list of active clients.

To run the example, we first have to install required dependencies.

pip install fastapi asyncio uvicorn websockets

Next, we start the server with the following command.

uvicorn main:app --port 8000

Now, open the browser and navigate to http://localhost:8000. You should be presented with a simple application that you can use to send and receive messages. If you open another tab of the same address, you’ll be able to communicate between tabs like in an actual chatroom. By default your username will be Anonymous, but you can change it.

Conclusion

Web sockets really unlock the real-time potential of web applications which results in a faster, snappier and overall improved user experience. Today, web sockets are well supported across various browsers, client- and server-side libraries and frameworks.

However, web sockets deviate from the synchronous HTTP request-response communication model, and this has certain implications. The first is that in order to be efficient, web sockets require an appropriate web application stack, namely one that supports long-lived connections.

Second, since web sockets don’t work on top of HTTP, but rather next to it, they cannot take advantage of certain HTTP features, such as compression or HTTP/2 multiplexing, and they lack re-connection, authentication and similar high-level mechanisms. These have to be provided by developers themselves either by implementing them manually or importing them from a 3rd party library.

Glossary

WebSocket

A protocol for real-time communication through a sustained TCP connection.