Skip to main content

如何统计已连接客户端数量

¥How to count the number of connected clients

独立式

¥Standalone

使用单个 Socket.IO 服务器时,以下代码片段适用:

¥The following snippets apply when using a single Socket.IO server:

全局

¥Globally

function totalCount() {
return io.engine.clientsCount;
}

const count = totalCount();

此值是服务器上的底层连接数。

¥This value is the number of low-level connections on the server.

在主命名空间中

¥In the main namespace

function totalCount() {
return io.of("/").sockets.size;
}

const count = totalCount();

如果你使用没有任何中间件的单个命名空间,则此值将等于 io.engine.clientsCount

¥If you are using a single namespace without any middleware, this value will be equal to io.engine.clientsCount.

如果你使用多个命名空间,例如:

¥If you are using multiple namespaces, for example when:

  • 客户端 A 连接到主命名空间(/

    ¥client A is connected to the main namespace (/)

  • 客户端 B 连接到 /orders 命名空间

    ¥client B is connected to the /orders namespace

  • 客户端 C 同时连接到主命名空间和 /orders 命名空间(通过单个连接多路复用)

    ¥client C is connected to both the main and the /orders namespaces (multiplexed over a single connection)

那么在这种情况下 io.engine.clientsCount 将为 3,而 totalCount() 仅为 2。

¥Then in that case io.engine.clientsCount will be 3, while totalCount() is only 2.

在命名空间中

¥In a namespace

function countInNamespace(namespace) {
return io.of(namespace).sockets.size;
}

const count = countInNamespace("/chat");

在房间中

¥In a room

function countInRoom(room) {
return io.of("/").adapter.rooms.get(room)?.size || 0;
}

const count = countInRoom("news");

集群

¥Cluster

当扩展到多个 Socket.IO 服务器时,计算连接的客户端数量会稍微复杂一些。

¥When scaling to multiple Socket.IO servers, computing the number of connected clients is a bit more complex.

让我们回顾几种解决方案及其优缺点:

¥Let's review several solutions and their pros and cons:

解决方案 1:fetchSockets()

¥Solution 1: fetchSockets()

fetchSockets() 方法向集群中的每个节点发送请求,这些节点使用其本地套接字实例(当前连接到节点的实例)进行响应。

¥The fetchSockets() method sends a request to every node in the cluster, which respond with their local socket instances (the ones that are currently connected to the node).

  • 在主命名空间中

    ¥in the main namespace

async function totalCount() {
const sockets = await io.fetchSockets();
return sockets.length;
}

const count = await totalCount();
  • 在一个房间里

    ¥in a room

async function totalCount(room) {
const sockets = await io.in(room).fetchSockets();
return sockets.length;
}

const count = await totalCount("news");

但是,不建议使用此解决方案,因为它包含大量有关套接字实例的详细信息(id、房间、握手数据),因此扩展性不佳。

¥However, this solution is not recommended, as it includes a lot of details about the socket instances (id, rooms, handshake data) and thus will not scale well.

参考:fetchSockets()

¥Reference: fetchSockets()

解决方案 2:serverSideEmit()

¥Solution 2: serverSideEmit()

类似地,serverSideEmit() 方法向集群中的每个节点发送一个事件,并等待它们的响应。

¥Similarly, serverSideEmit() method sends an event to every node in the cluster, and waits for their responses.

  • 在主命名空间中

    ¥in the main namespace

function localCount() {
return io.of("/").sockets.size;
}

io.on("totalCount", (cb) => {
cb(localCount());
});

async function totalCount() {
const remoteCounts = await io.serverSideEmitWithAck("totalCount");

return remoteCounts.reduce((a, b) => a + b, localCount());
}

const count = await totalCount();
  • 在一个房间里

    ¥in a room

function localCount(room) {
return io.of("/").adapter.rooms.get(room)?.size || 0;
}

io.on("totalCount", (room, cb) => {
cb(localCount(room));
});

async function totalCount(room) {
const remoteCounts = await io.serverSideEmitWithAck("totalCount", room);

return remoteCounts.reduce((a, b) => a + b, localCount(room));
}

const count = await totalCount("news");

此方法稍好一些,因为每个服务器仅返回连接的客户端数量。但是,如果频繁调用,它可能不适合,因为它会在服务器之间产生大量的闲聊。

¥This method is a bit better, as each server only returns the number of connected clients. However, it may not be suitable if called frequently, as it will generate a lot of chatter between the servers.

参考:serverSideEmitWithAck()

¥Reference: serverSideEmitWithAck()

解决方案 3:外部存储

¥Solution 3: external store

此用例最有效的解决方案是使用 Redis 等外部存储。

¥The most efficient solution for this use case is to use an external store such as Redis.

以下是使用 redis 的简单实现:

¥Here's a naive implementation using the redis package:

io.on("connection", async (socket) => {
socket.on("disconnect", async () => {
await redisClient.decr("total-clients");
});

// remember to always run async methods after registering event handlers!
await redisClient.incr("total-clients");
});

async function totalCount() {
const val = await redisClient.get("total-clients");
return val || 0;
}

const count = await totalCount();

上述解决方案的唯一问题是,如果一台服务器突然崩溃,则计数器将无法正确更新,然后报告的数字将高于实际情况。

¥The only problem with the solution above is that, if one server abruptly crashes, then the counter will not be updated properly and will then report a number that is higher than the reality.

为了防止这种情况,一个常见的解决方案是每个 Socket.IO 服务器都有一个计数器,以及一个定期检查每个服务器状态的清理过程:

¥To prevent this, one common solution is to have a counter per Socket.IO server, and a cleanup process which periodically checks the state of each server:

在 Redis 中:

¥In Redis:

类型内容
processesSet[process1, process2]
process1:is-up字符串(+ 到期时间)1
process2:is-up字符串(+ 到期时间)1
total-clients字符串5
process1:total-clients字符串3
process2:total-clients字符串2

在每个节点上:

¥On each node:

// on startup
const processId = randomUUID();
await redisClient.multi()
.sAdd("processes", processId)
.set(`${processId}:is-up`, "1", { EX: 10 })
.exec();

setInterval(async () => {
await redisClient.expire(`${processId}:is-up`, 10);
}, 5000);

process.on("SIGINT", async () => {
await io.close(); // cleanly close the server and run the "disconnect" event handlers
process.exit(0);
});

io.on("connection", async (socket) => {
socket.on("disconnect", async () => {
await redisClient.multi()
.decr(`${processId}:total-clients`)
.decr("total-clients")
.exec();
});

await redisClient.multi()
.incr(`${processId}:total-clients`)
.incr("total-clients")
.exec();
});

async function totalCount() {
const val = await redisClient.get("total-clients");
return val || 0;
}

const count = await totalCount();

清理过程:

¥Cleanup process:

setInterval(async () => {
const processes = await redisClient.sMembers("processes");
const states = await redisClient.mGet(processes.map(p => `${p}:is-up`));

for (let i = 0; i < processes.length; i++) {
if (states[i] === "1") {
continue;
}

const processId = processes[i];
const count = await redisClient.get(`${processId}:total-clients`);

await redisClient.multi()
.sRem("processes", processId)
.del(`${processId}:total-clients}`)
.decrBy("total-clients", count || 0)
.exec();
}
}, 5000);

以上就是全部内容了,感谢大家的阅读!

¥That's all folks, thanks for reading!

也可以看看:如何统计已连接用户数量

¥See also: How to count the number of connected users

返回示例列表

¥Back to the list of examples