Ir para o conteúdo principal

How to emit to a list of sockets

In Socket.IO, there are several ways to emit an event to a specific subset of connected sockets:

Room

In most applications, the recommended approach is to use rooms: create rooms based on your domain model, such as user IDs, organizations, projects, conversations, or subscriptions, then emit to those rooms.

Reference: Rooms

Example targeting a specific user:

function computeUserId(handshake) {
// compute the user ID based on the headers/cookies/etc.
}

io.use(async (socket, next) => {
const userId = await computeUserId(socket.handshake);

socket.join(`user:${userId}`);

next();
});

// then later
io.to("user:123").emit("event", "to this specific user");

Similarly, to target a specific project:

function computeUserId(handshake) {
// compute the user ID based on the headers/cookies/etc.
}

io.use(async (socket, next) => {
const userId = await computeUserId(socket.handshake);
const projects = await getProjectsForUser(userId);

for (const project of projects) {
socket.join(`project:${project.id}`);
}

next();
});

// then later
io.to("project:123").emit("event", "to all users working on this project");

Pros

  • easy to implement
  • performant

Cons

  • you need to keep the rooms up-to-date:

  • there is no way to do an intersection between rooms (for example, all sockets with a specific role in the organization and assigned to a particular project)

info

io.to("org:123:manager").to("project:456").emit("event") will do a union of the rooms and emit to all sockets which are in either the org:123:manager or project:456 rooms (or both).

Namespace

Namespaces are another way to organize your Socket.IO server into logical groups.

Reference: Namespaces

Example with a namespace for admin users:

const adminNamespace = io.of("/admin");

async function isAdmin(handshake) {
// ...
}

adminNamespace.use(async (socket, next) => {
if (await isAdmin(socket.handshake)) {
next();
} else {
next(new Error("not authorized"));
}
});

// then later
adminNamespace.emit("event", "to all admin users");

Or with one namespace per organization:

const namespaces = io.of(/^\/org:\d+$/);

async function isMemberOfOrganization(handshake, orgId) {
// ...
}

namespaces.use(async (socket, next) => {
const orgId = socket.nsp.name.slice("/org:".length);

if (await isMemberOfOrganization(socket.handshake, orgId)) {
next();
} else {
next(new Error("not authorized"));
}
});

namespaces.on("connection", (socket) => {
const namespace = socket.nsp;

namespace.emit("event", "to all users in this organization");
});

Pros

  • easy to implement
  • performant

Cons

  • you need to keep the namespaces up-to-date:
    • call socket.disconnect() or io.disconnectSockets() (in the example above, when the user is removed from an organization)
    • connect to the namespace from the client side when the user is added to an organization

Dynamic subset of sockets

If the solutions above don't suit your needs, you can always loop through all connected sockets and filter the sockets you want to emit to:

Standalone version

Example if you have a single Socket.IO server:

function emitToMatchingSockets(filter, event, arg) {
for (const socket of io.of("/").sockets.values()) {
if (filter(socket)) {
socket.emit(event, arg);
}
}
}

emitToMatchingSockets(
(socket) => {
return (
socket.data.orgId === 123 &&
socket.data.role === "manager" &&
socket.data.projectIds.includes(456)
);
},
"event",
"to this specific subset of users",
);

Cluster version

Example if you have multiple Socket.IO servers:

function localEmit({ orgId, role, projectId }, event, arg) {
function filter(socket) {
return (
socket.data.orgId === orgId &&
socket.data.role === role &&
socket.data.projectIds.includes(projectId)
);
}

for (const socket of io.of("/").sockets.values()) {
if (filter(socket)) {
socket.emit(event, arg);
}
}
}

io.on("emitToMatchingSockets", (filters, event, arg) => {
localEmit(filters, event, arg);
});

function emitToMatchingSockets(filters, event, arg) {
// emit to sockets connected on this server
localEmit(filters, event, arg);

// notify the other servers
io.serverSideEmit("emitToMatchingSockets", filters, event, arg);
}

emitToMatchingSockets(
{
orgId: 123,
role: "manager",
projectId: 456,
},
"event",
"to this specific subset of users",
);

References:

Pros

  • you can apply any filter

Cons

  • a bit less performant, since the payload will be encoded for each socket

That's all folks, thanks for reading!

Back to the list of examples