Engine.IO, Socket.IO & WebSocket: How Real-Time Communication Works
This article explains the relationships among WebSocket, Engine.IO, and Socket.IO, details Engine.IO’s features, handshake, upgrade and disconnect mechanisms, and provides practical code examples for building real‑time communication in Node.js applications, including packet encoding, transport options, and debugging techniques.
Part of a series on WebSocket basics and applications, this article reviews the relationships between WebSocket, Engine.IO, and Socket.IO, and demonstrates how to capture WebSocket packets.
1. Relationship between WebSocket, Engine.IO, and Socket.IO
WebSocket is a full‑duplex protocol over a single TCP connection, enabling simple client‑server data exchange and server‑initiated pushes after a single handshake.
Socket.IO builds a real‑time bidirectional channel on top of WebSocket (or falls back to HTTP long‑polling) for Node.js and browsers.
Engine.IO is an abstraction layer used by Socket.IO to handle the transport layer, supporting multiple browsers, devices, and networks. It wraps WebSocket and HTTP long‑polling, providing a unified socket protocol.
2. Engine.IO Features
Engine.IO establishes the underlying connection and provides:
Multiple transport channels and upgrade mechanisms
Disconnect detection
2.1 Transport Channels
Two main transport implementations are used:
HTTP long‑polling
WebSocket
2.1.1 HTTP long‑polling
Long‑polling consists of a series of HTTP requests:
Long‑running GET requests for receiving data
Short‑running POST requests for sending data
Multiple emits may be combined into a single request.
2.1.2 WebSocket
The WebSocket transport provides a low‑latency, bidirectional channel. Each emit is sent as a separate WebSocket frame, sometimes split into two frames.
2.2 Handshake
When Engine.IO establishes a connection, the server sends a JSON handshake packet:
<code>{
"sid": "FSDjX-WRwSA4zTZMALqx",
"upgrades": ["websocket"],
"pingInterval": 25000,
"pingTimeout": 20000
}</code>sid : session ID, included in all subsequent requests
upgrades : list of transports the server can upgrade to
pingInterval and pingTimeout : heartbeat configuration
2.3 Upgrade Mechanism
By default the client starts with HTTP long‑polling. If the WebSocket upgrade succeeds, the client switches to WebSocket after flushing the polling buffer.
Ensure the send queue is empty
Mark the current transport as read‑only
Open a new connection with the alternative transport
Close the original transport if the upgrade succeeds
2.4 Disconnect Detection
Engine.IO considers the connection closed when any of the following occurs:
A HTTP request fails
The WebSocket connection closes
socket.disconnect() is called on either side
The heartbeat (ping/pong) fails
The server sends a ping every
pingIntervalms; the client must reply with a pong within
pingTimeoutms, otherwise the connection is considered closed.
3. Engine.IO Protocol
3.1 Session Flow
Transport channel is opened via an Engine.IO URL
Server sends a JSON handshake containing
sid,
upgrades,
pingInterval, and
pingTimeoutClient replies to server pings with pong packets
Message packets are exchanged
Polling transports can send a close packet to terminate the socket
Session Example
Open request (polling):
<code>GET /engine.io/?EIO=4&transport=polling&t=N8hyd6w
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
0{"sid":"N-YWtQT1K9uQsb15AAAD","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}</code>Message in (server → client):
<code>GET /engine.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
4hey</code>Message out (client → server):
<code>POST /engine.io/?EIO=4&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC
Content-Type: text/plain; charset=UTF-8
4hello\x1e4world</code>WebSocket upgrade request:
<code>GET /engine.io/?EIO=4&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC
HTTP/1.1 101 Switching Protocols</code>3.2 URLs
Engine.IO URLs have the form
/engine.io/[?query]. Reserved query keys include
transport,
j(JSONP),
sid,
EIO(protocol version), and
t(cache‑busting).
3.3 Encoding
Two encoding types are used:
packet : a single UTF‑8 string or binary blob, prefixed with a type identifier
payload : a series of encoded packets separated by the record separator
\x1eBinary data in a payload is base64‑encoded and preceded by a
bmarker.
3.4 Transport Channels
Engine.IO must support three transport types:
WebSocket
Server‑Sent Events (SSE)
Polling (XHR, JSONP)
Polling
Polling uses periodic GET requests to receive data and POST requests to send data. CORS support is required for XHR, and JSONP responses must wrap the payload in a JavaScript callback.
Server‑Sent Events
Clients receive data via an
EventSourceobject and send data via
XMLHttpRequest.
WebSocket
WebSocket frames carry messages directly; the payload encoding described above is not used.
3.5 Transport Upgrade
Connections start with polling. The client probes WebSocket with a
2probepacket; if the server replies
3probe, the client sends an upgrade packet (
5) and switches to WebSocket after flushing buffers.
3.6 Timeouts
Both client and server use
pingIntervaland
pingTimeoutfrom the handshake to detect unresponsive peers. Missing a pong within the timeout marks the connection as closed.
4. Key Points
Engine.IO is the transport layer underlying Socket.IO.
Both sides must use matching implementations (Engine.IO ↔ Socket.IO).
Engine.IO’s heartbeat (ping/pong) is sent as regular message packets, not as WebSocket control frames.
5. Simple Example
Server (Node.js)
<code>const engine = require('engine.io');
const server = engine.listen(3000, { cors: { origin: '*' } });
server.on('listen', () => console.log('listening on 3000'));
server.on('connection', socket => {
console.log('new connection');
socket.send('utf 8 string');
socket.send(Buffer.from('hello world')); // binary data
});
</code>Client (Node.js)
<code>const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost:3000');
socket.on('open', () => {
socket.emit('message from client');
socket.on('message', data => {
console.log('receive message: ' + data);
socket.send('ack from client.');
});
socket.on('close', e => console.log('socket close', e));
});
</code>When the client specifies
{ transports: ['websocket'] }, the initial polling phase is skipped and the handshake occurs directly over WebSocket.
<code>const socket = new Socket('ws://localhost:3000', { transports: ['websocket'] });
</code>Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.