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

实时聊天室

  • 前言
    • 效果图
    • 涉及技术
      • springboot
      • layui
      • websocket
  • 实现思路
    • websocket在springboot下的实现
    • 前端实现
      • 建立websocket连接
      • 前端对应的websocket方法
  • 代码实现
    • 后端代码
      • 建立连接时
      • 接收到消息时
      • 发送消息
      • 总代码
    • 前端代码
  • 总结

前言

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

效果图

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

涉及技术

springboot

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

layui

最近发现layui的模板属实很大气,因此就采用这个模块作为前端模块了,还有一个原因就是简单(前端小白不敢说话),如何使用可以参考layui的使用手册==>layui使用手册,复制即可用
springboot+websocket+layui制作的实时聊天室,后端开发入门样例_第2张图片

websocket

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

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

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

实现思路

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

websocket在springboot下的实现

有几个注解先了解一下,

@OnOpen         //建立socket连接时调用
@OnError        //服务端出现问题时调用
@OnClose        //socket连接断开时调用
@OnMessage      //服务端接收到信息之后调用

这几个注解直接标注在方法上代表当出现以上情况时,就直接调用对应标注的方法,如下图所示,当接收到服务端的信息时就直接调用onMessage方法
springboot+websocket+layui制作的实时聊天室,后端开发入门样例_第3张图片

前端实现

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

建立websocket连接

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

 webSocket = new WebSocket("ws://192.168.43.220:9010/websocket/" + getParams("id"));

前端对应的websocket方法

前端的方法如下图所示,可以看到和上面的名字都是一一对应的,具体作用和后端的方法是一致的
springboot+websocket+layui制作的实时聊天室,后端开发入门样例_第4张图片
到此我们就可以进行代码的编写了。

代码实现

后端代码

建立连接时

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

 @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的功能。

  */
    @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 sendMessageTo(String message, String ToUserName) throws IOException {
        WebSocket webSocket = clients.get(ToUserName);
        if (webSocket != null) {
            webSocket.session.getAsyncRemote().sendText(message);
        } else {
            log.info("未上线");
        }

    }

总代码

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;
    }

}

前端代码

前端代码有点杂,有兴趣的可以看一下,没兴趣的复制就可以用
springboot+websocket+layui制作的实时聊天室,后端开发入门样例_第5张图片



<head>
    <title>websockettitle>
    <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;">xiaowblogh1>
                    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 ">9span>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 = '
' + obj.textMessage + '


'
$("#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 = '
\
\
\
\ + users[e].avator + '" style="height: 80%;width: 100%; border-radius: 100px;" />\
\
\
\

' + users[e].username + '

\
\
\
\


'
$('#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 = '
' + data.data[e].info + '


'
$("#info").append(html); } else { html = '
' + data.data[e].info + '




'
$("#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 = '
' + selectText + '




'
$("#info").append(html); //滚动条到最低部 $("#info").animate({ scrollTop: $("#info").prop("scrollHeight") }, 400); //0.4秒内滚到底部 }
script> html>

总结

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

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

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

springboot+websocket+layui制作的实时聊天室,后端开发入门样例_第6张图片

你可能感兴趣的:(后端,java,前端,java,spring,springboot,websocket,layui)