前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >springboot+websocket+layui制作的实时聊天室,后端开发入门样例

springboot+websocket+layui制作的实时聊天室,后端开发入门样例

作者头像
小王不头秃
发布2024-06-19 16:38:02
1400
发布2024-06-19 16:38:02
举报

前言

复习感觉无聊的时候就想拿以前学习的东西做几个小案例,这段时间在搭一个博客网站,正好做到私信这个模块,突然想试试看看可不可以做成一个实时通信的私信功能,思路一来就一发不可收拾,开整开征。

效果图

如下图所示,可以实现文字的即时通讯,图片发送啥的还不支持,有空再搞。(UI有点丑,但能用就彳亍),但功能总归还是比较齐全,不仅仅只是websocket的双工通信,包括但不限于聊天记录的存储,过往聊天记录查看等功能。

涉及技术

springboot

对于我这种熟悉java的人来说,做后端那肯定离不开springboot了,不得不说,springboot yyds!!!如果不是很清楚怎么去搭建一个springboot项目的话,可以去看看手把手搭建一个springboot项目这篇博客。

layui

最近发现layui的模板属实很大气,因此就采用这个模块作为前端模块了,还有一个原因就是简单(前端小白不敢说话),如何使用可以参考layui的使用手册==>layui使用手册,复制即可用

websocket

能做成这个聊天模块最大的功臣,来看下百度给他的定义

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。 WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

可能些许有些难懂,但只要明白这个东西可以全双工通信就可以了,主要的作用就是浏览器可以通过websocket向服务端发送消息,同样服务端也可向浏览器发送消息,这不就是咱们平常的聊天吗,我可以和你说话,同时你也可以和我说话。

实现思路

现在我们聊一下怎么实现这个聊天室,最基础的聊天室一定需要两个角色,分别是发送方和接收方,并且在这种情形下,发送方也是接收方,接收方也是发送方,即二者所拥有的功能应该是相同的。 在我们这个聊天室角度来看就是,两个角色都应该有发送消息和接收消息的功能,但是很明显ajax无法做到这个功能,因此我们采用websocket进行消息的接受与发送的服务。那么如何实现呢

websocket在springboot下的实现

有几个注解先了解一下,

代码语言:javascript
复制
@OnOpen         //建立socket连接时调用
@OnError        //服务端出现问题时调用
@OnClose        //socket连接断开时调用
@OnMessage      //服务端接收到信息之后调用

这几个注解直接标注在方法上代表当出现以上情况时,就直接调用对应标注的方法,如下图所示,当接收到服务端的信息时就直接调用onMessage方法

前端实现

在h5之后,html也支持socket编程了,所以咱们一般看到的网页都支持,但是确实有极少数一些老年浏览器不支持socket编程,这就没办法了。

建立websocket连接

html打开之后需要先于服务端建立起websocket连接,这样才能与服务端进行交互。下面代码就是建立连接,注意这里前面是ws不是之前的http了。

代码语言:javascript
复制
 webSocket = new WebSocket("ws://192.168.43.220:9010/websocket/" + getParams("id"));
前端对应的websocket方法

前端的方法如下图所示,可以看到和上面的名字都是一一对应的,具体作用和后端的方法是一致的

到此我们就可以进行代码的编写了。

代码实现

后端代码

建立连接时

此时会连接数据库,把之前和你聊过天的用户的信息读取出来,传递给浏览器,这里getinfos方法就是实现数据库操作,大家有兴趣可以直接写一下 注意这里会将我们此次websocket的session存储在内存中,方便其他websocket的session与我们通信,但我觉得存储容器可以换成redis,但还没改,有兴趣的可以改一下

代码语言:javascript
复制
 @OnOpen
    public void onOpen(@PathParam("username") String username, Session session) {
        onlineNumber++;
        log.info("现在来连接的客户id:" + session.getId() + "用户名:" + username);
        this.username = username;
        this.session = session;
        log.info("有新连接加入! 当前在线人数" + onlineNumber);
        try {
            clients.put(username, this);
            System.out.println(username);
            List infos = getInfos(Integer.parseInt(username));
            Map<String, Object> map2 = new HashMap<>();
            map2.put("messageType", 3);
            map2.put("infos", infos);
            sendMessageTo(JSON.toJSONString(map2), username);

        } catch (Exception e) {
            e.printStackTrace();
            log.info(username + "上线的时候通知所有人发生了错误");
        }


    }
接收到消息时

这里的作用主要就是把接收到的信息进行解析,然后使用sendinfoTo来发送消息。 这里websocket的session信息是以键值对的方式存储在内存中,其中key就是对应的账号,可以通过key来查找对应的session,从而实现对该用户的通信。因此这里还有找出对应的session的功能。

代码语言:javascript
复制
  */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            JSONObject jsonObject = JSON.parseObject(message);
            String textMessage = jsonObject.getString("message");
            String from = jsonObject.getString("from");
            String to = jsonObject.getString("to");
            //如果不是发给所有,那么就发给某一个人
            //messageType 1在线人员信息 2普通消息
            Map<String, Object> map1 = new HashMap<>();
            map1.put("messageType", 2);
            map1.put("textMessage", textMessage);
            map1.put("fromusername", from);
            map1.put("tousername", to);
            sendInfoTo(JSON.toJSONString(map1), to, textMessage);
        } catch (Exception e) {
            log.info("发生了错误了");
        }
    }
发送消息

没啥好说的,就是发送信息

代码语言:javascript
复制
    public void sendMessageTo(String message, String ToUserName) throws IOException {
        WebSocket webSocket = clients.get(ToUserName);
        if (webSocket != null) {
            webSocket.session.getAsyncRemote().sendText(message);
        } else {
            log.info("未上线");
        }

    }
总代码
代码语言:javascript
复制
package com.xiaow.community.common.vo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiaow.community.common.fegin.service.AccounService;
import com.xiaow.community.entity.Message;
import com.xiaow.community.service.MessageService;
import com.xiaow.community.util.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocket {


    /**
     * 在线人数
     */
    public static int onlineNumber = 0;
    /**
     * 以用户的姓名为key,WebSocket为对象保存起来
     */
    private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
    /**
     * 会话
     */
    private Session session;
    /**
     * 用户名称
     */
    private String username;

    /**
     * OnOpen 表示有浏览器链接过来的时候被调用
     * OnClose 表示浏览器发出关闭请求的时候被调用
     * OnMessage 表示浏览器发消息的时候被调用
     * OnError 表示有错误发生,比如网络断开了等等
     */


    //获取发消息的对象
    public List getInfos(Integer tid) {
        MessageService messageService = SpringUtil.getBean(MessageService.class);
        List<Message> list = messageService.getTo(tid);
        List<Message> list1 = messageService.getFrom2(tid);
        list.addAll(list1);
        System.out.println(list);
        Collections.sort(list, Comparator.comparing(Message::getSendt).thenComparing(Message::getSendt).reversed());
        //进行去重
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < list.size(); i++) {
            Integer key;
            if (list.get(i).getFrom2() == tid) {
                key = list.get(i).getTo2();
            } else {
                key = list.get(i).getFrom2();
            }
            if (set.contains(key)) {
                list.remove(i);
                i--;
                continue;
            }
            set.add(key);
        }
        List acs = new LinkedList();
        set.forEach(s -> {
            acs.add(s);
        });
        AccounService accounService = SpringUtil.getBean(AccounService.class);
        List getonesasfegin = accounService.getonesasfegin(acs);
        return getonesasfegin;
    }

    public void addinfo(Message message) {
        MessageService messageService = SpringUtil.getBean(MessageService.class);
        messageService.save(message);
    }


    /**
     * 建立连接
     *
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session) {
        onlineNumber++;
        log.info("现在来连接的客户id:" + session.getId() + "用户名:" + username);
        this.username = username;
        this.session = session;
        log.info("有新连接加入! 当前在线人数" + onlineNumber);
        try {
            clients.put(username, this);
            System.out.println(username);
            List infos = getInfos(Integer.parseInt(username));
            Map<String, Object> map2 = new HashMap<>();
            map2.put("messageType", 3);
            map2.put("infos", infos);
            sendMessageTo(JSON.toJSONString(map2), username);

        } catch (Exception e) {
            e.printStackTrace();
            log.info(username + "上线的时候通知所有人发生了错误");
        }


    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.info("服务端发生了错误" + error.getMessage());
    }

    /**
     * 连接关闭
     */
        @OnClose
    public void onClose() {
        System.out.println("推出账户:" + username);
        onlineNumber--;
        clients.remove(username);
        log.info("有连接关闭! 当前在线人数" + onlineNumber);
    }

    /**
     * 收到客户端的消息
     *
     * @param message 消息
     * @param session 会话
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            JSONObject jsonObject = JSON.parseObject(message);
            String textMessage = jsonObject.getString("message");
            String from = jsonObject.getString("from");
            String to = jsonObject.getString("to");
            //如果不是发给所有,那么就发给某一个人
            //messageType 1在线人员信息 2普通消息
            Map<String, Object> map1 = new HashMap<>();
            map1.put("messageType", 2);
            map1.put("textMessage", textMessage);
            map1.put("fromusername", from);
            map1.put("tousername", to);
            sendInfoTo(JSON.toJSONString(map1), to, textMessage);
        } catch (Exception e) {
            log.info("发生了错误了");
        }
    }

    //进行发消息的功能  即向前端推送信息
    public void sendInfoTo(String message, String ToUserName, String info) throws IOException {
        System.out.println(message);
        WebSocket webSocket = clients.get(ToUserName);
        if (webSocket != null) {
            webSocket.session.getAsyncRemote().sendText(message);
        } else {
            log.info("未上线");
        }
        Message message1 = new Message()
                .setFrom2(Integer.parseInt(username))
                .setTo2(Integer.parseInt(ToUserName))
                .setInfo(info)
                .setSendt(LocalDateTime.now())
                .setState(0);
        addinfo(message1);
    }

    public void sendMessageTo(String message, String ToUserName) throws IOException {
        WebSocket webSocket = clients.get(ToUserName);
        if (webSocket != null) {
            webSocket.session.getAsyncRemote().sendText(message);
        } else {
            log.info("未上线");
        }

    }


    public void sendMessageAll(String message, String FromUserName) throws IOException {
        for (WebSocket item : clients.values()) {
            item.session.getAsyncRemote().sendText(message);
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineNumber;
    }

}

前端代码

前端代码有点杂,有兴趣的可以看一下,没兴趣的复制就可以用

代码语言:javascript
复制
<!DOCTYPE html>

<head>
    <title>websocket</title>
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>
    <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="/assets/blog.js"></script>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="/assets/jquery.min.js"></script>
    <link rel="stylesheet" href="/assets/layui-v2.6.8/layui/css/layui.css" media="all">
    <script src="/assets/layui-v2.6.8/layui/layui.js" charset="utf-8"></script>
</head>

<body>

    <ul class="layui-nav" lay-bar="disabled">
        <div class="layui-row">
            <div class="layui-col-md4">
                <div class="grid-demo grid-demo-bg1" style="padding: 20px 0;">
                    <a href="./bloglist.html" style="color: white;">
                        <h1 style="font-size: 20px;">xiaowblog</h1>
                    </a>
                </div>
            </div>
            <div class="layui-col-md4">
                <div class="grid-demo">
                    <div class="layui-input-block " style="padding: 10px 0;">
                        <input type="text " name="title " required lay-verify="required " placeholder="请输入内容 " autocomplete="off " class="layui-input ">

                    </div>
                </div>
            </div>
            <div class="layui-col-md4 ">
                <div class="grid-demo grid-demo-bg1 ">
                    <div style="text-align: right; ">
                        <li class="layui-nav-item ">
                            <a href=" ">带徽章<span class="layui-badge ">9</span></a>
                        </li>
                        <li class="layui-nav-item ">
                            <a href=" ">小圆点<span class="layui-badge-dot "></span></a>
                        </li>
                        <li class="layui-nav-item " lay-unselect=" " style="left: 0px; ">
                            <a href="javascript:; "><img src="//t.cn/RCzsdCq " class="layui-nav-img "></a>
                            <dl class="layui-nav-child ">
                                <dd><a href="./test.html ">写文章</a></dd>
                                <dd><a href="javascript:; ">横线隔断</a></dd>
                                <hr>
                                <dd style="text-align: center; "><a href=" ">退出</a></dd>
                            </dl>
                        </li>
                    </div>
                </div>
            </div>
        </div>

    </ul>
    <div class="layui-row layui-col-space1" style="width: 70%;height: 800px; margin-top: 5%;margin-left: 15%;">
        <div class="layui-col-md3 " id="friends" style="background-color: darkcyan; height: 100%;">

        </div>
        <div class="layui-col-md9 " style="height: 100%;border-top:1px solid #000;">
            <div id="top" style="height: 5%;background-color: darkcyan;text-align: center;">
                <h1 id="topname" style="color: white;"></h1>
            </div>
            <div id="info" style="height: 65%;margin: 5px;overflow:auto">


            </div>
            <div id="toolbar" style="height: 5%;text-align: center;">

            </div>
            <div id="text" style="height: 22%;">
                <textarea id="infotext" style="height: 80%;width: 100%;  
                resize: none;
                cursor: pointer;"></textarea>
                <div style="height: 20%;padding: 5px;"><button id="sendinfo" value="1" class="layui-btn">发送</button></div>

            </div>
        </div>

    </div>
</body>

<script>
    $('#sendinfo').click(function() {
        send()
    })
</script>

<script type="text/javascript">
    var flist = new Array();
    var webSocket;
    var commWebSocket;
    if ("WebSocket" in window) {
        webSocket = new WebSocket("ws://192.168.43.220:9010/websocket/" + getParams("id"));

        //连通之后的回调事件
        webSocket.onopen = function() {
            //webSocket.send( document.getElementById('username').value+"已经上线了");
            console.log("已经连通了websocket");
        };

        //接收后台服务端的消息
        webSocket.onmessage = function(evt) {
            var received_msg = evt.data;
            var obj = JSON.parse(received_msg);
            //普通消息
            if (obj.messageType == 2) {
                if (obj.fromusername == $("#sendinfo").attr('value')) {
                    html = '<div style="padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + obj.textMessage + '</div><br><br>'
                    $("#info").append(html);

                    $("#info").animate({
                        scrollTop: $("#info").prop("scrollHeight")
                    }, 400); //0.4秒内滚到底部
                } else {
                    var state = 0;
                    $(".f_item").each(function() {
                        if ($(this).attr('value') == obj.fromusername) {
                            $(this).find('.layui-badge-dot').css("display", "block")
                            state = 1;
                        }
                    })
                    if (state = 0) {
                        //说明之前没有聊过天 ,再将改人的信息加入聊天栏中,需要访问account的接口获取用户数据
                    }
                }

            }

            //渲染聊天栏朋友
            if (obj.messageType == 3) {
                users = obj.infos;
                for (e in users) {
                    html = '            <div class="f_item" value=' + users[e].acid + ' style="height: 50px;padding: 10px;">\
                <div class="layui-row layui-col-space1">\
                    <div class="layui-col-md3">\
                        <div class="grid-demo grid-demo-bg1">\
                            <img src="' + users[e].avator + '" style="height: 80%;width: 100%; border-radius: 100px;" />\
                        </div>\
                    </div>\
                    <div class="layui-col-md9">\
                        <div class="grid-demo" style="text-align: center;"><h1 style="color:white;" class="username">' + users[e].username + '</h1> <span class="layui-badge-dot" style="display:none"></span>\
                        </div>\
                    </div>\
                </div>\
            </div><br><br>'
                    $('#friends').append(html)
                    flist.push(users[e].acid)
                }
                var item = $('.f_item').click(function() {
                    $(this).find('.layui-badge-dot').css("display", "none")
                    $('#info').html("")
                    $('#topname').text($(this).find('.username').text())
                    $("#sendinfo").attr('value', $(this).attr('value'))
                        //渲染消息
                    $.ajax({
                        url: "http://localhost:9010/message/getByToAndFrom?to=" + getParams("id") + "&from=" + $(this).attr('value'), //路径 只需改为你的路径即可
                        type: "get",
                        dataType: "json",
                        success: function(data) {
                            for (e in data.data) {
                                if (data.data[e].to2 == getParams("id")) {
                                    html = '<div style=" word-break: break-all;word-wrap: break-word;padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + data.data[e].info + '</div><br><br>'
                                    $("#info").append(html);
                                } else {
                                    html = '<div style="word-break: break-all;word-wrap: break-word;float:right;padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + data.data[e].info + '</div><br><br><br><br>'
                                    $("#info").append(html);
                                }
                            }
                        }
                    })
                    $("#info").animate({
                        scrollTop: $("#info").prop("scrollHeight")
                    }, 400); //0.4秒内滚到底部

                })
            }
        };

        //连接关闭的回调事件
        webSocket.onclose = function() {
            console.log("连接已关闭...");
        };
    } else {
        // 浏览器不支持 WebSocket
        alert("您的浏览器不支持 WebSocket!");
    }

    //发送消息
    function send() {
        var selectText = $("#infotext").val();
        var message = {
            "message": selectText,
            "to": $('#sendinfo').attr('value'),
            "from": getParams("id")
        }
        webSocket.send(JSON.stringify(message));
        $("#infotext").val("");
        html = '<div style=" word-break: break-all;word-wrap: break-word;float:right;padding: 15px;background-color: darkcyan;border-radius: 10px;width:auto; display:inline-block !important; display:inline;color:white;">' + selectText + '</div><br><br><br><br>'
        $("#info").append(html);
        //滚动条到最低部
        $("#info").animate({
            scrollTop: $("#info").prop("scrollHeight")
        }, 400); //0.4秒内滚到底部
    }
</script>

</html>

总结

这一次收获不小,之前接触的都是ajax这种http响应的内容,现在突然接触这种全双工的内容,并且可以作出一个东西,感觉收获还是不小的,全部后端代码已上传码云,利用springcloud-alibaba搭建的一个微服务博客后端,目前还在完善中,这一次聊天的模块是其中的一部分,有兴趣的可以看一下完整代码 微服务博客

如果有任何问题可以随时交流,对了最后说一下不足,如果为了应对高并发,一台这样的服务器是承受不住的,需要在多台服务器搭建这样的websocket服务,但是多台服务之间该怎么交互。 个人认为可以采用redis的订阅和发布功能,每台服务器都订阅这个频道,一旦有消息传入,redis即发布消息,各台服务器根据目标id判断是不是接入自己服务的用户从而选择是否通知,从而实现一个简陋的聊天服务集群。

好了,今天就到这了,有缘再写

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 效果图
      • 涉及技术
        • springboot
        • layui
        • websocket
    • 实现思路
      • websocket在springboot下的实现
        • 前端实现
          • 建立websocket连接
          • 前端对应的websocket方法
      • 代码实现
        • 后端代码
          • 建立连接时
          • 接收到消息时
          • 发送消息
          • 总代码
        • 前端代码
        • 总结
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档