如何统计已连接客户端数量
¥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.
¥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.
¥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:
键 | 类型 | 内容 |
---|---|---|
processes | Set | [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