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.

Date Created:
Last Edited:
1 48

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:

  1. initial_headers
    1. This is emitted just before writing the response headers of the first HTTP request of the session (the handshake), allowing you to customize them.
  2. headers
    1. This is emitted just before writing the response headers of each HTTP request of the session, allowing you to customize them.
  3. connection_error
    1. This is emitted when a connection has been abnormally closed.
    2. Error Code Meaning:
      1. 0: Transport Unknown
      2. 1: Session ID Unknown
      3. 2: Bad Handshake Method
      4. 3: Bad Request
      5. 4: Forbidden
      6. 5: Unsupported Protocol Version
Utility Methods
  • socketsJoin: makes the matching socket instances join the specified rooms
  • socketsLeave: makes the matching socket instances leave the specified rooms
  • disconnectSockets: makes the matching socket instances disconnect
  • fetchSockets: returns the matching socket instances
  • serverSideEmit: 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.

Server/Client Interaction

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
  • 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.

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:

  1. enabling sticky session, if HTTP long-polling is enabled (which is the default)
  2. 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

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

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.

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

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.

Rooms

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.

Adapter and Emitters

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).

Namespaces

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

Insert Math Markup

ESC
About Inserting Math Content
Display Style:

Embed News Content

ESC
About Embedding News Content

Embed Youtube Video

ESC
Embedding Youtube Videos

Embed TikTok Video

ESC
Embedding TikTok Videos

Embed X Post

ESC
Embedding X Posts

Embed Instagram Post

ESC
Embedding Instagram Posts

Insert Details Element

ESC

Example Output:

Summary Title
You will be able to insert content here after confirming the title of the <details> element.

Insert Table

ESC
Customization
Align:
Preview:

Insert Horizontal Rule

#000000

Preview:


View Content At Different Sizes

ESC

Edit Style of Block Nodes

ESC

Edit the background color, default text color, margin, padding, and border of block nodes. Editable block nodes include paragraphs, headers, and lists.

#ffffff
#000000

Edit Selected Cells

Change the background color, vertical align, and borders of the cells in the current selection.

#ffffff
Vertical Align:
Border
#000000
Border Style:

Edit Table

ESC
Customization:
Align:

Upload Lexical State

ESC

Upload a .lexical file. If the file type matches the type of the current editor, then a preview will be shown below the file input.

Upload 3D Object

ESC

Upload Jupyter Notebook

ESC

Upload a Jupyter notebook and embed the resulting HTML in the text editor.

Insert Custom HTML

ESC

Edit Image Background Color

ESC
#ffffff

Insert Columns Layout

ESC
Column Type:

Select Code Language

ESC
Select Coding Language

Insert Chart

ESC

Use the search box below

Upload Previous Version of Article State

ESC