这个项目原本是我学生时代为了找工做的一个练手项目,可是没想到受到了不少的关注,star也快要破K了,这也激励着我不断去完善他,一方面是得对得起关注学习的人,另外一方面也是想让本身能过经过慢慢完善一个项目来让本身提升。node
今天给你们带来的是基于Websocket+Node+Redis未读消息功能,可能更加偏向于实战方向,须要对Websocket和Node有一些了解,固然不了解也能够看看效果,效果连接( www.qiufengh.com/ )说不定会激起你学习的动力~git
下面我经过本身思考的方式来进行讲解,代码可能讲的很少,可是核心逻辑都进行了讲解,上面也有github地址,有兴趣的能够进行详细地查看。本身的idea或多或少会有一些不成熟,可是我仍是厚着脸皮出来抛头露脸,若是有什么建议还请你们多多提出,能让我更加完善这个做品。github
首先对于消息未读,你们都很熟悉,就是各类聊天的时候,出现的红点点,且是强迫症者必须清理的一个小点点,如👇所示。我会带你们实现一个这样的功能。 web
因为一对一的方式更加简单,我如今只考虑多对多的状况,也就是在一个房间(也能够称为群组,后面都以房间称呼)中的未读消息,那么设计这样的一个功能,首相我将它分红了3种用户。redis
这种场景就至关于咱们退出微信,可是别人在房间里发的消息,当咱们再次打开的时候依然可以看到房间增加的未读消息。mongodb
这种场景就是至关咱们停留在聊天列表页面,当他人在房间中发送消息,咱们可以实时的看到未读消息的条数在增加。数据库
场景示例。 ubuntu
这种场景其实就比较普通了,当别人发送新的消息,咱们就能实时看到,此时是不须要标记未读消息的。windows
场景示例。
主要流程能够简化为三个部分,分别为用户,推送功能,消息队列。
用户能够是消息提供者也能够是消息接受者。如下就是这个过程。
固然在这个过程当中涉及比较复杂的消息的存储,如何推送,获取,同步等问题,下面就是对这个过程进行详细的描述
图上的流程解释
A. 存储在Node缓存中的房间用户列表(此处信息也能够存在Redis中)
B. 存储在Redis中的未读消息列表
C. 存储在MongoDB中的未读消息列表
Node: 8.5.0 +
Npm: 5.3.0 +
MongoDB
Redis
Redis 是互联网技术领域使用最为普遍的存储中间件,它是「Remote Dictionary Service」的首字母缩写,是一个高性能的key-value数据库。具备性能极高,丰富的数据类型,原子,丰富的特性等优点。
redis 具备如下5种数据结构
想要深刻了解这5种存储结构能够查看www.runoob.com/w3cnote/red…
windows
mac
brew install redis
ubuntu
apt-get install redis
redhat
yum install redis
centos
运行客户端
redis-cli
windows
mac
源码编译
在本项目中咱们用String 来存储用户的未读消息记录,利用其incr命令来进行自增操做。利用Hash结构 来存储咱们websocket链接时用户的socket-id。
上面说了计数利用Redis的Stirng数据结构, 在Redis 咱们的计数key-value是这样的。
username-roomid - number
例子: hua1995116-room1 - 1
咱们的Socket-id则为Hash结构。
例子:
本项目一开始就使用了MongoDB,Node自然搭配的MongoDB的优点,这里就再也不进行讲解,Node操做MongoDB的模块叫作mongoose,具体的参数方法,能够查看官方文档。
MongoDB下载地址
可视化下载地址
下面咱们经过一开始的3种用户的场景来具体说明实现的代码。
客户端在登陆时会发送一个login事件,如下是后端逻辑。
// 创建链接
socket.on('login',async (user) => {
console.log('socket login!');
const {name} = user;
if (!name) {
return;
}
socket.name = name;
const roomInfo = {};
// 初始化socketId
await updatehCache('socketId', name, socket.id);
for(let i = 0; i < roomList.length; i++) {
const roomid = roomList[i];
const key = `${name}-${roomid}`;
// 循环全部房间
const res = await findOne({username: key});
const count = await getCacheById(key);
if(res) {
// 数据库查数据, 若缓存中没有数据,更新缓存
if(+count === 0) {
updateCache(key, res.roomInfo);
}
roomInfo[roomid] = res.roomInfo;
} else {
roomInfo[roomid] = +count;
}
}
// 通知本身有多少条未读消息
socket.emit('count', roomInfo);
});
复制代码
用户从离线变成在线状态,创建socket链接时候,会发送一个login事件, 服务端就会去查询当前用户的未读消息状况,从MongoDB和Redis分别查询,若Redis中没有数据,则像数据库查询。
客户端在加入房间说话会发送一个room事件,如下是后端逻辑
// 加入房间
socket.on('room', async (user) => {
console.log('socket add room!');
const {name, roomid} = user;
if (!name || !roomid) {
return;
}
socket.name = name;
socket.roomid = roomid;
if (!users[roomid]) {
users[roomid] = {};
}
// 初始化user
users[roomid][name] = Object.assign({}, {
socketid: socket.id
}, user);
// 初始化user
const key = `${name}-${roomid}`;
await updatehCache('socketId', name, socket.id);
// 进入房间默认置空,表示所有已读
await resetCacheById(key);
// 进行会话
socket.join(roomid);
const onlineUsers = {};
for(let item in users[roomid]) {
onlineUsers[item] = {};
onlineUsers[item].src = users[roomid][item].src;
}
io.to(roomid).emit('room', onlineUsers);
global.logger.info(`${name} 加入了 ${roomid}`);
});
复制代码
服务端接收到客户端发送的room事件,来重置该用户房间内的未读消息,而且该用户加入房间列表。
客户端在加入房间说话会发送一个message事件,如下是后端逻辑
socket.on('message', async (msgObj) => {
console.log('socket message!');
//向全部客户端广播发布的消息
const {username, src, msg, img, roomid, time} = msgObj;
if(!msg && !img) {
return;
}
... // 此处为向数据库存入消息
const usersList = await gethAllCache('socketId');// 全部用户列表
usersList.map(async item => {
if(!users[roomid][item]) { // 判断是否在房间内
const key = `${item}-${roomid}`
await inrcCache(key);
const socketid = await gethCacheById('socketId', item);
const count = await getCacheById(key);
const roomInfo = {};
roomInfo[roomid] = count;
socket.to(socketid).emit('count', roomInfo);
}
})
复制代码
此步骤略微复杂,主要是房间中的用户发送消息,须要通过判断,哪部分用户须要计数,哪部分用户不须要计数,从图中能够看出,不在房间内的用户都须要计数。
接下来还须要推送,那么哪些用户须要实时地推送呢,对的,就是那些在线用户而且不在房间内的用户。所以在这里也须要一个判断。
这样就完美了,可以精确地给用户增长计数,而且精确地推送给须要的用户。
在线演示: www.qiufengh.com/
github地址: github.com/hua1995116/…
若是有什么建议或者疑问能够加入微信群进行探讨。