基於Socket.io的Web聊天

最近作了聊天,功能點大概以下:

  1. 羣聊
  2. @全員
  3. 羣主禁言
  4. 聊天表情😊

技術選擇

  1. 基於Socket.io作技術基礎
  2. 基於node-redis作聊天消息緩存
  3. 基於node-schedule定時存儲Redis中的聊天消息

服務器端

const server = require('http').createServer();

const io = require('socket.io')(server, {
  path: '/socket/chat',
  serveClient: false
});

server.listen(3000);
複製代碼

客戶端

  • 若有須要代理/socket/chat/path設置一致
  • 不設置path,就是默認的socket.io
proxy: {
    "/socket/chat/*": {
      "target": "ws://localhost:8000/socket/chat",
      "ws": true
    },
}
複製代碼
  • 鏈接socket
const ws = io('localhost:4000', {
  path: '/socket/chat',
  transports: ['websocket'],
  reconnection: true,
});

ws.on('connect', () => {});

ws.on('error', error => {
  console.log(error);
});

ws.on('disconnect', reason => {
  console.log('disconnect', reason);
});

window.addEventListener('beforeunload', () => {
  ws.emit('client:disconnect');
});
複製代碼
  1. Web端加入房間
// Web端
socket.emit('join', id);
// Server端
socket.on('join', roomId => {
    socket.join(roomId, error => {
      if (error) {
        server.log([...errorTags, 'join'], { roomId, error });
      }
      socket.on('user-send', listener);
    });
});
複製代碼
  1. 進入房間後,監聽Server端的消息
// Web端
socket.on('chat:room:server-send', this.handleSocket);
// Server端
socket.to(msg.roomId).emit('server-send', newMsg);
複製代碼
  1. 聊天消息存儲到Redis
const key = `CHAT:ROOM:${id}`;
await redis.ZADDAsync(key, msg.time, JSON.stringify(msg));
複製代碼
  1. 分頁獲取Redis消息
// Web端
// 獲取歷史消息
socket.emit(
  'room.msg:list',
  { roomId: id, ps, pn, isBottom: true },
  this.handleHistoryMessage
);
// Server端
socket.on(
    'room.msg:list',
    async ({ roomId, ps = 20, pn = 1 }, fn) => {
      const key = getKey('CHAT:MSG', roomId);

      const data = await redis.zrevrangebyscoreAsync(
        key,
        '+inf',
        '-inf',
        'LIMIT',
        (pn - 1) * ps,
        ps
      );
      const list = data.map(it => JSON.parse(it));
      fn({ list, pn, isBottom });
    }
  );
複製代碼
  1. 定時存儲到Mongo
  • 定時存儲,保持Redis中一直存儲1e3條之內的數據
  • 超過1e3條的數據,會定時同步到Mongo,而後在Redis中刪除
  • 每次同步1e3條
const start = 1e3;
const count = 1e3;
const chat2DB = async () => {
    const list = await redis.keysAsync('CHAT:ROOM:*');
    list.forEach(async key => {
        const msgList = await redis.zrevrangebyscoreAsync(
            key,
            '+inf',
            '-inf',
            'LIMIT',
            start,
            count
        );
        if (msgList.length) {
            const bulk = msgList.map(it => JSON.parse(it));
            await chatModel.create(...bulk, (err) => {
                if (err) {
                    // looger('打印錯誤平常,同步失敗');
                }
                redis.ZREMAsync(key, ...msgList);
            });
        }
    });
}


var schedule = require('node-schedule');

const rule = '0 */1 * * * *'; // 每分鐘執行一次
schedule.scheduleJob(rule, chat2DB);
複製代碼
  1. 表情未完待續...
  2. socket創建太多帶來的單機使用問題...
  3. @全員,單獨創建一個監聽事件
// Web端
socket.on('chat:@all:server:send', () => {
      message.info('有人@你');
});

// Server
socket.to(msg.roomId).emit('chat:@all:server:send', msg);
複製代碼

相關方法

  1. 初始化歷史消息或者每次接受到聊天消息時,自動滾動到底部,查看最新消息
// 滾動到底部
scrollTop = () => {
    setTimeout(() => {
      this.newsCon.scrollTop = 100000000;
    }, 200);
};
複製代碼
  1. 上滑加載更多歷史消息
_onScrollEvent = () => {
    const { id } = this.props;
    let { pn } = this.state;
    if (this.newsCon.scrollTop === 0) {
      pn += 1;
      socket.emit(
        'room.msg:list',
        { roomId: id, ps, pn },
        this.handleHistoryMessage
      );
    }
  };
 // React中div
<div
  className="news-con"
  ref={e => {
    this.newsCon = e;
  }}
  onScrollCapture={debounce(this._onScrollEvent, 100)}
>
</div>
複製代碼

相關技術學習文章

End

  • 第一次作聊天相關技術,有任何問題歡迎交流
相關文章
相關標籤/搜索