Next.js通过Socket.io和redis实现消息推送模块(三)socket.io结合redis统计当前在线用户数量

1,186 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言:

最近在完成大三的一个课程期末设计,独立完成做了一个博客社区,主要技术栈是:

前端:next.js + mobx + ts + antd;

后台管理系统:vue3.0 + pinia + ts + elementUI

后端:next.js + ts + 阿里云oss存储 + redis

开发的时候遇到了很多坑,后面会通过文章的方式总结自己在开发过程中踩到的坑以及一些小经验。

由于博客社区嘛,像咱们大掘金平台,是有系统消息、点赞消息啥的等信息推送,然后我也想要做一个横幅(就是系统维护中,或者是出了什么问题,想要用户感知到,所以就想做一个系统消息推送),一开始通过websocket来实现,但是next.js这个框架似乎不太支持,而是支持了一个封装websocket的socket.io这个第三方依赖。所以就使用了socket.io来进行消息推送和在线人数统计的操作,希望大家喜欢~

前面的文章大家也可以看看~:

本系列第一篇:juejin.cn/post/709946… 本系列第一篇:juejin.cn/post/709966…

一、需求

需求很简单,统计在线人数,肯定要用到websocket的,在next.js中如何使用封装websocket的socket.io在之前的文章里面已经讲解如何使用了,大家可以看一下juejin.cn/post/709946…

并且还要标识每一个用户,由于这个博客是有访客和正式用户两种,所以我们用来标识用户的user_id的话,如果是正式用户就是我们数据库中user表的id,如果是访客模式的话,我们就要定义一个唯一的id(我在系统中是通过时间戳 new Date().getTime() + 六位随机数的方式)

之后就是在每次用户“上线”的时候发送给server.js一个事件,就是告诉浏览器哪个用户上线了,并加入到一个数组中,然后用户离线的时候就会触发socketio自带的disconnect事件,然后就把用户从这个数组删掉,我们就获取这个数组的长度就可以拿到当前在线用户的数量了。

就有问题: 1、用户上线我们怎么知道? 2、这个存储在线用户的数组怎么定义? 3、disconnect事件监听到用户离线后 要做什么?

解决问题: 1、用户上线可以参考,我之前写的文章,就是监听浏览器的visibility这个事件juejin.cn/post/709946… 也就是:用户打开页面的时候。浏览器监听到visibility事件的时候

那你可能会问了,如果用户把页面切到小窗口(没有关掉页面),再打开页面也会执行visibility事件也,这样不就会重复的统计当前在线人数了吗,这个就涉及到我们redis数据结构设计了,其实就是使用一个set数组(数组内值不能重复),然后传递userId给这个set数组。

问题二和问题三,数据结构怎么定义,disconnect要调用什么操作,就看下面的二、redis数据结构设计就行

二、redis数据结构设计

关于怎么在next.js中使用redis可以参考我之前分享的这篇文章~juejin.cn/post/709715…

我们通过userId来标识每一个不同的用户,并且由于是在线用户的统计,但是每一个用户可能会发送给服务器多次“我在线”通知,所以就不能重复的统计同一个用户,所以我们选用redis中的set数组(不能出现重复值的数组),具体操作方法参考www.runoob.com/redis/redis… 下面我们直接上手操作。

如果用户上线了:

await redis.sadd('s_online_user' , userId)

也就是给这个set数组添加用户userId就行。如果这个set结构里面已经有这个userId了就不会重复添加

如果用户下线了,我们直接把这个值从set数组里面直接移除就行了

await redis.srem('s_online_user' , userId)

如果要统计当前在线人数的话,直接:

const allOnlineNum = await redis.scard('s_online_user')

对于上面的sadd、srem、scard方法的解释可以参考www.runoob.com/redis/redis…

三、前后端代码实现

3.1、后端代码实现

有了需求和数据结构设计了,下面我们就直接上前后端实现的代码,之后再进行解释,let is go!

Next.js的server.js实现:

// 参考medium的代码
const app = require('express')()
const server = require('http').Server(app)
const io = require('socket.io')(server, {cors:true})
const next = require('next')
const Redis = require('ioredis')
const redis = new Redis(6379)

const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = 3000;
const nextApp = next({ dev, hostname, port })
const nextHandler = nextApp.getRequestHandler()

let socketPort = 3000

io.on('connect', socket => {
  socket.on('clientOnline', async (userId) => {
    console.log('用户上线了')
    if (userId) {
      await redis.sadd('s_online_user' , userId)
    }
  })
  socket.on('disconnect', async (data) => {
    console.log('用户关闭了', socket.id)
    const userId = await redis.hget('user_socketUserId', socket.id)
    await redis.srem('s_online_user' , userId)
  })
  socket.on('clientConnect', async (userId) => {
    // 存在redis的 key value数据结构中
    await redis.hset('user_socketUserId', socket.id, userId)
  })
})

nextApp.prepare().then(() => {

  app.get('*', (req, res) => {
    return nextHandler(req, res)
  })
  app.post('*', (req, res) => {
    return nextHandler(req, res)
  })

  server.listen(socketPort, err => {
    if (err) throw err
    console.log(`socket io ready on http://localhost:${port}`)
  })
})


解释:我们在next.js的server自定义的服务器,关于怎么自定义服务器可以参考我之前分享的文章: juejin.cn/post/709946…

有一个问题,就是server.js监听用户socket.io断开是一个内置的disconnect事件,并不是用户前端emit的,所以disconnect就无法从前端用户获取到信息(userId),这样就完不成我们说的“用户退出,服务器监听到disconnect事件,然后把用户的userId从redis的set结构中移除”,那我们要如何在disconnect中获取到用户id呢。 有!

我们在socket中其实是通过socket.id用来标识不同的连接的,也就是说,每一个socket.id其实就可以代表一个用户了。所以我们可以 ** 在一开始用户和socket.io服务器进行连接的时候传入userId,来把这个连接的socket.id和用户的userId存起来,通过一个键值对的方式,key是socket.id,value是userId,然后我们就可以在浏览器触发disconnect的时候通过当前断开连接的socket.io映射得到userId就ok了

代码中,我们通过:redis的哈希表的方式来存储这个key value

await redis.hset('user_socketUserId', socket.id, userId)

具体的redis中哈希表的操作可以参考:www.runoob.com/redis/redis…

然后在disconnect的时候:

 const userId = await redis.hget('user_socketUserId', socket.id)
 await redis.srem('s_online_user' , userId)

这样的话就解决了我们的问题了

3.2、前端代码实现

我们在之前Layout.ts组件代码基础上进行修改,具体layout.ts里面的代码可以参考我之前的文章:juejin.cn/post/709946…

import { useEffect } from "react";
import { observer } from "mobx-react-lite"
import io from 'socket.io-client'
import { notification } from 'antd';

var socket : any
const Layout = ({ children } : any) => {
  const randomUserId = new Date().getTime() + Math.floor(Math.random()*Math.floor(6))
  
  const clineOnline = () => {
    console.log('上线', userId)
    if (userId) {
      socket.emit('clientOnline', userId)
    } else {
      socket.emit('clientOnline', randomUserId)
    }
  }  
  
  useEffect(() => {
    if (!socket) {
      socket = io('http://localhost:3000')
    }
    if (userId) {
      socket.emit('clientConnect', userId)
    } else {
      socket.emit('clientConnect', randomUserId)
    }
    
    document.addEventListener('visibilitychange',function(){
      var isHidden = document.hidden;
      if(!isHidden){
        clineOnline()
      }
    })
    
 }, [])
   
return (
    <div>
        Layout container
    </div>
  )
}

export default observer(Layout)

搞定,我们在初始化socket的时候就给server.js的socket.io服务器发送clientConnect事件,并且带着用户端的唯一标识userId(如果是访客的话就生成一个随机的不重复id就行)

然后监听浏览器的visibilitychange事件判断用户是否正在查看当前网站

四、下一节内容

《next.js使用socket.io结合redis给指定的用户推送消息,并且进行在线和离线的两种操作方法,在线就实时推送,不在线就存在redis里面,等到用户下次上线的时候发送通知》敬请期待~