统计在线用户数是一个很常见的功能,在这篇文章,我们主要讨论实现过程中的问题,以及介绍作者本人的实现方式。
以作者的经验看来,统计在线用户数主要有以下2个问题:
1. 何时记录为在线状态,何时记录为离线状态
这个问题白说了就是我们如何来判断用户是在线还是离线。对于在线判断还是比较容易的,只要用户访问了页面,或者调用了API,我们就知道用户是在线的,甚至我们可以给客户端开放一个心跳的接口,让客户端定时调用,表示客户端处于活动状态。但是对于离线的判断就比较困难了,大多数web应用是用http短连接的方式进行交互,不存在状态维护,客户端关掉浏览器甚至关机,服务端是不知道的。因此作者的做法是统计在1分钟之内(或者3分钟之内,随便你喜欢)的在线数,向精确靠拢。
2. 客户端唯一标识
当服务端接收到多个请求时,我们如何确定是同一个客户端发出还是多个客户端发出的?对于必须要注册才能使用的应用来说,这个比较简单,用户的id就是客户端的唯一标识。而对于允许游客访问的应用,我们就要想办法给客户端制造一个唯一标识:
移动应用客户端 - 可以使用设备的UUID,硬件序列号等信息,作为客户端唯一标识。
浏览器应用客户端 - 可以使用浏览器的user-agent信息,加上客户端ip,作为客户的唯一标识。但是我们还需要考虑到这样的场景,对于一家公司/机构/单位,或者是一栋大楼,拥有数十台甚至成百上千台电脑,但是对于出口网络,仅仅只有1个或者数个IP。如果仅仅使用 user-agent + client-ip 来作唯一标识,那么很容易造成客户端身份重复,实际一栋大楼里有100个在线用户,但很可能只识别出20个不同的客户端。
那么客户端用硬件的信息(如网卡MAC地址等)来作唯一标识呢?作者认为对于浏览器客户端来说,要获取这样的硬件信息十分困难,而且这样意味着对客户端过分的依赖。
那么作者的做法是这样的:
使用 Redis的有序集 来记录用户的在线时间和客户端唯一标识。使用 ZCOUNT 命令可以很容易的统计出某个时间段内的用户数。
获取请求中 cookies 或 headers 中的 identifyid 数据,如果不存在,则使用:md5(user-agent + client-ip + Date.now()) 这样的规则来生成一个UUID,并写到cookies里响应给客户端,下次请求客户端就会附带上这个数据作为我们识别它的唯一标识。
Talk is cheap. Show me the code. -- Linus Torvalds
构建项目工程
按照例常,我们先看看工程最后的结构:
我们还是使用 weroll 来构建这个项目,这里我们就不冗述了,请参考:
weroll 项目NPM主页
weroll - 快速搭建Node.js应用程序脚手架 (1)- 2分钟Demo
weroll - 快速搭建Node.js应用程序脚手架 (2)- 使用Schedule实现一个服务器性能监控应用
定义心跳API
首先我们来定义一个处理客户端心跳请求的API,客户端可以定时调用,保持自己的在线状态。我们将使用 Redis有序集 来维护客户端的在线状态。
Redis有序集 的每一条数据由score和member组成,我们可以理解为分数和值。分数的作用是排序,而值是一个字符串,如果同一个值以不同的分数多次写入redis,则最近的一次分数将会覆盖之前的分数,同一个值在有序集中只有一条数据。关于Redis有序集命令可以参考这里。
我们把客户的UUID作为有序集的值,而把心跳触发的时间戳作为分数,这样客户的每一次心跳,都会刷新在Redis有序集中的排序位置。
/* ./server/service/UserService.js */
exports.config = {
name: "user",
enabled: true,
security: {
//@heartbeat 用户心跳
"heartbeat":{ needLogin:false }
}
};
var Redis = require("weroll/model/Redis");
//使用Redis的有序集来维护用户的在线时间
function keepAlive(identifyID, offline) {
var redisKey = Redis.join("user_alive_sort");
if (offline) {
//如果用户登出,则从有序集中删除用户信息
Redis.do("zrem", [ redisKey, identifyID ], function(err) {
if (err) return console.error("Redis.zrem('user_alive_sort') error --> ", err);
console.log( "user offline: " + identifyID );
});
} else {
//如果用户登入或触发心跳,则刷新有序集中的用户最新在线时间
Redis.do("zadd", [ redisKey, Date.now(), identifyID ], function(err) {
if (err) return console.error("Redis.zadd('user_alive_sort') error --> ", err);
console.log( "keep user alive: " + identifyID );
});
}
}
exports.heartbeat = function(req, res, params) {
//写入在线状态
//req._identifyID 表示客户端的UUID,由weroll自动生成
keepAlive(req._identifyID);
//响应客户端
res.sayOK();
}
以上代码我们定义了一个名为 user.heartbeat 的API,用来处理客户端心跳。keepAlive 方法则封装了对 Redis 的写数据操作。在weroll中如何使用Redis请参考官方文档 weoll - Guide : Redis。
req._identifyID 表示客户端的UUID,由weroll自动生成。我附上它封装的主要代码给予参考,感兴趣的可以直接看github上的源码:
//解析客户端IP
req._clientIP = Utils.parseIP(req);
//尝试从cookies中获取UUID
var identifyid = req.cookies.identifyid;
if (!identifyid) {
//生成UUID,并写到响应cookies里
identifyid = md5(req.headers["user-agent"] + req._clientIP + Date.now());
res.cookie("identifyid", identifyid);
}
req._identifyID = identifyid;
req._clientIP 标识客户端IP,weroll也在请求接收时进行了解析,开发者可以直接使用。相关的解析代码是这样的:
exports.parseIP = function (req) {
try {
//req.headers['X-Real-IP']和req.headers['X-Forwarded-For']用于
//解析由Nginx或其他web运行容器代理转发的请求的客户端IP
var ip = req.headers['X-Real-IP'] ||
req.headers['X-Forwarded-For'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
if (ip == "::1" || ip == "127.0.0.1") ip = "0.0.0.0";
return ip;
} catch (err) {
return "unknown"
}
}
以上就是心跳API的定义,我们已经完成了在线状态的写操作。接下来我们来实现读操作。
实现页面显示当前在线用户数
让我们来定义一个index.html页面的路由,创建 ./server/router/index.js 脚本:
/* ./server/router/index.js */
var Redis = require("weroll/model/Redis");
//计算当前的在线用户数, range表示N分钟以内的在线数
function countOnlineUser(range, callBack) {
range = range * 60 * 1000;
var now = Date.now();
var fromTime = now - range;
Redis.do("zcount", [ Redis.join("user_alive_sort"), fromTime, "+inf" ], function(err, res) {
if (err) return callBack(err);
//redis返回的结果即是这个时段内的在线用户数
var result = { num:Number(res), fromTime:fromTime, toTime:now };
callBack(null, result);
});
}
function renderIndexPage(req, res, output, user) {
//强制触发一次心跳
req.callAPI("user.heartbeat", {}, function () {
//获得1分钟内的在线数
countOnlineUser(1, function(err, result) {
//如果有异常,则跳到error.html页面来显示错误
if (err) return output(null, err);
//渲染页面
output(result);
});
});
}
exports.getRouterMap = function() {
return [
{ url: "/", view: "index", handle: renderIndexPage, needLogin:false },
{ url: "/index", view: "index", handle: renderIndexPage, needLogin:false }
];
}
获得在线用户数的关键就是 Redis的ZCOUNT 命令,开发者可以自己定义统计的时间段,比如1分钟内的,那么意味着如果客户端超过1分钟没有触发心跳,则在有序集中的位置就会下降,以至于被排除在时间段内。
最好我们来写一个简单的HTML页面来显示当前在线数:
<!-- ./client/views/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OnlineApp</title>
</head>
<body>
<div style="padding: 20px; font-size:24px;">
当前在线用户数: {{data.num}}
</div>
</body>
</html>
weroll默认使用 nunjucks 作为模板引擎,详细使用请参考nunjucks官方文档。
最后启动项目,打开浏览器,使用两个不同的浏览器分别进入网址 http://localhost:3000/ 看看实际效果。
这里我们就不处理客户端定时调用心跳API了,各位看官可以自己用ajax来实现。
完整代码在github上:https://github.com/jayliang70...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。