实时聊天室

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

前言

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

效果图

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

实时聊天mysql表怎么设计 实时聊天室_服务端

涉及技术

springboot

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

layui

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

实时聊天mysql表怎么设计 实时聊天室_服务端_02

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方法

实时聊天mysql表怎么设计 实时聊天室_服务端_03

前端实现

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

建立websocket连接

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

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

前端对应的websocket方法

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

实时聊天mysql表怎么设计 实时聊天室_服务端_04


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

代码实现

后端代码

建立连接时

此时会连接数据库,把之前和你聊过天的用户的信息读取出来,传递给浏览器,这里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;
    }

}

前端代码

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

实时聊天mysql表怎么设计 实时聊天室_实时聊天mysql表怎么设计_05

<!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判断是不是接入自己服务的用户从而选择是否通知,从而实现一个简陋的聊天服务集群。

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

实时聊天mysql表怎么设计 实时聊天室_服务端_06