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 WebSocket. This article covers WebSocket.
How WebSocket works
WebSocket allows the client and the server to asynchronously, arbitrarily and in real-time exchange messages. By contrast, the HTTP request-response cycle is considered synchronous.
The WebSocket connection begins when the client sends a typical HTTP request to the server. The server responds with an HTTP response, using code 101 Switching Protocols
. On the same underlying TCP connection, the HTTP then changes to a bidirectional and binary protocol in which either side can send messages at any time.
The browser handles the connection in JavaScript using a WebSocket
object, which fires events when:
- a connection is established or closed
- a message is received
- an error occurs.
The same object is also used to send messages to the server, or to close the connection.
How this process works on the server side depends on the programming language and framework used.
But to achieve any semblance of efficiency and support more than a few concurrently connected clients, the server has to support long-lived connections - connections that are kept open the entire time that the client is connected. Typically, these are best achieved with asynchronous web frameworks, since they can handle multiple I/O operations simultaneously.
Message format
Once the web socket connection is established, messages can flow in any direction at any time, and the application is no longer bound to the HTTP request-response cycle. Consequentially, additional mechanisms are required 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 process 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 WebSocket 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. 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 WebSocket 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}`);
};
WebSocket identifies endpoints 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();
WebSocket on the server-side
The server-side code depends on the choice of the programming language and the web application framework. In this example, we're using Python with FastAPI and uvicorn to implement a simple real-time browser-based chat application.
The first endpoint, mapped to /
, serves some simple HTML and JavaScript that implement a simple browser-based WebSocket client. The other endpoint, mapped to /ws
, implements the server-side of the web socket connection.
Here's the code:
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 purpose 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 app.clients
, 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, install required dependencies with the command:
pip install fastapi asyncio uvicorn websockets
Next, start the server with the 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
WebSocket really unlocks the real-time potential of web applications, resulting in a faster, snappier and overall improved user experience. Today, WebSocket is well supported across various browsers, client- and server-side libraries and frameworks.
However, WebSocket deviates from the synchronous HTTP request-response communication model, and this has certain implications. The first is that in order to be efficient, WebSocket requires a web application stack that supports long-lived connections.
Second, since WebSocket doesn't work on top of HTTP, but rather next to it, it cannot take advantage of certain HTTP features, such as compression or HTTP/2 multiplexing, and they lack reconnection, 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.