A WebSocket upgrade begins as an ordinary HTTP request, which Node exposes as the upgrade event on an http.Server, leaving you raw RFC 6455 frames. Few write that by hand; Socket.IO 24,482 (https://github.com/socketio/socket.io 63,210 ) (4.8.3, MIT) adds reconnection, acknowledgements, rooms, namespaces and a long-polling fallback.
import { Server } from 'socket.io';
import { io as connect } from 'socket.io-client';
const server = new Server(3010, { cors: { origin: 'https://app.example.com' } });
server.on('connection', (socket) => {
console.log('server:', socket.id, 'via', socket.conn.transport.name);
socket.conn.on('upgrade', (t) => console.log('server: upgraded to', t.name));
socket.join('room:books');
socket.on('price', (payload, ack) => {
server.to('room:books').emit('price', { ...payload, at: 'server' });
ack({ ok: true });
});
});
const client = connect('http://127.0.0.1:3010');
client.on('price', (p) => console.log('client: broadcast', p.isbn, p.at));
client.on('connect', async () => {
console.log('client: ack', await client.emitWithAck('price', { isbn: '978-1', usd: 42 }));
client.close(); server.close();
});server: yR0S0qbT4HQtz2NWAAAA via polling
client: broadcast 978-1 server
server: upgraded to websocket
client: ack { ok: true }The session starts on polling and upgrades to websocket a moment later: Engine.IO, the transport underneath, establishes a connection guaranteed to work before trying a better one — also why a Socket.IO client cannot talk to a plain WebSocket server. emitWithAck turns the acknowledgement callback — the optional last argument of an emit — into a promise. ws 22,807 (https://github.com/websockets/ws 22,807 ) is the minimal alternative it uses internally (GraphQL and Real-Time APIs).