Closing a Connection
A WebSocket stays open until one side closes it. Either side can.
Closing normally
The client usually closes. When Marcus closes the tab, goes to another page, or reloads the page, the browser closes the page’s WebSockets on its own. Our code does not have to do anything. The page’s code can also close a WebSocket while the page stays open, by calling socket.close(). It would do that, for example, if Marcus switched from course 7 to course 8 without reloading the page: the client closes the WebSocket for course 7 and opens one for course 8.
The server can close a connection too. For example, it closes all of its connections before it restarts for a new release.
Closing is a short exchange of frames, called the closing handshake. The side that closes sends a close frame, a small frame with a status code that says why. Code 1000 means a normal close, and code 1001 means “going away”, which is what a browser sends when the user leaves the page. The other side answers with a close frame of its own. Then the connection is shut.
What happens next on each side:
- The server runs the code we registered for a closed connection. It removes the connection from the set of each of its rooms, so
sendToRoomno longer sends on it. - The client, if the page is still open, gets a
closeevent from the browser. We will see shortly what our client does with it.
When nobody closes it
Sometimes a connection stops working and nobody sends a close frame. For example, Marcus’s laptop goes to sleep with Courselore open, the browser crashes, or the network drops. While the laptop sleeps, nothing on it receives or answers anything. To the server, Marcus’s connection still looks open, and it is still in the sets of Marcus’s four rooms. The server sends messages on it, and they reach nobody. The connection keeps its resources (such as memory) until the server finds out.
Pings and pongs
The WebSocket protocol has a way to find out if a connection is still alive. Besides close frames, it defines two more small frames, a ping and a pong. When one side receives a ping, it must answer with a pong.
Each side does a different part of the work:
- The browser answers every ping with a pong on its own. The page’s code never sees the pings and cannot send one.
- The server’s WebSocket library can send a ping on a connection and tells our code when a pong comes back.
- Our server code decides when to send pings and what to do when no pong comes back.
Here is that code, simplified:
// Every 30 seconds, check every open connection
setInterval(() => {
for (const socket of openConnections) {
if (!socket.answeredLastPing) {
socket.terminate(); // no pong since the last ping: the connection is dead
} else {
socket.answeredLastPing = false;
socket.ping();
}
}
}, 30 * 1000);
When a pong arrives on a connection, our code sets that connection’s answeredLastPing back to true. Marcus’s sleeping laptop cannot answer, so 30 seconds later, the server ends the connection. Ending it runs the same code as a normal close, which removes the connection from every set.
Reconnecting
How does the page know that its connection is gone? When the laptop wakes up, the browser finds that the connection no longer works, because the server has already ended it or because the network reports it broken. The browser then gives the page a close event on the WebSocket, as it does for a normal close.
So the client handles every kind of close the same way. Here is that code, simplified:
function connect() {
const socket = new WebSocket("wss://courselore.example/courses/7/live");
socket.onopen = () => catchUp(); // read the messages missed while closed
socket.onmessage = (event) => handleUpdate(JSON.parse(event.data));
socket.onclose = () => setTimeout(connect, 1000); // try again after a second
}
connect();
When the WebSocket closes, for any reason, the client waits a second and calls connect again, which opens a new WebSocket. When the new one opens, catchUp reads the messages that were posted while the client had no connection. catchUp reads the missed messages with the REST API, because the WebSocket, as we use it, offers no way to retrieve them. Suppose the newest message on Marcus’s screen in Team 4’s room is 61851. Then catchUp sends:
GET /rooms/12/messages?after=61851
This returns the messages that were posted while Marcus’s connection was closed. From that point on, the new WebSocket receives the new messages as they are posted.