Socket.io Notes
continuing to takes notes on Socket.io after having read the Documentation in a daily reading article. I am taking these notes while implementing socket.io functionality on this site.
References
Notes
These notes are a continuation of this daily reading article on the Socket.io documentation.
Server
Installation
$ npm i socket.io
By default, Sockt.io uses the WebSocket server provided by the node ws package. You can also use the eiows package or the WebSockets.js packages by specifying the wsEngine
property when initializing the server.
Initializing
You can initialize a Socket.io server as a standalone server or as part of a HTTP(S) server.
The Server Instance
The Server instance (often called io
in code examples) has a view attribute that may be of use. It inherits all the methods from the main namespace, like namespace.use()
and namespace.allSockets()
.
engine
The engine property is a reference to the underlying Engine.IO server. This can be used to fetch the number of currently connected clients or to generate a custom session ID.
const uuid = require("uuid");
const count = io.engine.clientsCount;
// may or may not be similar to the count of Socket instances in the main namespace, depending on your usage
const count2 = io.of("/").sockets.size;
io.engine.generateId = (req) => {
return uuid.v4(); // must be unique across all Socket.IO servers
}
The Engine.IO server emits three special events:
initial_headers
- This is emitted just before writing the response headers of the first HTTP request of the session (the handshake), allowing you to customize them.
headers
- This is emitted just before writing the response headers of each HTTP request of the session, allowing you to customize them.
connection_error
- This is emitted when a connection has been abnormally closed.
- Error Code Meaning:
- 0: Transport Unknown
- 1: Session ID Unknown
- 2: Bad Handshake Method
- 3: Bad Request
- 4: Forbidden
- 5: Unsupported Protocol Version
Utility Methods
socketsJoin
: makes the matching socket instances join the specified roomssocketsLeave
: makes the matching socket instances leave the specified roomsdisconnectSockets
: makes the matching socket instances disconnectfetchSockets
: returns the matching socket instancesserverSideEmit
: This method allows to emit events to the other Socket.IO servers of the cluster, in a multi-server setup.
These methods share the same semantics as broadcasting, and the same filters apply.
Events
The server instance emits one single event:
connection
: This event is fired upon a new connection. The first argument is a Socket instance.
io.on("connection", (socket) => {
// ...
});
The Socket Instance
A Socket
is a fundamental class for interacting with the client. It inherits all the methods of the Node.js EventEmitter.
Attributes
id
- Each new connection is assigned a random 20-chaacters identifier.
handshake
- This object contains some details about the handshake that happens at the beginning of the Socket.io session.
{
headers: /* the headers of the initial request */
query: /* the query params of the initial request */
auth: /* the authentication payload */
time: /* the date of creation (as string) */
issued: /* the date of creation (unix timestamp) */
url: /* the request URL string */
address: /* the ip of the client */
xdomain: /* whether the connection is cross-domain */
secure: /* whether the connection is secure */
}
Example:
{
"headers": {
"user-agent": "xxxx",
"accept": "*/*",
"host": "example.com",
"connection": "close"
},
"query": {
"EIO": "4",
"transport": "polling",
"t": "NNjNltH"
},
"auth": {
"token": "123"
},
"time": "Sun Nov 22 2020 01:33:46 GMT+0100 (Central European Standard Time)",
"issued": 1606005226969,
"url": "/socket.io/?EIO=4&transport=polling&t=NNjNltH",
"address": "::ffff:1.2.3.4",
"xdomain": false,
"secure": true
}
rooms
- This is a reference to the rooms the Socket is currently in.
data
- An arbitrary object that can be used in conjunction with the
fetchSockets()
utility method
- An arbitrary object that can be used in conjunction with the
conn
- A reference to the underlying Engine.io socket.
As long as you don't overwrite any existing attribute, you can attach any attribute to the Socket instance and use it later.
Middleware
Middlewares look like express middlewares, but they are called for each incoming packet.
socket.use(([event, ...args], next) => {
// do something with the packet (logging, authorization, rate limiting...)
// do not forget to call next() at the end
next();
});
The next
method can be called with an error object. In that case, the event will not reach the registered event handlers and an error
event will be emitted.
Events
disconnect
- Event is fired by the socket instance upon disconnection.
disconnecting
- Event is similar to the
disconnect
but is fired a bit earlier.
- Event is similar to the
Middlewares
A middleware function is a function that gets executed for every incoming connection. They can be used for:
- logging
- authentication / authorization
- rate limiting
This function is only executed once per connection.
A middleware function has access to the Socket instance and to the next registered middleware function. You can register several middleware functions and they will be executed sequentially.
io.use((socket, next) => {
next();
});
io.use((socket, next) => {
next(new Error("thou shall not pass"));
});
io.use((socket, next) => {
// not executed, since the previous middleware has returned an error
next();
});
Note: The Socket instance is not actually connected when the middleware gets executed, which means that no disconnect
event will be emitted if the connection eventually fails. Since they are not bound to a usual HTTP request/response cycle, Socket IO middlewares are not really compatible with Express middlewares. That being said, starting in 4.6.0
, Express middlewares are now supported by the underlying engine:
import session from "express-session";
io.engine.use(session({
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}));
Behind A Reverse Proxy
How to deploy Socket.io behind nginx reverse proxy. The content of /etc/nginx/nginx.conf
:
http {
server {
listen 80;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
Using Multiple Nodes
When deploying multiple Socket.IO servers, there are two things to take care of:
- enabling sticky session, if HTTP long-polling is enabled (which is the default)
- Using a compatible adapter
If you plan to distribute the load of connections among different processes or machines, you have to make sure that all requests associated with a particular session ID reach the process that originated them. To achieve sticky-session, there are two main solutions:
- routing clients based on a cookie (recommended solution)
- routing clients based on their originating address
- nginx (IP-based)
- nginx Ingress (Kubernetes) (IP-based)
- Apache HTTPD (cookie-based)
- HAProxy (cookie-based)
- Traefik (cookie-based)
- Node.js
cluster
module
Handling Cors
You need to explicitly enable Cross-Origin Resource Sharing (CORS). All options will be forwarded to the cors package.
// server-side
const io = new Server(httpServer, {
cors: {
origin: "https://example.com",
allowedHeaders: ["my-custom-header"],
credentials: true
}
});
// client-side
import { io } from "socket.io-client";
const socket = io("https://api.example.com", {
withCredentials: true, // with cookies
extraHeaders: {
"my-custom-header": "abcd"
}
});
Client
Installation
$ npm install socket.io-client
You want to exclude debug from your browser bundle. With webpack, you can use webpack-remove-debug.
Client Initialization
If your front is served on the same domain as the server, you can simply use:
const socket = io();
// NOTE: you can use either wss/ws: or http/https:
const socket = io("https://server-domain.com"); // different origin
const socket = io("/admin"); // namespace
The Socket Instance
A Socket
is the fundamental class for interacting with the server. It inherits most methods of the Node.js EventEmitter.
Attributes
id
: Each new connection is assigned a random 20-chaacters identifier. This identifier is synced with the value on the server-side.connected
: This attribute describes whether the socket is currently connected to the server.io
: A reference to the underlying Manager.
Lifecycle
Events
connect
- Event fired by the Socket instance upon connection and reconnection.
disconnect
- Event is fired upon disconnection.
connect_error
- This event is fired upon connection failure. The
socket.active
attribute indicates whether the socket will automatically try to reconnect after a small randomized delay.
- This event is fired upon connection failure. The
Offline Behavior
By default, any event emitted while the socket is not connected will be buffered until reconnection. While useful in most cases, it could result in a huge spike of events when the connection is restored.
Events
Emitting Events
There are several ways to send events between the server and the client.
Basic Emit
The Socket.IO API is inspired by the Node.js EventEmitter, which means you can emit events on one side and register listeners on the other.
// Server
io.on("connection", (socket) => {
socket.emit("hello", "world");
});
// Client
socket.on("hello", (arg) => {
console.log(arg); // world
});
You can send any number of arguments, and all serializable data structures are supported. There is no need to run JSON.stringify()
on objects as it will be done for you.
Acknowledgements
Events are great, but in some cases you may want a more classic request-response API. In Socket.IO, this feature is named acknowledgements.
You can add a callback as the last argument of the emit()
, and this callback will be called once the other side acknowledges the event:
// Server
io.on("connection", (socket) => {
socket.on("update item", (arg1, arg2, callback) => {
console.log(arg1); // 1
console.log(arg2); // { name: "updated" }
callback({
status: "ok"
});
});
});
// Client
socket.emit("update item", "1", { name: "updated" }, (response) => {
console.log(response.status); // ok
});
Timeouts with Emits:
socket.timeout(5000).emit("my-event", (err) => {
if (err) {
// the other side did not acknowledge the event in the given delay
}
});
Listening to Events
There are several ways to handle events that are transmitted between the server and the client.
socket.on(eventName,listener)
Adds the listener function to the end of the listeners array for the event named eventName.
socket.once(eventName,listener)
Adds a one-time listener function for the event named eventName.
socket.off(eventName,listener)
Removes the specified listener from the listener array for the event named eventName.
socket.removeAllListeners(eventName)
Removes all listeners, or those of the specified eventName.
socket.onAny(listener)
Adds a listener that will be fired when any event is emitted.
socket.prependAny(listener)
Adds a listener that will be fired when any event is emitted. The listener is added to the beginning of the listeners array.
socket.offAny([listener])
Removes all catch-all listeners, or the given listener.
socket.onAnyOutgoing(listener)
Register a new catch-all listener for outgoing packets.
socket.prependAnyOutgoing(listener)
Register a new catch-all listener for outgoing packets. The listener is added to the beginning of the new listeners array.
socket.offAnyOutgoing([listener])
Removes the previously registered listener. If no listener is provided, all catch-all listeners are removed.
Broadcasting Events
Socket.io makes it easy to send events to all connected clients.
Broadcasting to all Connected Clients
io.on("connection", (socket) => {
socket.broadcast.emit("hello", "world");
});
Rooms
A room is an arbitrary channel that sockets can join
and leave
. It can be used to broadcast events to a subset of clients.
You can call join
to subscribe the socket to a given channel:
io.on("connection", (socket) => {
socket.join("some room");
});
Then simply use to
and in
when broadcasting or emitting:
io.to("some room").emit("some event");
// Exclude a room
io.except("some room").emot("some event");
io.to("room1").to("room2").to("room3").emit("some event");
Upon disconnection, sockets leave
all the channels they were part of automatically, and no special teardown is needed on your part. You can fetch the rooms the Socket was in by listening to the disconnecting
event.
Implementation Details
The room
feature is implemented by what we call an Adapter. This Adapter is a servicer-side component which is responsible for:
- storing the relationships between the Socket instances and the rooms
- broadcasting events to all (or a subset of) clients
Adapter
Introduction
An adapter is a server-side component which is responsible for broadcasting events to all or a subset of clients. When scaling to multiple Socket.IO servers, you will need to replace the default in-memory adapter by another implementation, so the events are properly routed to all clients.
Accessing Adapter:
// main namespace
const mainAdapter = io.of("/").adapter; // WARNING! io.adapter() will not work
// custom namespace
const adminAdapter = io.of("/admin").adapter;
Each Adapter instance emits the following events:
create-room
delete-room
join-room
leave-room
Most adapter implementations come with their own associated emitter package, which allows communicating to the group of Socket.IO servers from another Node.js process.
I am not going to go over all of the adapter implementations here, but there are adapters for Postgres, Redis, and AWS SQS (and others).
Advanced
Namespaces
A namespace is a communication channel that allows you to split the logic of your application over a single shared connection (also called multiplexing
).
Each namespace has its own event handlers, rooms, and middlewares. The main namespace is called /
. The io
instance inherits all of its methods. The return value of the of()
method is what we call the parent namespace, from which you can register middlewares and broadcast events.
Comments
You have to be logged in to add a comment
User Comments
There are currently no comments for this article.