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

前言

https://github.com/hua1995116... html

這個項目原本是我學生時代爲了找工做的一個練手項目,可是沒想到受到了不少的關注,star也快要破K了,這也激勵着我不斷去完善他,一方面是得對得起關注學習的人,另外一方面也是想讓本身能過經過慢慢完善一個項目來讓本身提升。node

今天給你們帶來的是基於Websocket+Node+Redis未讀消息功能,可能更加偏向於實戰方向,須要對Websocket和Node有一些瞭解,固然不瞭解也能夠看看效果,效果連接( https://www.qiufengh.com/ )說不定會激起你學習的動力~git

下面我經過本身思考的方式來進行講解,代碼可能講的很少,可是核心邏輯都進行了講解,上面也有github地址,有興趣的能夠進行詳細地查看。本身的idea或多或少會有一些不成熟,可是我仍是厚着臉皮出來拋頭露臉,若是有什麼建議還請你們多多提出,能讓我更加完善這個做品。github

設計

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

因爲一對一的方式更加簡單,我如今只考慮多對多的狀況,也就是在一個房間(也能夠稱爲羣組,後面都以房間稱呼)中的未讀消息,那麼設計這樣的一個功能,首相我將它分紅了3種用戶。redis

  • 離線用戶
  • 在線用戶
  • 在線用戶且進入羣組的用戶

離線用戶

這種場景就至關於咱們退出微信,可是別人在房間裏發的消息,當咱們再次打開的時候依然可以看到房間增加的未讀消息。mongodb

在線用戶

這種場景就是至關咱們停留在聊天列表頁面,當他人在房間中發送消息,咱們可以實時的看到未讀消息的條數在增加。數據庫

場景示例。ubuntu

在線用戶且在房間的用戶

這種場景其實就比較普通了,當別人發送新的消息,咱們就能實時看到,此時是不須要標記未讀消息的。windows

場景示例。

流程圖

主要流程能夠簡化爲三個部分,分別爲用戶,推送功能,消息隊列。

用戶能夠是消息提供者也能夠是消息接受者。如下就是這個過程。
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種存儲結構能夠查看http://www.runoob.com/w3cnote/redis-use-scene.html

安裝

windows

http://www.cnblogs.com/jaign/...

mac

brew install redis

ubuntu

apt-get install redis

redhat

yum install redis

centos

https://www.cnblogs.com/zuido...

運行客戶端

redis-cli

可視化工具安裝

windows

https://pan.baidu.com/s/1kU8sY3P

mac

https://pan.baidu.com/s/10vpd...

源碼編譯

http://docs.redisdesktop.com/...

項目中的數據結構

在本項目中咱們用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,具體的參數方法,能夠查看官方文檔。

https://mongoosejs.com/docs/4.x/index.html

MongoDB下載地址

https://www.mongodb.com/downl...

可視化下載地址

https://github.com/mrvautin/a...

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);
    }
})

此步驟略微複雜,主要是房間中的用戶發送消息,須要通過判斷,哪部分用戶須要計數,哪部分用戶不須要計數,從圖中能夠看出,不在房間內的用戶都須要計數。

接下來還須要推送,那麼哪些用戶須要實時地推送呢,對的,就是那些在線用戶而且不在房間內的用戶。所以在這裏也須要一個判斷。

這樣就完美了,可以精確地給用戶增長計數,而且精確地推送給須要的用戶。

後記

在線演示: https://www.qiufengh.com/

github地址: https://github.com/hua1995116...

若是有什麼建議或者疑問能夠加入微信羣進行探討。

相關文章
相關標籤/搜索