(eblog)9、即时群聊开发,聊天记录等

4,623 阅读12分钟

公众号:MarkerHub(关注获取更多项目资源)

eblog 代码仓库:github.com/markerhub/e…

eblog 项目视频: www.bilibili.com/video/BV1ri…


开发文档目录:

(eblog)1、项目架构搭建、首页页面初始化

(eblog)2、整合Redis,以及项目优雅的异常处理与返回结果封装

(eblog)3、用Redis的zset有序集合实现一个本周热议功能

(eblog)4、自定义 Freemaker 标签实现博客首页数据填充

(eblog)5、博客分类填充、登录注册逻辑

(eblog)6、博客发布收藏、用户中心的设置

(eblog)7、消息异步通知、细节调整

(eblog)8、博客搜索引擎开发、后台精选

(eblog)9、即时群聊开发,聊天记录等


前后端分离项目vueblog请点击这里:超详细!4小时开发一个SpringBoot+vue前后端分离博客项目!!


群聊开发

今天我们就来完成一个聊天室的功能。在课程中,我们讲过一个例子 springLayIM,例子也是集成了 layim 实现了网页版的聊天功能。我们这次写的没这么复杂,我们主要学会前后端交互的过程即可。

技术选型:

  • 前端 layim、websocket

  • 后端 t-io websocekt 版

首先我们先来吧 layim 的界面先运行起来。layim 是 layui 的一个付费模块,首先我们把 layui 的静态资源包放到 static 中,关于 layim,因为不是完全开源的产品,所以我就不给出具体的包了。

layim 官网

引入 layIM

首先引进相关 layim 模块插件,这个插件不是开源的,线上使用需要进行捐赠,同学们如果需要在商业中使用最好进行捐赠哈。

然后按照官网给出的例子,我们来搞个最简单的 hello word。在这之前,我们先要获取一下插件,原则上我们应该通过捐赠形式获取,不过只为了学习,所以就直接从网络上搜索了一个,相关的 js 如下:

然后我们看官方文档说明:www.layui.com/doc/modules…

这段 js 我们放到哪呢,我想的效果是这样的,在首页的正下方有个群聊按钮,点击之后可以打开群聊窗口进行群聊。所以为了所有页面都能聊天,我把 js 写在了全局模板中

  • templates/inc/layout.ftl
<script type="application/javascript">
    $(function () {
        layui.use('layim', function(layim){
            //先来个客服模式压压精
            layim.config({
                brief: true //是否简约模式(如果true则不显示主面板)
                ,min: true
            }).chat({
                name: '客服姐姐'
                ,type: 'friend'
                ,avatar: 'http://tp1.sinaimg.cn/5619439268/180/40030060651/1'
                ,id: -2
            });
            layim.setChatMin(); //收缩聊天面板
        });
    });
</script>

这段 js 的效果如下,我们来分析一下:layim.config 表示进行初始化配置,brief: true 表示简约模式,只有一个聊天窗口,.chat 是声明并打开一个聊天窗口,layim.setChatMin(); 表示收缩聊天面板。 

点击之后的效果:

ok,上面是我们最简单的一个聊天窗口已经可以展示出来了,不过现在还没有功能,还不能相互聊天,接下来我们会给每个窗口一个身份,然后进行相互聊天。

t-io 集成 websocket

上面我们引入 layim 之后就可以看到一个聊天窗口了。我们来看下我们要去完成什么功能:

需求

  • 实现一个无区别群聊的功能

  • 登录网站的用户就能开始群聊

  • 未登录用户可以匿名聊天

功能

  • 聊天信息群发

  • 历史消息记录

  • 匿名聊天

接下来我们一步步去完成。

首先我们后端集成一下 t-io 与 websocket。回顾一下学习过的 t-io 课程的内容。

首先 t-io 使用的 helloword:

(初始化服务器)

(客户端与服务端通讯流程)

然后集成 t-io 之后要去实现的消息逻辑处理:

常用类说明:

通过以上内容的回顾我们知道了几个比较关键的类,也是我们再初始化启动 t-io 服务的几个关键类。下面我们去把 t-io 去启动起来。

在 t-io 中,要启动服务我们最终调用的代码是:tioServer.start();

因为我们这次要实现的功能是 t-io 集成 websocket。而 t-io 为我们已经帮我们集成了一套代码,帮我们省去了协议升级等步骤,这样我我们就不需要像我们课程里的例子这样去手动写很多升级协议等代码了。

可以先来感受一下官网给出的例子 tio-websocket-showcase:

例子中我们需要例会 http 包下的代码,这是类似于 springmvc 的用法,t-io 也帮我们写了一套 mvc 代码。那这个项目怎么启动呢?

很多简单:直接运行 ShowcaseWebsocketStarter 的 main 方法即可启动项目,然后访问 localhost 就能看到界面了,大家在做我们作业项目之前,可以先去看下这个基本聊天的功能是怎么实现的,然后我们再回头做自己的项目就会觉得简单很多了。

我们接着我们作业:集成 t-io 的 websocket。因为是直接有一套集成框架,所以我这里直接引入最新版本:mvnrepository.com/artifact/or…

<!-- https://mvnrepository.com/artifact/org.t-io/tio-websocket-server -->
<dependency>
    <groupId>org.t-io</groupId>
    <artifactId>tio-websocket-server</artifactId>
    <version>3.2.5.v20190101-RELEASE</version>
</dependency>

然后结合刚刚看的项目例子,我们先来说明一下几个比较重要的类

  • IWsMsgHandler(握手、消息处理类)

  • 这个是消息处理的接口,包括握手时、握手完成后、消息处理等方法

  • 会在 org.tio.websocket.server.WsServerAioHandler 中调用,而这个类实现了 ServerAioHandler。里面有我们熟悉的 decode、encode、handler 三个方法。

  • WsServerStarter(ws 服务启动类)

  • 针对 ws,tio 封装了很多涉及到的东西,从而使配置更加简便,他的 start 方法中可以看出其实就是我们熟悉的 tioServer.start。

  • ServerGroupContext(配置类)

  • 这个我们就比较熟悉了,服务端的配置类,可以配置心跳时间等。

以上就是我们需要清除的 3 个类。有了这 3 个类之后我们就可以启动我们的服务,进行 ws 的连接了。

那么好,我们来写个配置类。com.homework.im.config.ImServerAutoConfig

首先需要指定端口,所以加上一下代码,大家在 yml 上自行加上配置。

@Value("${im.server.port}")
private Integer imPort;

  • application.yml
im:
  server:
    ip: 127.0.0.1
    port: 9326

然后我们回顾一下需要配置的东西:

  • 第一步、初始化 IWsMsgHandler、ServerGroupContext,然后配置到 WsServerStarter,调用 start 方法启动服务。

  • 第二步、因为消息类型有很多(发送消息、心跳消息、加好友等),所以我们还需要初始化一下消息类型对应的处理类容器 Map,这样我们在处理消息的时候直接根据类型就能找到对应的消息处理类调用就行了。

好了,有了想法之后我们就去实现一下。

  • com.example.config.ImServerAutoConfig
@Slf4j
@Data
@Configuration
@Order(value = Integer.MAX_VALUE)
public class ImServerAutoConfig {
    @Value("${im.server.port}")
    private Integer imPort;
    @Bean
    public ImServerStarter imServerStarter() {
        try {
            ImServerStarter imServerStarter = new ImServerStarter(imPort);
            imServerStarter.start();
            //初始化消息处理器工程
            MsgHandlerFactory.init();
            log.info("---------> im server started !");
            return imServerStarter;
        } catch (IOException e) {
            log.error("im server 启动失败~~", e);
        }
        return null;
    }
}

大家先看下上面的代码,我做了两步,首先我定义了一个自定义类 ImServerStarter,把端口传进去,这里面的逻辑大概就是初始化 IWsMsgHandler、ServerGroupContext,然后配置到 WsServerStarter 等。然后 start() 其实就是完成了初始化之后调用 tio 的 wsServerStarter.start()。这样我们就完成了刚才说的第一步。 再来看下自定义类 ImServerStarter 的具体代码:

@Slf4j
public class ImServerStarter {
    private ImWsMsgHandler imWsMsgHandler;
    private WsServerStarter wsServerStarter ;
    private ServerGroupContext serverGroupContext;
    public ImServerStarter(int imPort) throws IOException {
        imWsMsgHandler = new ImWsMsgHandler();
        wsServerStarter = new WsServerStarter(imPort, imWsMsgHandler);
        serverGroupContext = wsServerStarter.getServerGroupContext();
        serverGroupContext.setHeartbeatTimeout(1000 * 60);
    }
    public void start() throws IOException {
        this.wsServerStarter.start();
    }
}

看起来是不是挺简单的,我可调整了几次写出来的,哈哈。如果不熟悉 tio 的用法同学回去回顾一下我们的课程内容,tio 的用法不算难,主要的类就几个,底层都帮我们封装好了,所以用起来挺简单。 然后看下第二步的内容:

//初始化消息处理器工程
MsgHandlerFactory.init()

这里是初始化消息处理器,我们说过消息可能会有多种类型,对应不同的处理器,所以这里我们就先初始化一下,放在一个静态的 map 里面,以后想要处理器的话就直接调用这个工厂的方法从 map 容器里面获取就可以了。

  • com.example.im.handler.MsgHandlerFactory
/**
 * 1、消息处理器初始化工程
 * 2、根据类型获取消息处理器
 */
public class MsgHandlerFactory {
    private static boolean isInit = false;
    private static Map<String, MsgHandler> handlerMap = new HashMap<>();
    /**
     * 得预先初始化消息处理器
     */
    public static void init(){
        if(isInit){ return; }
        handlerMap.put(Constant.IM_MESS_TYPE_CHAT, new ChatMsgHandler());
        handlerMap.put(Constant.IM_MESS_TYPE_PING, new PingMsgHandler());
        isInit = true;
    }
    public static MsgHandler getMsgHandler(String type) {
        return handlerMap.get(type);
    }
}

从上面我们可以看到有几个一个接口 MsgHandler ,两个实现类 ChatMsgHandler、PingMsgHandler,调用 getMsgHandler(String type) 就能返回具体的实现。MsgHandler 接口有个方法 handler,所以所有实现类都实现这个方法即可(就是消息处理逻辑)。 处理逻辑我们后面再说,经过上面的步骤,我们已经可以初始化启动 tio 的服务了,并且消息处理器也有了,写来的任务我们就是让前端 ws 和后端集成起来,然后去处理对应的消息(后面大部分都是围绕 ImWsMsgHandler 开发)。

实现前端与后端联调

1、前后端建立 ws 连接

后端是使用的 ws 版本的 tio,前端我们也用 ws 来建立连接。回顾一下我们以前说 websocket 的课程内容,前端建立 ws 连接很简单:

var
 socket 
=
 
new
 
WebSocket
(
'ws://localhost:9326'
);

就搞定了,然后 socket 有几个回调方法,分别是 socket.onopen、socket.onmessage、socket.onclose 等,也是我们主要去使用的几个方法。 之前我们已经写了一个 layim 的简单例子能把聊天窗口渲染出来了,我们在 layim.config 初始化配置完了之后,建立 ws 连接。因为等会还设计到心跳、断开重连等逻辑处理,所以我需要调整一下 js 的结构,让 js 更加符合我们 java 的思想。

首先在新建并引入 im.js 这个 js 文件,然后在 js 里面我新建了一个 tio 类,tio.ws 内部类,先按照这个说法吧,不知道对不对。= {} 表示这个定义一个对象。

if (typeof(tio) == "undefined") {
    tio = {};
}
tio.ws = {};

然后我们往 tio.ws 里面写方法,以后需要用到的地方就可以新建一个 tio.ws,然后调用对应方法即可。是不是很符合面对对象思想。哈哈

//这个相当于构造函数吧
tio.ws = function ($, layim) {
}

这里面我们主要有几个方法

  • 建立连接方法,同时监听 ws 消息接受、关闭、异常等

  • 心跳、断开重连

  • 发送消息

  • 初始化聊天窗口数据(比如窗口标题头像等、离线聊天记录)

我们一一讲解,首先来看建立连接:

  • static/js/im.js

其实围绕着 ws 的几个方法展开的逻辑。layim.getMessage 是 layim 的接口,这个接口让我们把对话 json 信息显示下窗口上。

2、心跳与断开重连

每次发送消息我们都会记录一下最后发送消息的时间,用于心跳时候如果未发送消息太久就心跳一下表示活着,这样 ws 连接不会被服务端销毁。

当服务端出现故障时候,前端会一直自动尝试重连,会回调 onclose,所以我们只需要在 onclose 方法 im 重连即可。

然后来看下 ping 的逻辑

其实就是启动一个定时器,然后发送时间久没发送消息就自动发送心跳包,注意指定心跳消息类型是 pingMessage,这样服务端就能获取到对应处理器处理了。

发生 ws 异常时候我们需要删除这个定时器,重连时候再开启心跳。

3、发送消息

然后我们来看下发送消息的方法

this.sendChatMessage = function(res) {
    //监听到上述消息后,就可以轻松地发送socket了
    this.socket.send(JSON.stringify({
        type: 'chatMessage' //随便定义,用于在服务端区分消息类型
        ,data: res
    }));
}

可以看到,其实就是使用 socket.send 方法,注意消息类型 chatMessage。res 里面是要发送的消息。 什么时候触发?这个得涉及到 layim 的接口了。我们等下看。

4、完成前端整个流程的初始化

上面我们在 tio.ws 这个对象里面定义了好多方法,建立连接、心跳、发送消息等等。那么我们现在就去使用一下这个对象。

  • static/js/index.js
layui.use('layim', function (layim) {
    var $ = layui.jquery;
    //初始化layim
    layim.config({
        brief: true //是否简约模式(如果true则不显示主面板)
        ,voice: false
        ,members: {
            url: '/chat/getMembers'
        },
        chatLog: layui.cache.dir + 'css/modules/layim/html/chatlog.html'
});
    //建立ws链接,监听消息
    var tiows = new tio.ws($, layim);
    tiows.connect();
    //初始化群聊离线信息
    tiows.initHistroyMess();
    //打开群聊窗口并初始化个人信息
    tiows.openChatWindow();
    //发送消息
    layim.on('sendMessage', function(res){
        tiows.sendChatMessage(res);
    });
});

以上代码中,我们初始化 layim、建立 ws 连接、初始化群聊消息、个人消息,然后打开群聊窗口,最后再监听 layim 的发送消息回调方法,刚才说的什么时候调用发送消息就是这里触发。

5、数据初始化

上面我们还调用了初始化数据的相关接口,这里补充一下。layim 对格式是有一定要求的。大家可以看下文档:

包括个人信息,临时窗口的信息等。

this.initChatData = function () {
    $.ajax({
        url: '/chat/getMineAndGroupData',
        async: false,
        success: function (data) {
            mine = data.data.mine;
            group = data.data.group;
        }
    });
}

这里涉及到的后端接口有两个:

  • 获取用户信息和群聊信息 getMineAndGroupData

  • 获取群成员名单 getMembers

我们需要安装官方文档说明的格式返回对应的 json 数据,所以我对应做了一些数据封装类(VO),比如:ImUser 等。

  • com.example.controller.ChatController
@RestController
@RequestMapping("/chat")
public class ChatController extends BaseController {
    @Autowired
    ChatService chatService;
    @GetMapping("/getMineAndGroupData")
    public Result getMineAndGroupData(HttpServletRequest request) {
        //获取用户信息
        ImUser user = chatService.getCurrentImUser();
        //默认群
        Map<String, Object> group = new HashMap<>();
        group.put("name", "社区群聊");
        group.put("type", "group");
        group.put("avatar", "http://tp1.sinaimg.cn/5619439268/180/40030060651/1");
        group.put("id", Constant.IM_DEFAULT_GROUP_ID);
        group.put("members", 0);
        return Result.succ(MapUtil.builder()
                .put("mine", user)
                .put("group", group)
                .map());
    }
}

这里面的 ImUser user = chatService.getCurrentImUser(); 是获取当前用户信息,分为两种情况,已登录和未登录,所以我做了区分,匿名用户给了一个随机 id,然后保存在 session 中,这样匿名会话会一直是 id 直到窗口关闭。

  • com.example.service.impl.ChatServiceImpl
@Override
public ImUser getCurrentImUser() {
    AccountProfile profile = (AccountProfile)SecurityUtils.getSubject().getPrincipal();
    ImUser user = new ImUser();
    if(profile != null) {
        user.setId(profile.getId());
        user.setAvatar(profile.getAvatar());
        user.setUsername(profile.getUsername());
        user.setMine(true);
        user.setStatus(ImUser.ONLINE_STATUS);
    } else {
        user.setAvatar("http://tp1.sinaimg.cn/5619439268/180/40030060651/1");
        // 匿名用户处理
        Long imUserId = (Long) SecurityUtils.getSubject().getSession().getAttribute("imUserId");
        user.setId(imUserId != null ? imUserId : RandomUtil.randomLong());
        SecurityUtils.getSubject().getSession().setAttribute("imUserId", user.getId());
        user.setSign("never give up!");
        user.setUsername("匿名用户");
        user.setStatus(ImUser.ONLINE_STATUS);
    }
    return user;
}

然后还有一个获取在线用户的接口主要是是个方法:chatService.findAllOnlineMembers(),用户握手完成协议升级并上线之后,我们就会把当前用户的信息保存到 redis 中,所以这个方法,我们就是从 redis 中获取出来即可,我们在后面会讲到。 以上就是前端与后端联调的过程。启动服务器,打开首页,就能看到聊天窗口,F12 看到已经建立 ws 链接。发送消息后,我们在 com.homework.im.server.ImWsMsgHandler#onText 方法中可以接收到消息。

下面我们针对接受消息并处理来展开说明

后端消息处理

在初始化 tio 服务的时候我们就说过,消息处理、握手等会围绕着 com.example.im.server.ImWsMsgHandler 这个类来进行。这里面有个几个方法是我们需要注意的:

  • 握手前 handshake

  • 握手后 onAfterHandshaked

  • 字符消息处理 onText

我们现在要做的是个一个群聊功能,我们来梳理一下需求:

  • 用户登录成功之后,我们把用户 id 绑定到 tio 的通道中,然后提醒全部用户该用户上线

  • 一个用户发送消息时候,所有的用户都可以接收到

好,我们来看下,绑定用户 id 到 tio 这个功能可以在握手前或者握手后,提醒全部用户上线应该握手后。

接收到用户发送消息,我们会找到对应处理器,然后处理完毕之后群发给所有用户

ok,搞定。

我们来看下代码:

  • 握手前

  • 握手后,这里还有点问题,聊天窗口上显示不出系统消息,有点 bug。。。。

  • 消息处理

关于消息类型:

发送消息是 chatMessage。

我们找到处理器:

  • com.example.im.handler.impl.ChatMsgHandler
@Slf4j
@Component
public class ChatMsgHandler implements MsgHandler {
    @Override
    public void handler(String data, WsRequest wsRequest, ChannelContext channelContext) {
        ChatInMess chatMess = JSONUtil.toBean(data, ChatInMess.class);
        log.info("--------------> {}", chatMess.toString());
        ImUser mine = chatMess.getMine();
        ImTo to = chatMess.getTo();
        ImMess responseMess = new ImMess();
        responseMess.setContent(mine.getContent());
        responseMess.setAvatar(mine.getAvatar());
        responseMess.setMine(false);//是否我自己发的信息(自己不需要发送给自己)
        responseMess.setUsername(mine.getUsername());
        responseMess.setFromid(mine.getId());
        responseMess.setTimestamp(new Date());
        responseMess.setType(to.getType());
        responseMess.setId(Constant.IM_DEFAULT_GROUP_ID);//群组的id
        ChatOutMess chatOutMess = new ChatOutMess(Constant.IM_MESS_TYPE_CHAT, responseMess);
        String responseData = JSONUtil.toJsonStr(chatOutMess);
        log.info("群发消息 =========> {}", responseData);
        //用tio-websocket,服务器发送到客户端的Packet都是WsResponse
        WsResponse wsResponse = WsResponse.fromText(responseData, "utf-8");
        ChannelContextFilter filter = new ChannelContextFilterImpl();
        ((ChannelContextFilterImpl) filter).setCurrentContext(channelContext);
        //群发
        Tio.sendToGroup(channelContext.groupContext, Constant.IM_GROUP_NAME, wsResponse, filter);
        //保存群聊信息
        ChatService chatService = (ChatService) SpringUtil.getBean("chatService");
        chatService.setGroupHistoryMsg(responseMess);
    }
}

其实还算简单对吧~ 无非就是数据的拼凑。需要注意的是,这个有个通道过滤器,也就是说本人发送的消息不会群发给本人,而是前端直接展示的,所以我们需要写了一个过滤器:

  • com.example.im.handler.filter.ChannelContextFilterImpl
/**
 * 通道过滤器
 */
@Data
public class ChannelContextFilterImpl implements ChannelContextFilter {
    private ChannelContext currentContext;
    /**
     * 过滤掉自己,不需要发送给自己
     * @param channelContext
     * @return
     */
    @Override
    public boolean filter(ChannelContext channelContext) {
        if(currentContext.userid.equals(channelContext.userid)) {
            return false;
        }
        return true;
    }
}

这样当通道 userid 和需要发送的通道 userid 相同时候就返回 false,表示跳过。

  • 用户退出

用户退出的时候我们需要关闭通道,这样群发的时候才不会往这里面发送,同时可以统计实时在线人数。代码很简单,直接调用 Tio.remove 即可:

@Override
public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
    Tio.remove(channelContext, channelContext.userid + " - 退出群聊了~");
    return null;
}

总结一下,我们用了 tio 的几个接口

  • 群发接口 Tio.sendToGroup

  • 绑定用户接口 Tio.bindUser

    • 用户退出 *Tio.remove

基本上群聊我们就只会用到上面几个 api,用法不复杂。下面我们来测试一下。因为用户 Id 的问题,所以这里还有点混乱,不过没关系,我们只要测试一下消息发送接收群发功能先。

运行项目后,打开链接来感受一下聊天室哈。

获取群成员与聊天记录

上一个版本的迭代中,我们已经完成了群成员之间的基本通信,这个版本我们来完成把获取群聊成员,群离线信息等完成它。

1、获取群成员

群成员的信息获取,是监听用户的上线与下线来决定成员是否在线。

  • 上线:把该成员信息保存到 redis 中

  • 下线:从缓存中删除该成员信息

我们来看下 layim 的获取群成员需要返回的数据结构

从上面我们知道,其实就是往 list 里面添加群成员的基本信息而已,在界面中其实只需要 id,username、avatar 三要素,其他不要也可以。

知道了结构之后,我们来想下这个缓存应该用什么结构呢?明显这里是个列表,可以用 list、set,为了防重复,用 set 更好。什么情况会重复,当 id,username、avatar 三要素完全相同的时候就是当做是同一个元素,就不会重复。如果用户修改了头像或用户名,那么就会重复,因此,我们不能让列表被这些因素影响,只有用户的 id 不会修改,所以这个列表里面,我们只放 id。但是只有 id 的话,username、avatar 去哪获取呢,我们可以使用一个 hash 结构把用户的这些基本信息保存起来。

所以总结一下以上我们的分析,要完成这个功能我们需要用到 redis 的两种结构

  • set

  • hash

接下来我们来写代码把功能完成。

首先我们来写下获取群成员的接口:

/**
 * 应该从通道里面获取所有用户信息
 * @return
 */
@ResponseBody
@GetMapping("/getMembers")
public Result getMembers() {
    Set<Object> members = chatService.findAllOnlineMembers();
    log.info("获取群成员---------->" + JSONUtil.toJsonStr(members));
    return Result.ok(MapUtil.of("list", members));
}

  • 实现类:com.homework.im.service.impl.ChatServiceImpl#findAllOnlineMembers
@Override
public Set<Object> findAllOnlineMembers() {
    Set<Object> ids = redisUtil.sGet(Constant.ONLINE_MEMBERS_KEY);
    Set<Object> results = new HashSet<>();
    if(ids == null) return results;
    ids.forEach((id) -> {
        Map<Object, Object> map = redisUtil.hmget((String) id);
        results.add(map);
    });
    return results;
}

上面步骤,我们先从 set 列表获取 ids,然后在循环获取用户信息放到列表中返回。 那信息是什么时候保存进去的,上面我们说过是监听用户的上线下线,我们来看下:

  • 上线

  • 下线

  • 主要逻辑

上面我们运用了缓存完成了这个功能,在我们开发中,可以多运用 redis 缓存来帮我们完成开发,提高网站的访问速度。

2、获取聊天记录

接下来我们来获取一下群历史聊天记录。同样我们要先了解一下 layim 接口的设计:

看下 css/modules/layim/html/chatlog.html 目录下的页面 html,里面 js 有 json 的格式说明。

首选配置一下:

接下来我们分析一下:

首先我们需要把聊天记录保存起来,同样可以使用我们的 redis 缓存来记录。可以给个有效期,只 1 天有效等等。这里同样可以使用列表 list 来保存聊天记录。什么时候去保存呐,其实历史消息的格式和发送消息的格式是一样的,所以我们可以在发送消息的时候把消息保存到缓存中,当我们打开聊天窗口时候,从缓存中读取消息,然后再窗口上循环显示出来。

我们先写好获取和保存消息两个接口:

  • 只获取最近 count 条记录

  • com.example.service.impl.ChatServiceImpl#getGroupHistoryMsg

  • 什么时候调用:暴露获取历史消息接口

@Override
public List<Object> getGroupHistoryMsg(int count) {
    long length = redisUtil.lGetListSize(Constant.GROUP_HISTROY_MSG_KEY);
    return redisUtil.lGet(Constant.GROUP_HISTROY_MSG_KEY, length - count < 0 ? 0 : length - count, length);
}

  • 保存消息 24 小时(可以灵活设计)

  • com.example.service.impl.ChatServiceImpl#setGroupHistoryMsg

  • 什么时候调用:发送信息时候

@Override
public boolean setGroupHistoryMsg(ImMess imMess) {
    return redisUtil.lSet(Constant.GROUP_HISTROY_MSG_KEY, imMess, 24 * 60 * 60);
}

具体的 js 的代码我就不贴出来了,同学们在搭建过程中可以对比下代码改动。

效果:

作业总结

ok,终于我们的课程作业已经接近尾声了,同学们也辛苦了。对于我们学习过的知识点希望大家能好好通过这个课程作业巩固一下。

我们下次再见~

-----------------------------------------------------------(完)----------------------------------------------------------

撰写人:吕一明

公众号:MarkerHub