Writing WebSocket client applications
In this guide we'll walk through the implementation of a WebSocket-based ping application. In this application, the client sends a "ping" message to the server every second, and the server responds with a "pong" message. The client listens for "pong" messages and logs them, keeping track of how many message exchanges there have been.
Although this is a pretty minimal application, it covers the fundamental points involved in writing a WebSocket client.
You can find the complete example at https://github.com/mdn/dom-examples/tree/main/websockets. The server side is written in Deno, so you'll have to install that first if you want to run the example locally.
Creating a WebSocket
object
To communicate using the WebSocket protocol, you need to create a WebSocket
object. As soon as you create this object, it will start trying to connect to the specified server.
const wsUri = "ws://127.0.0.1/";
const websocket = new WebSocket(wsUri);
The WebSocket
constructor takes one mandatory argument — the URL of the WebSocket server to connect to. In this case, since we're running the server locally, we're using the localhost address.
Note:
In this example we're using the ws
protocol for the connection, because in the example we're connecting to localhost. In a real application, web pages should be served using HTTPS, and the WebSocket connection should use wss
as the protocol.
The constructor takes another optional argument protocols
, which allows a single server to implement multiple sub-protocols. We're not using this feature in our example.
The constructor will throw a SecurityError
if the destination doesn't allow access.
This may happen if you attempt to use an insecure connection (most user agents now require a secure link for all WebSocket connections unless they're on the same device or possibly on the same network).
Listening for the open
event
Creating a WebSocket
instance starts the process of establishing a connection to the server. Once the connection is established, the open
event is fired, and after this point the socket is able to transmit data.
In the example code below, when the open
event is fired, we start sending one "ping" message to the server every second, using the Window.setInterval()
API:
websocket.addEventListener("open", () => {
log("CONNECTED");
pingInterval = setInterval(() => {
log(`SENT: ping: ${counter}`);
websocket.send("ping");
}, 1000);
});
Listening for errors
If an error occurs while the connection is being established or at any time after it is established, the error
event will be fired.
Our application doesn't do anything special on error, but we do log it:
websocket.addEventListener("error", (e) => {
log(`ERROR`);
});
On an error, the connection is closed and the close
event will be fired.
Sending messages
We've already seen that once the connection is established, we can use the send()
method to send messages to the server:
websocket.addEventListener("open", () => {
log("CONNECTED");
pingInterval = setInterval(() => {
log(`SENT: ping: ${counter}`);
websocket.send("ping");
}, 1000);
});
In our example we send text, but you can also send binary data as a Blob
, ArrayBuffer
, TypedArray
, or DataView
.
A common approach is to use JSON to send serialized JavaScript objects as text. For example, instead of just sending the text message "ping", our client could send a serialized object including the number of messages exchanged so far:
const message = {
iteration: counter,
content: "ping",
};
websocket.send(JSON.stringify(message));
The send()
method is asynchronous: it does not wait for the data to be transmitted before returning to the caller. It just adds the data to its internal buffer and begins the process of transmission. The WebSocket.bufferedAmount
property represents the number of bytes that have not yet been transmitted. Note that the WebSockets protocol uses UTF-8 to encode text, so bufferedAmount
is calculated based on the UTF-8 encoding of any buffered text data.
Receiving messages
To receive messages from the server, we listen for the message
event.
Our message event handler logs the received message, and increments our count of the number of message exchanges that have occurred:
websocket.addEventListener("message", (e) => {
log(`RECEIVED: ${e.data}: ${counter}`);
counter++;
});
The server can also send binary data, which is exposed to clients as a Blob
or an ArrayBuffer
, based on the value of the WebSocket.binaryType
property.
As we saw for sending messages, the server can also send JSON strings, which the client can then parse into an object:
websocket.addEventListener("message", (e) => {
const message = JSON.parse(e.data);
log(`RECEIVED: ${message.iteration}: ${message.content}`);
counter++;
});
Handling disconnect
When the connection is closed, because either the client or the server closed it or because an error occurred, the close
event will be fired.
Our application listens for the close
event and cleans up the interval timer when it is fired:
websocket.addEventListener("close", () => {
log("DISCONNECTED");
clearInterval(pingInterval);
});
Working with the bfcache
The back/forward cache, or bfcache, enables much faster back and forward navigation between pages that the user has recently visited. It does this by storing a complete snapshot of the page, including the JavaScript heap.
The browser pauses and then resumes JavaScript execution when a page is added to or restored from the bfcache. This means that, depending on what the page is doing, it's not always safe for the browser to use the bfcache for the page. If the browser determines that it is not safe, the page will not be added to the bfcache, and the user will not get the performance benefit that it can bring.
Different browsers use different criteria for adding a page to the bfcache, and having an open WebSocket connection may prevent the browser adding your page to the bfcache. This means it's good practice to close your connection when the user has finished with your page. The best event to use for this is the pagehide
event.
We do this in our example app:
window.addEventListener("pagehide", () => {
if (websocket) {
log("CLOSING");
websocket.close();
websocket = null;
window.clearInterval(pingInterval);
}
});
Conversely, by listening for the pageshow
event, you can seamlessly start the connection again when the page is restored from the bfcache. Since the pageshow
event also fires on page load, it can also be used to start the WebSocket connection when the page is first loaded:
let websocket = null;
window.addEventListener("pageshow", () => {
log("OPENING");
websocket = new WebSocket(wsUri);
websocket.addEventListener("open", () => {
log("CONNECTED");
pingInterval = setInterval(() => {
log(`SENT: ping: ${counter}`);
websocket.send("ping");
}, 1000);
});
websocket.addEventListener("close", () => {
log("DISCONNECTED");
clearInterval(pingInterval);
});
websocket.addEventListener("message", (e) => {
log(`RECEIVED: ${e.data}: ${counter}`);
counter++;
});
websocket.addEventListener("error", (e) => {
log(`ERROR: ${e.data}`);
});
});
If you run our example, try navigating to a different page, then back to the example. In Chrome, you should see that the example starts the connection again, and keeps its original context: so, for example, it remembers the count of exchanged messages.
See the web.dev article on the bfcache for more context on bfcache compatibility and the WebSockets API.
On browsers that support it, you can use the notRestoredReasons
property of the Performance API to get the reason a page was not added to the bfcache.
Security considerations
WebSockets should not be used in a mixed content environment; that is, you shouldn't open a non-secure WebSocket connection from a page loaded using HTTPS or vice versa. Most browsers now only allow secure WebSocket connections, and no longer support using them in insecure contexts.