訊息未讀之點不完的小紅點(Node+Websocket)

藍色的秋風發表於2019-02-16

前言

github.com/hua1995116/…

這個專案本來是我學生時代為了找工作的一個練手專案,但是沒想到受到了很多的關注,star也快要破K了,這也激勵著我不斷去完善他,一方面是得對得起關注學習的人,另一方面也是想讓自己能過通過慢慢完善一個專案來讓自己提高。

訊息未讀之點不完的小紅點(Node+Websocket)

今天給大家帶來的是基於Websocket+Node+Redis未讀訊息功能,可能更加偏向於實戰方向,需要對Websocket和Node有一些瞭解,當然不瞭解也可以看看效果,效果連結( www.qiufengh.com/ )說不定會激起你學習的動力~

下面我通過自己思考的方式來進行講解,程式碼可能講的不多,但是核心邏輯都進行了講解,上面也有github地址,有興趣的可以進行詳細地檢視。自己的idea或多或少會有一些不成熟,但是我還是厚著臉皮出來拋頭露臉,如果有什麼建議還請大家多多提出,能讓我更加完善這個作品。

設計

首先對於訊息未讀,大家都很熟悉,就是各種聊天的時候,出現的紅點點,且是強迫症者必須清理的一個小點點,如?所示。我會帶大家實現一個這樣的功能。

訊息未讀之點不完的小紅點(Node+Websocket)

由於一對一的方式更加簡單,我現在只考慮多對多的情況,也就是在一個房間(也可以稱為群組,後面都以房間稱呼)中的未讀訊息,那麼設計這樣的一個功能,首相我將它分成了3種使用者。

  • 離線使用者
  • 線上使用者
  • 線上使用者且進入群組的使用者

離線使用者

這種場景就相當於我們退出微信,但是別人在房間裡發的訊息,當我們再次開啟的時候依然能夠看到房間增長的未讀訊息。

線上使用者

這種場景就是相當我們停留在聊天列表頁面,當他人在房間中傳送訊息,我們能夠實時的看到未讀訊息的條數在增長。

場景示例。 訊息未讀之點不完的小紅點(Node+Websocket)

線上使用者且在房間的使用者

這種場景其實就比較普通了,當別人傳送新的訊息,我們就能實時看到,此時是不需要標記未讀訊息的。

場景示例。 訊息未讀之點不完的小紅點(Node+Websocket)

流程圖

主要流程可以簡化為三個部分,分別為使用者,推送功能,訊息佇列。

使用者可以是訊息提供者也可以是訊息接受者。以下就是這個過程。

image

當然在這個過程中涉及比較複雜的訊息的儲存,如何推送,獲取,同步等問題,下面就是對這個過程進行詳細的描述

image

圖上的流程解釋

A. 儲存在Node快取中的房間使用者列表(此處資訊也可以存在Redis中)

B. 儲存在Redis中的未讀訊息列表

C. 儲存在MongoDB中的未讀訊息列表

  1. 使用者1進入首頁。
  2. 使用者1進入房間,重置使用者在房間1的未讀訊息,觸發更新模組去更新B未讀訊息列表。
  3. 使用者1向向房間B中傳送了一條訊息。
  4. 後端需要去獲取房間使用者列表,判斷使用者是否在房間?
  5. 是,因為在房間中的使用者已經讀取了最新訊息,不需要進行計數。
  6. 否,若使用者不在房間中,更新其的未讀訊息計數
  7. 從快取中獲取使用者的訊息進行分發。
  8. 使用者2登入我們的專案,從離線使用者變成了線上使用者。
  9. 使用者2登入時,觸發查詢模組,去獲取其當前在各個房間未讀訊息情況。
  10. 查詢模組去查詢Redis中的未讀訊息,若Redis中沒有資料,會繼續向資料庫中查詢,若沒有則返回0給使用者。
  11. 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

www.cnblogs.com/jaign/artic…

mac

brew install redis

ubuntu

apt-get install redis

redhat

yum install redis

centos

www.cnblogs.com/zuidongfeng…

執行客戶端

redis-cli

視覺化工具安裝

windows

pan.baidu.com/s/1kU8sY3P

mac

pan.baidu.com/s/10vpdhw7Y…

原始碼編譯

docs.redisdesktop.com/en/latest/i…

專案中的資料結構

在本專案中我們用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,具體的引數方法,可以檢視官方文件。

mongoosejs.com/docs/4.x/in…

MongoDB下載地址

www.mongodb.com/download-ce…

視覺化下載地址

github.com/mrvautin/ad…

websocket + node 實現

下面我們通過一開始的3種使用者的場景來具體說明實現的程式碼。

離線使用者變成線上使用者

image

客戶端在登入時會傳送一個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中沒有資料,則像資料庫查詢。

線上使用者進入房間

image

客戶端在加入房間說話會傳送一個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事件,來重置該使用者房間內的未讀訊息,並且該使用者加入房間列表。

在房間中的使用者傳送訊息

image

客戶端在加入房間說話會傳送一個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/…

如果有什麼建議或者疑問可以加入微信群進行探討。

訊息未讀之點不完的小紅點(Node+Websocket)

更多請關注

訊息未讀之點不完的小紅點(Node+Websocket)

相關文章