這個項目原本是我學生時代爲了找工做的一個練手項目,可是沒想到受到了不少的關注,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/…
若是有什麼建議或者疑問能夠加入微信羣進行探討。