前言
這個專案本來是我學生時代為了找工作的一個練手專案,但是沒想到受到了很多的關注,star也快要破K了,這也激勵著我不斷去完善他,一方面是得對得起關注學習的人,另一方面也是想讓自己能過通過慢慢完善一個專案來讓自己提高。
今天給大家帶來的是基於Websocket+Node+Redis未讀訊息功能,可能更加偏向於實戰方向,需要對Websocket和Node有一些瞭解,當然不瞭解也可以看看效果,效果連結( www.qiufengh.com/ )說不定會激起你學習的動力~
下面我通過自己思考的方式來進行講解,程式碼可能講的不多,但是核心邏輯都進行了講解,上面也有github地址,有興趣的可以進行詳細地檢視。自己的idea或多或少會有一些不成熟,但是我還是厚著臉皮出來拋頭露臉,如果有什麼建議還請大家多多提出,能讓我更加完善這個作品。
設計
首先對於訊息未讀,大家都很熟悉,就是各種聊天的時候,出現的紅點點,且是強迫症者必須清理的一個小點點,如?所示。我會帶大家實現一個這樣的功能。
由於一對一的方式更加簡單,我現在只考慮多對多的情況,也就是在一個房間(也可以稱為群組,後面都以房間稱呼)中的未讀訊息,那麼設計這樣的一個功能,首相我將它分成了3種使用者。
- 離線使用者
- 線上使用者
- 線上使用者且進入群組的使用者
離線使用者
這種場景就相當於我們退出微信,但是別人在房間裡發的訊息,當我們再次開啟的時候依然能夠看到房間增長的未讀訊息。
線上使用者
這種場景就是相當我們停留在聊天列表頁面,當他人在房間中傳送訊息,我們能夠實時的看到未讀訊息的條數在增長。
場景示例。
線上使用者且在房間的使用者
這種場景其實就比較普通了,當別人傳送新的訊息,我們就能實時看到,此時是不需要標記未讀訊息的。
場景示例。
流程圖
主要流程可以簡化為三個部分,分別為使用者,推送功能,訊息佇列。
使用者可以是訊息提供者也可以是訊息接受者。以下就是這個過程。
當然在這個過程中涉及比較複雜的訊息的儲存,如何推送,獲取,同步等問題,下面就是對這個過程進行詳細的描述
圖上的流程解釋
A. 儲存在Node快取中的房間使用者列表(此處資訊也可以存在Redis中)
B. 儲存在Redis中的未讀訊息列表
C. 儲存在MongoDB中的未讀訊息列表
- 使用者1進入首頁。
- 使用者1進入房間,重置使用者在房間1的未讀訊息,觸發更新模組去更新B未讀訊息列表。
- 使用者1向向房間B中傳送了一條訊息。
- 後端需要去獲取房間使用者列表,判斷使用者是否在房間?
- 是,因為在房間中的使用者已經讀取了最新訊息,不需要進行計數。
- 否,若使用者不在房間中,更新其的未讀訊息計數
- 從快取中獲取使用者的訊息進行分發。
- 使用者2登入我們的專案,從離線使用者變成了線上使用者。
- 使用者2登入時,觸發查詢模組,去獲取其當前在各個房間未讀訊息情況。
- 查詢模組去查詢Redis中的未讀訊息,若Redis中沒有資料,會繼續向資料庫中查詢,若沒有則返回0給使用者。
- Redis快取將會每分鐘和資料庫同步一次,保證資料的持久化。
環境
-
Node: 8.5.0 +
-
Npm: 5.3.0 +
-
MongoDB
-
Redis
為什麼是redis ?
介紹
Redis 是網際網路技術領域使用最為廣泛的儲存中介軟體,它是「Remote Dictionary Service」的首字母縮寫,是一個高效能的key-value資料庫。具有效能極高,豐富的資料型別,原子,豐富的特性等優勢。
redis 具有以下5種資料結構
- String——字串
- Hash——字典
- List——列表
- Set——集合
- Sorted Set——有序集合
想要深入瞭解這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結構。
- socketId
- username - socketid
例子:
- socketId
- hua1995116 - En4ilYqDpk-P5_tzAAAG
MongoDB
本專案一開始就使用了MongoDB,Node天然搭配的MongoDB的優勢,這裡就不再進行講解,Node操作MongoDB的模組叫做mongoose,具體的引數方法,可以檢視官方文件。
MongoDB下載地址
視覺化下載地址
websocket + node 實現
下面我們通過一開始的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/…
如果有什麼建議或者疑問可以加入微信群進行探討。