Socket.io
I am having some problems with node/ws, and I read that the socket.io library handles common problems with WebSockets, so I am going to try to reimplement the WebSocket functionality of this site with socket.io to try to improve performance.
References
Notes
Introduction
Socket.io is a library that enables low-latency, bidirectional, and event-based communication between a client and server.
The Socket.io connection can be established with different low-level transports:
- HTTP long-polling
- WebSocket
- WebTransport
Socket.io will automatically pick the best available option, depending on:
- the capabilities of the browser
- the network (some networks block WebSocket and/or WebTransport connections)
Although Socket.io uses WebSocket for transport when possible, it adds additional metadata to each packet. That is why a WebSocket client will not be able to successfully connect to a Socket.io server, and a Socket.io client will not be able to connect to a plain WebSocket server either.
// WARNING: the client will NOT be able to connect!
const socket = io("ws://echo.websocket.org");
The Socket.io library keeps an open TCP connection to the server, which may result in a high battery drain for users.
Features
HTTP Long-Polling Fallback
The connection will fall back to HTTP long-polling in case the WebSocket connection cannot be established. Even if most browsers now support WebSockets, it is still a great feature as we still receive reports from users that cannot establish a WebSocket connection because they are behind some misconfigured proxy.
Automatic Reconnection
Socket.io includes a heartbeat connection which periodically checks the state of the connection - so that the server and client can both become aware of the broken status of the link. When the client eventually gets disconnected, it automatically reconnects with an exponential back-off delay, in order to not overwhelm the server.
Packet Buffering
The packets are automatically buffered when the client is disconnected and will be sent upon reconnection.
Acknowledgements
Socket.io provides a convenient way to send and receive a response:
Sender
socket.emit("hello", "world", (response) => {
console.log(response); // "got it"
});
Receiver
socket.on("hello", (arg, callback) => {
console.log(arg); // "world"
callback("got it");
});
Broadcasting
On the server side, you can send an event to all connected clients or to a subset of connected clients.
// to all connected clients
io.emit("hello");
// to all connected clients in the "news" room
io.to("news").emit("hello");
Multiplexing
Namespaces allow you to split the logic of your application over a single shared connection. This can be useful if you want to create an admin
channel that only authorized users can join.
io.on("connection", (socket) => {
// classic users
});
io.of("/admin").on("connection", (socket) => {
// admin users
});
How it works
The bidirectional channel between the Socket.io server (Node.js) and the Socket.io client (browser, Node.js, or another) is established with a WebSocket connection whenever connection, and will use HTTP long-polling as fallback.
The codebase is split into two distinct layers:
- the low-level plumbing: what is called Engine.io, the engine inside Socket.io
- the high-level API: the Socket.IO itself
Engine.io is responsible for establishing the low-level connection between the server and the client. It handles:
- the various transports and the upgrade mechanism
There are currently two implemented transports:
The HTTP long-polling transport (also simply referred as "polling") consists of successive HTTP requests:
- long-running
GET
requests ,for receiving data from the server - short-running
POST
requests, for sending data to the server
Due to the nature of the transport, successive emits may be concatenated and sent within the same HTTP request.
The WebSocket transport consists of a WebSocket connection, which provides a bidirectional and low-latency communication channel between the server and the client. Due to the nature of the transport, each emit is sent in its own WebSocket frame (some emits may even result in two distinct WebSocket frames).
Handshake
At the beginning of the Engine.io connection, the server sends some information:
{
"sid": "FSDjX-WRwSA4zTZMALqx",
"upgrades": ["websocket"],
"pingInterval": 25000,
"pingTimeout": 20000
}
- the
sid
is the ID of the session, it must be included in thesid
parameter in all subsequent HTTP requests - the
upgrades
array contains the list of allbetter
transports supported by the server - the
pingInterval
andpingTimeout
values are used in the heartbeat mechanism
Upgrade Mechanism
By default, the client established the connection with the HTTP long-polling transport. While WebSocket is clearly the best way to establish a bidirectional communication, experience has shown that is not always possible, due to corporate proxies, personal firewall, antivirus software...
An unsuccessful WebSocket connection can translate in up to 10 seconds of waiting for the realtime application to begin exchanging data. Engine.IO focuses on reliability and user experience first, marginal potential UX improvements and increased server performance second. To upgrade, the client will:
- ensure its outgoing buffer is empty
- put the current transport in read-only mode
- try to establish a connection with the other transport
- if successful, close the first transport
- handshake (contains the session ID (
sid
) that is used in subsequent requests) - send data (HTTP long-polling)
- receive data (HTTP long-polling)
- upgrade (WebSocket)
- receive data (HTTP long-polling, closed once the WebSocket connection in 4. is successfully established)
The Engine.io connection is considered as closed when:
- one HTTP request (either GET or POST) fails
- the WebSocket connection is closed
socket.disconnect()
is called on the server-side or the client-side
There is also a heartbeat mechanism that checks that the connection between the server and the client is still up and running. At a given interval (the pingInterval
value sent in the handshake) the server sends a PING packet and the client has a few seconds (the pingTimeout
value) to send a PONG packet back. If the server does not receive a PONG packet back, it will consider that the connection is closed. Conversely, if the client does not receive a PING packet within pingInterval + pingTimeout
, it will consider the connection is closed.
Socket.IO provides some additional features over the Engine.IO connection:
- automatic reconnection
- packet buffering
- acknowledgements
- broadcasting to all or a subset of clients
- multiplexing
Delivery Guarantees
Socket.IO does guarantee message ordering, no matter which low-level transport is used. This is achieved thanks to:
- the guarantees provided by the underlying TCP connection
- the careful design of the upgrade mechanism
Socket.io provides an at most once guarantee of delivery:
- if the connection is broken while an event is being sent, then there is no guarantee that the other side has received it, and there will be no retry upon reconnection
- A disconnected client will buffer events until reconnection
- There is no such buffer on the server, which means that any event that was missed by a disconnected client will not be transmitted to the client upon reconnection.
From the client side, you can achieve an at least once guarantee with acknowledgements and timeouts:
function emit(socket, event, arg) {
socket.timeout(2000).emit(event, arg, (err) => {
if (err) {
// no ack from the server, let's retry
emit(socket, event, arg);
}
});
}
emit(socket, "foo", "bar");
Connection State Recovery
Connection state recovery is a feature which allows restoring a client's state after a temporary connection, including any missed packets. Under real conditions, a Socket.IO client will inevitably experience temporary disconnections, regardless of the quality of the connection. This feature will help you cope with such disconnections, but unless you want to store the packets and sessions forever (by setting maxDisconnectionDuration
to Infinity
), you can't be assured that the recovery will always be successful.
Client stat recovery must be enabled by the server:
const io = new Server(httpServer, {
connectionStateRecovery: {
// the backup duration of the sessions and the packets
maxDisconnectionDuration: 2 * 60 * 1000,
// whether to skip middlewares upon successful recovery
skipMiddlewares: true,
}
});
Upon an unexpected disconnection, the server will store the id
, the rooms and the data
attribute of the socket. Then, upon reconnection, the server will try to restore the state of the client. The recovered
attribute indicates whether this recovery was successful:
Server
io.on("connection", (socket) => {
if (socket.recovered) {
// recovery was successful: socket.id, socket.rooms and socket.data were restored
} else {
// new or unrecoverable session
}
});
Client
socket.on("connect", () => {
if (socket.recovered) {
// any event missed during the disconnection period will be received now
} else {
// new or unrecoverable session
}
});
Memory Usage
The resources consumed by your Socket.io server will mainly depend on:
- the number of connected clients
- the number of messages received and sent per second
Comments
You have to be logged in to add a comment
User Comments
There are currently no comments for this article.