SpringBoot+SpringCloud+Netty打造分布式在线消息推送服务(实例)

4,295 阅读15分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第28天,点击查看活动详情

前言

其实关于这个的话,我先前的几篇博文: SpringBoot+Netty+Vue+Websocket实现在线推送/聊天系统

实用水文篇--SpringBoot整合Netty实现消息推送服务器

其实已经说的非常明白了,但是每想到后台还是有人找我要代码,因为完整的代码其实在博文里面都是贴出来了的,当然前面的博文我都是给一个类似于脚手架的东西,没有给出实际的案例。那么今天也是直接给出代码的案例吧,这里再次声明,所有的代码都是在这两篇博文里面有的,如果做了修改在本文当中会给出提示。此外的话,我们的项目是完全开源的,但是在开发阶段不开源,因为有一些铭感信息。在开发完成之后开源,这里请见谅,当然也是为什么我没有办法给出源码的原因(暂时)也是为什么你可以在我的那两篇博文看到完整的代码信息的原因。

Tips: 在我的频道只会告诉你怎么做饭,不会把饭菜做好了再给你,如果需要,那是另外“价格”。

那么废话不多说,我们开始。

技术架构

我们今天来看到我们的一个案例。首先是我们的技术架构: 在这里插入图片描述

那么在我们今天的话是这样的:

  1. 用户上传博文
  2. 博文通过之后发送审核消息给前端
  3. 前端对消息进行展示

效果图

这个是我们上传博文,博文通过之后会看到消息有个提示。 在这里插入图片描述

之后到具体的页面可以看到消息 在这里插入图片描述 因为图片是中午截取的,有个小bug没有调整,但是不要在意这些,这个bug是很简单的,因为一开始我这边做过初始化,没有清除缓存罢了。

后端项目

之后的话来看到我们的后端的一个服务情况。 在这里插入图片描述

我们要进行互动的服务就是这三个,一个是网关,一个是博文的服务,还有就是我们的消息服务器。因为我们是用户上传成功后再显示的。

那么关于博客的模块的话在这块有完整的说明: SpringBoot + Vue实现博文上传+展示+博文列表

我们这边做的就是一个系列。当然只是演示实际上,你用先前我给出的代码是完全可以实现效果的,我们这边只是说用那一套代码来真正做我们具体的业务。

消息数据定义

存储结构

那么在开始之前的话,我们这边对我们的消息服务器设计了对应的数据库用来存储消息。

这一块看你自己,我们这边肯定是要的。 在这里插入图片描述

那么我们这次的案例需要使用到的表是这个表: 在这里插入图片描述

消息状态

之后的话,我们需要对我们的消息进行定义。 我们在这里对消息的状态做出如下定义:

  1. 消息具备两者状态,针对两个情况
  2. 签收状态,即,服务器确定用户在线,并且将消息传输到了客户端,为签收状态。
  3. 阅读状态,在保证已签收的情况下,用户是否已经阅读消息,这部分的逻辑有客户端代码处理。
  4. 对应未签收的消息,用户上线时,请求服务器是否存在未签收的消息,如果有,进行统一读取,存储到本地
  5. 对于未读消息,主要是对用户的状态进行一个判断,消息已经缓存到用户本地。

那么此时的话,我们就已经说清楚了这个。在我们的数据库里面status这个字段就是用来判断用户是不是签收了消息的。至于用户到底有没有读取消息,那么完全就是客户端需要做的判断了。

当然你也可以设计为全部由服务端来处理。

Netty消息服务

项目结构

ok,说完了这个的话,我们再来看到我们的消息服务端是怎么处理的。

首先我们依然是和先前的博文一样,保留了先前的东西。 但是我们这边多了Controller,和Dao层。 在这里插入图片描述 那么在这边的话,我们需要关注的只有这几个东西: 在这里插入图片描述

这几个东西就是我们来实现前面的效果的实际的业务代码。

除了这些当然还有我们的Dao,但是这个是根据你的业务来的,这里我就不展示了,类比嘛。

改动

那么说完了这些,我们来看到和先前的代码有哪些改动的东西。

消息bean

首先是我们的消息的改动。 在这里插入图片描述

@AllArgsConstructor
@NoArgsConstructor
@ToString
/**
 * 由于我们这边Netty处理的消息只有注册,所以话这里只需要
 * 保留action和userid即可
 * */
public class DataContent implements Serializable {
    private Integer action;
    private String userid;
}

那么我们的消息的类型是这样的:

public enum MessageActionEnum {

    //定义消息类型

    CONNECT(1,"第一次(或重连)初始化连接"),
    CHAT(2,"聊天消息"),
    SIGNED(3,"消息签收"),
    KEEPALIVE(4,"客户端保持心跳"),
    PULL_FRIEND(5, "拉取好友"),
    HOLEADUITMSG(6,"审核消息");


    public final Integer type;
    public final String content;
    MessageActionEnum(Integer type,String content) {
        this.type = type;
        this.content = content;
    }

}

消息处理器

既然我们的这个消息类型变了,那么我们的这个代码也变了: 在这里插入图片描述

@Component
@ChannelHandler.Sharable

public class ServerListenerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private static final Logger log = LoggerFactory.getLogger(ServerBoot.class);
    static {
        //先初始化出来
        UserConnectPool.getChannelMap();
        UserConnectPool.getChannelGroup();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        String content = msg.text();
        /**获取客户端传过来的消息*/
        DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
        assert dataContent != null;
        Integer action = dataContent.getAction();
        Channel channel =  ctx.channel();
        /**
         * 根据消息类型对其进行处理,我们这里只做两个事情
         * 1. 注册用户
         * 2. 心跳在线
         * */
        if(Objects.equals(action, MessageActionEnum.CONNECT.type)){
            /**
             * 2.1 当websocket 第一次 open 的时候,
             * 初始化channel,把用的 channel 和 userid 关联起来
             * */
            String userid = dataContent.getUserid();
            AttributeKey<String> key = AttributeKey.valueOf("userId");
            ctx.channel().attr(key).setIfAbsent(userid);
            UserConnectPool.getChannelMap().put(userid,channel);
            UserConnectPool.output();

        } else if(Objects.equals(action, MessageActionEnum.KEEPALIVE.type)){
            /**
             * 心跳包的处理
             * */

            System.out.println("收到来自channel 为["+channel+"]的心跳包"+dataContent);
            channel.writeAndFlush(
                    new TextWebSocketFrame(
                            JsonUtils.objectToJson(R.ok("返回心跳包").
                                    put("type", MessageActionEnum.KEEPALIVE.type))
                    )
            );
            System.out.println("已返回消息");

        }

    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //接收到请求
        log.info("有新的客户端链接:[{}]", ctx.channel().id().asLongText());
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        ctx.channel().attr(key).setIfAbsent("temp");
        UserConnectPool.getChannelGroup().add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        String chanelId = ctx.channel().id().asShortText();
        log.info("客户端被移除:channel id 为:"+chanelId);
        removeUserId(ctx);
        UserConnectPool.getChannelGroup().remove(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        //发生了异常后关闭连接,同时从channelgroup移除
        ctx.channel().close();
        removeUserId(ctx);
        UserConnectPool.getChannelGroup().remove(ctx.channel());

    }

    /**
     * 删除用户与channel的对应关系
     */
    private void removeUserId(ChannelHandlerContext ctx) {
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        String userId = ctx.channel().attr(key).get();
        UserConnectPool.getChannelMap().remove(userId);
    }
}

这个就是我们核心的消息处理器。

那么其他的关于Netty的玩意我压根没有改动。

消息转换pojo工具

这里还有咱们的消息转换的工具类。这个的话,我也给一下:

public class JsonUtils {

    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 将对象转换成json字符串。
     * <p>Title: pojoToJson</p>
     * <p>Description: </p>
     * @param data
     * @return
     */
    public static String objectToJson(Object data) {
        try {
            String string = MAPPER.writeValueAsString(data);
            return string;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json结果集转化为对象
     *
     * @param jsonData json数据
     * @param beanType 对象类型
     * @return
     */
    public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
        try {
            T t = MAPPER.readValue(jsonData, beanType);
            return t;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json数据转换成pojo对象list
     * <p>Title: jsonToList</p>
     * <p>Description: </p>
     * @param jsonData
     * @param beanType
     * @return
     */
    public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) {
        JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list = MAPPER.readValue(jsonData, javaType);
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

}

前面应该是有给的。

重点就是咱们的这个Controller这一块。

审核消息处理

controller

我们首先来看到我们的Controller

@RestController
@RequestMapping("/message/holeAduit")
public class HoleAduitMsgController {

    @Autowired
    HoleAduitMsgService holeAduitMsgService;

    @PostMapping("/aduit")
    public R holeAduitMsg(@Validated @RequestBody HoleAduitMsgQ holeAduitMsgQ){
        return holeAduitMsgService.holeaduitMsg(holeAduitMsgQ);
    }
}

我们只看到这一个接口,因为其他的都是类似的。

那么这里的话我们还是需要一个请求的实体类的。 那么这个实体类的话是这个样子的:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class HoleAduitMsgQ {
    @NotEmpty
    private String userid;
    private String msg;
    private String msgtitle;
    private Long linkid;
    private Integer type;
}

这个实体类的话,是被我封装了这里: 在这里插入图片描述 因为我们是一个微服务,所以的话,对应的这个请求我们都是放在了第三方的一个包下面。 那么对于的还有咱们暴露出来的服务。


@FeignClient("message")
@RequestMapping("/message/holeAduit")
public interface FeignHoleAduitMsgService {

    @PostMapping("/aduit")
    public R holeAduitMsg(@RequestBody HoleAduitMsgQ holeAduitMsgQ);
}

之后的话,我们可以看到具体的实现类。

实现类

@Service
public class HoleAduitMsgServiceImpl implements HoleAduitMsgService {

    @Autowired
    HoleAuditService auditService;

    @Override
    public R holeaduitMsg(HoleAduitMsgQ holeAduitMsgQ) {
        //1.对消息进行存储,只要用户在线的话,我们就直接先给他签收一下
        String userid = holeAduitMsgQ.getUserid();
        Channel channel = UserConnectPool.getChannelFromMap(userid);
        HoleAuditEntity holeAuditEntity = new HoleAuditEntity();
        BeanUtils.copyProperties(holeAduitMsgQ,holeAuditEntity);
        holeAuditEntity.setCreateTime(DateUtils.getCurrentTime());

        if(channel!=null){
            //这边只是保证存在,双层保险,这个时候的话就是在线
            Channel realChannel = UserConnectPool.getChannelGroup().find(channel.id());
            if(realChannel!=null){
                holeAuditEntity.setStatus(1);
                //我们这边直接转发消息就好了,不需要再额外处理
                realChannel.writeAndFlush(
                        new TextWebSocketFrame(
                                JsonUtils.objectToJson(
                                        Objects.requireNonNull(R.ok().put("data", holeAuditEntity))
                                                .put("type", MessageActionEnum.HOLEADUITMSG.type)
                                )
                        )
                );
            }
        }

        //这里进行消息的存储
        auditService.save(holeAuditEntity);
        return R.ok();
    }
}

这里面的逻辑其实非常简单,就几个步骤。

1.接受请求 2.判断用户是否在线,在线推送,并保存设置为已签收(消息) 如果不在线,不进行推送,但是保存消息并设置为未签收

这里的话就是非常简单的。

服务调用

之后的话,就是我们的调用。我们的调用是在我们的博客服务进行调用的。

我们先看到我们完整的博客服务的实现类。

public class BlogUpServiceImpl implements BlogUpService {

    @Autowired
    FeignUserService feignUserService;
    @Autowired
    ContentService contentService;
    @Autowired
    FeignHeadimgService feignHeadimgService;
    @Autowired
    WordFilter wordFilter;
    @Autowired
    BlogService blogService;
    @Autowired
    FeignLogActicleService feignLogActicleService;
    @Autowired
    RedisUtils redisUtils;

    @Autowired
    FeignHoleAduitMsgService feignHoleAduitMsgService;

    private final static Double threshold = 0.05;
    /**
     * 接口对用户进行十分钟限制
     *  1.完成用户博文的上传
     *  2.存储用户博文,博文对应信息
     *  3.修改用户日志
     * */
    @Override
    public R blogUp(UpBlogEntity entity) {
        String userid = entity.getUserid();
        String backMessage = "success";
        //接口限流
        if(redisUtils.hasKey(RedisTransKey.getBlogUpKey(entity.getUserid()))){
            return R.error(BizCodeEnum.OVER_UPBLOG.getCode(), BizCodeEnum.OVER_UPBLOG.getMsg());
        }
        R info = feignUserService.info(userid);
        String userString = FastJsonUtils.toJson(info.get("user"));
        UserEntity user = FastJsonUtils.fromJson(userString, UserEntity.class);
        if(user!=null){
            String context = entity.getContext();
            String blogInfo = entity.getInfo();
            /**
             * 先对context和bloginfo进行校验,是否为存在不友好的信息
             * */
            int countContext = wordFilter.wordCount(context);
            int countInfo = wordFilter.wordCount(blogInfo);
            int status = 1;
            //博文的摘要过滤,只要摘要没有过,直接先打回去!
            if(countInfo>=blogInfo.length()*threshold){
                return R.error(BizCodeEnum.BAD_BLOGINFO.getCode(),BizCodeEnum.BAD_BLOGINFO.getMsg());
            }
            //博文内容的过滤
            if(countContext>=context.length()*threshold){
                //直接就是没有通过审核
                return R.error(BizCodeEnum.BAD_CONTEXT.getCode(),BizCodeEnum.BAD_CONTEXT.getMsg());
            }else if (countContext>0&&countContext<context.length()*threshold){
                backMessage="哇!您的提交直接通过了呢!";
            }else {
                status = 2;
                context = wordFilter.replace(context, '*');
                backMessage="您的提问已提交,正在等待审核哟!";
            }
            //预存储content
            ContentEntity contentEntity = new ContentEntity();
            contentEntity.setContent(context);
            contentEntity.setVersion("1.0");
            contentEntity.setCreateTime(DateUtils.getCurrentTime());
            contentService.save(contentEntity);
            Long contentid = contentEntity.getContentid();
            //预存储博文
            BlogEntity blogEntity = new BlogEntity();
            blogEntity.setBlogTitle(entity.getBlogTitle());
            blogEntity.setLevel(entity.getLevel());
            blogEntity.setBlogtype(entity.getBlogtype());
            //查询用户的头像信息
            R RHeadImg = feignHeadimgService.headimg(userid);
            String headImgString = FastJsonUtils.toJson(RHeadImg.get("headimg"));
            final HeadimgEntity headimg = FastJsonUtils.fromJson(headImgString, HeadimgEntity.class);
            if(headimg!=null){
                blogEntity.setUserImg(headimg.getImgpath());
            }
            blogEntity.setCreateTime(DateUtils.getCurrentTime());
            blogEntity.setUserNickname(user.getNickname());
            blogEntity.setUserid(userid);
            blogEntity.setStatus(status);
            blogEntity.setInfo(blogInfo);
            blogService.save(blogEntity);
            Long blogid = blogEntity.getBlogid();

            //完成正式存储
            contentEntity.setBlogid(blogid);
            blogEntity.setContentid(contentid);
            blogService.updateById(blogEntity);
            contentService.updateById(contentEntity);
            /**
             * 更新用户日志
             * */
            LogActicleEntity logActicleEntity = new LogActicleEntity();
            logActicleEntity.setAction(1);
            logActicleEntity.setUserid(userid);
            logActicleEntity.setArticleid(blogEntity.getBlogid());
            logActicleEntity.setArticleTitle(blogEntity.getBlogTitle());
            logActicleEntity.setCreteTime(blogEntity.getCreateTime());
            feignLogActicleService.save(logActicleEntity);

            /**
             * 发送消息
             * */
            if(status==1){
                /**
                 * 此时是直接通过了审核,那么直接进行发送
                 * 如果没有的话,那么就是后台通过审核由MQ发送消息
                 * */
                HoleAduitMsgQ holeAduitMsgQ = new HoleAduitMsgQ();
                holeAduitMsgQ.setMsg("您的博文"+blogEntity.getBlogTitle()+"直接通过了审核");
                holeAduitMsgQ.setMsgtitle("博文审核通过");
                holeAduitMsgQ.setUserid(user.getUserid());
                holeAduitMsgQ.setLinkid(blogid);
                holeAduitMsgQ.setType(1);
                feignHoleAduitMsgService.holeAduitMsg(holeAduitMsgQ);
            }

            /**
             * 设置标志
             */
            redisUtils.set(RedisTransKey.setBlogUpKey(entity.getUserid())
                    ,1,10, TimeUnit.MINUTES
            );
        }else{
            return R.error(BizCodeEnum.NO_SUCHUSER.getCode(),BizCodeEnum.NO_SUCHUSER.getMsg());
        }
        return R.ok(backMessage);
    }

}

这里面有注释,重点就是那个发送消息的。

到此的话,我们的后端就没啥事情了。

前端

之后的话就是我们的前端。

我们的前端主要是负责两件事情

  1. 向服务器注册(如果用户登录了的话)
  2. 保持心跳在线
  3. 接收服务器发送过来的消息,并保存,然后要通知用户

连接代码

首先是我们先前博文有说到的,我们的连接封装好的代码。

在这里插入图片描述 我这里是放在了socket包下面,其他的你们自己看着办。


// 导出socket对象
export {
  socket
}
import { Message } from 'element-ui'
// socket主要对象
var socket = {
  websock: null,
  /**
   * 这个是我们的ws的地址
   * */
  ws_url: "ws://localhost:9000/ws",

  userid: null,

  msgfunc: null,

  /**
   * 开启标识
   * */
  socket_open: false,
  /**
   * 心跳timer
   * */
  hearbeat_timer: null,
  /**
   * 心跳发送频率
   * */
  hearbeat_interval: 10000,
  /**
   * 是否开启重连
   * */
  is_reonnect: true,
  /**
   * 重新连接的次数
   * */
  reconnect_count: 3,
  /**
   * 当前重新连接的次数,默认为:1
   * */
  reconnect_current: 1,
  /**
   * 重新连接的时间类型
   * */
  reconnect_timer: null,
  /**
   * 重新连接的间隔
   * */
  reconnect_interval: 3000,

  /**
   * 登录后才进行连接
   * */

  /**
   * 初始化连接
   */
  init: () => {

    let loginToken = localStorage.getExpire("LoginToken");
    let userid = localStorage.getExpire("userid");
    if(loginToken==null && userid==null) {
      Message({
        message: '当前正在以游客身份访问',
        type: 'info',
      });
      return ;
    }
    if (!("WebSocket" in window)) {
      Message({
        message: '当前浏览器与网站不兼容丫',
        type: 'error',
      });
      console.log('浏览器不支持WebSocket')
      return null
    }

    // 已经创建过连接不再重复创建
    if (socket.websock) {
      return socket.websock
    }

    socket.websock = new WebSocket(socket.ws_url)
    socket.websock.onmessage = function (e) {
      socket.receive(e)
    }

    // 关闭连接
    socket.websock.onclose = function (e) {
      console.log('连接已断开')
      console.log('connection closed (' + e.code + ')')
      clearInterval(socket.hearbeat_interval)
      socket.socket_open = false

      // 需要重新连接
      if (socket.is_reonnect) {
        socket.reconnect_timer = setTimeout(() => {
          // 超过重连次数
          if (socket.reconnect_current > socket.reconnect_count) {
            clearTimeout(socket.reconnect_timer)
            return
          }

          // 记录重连次数
          socket.reconnect_current++
          socket.reconnect()
        }, socket.reconnect_interval)
      }
    }

    // 连接成功
    socket.websock.onopen = function () {
      Message({
        message: 'Welcome here',
        type: 'success',
      });
      let userid = localStorage.getExpire("userid");
      socket.userid = userid;
      console.log('连接成功')
      socket.socket_open = true
      socket.is_reonnect = true

      // 开启心跳
      socket.heartbeat()

      //注册用户
      let resit={
        "action": 1,
        "userid": userid
      }
      socket.send(resit)
    }
    // 连接发生错误
    socket.websock.onerror = function (err) {
      Message({
        message: '无法连接至服务器!',
        type: 'error',
      });
      console.log('WebSocket连接发生错误')
    }
  },
  /**
   * 获取websocket对象
   * */

  getSocket:()=>{
    //创建了直接返回,反之重来
    if (socket.websock) {
      return socket.websock
    }else {
      socket.init();
    }
  },

  getStatus:()=> {
    if (socket.websock.readyState === 0) {
      return "未连接";
    } else if (socket.websock.readyState === 1) {
      return "已连接";
    } else if (socket.websock.readyState === 2) {
      return "连接正在关闭";
    } else if (socket.websock.readyState === 3) {
      return "连接已关闭";
    }
  },

  /**
   * 发送消息
   * @param {*} data 发送数据
   * @param {*} callback 发送后的自定义回调函数
   */
  send: (data, callback = null) => {
    // 开启状态直接发送
    if (socket.websock.readyState === socket.websock.OPEN) {
      socket.websock.send(JSON.stringify(data))

      if (callback) {
        callback()
      }

      // 正在开启状态,则等待1s后重新调用
    } else if (socket.websock.readyState === socket.websock.CONNECTING) {
      setTimeout(function () {
        socket.send(data, callback)
      }, 1000)

      // 未开启,则等待1s后重新调用
    } else {
      socket.init()
      setTimeout(function () {
        socket.send(data, callback)
      }, 1000)
    }
  },

  /**
   * 接收消息
   * @param {*} message 接收到的消息
   */
  receive: (message) => {
    var recData = JSON.parse(message.data)
    /**
     *这部分是我们具体的对消息的处理
     * */
    if(socket.msgfunc==null){
      Message({
        message: 'receive需要传入一个func进行全局消息处理!',
        type: 'error',
      });
    }else {
      socket.msgfunc(recData)
    }
  },

  /**
   * 心跳
   */
  heartbeat: () => {
    console.log('socket', 'ping')
    if (socket.hearbeat_timer) {
      clearInterval(socket.hearbeat_timer)
    }

    socket.hearbeat_timer = setInterval(() => {
      //发送心跳包
      let data = {
        "action": 4,
        "userid": socket.userid
      }
      socket.send(data)
    }, socket.hearbeat_interval)
  },

  /**
   * 主动关闭连接
   */
  close: () => {
    console.log('主动断开连接')
    clearInterval(socket.hearbeat_interval)
    socket.is_reonnect = false
    socket.websock.close()
  },

  /**
   * 重新连接
   */
  reconnect: () => {
    console.log('发起重新连接', socket.reconnect_current)

    if (socket.websock && socket.socket_open) {
      socket.websock.close()
    }

    socket.init()
  },
}


这段代码里面主要有两个重要的地方、

  1. 初始化的连接
  2. 发送消息(这边的话我们基本上是通过http发送到具体的服务,然后由消息服务器转发的,但是心跳在线还是需要这个的)
  3. 接收到消息的处理方法(这个要自己实现的,怎么实现待会会有说明)

初始化

首先是初始化,我这边的话是做消息的推送,包括用户的聊天,所以的话,我们这边是需要全局使用的。那么这边的话,我们初始化需要在你的根页面进行使用。那么我这边是在这边在这里。你们自己的自己看着办。 在这里插入图片描述

接受消息

之后是我们接受消息的地方。

哪里需要哪里使用。我这边是在这个地方使用: 在这里插入图片描述

这部分的完整代码是这样的:

 created() {
    /**
     * 在这里我们负责对消息进行处理,我们把消息存储到缓存当中
     * 到具体的页面的时候,我们就加载,这样就好了。
     * */
    socket.msgfunc=this.msgfunc;
      //加载消息
    let messageNum_Local = localStorage.getExpire("messageNum");
    let messageContent_Local = localStorage.getExpire("messageContent");
    if(messageNum_Local){
      this.messageNum = messageNum_Local;
      this.total = messageNum_Local.total;
    }else {
      this.messageNum = messageNum;
      localStorage.setExpire("messageNum", this.messageNum,this.OverTime);
      this.total = 0;
    }
    if(messageContent_Local){
      this.messageContent = messageContent_Local;

    }else {
      this.messageContent = messageContent;
      //因为一开始有初始值,这个初始值是不要的
      delete this.messageContent[0];
      localStorage.setExpire("messageContent",this.messageContent,this.OverTime);
    }

  },
    msgfunc(res){
      //这个msgfunc是负责处理全局信息的
      if(res.type===4){
        //心跳正常
      }else {
        /**
         * 这里面就是我们接下来要做的逻辑了,
         * 这里的话消息是分为两个状态的,签收和读取
         * */
        let messageNum_Local = localStorage.getExpire("messageNum");
        messageNum_Local.total+=1;
        this.total = messageNum_Local.total;
        this.messageNum = messageNum_Local;
        //加载消息
        this.messageContent = localStorage.getExpire("messageContent");

        if(res.type===6){
          //这个时候是我们的审核消息
          this.messageNum.auditNum+=1;
          //头部插入消息
          this.messageContent.auditContent.unshift(res.data)
        }

        //当token过期的时候,我们这些东西都需要进行删除
        localStorage.setExpire("messageNum",this.messageNum,this.OverTime);
        localStorage.setExpire("messageContent",this.messageContent,this.OverTime);
      }

    },

我们在这个页面实现了对消息的处理,并且让我们的socket进行了指定。

那么这段代码主要做了这些事情

  1. 指定了消息的处理方法(初始化部分)
  2. 在消息处理部分,主要是针对消息的类型,进行不同的存储

我们着这边定义了两个消息存储的玩意。 在这里插入图片描述

这个就是我们定义存储消息的玩意

let messageNum = {
  total: 0,
  auditNum: 0,
};

let messageContent = {
    auditContent:[
      {
        userid: null,
        msg: null,
        msgtitle: null,
        linkid: null,
        createTime: null,
        msgid: null,
        status: null,
        type: null,
      }
    ]
}

export {
  messageNum,
  messageContent
}

刚刚的那一段代码就是为了存储消息。这里的话,目前是只定义了一个消息类型。

消息的展示

尽然我们把消息保存起来了,那么我们就需要进行读取了。 那么读取的话很简单啊,我们都存起来了,直接读取加载不就好了。 那么在这边的话,就是咱们的这个页面

<template>
  <div style="background-color: rgba(239,250,246,0.53)">
    <br>
    <div class="head" style="width: 90%;margin: 0 auto">

      <el-button style="margin-left:80%" type="primary" plain>清空所有</el-button>

    </div>
    <br>
    <div style="width: 80%;margin-left: 1%" class="main">
      <el-card shadow="hover" v-for="(message,index) in Messages" :key="index">

        <div style="height:80px">

          <div style="display:inline-block;margin-left: 5%">


            <p class="message">
              <router-link v-if="message.type==1" class="alink"
                           :to="{ path: '/blogshow',query:{'blogid':message.linkid}}"
              >
              {{message.msgtitle}}&nbsp;&nbsp;<i class="el-icon-circle-check"></i>
              </router-link>
            </p>

            <el-tooltip class="item" effect="dark" :content="message.msg" placement="top-start">
            <p class="message">

              查看详情:{{message.msg}}

            </p>
            </el-tooltip>
          </div>

          <div style="display:inline-block;margin-left: 20%">
            <p>
              <el-button  icon="el-icon-delete" ></el-button>
            </p>
            <p style="font-size: 8px">
              {{message.createTime}}
            </p>
          </div>

        </div>
        <br>
      </el-card>
    </div>
    <br>
    <div class="footer" style="margin: 0 auto;width: 100%;">
      <div class="block" >
        <el-pagination
          background
          layout="total, prev, pager, next, jumper"
          :total=total>
        </el-pagination>
      </div>
    </div>

  </div>

</template>

<script>
import {messageContent} from "../../socket/message";
export default {
  //这里的话是我们审核消息的具体的页面
  name: "auditInformation",
  data(){
    return{
      total: 0,
      Messages: messageContent.auditContent,
    }
  },

  created() {
    //如果在我们这边本地有缓存的话,那么我们就加载缓存的
    //如果没有缓存的话,那么我们就需要去求取服务器,加载
    let messageContent= localStorage.getExpire("messageContent");
    if(messageContent){
      //分页的话,我们在这边做切片即可
      this.Messages = messageContent.auditContent;

    }else {
      //这里去求取服务器拿到数据,然后重新保存在本地
    }
  }
}
</script>

<style scoped>
.message{
  width: 20em;
  overflow: hidden;
  text-overflow:ellipsis;
  white-space: nowrap;
}
.alink{
  text-decoration: none;
  color: #333333;
}

</style>

这块的实现现在还是很粗糙,反正就是一个案例。这下子的话完整的过程我是已经说明白了的。

其实如果有时间的话,完全是可以把这个封装成一个专门的消息处理中间件的。刚好有用,而且很有必要。这里的话,我可以有机会试试,仿造一下,也搞一个消息中间件玩玩。

效果

之后的 话就是我们的测试效果。 登录有提示: 在这里插入图片描述 这边有心跳包: 在这里插入图片描述 后端也可以看到连接 在这里插入图片描述

编写文章的时候,上传成功后有消息提示:

这个是我们上传博文,博文通过之后会看到消息有个提示。 在这里插入图片描述

然后还可以到具体的页面查看。 在这里插入图片描述

总结

这里的话,如果这个系列ok了的话,那么此时你已经学会了 CURD+Netty了。此时你已经可以脱离XXX管理系统了,可以做一个微信聊天之类的东西了。那么在我们这边的不足之处的话,还差一个就是聊天的信息加密,但是这个东西,怎么说呢,如果是类似于md5这种类型的加密,那么是不可能有人破解,但是我也不可能还原,我还原不了,你也就只能看到乱码。我要是可以解密,那么只是保证了在传输的时候是安全的,但是我一样可以看,除非聊天秘钥在你手里,但是你的秘钥总还是要再我们的服务器存储的,至少是要上传的,不然我怎么给你解密。所以这个我可能都不会去加密,顶多传输的时候加一下,甚至不加上一个https就完了。