SpringBoot整合Tomcat WebSocket(附用户聊天Demo)

目录

理论部分

 什么是websocket

         websocket协议握手、通信例图

websocket协议本协议有两部分:握手和数据传输。

客户端实现

websocket事件

websocket对象的相关方法

服务端实现

服务端类Endpoint

 服务端接收客户端数据

服务端推送数据给客户

服务端客户端通信总体流程

实操部分

导入依赖坐标:

创建配置类:

java代码(有详细解释):

 前端代码(Vue)

html

 css

编写过程记录

遇到的一个获取bean的问题:

最终效果展示


理论部分


        不要急!理论谁看不迷糊,急的话直接跳过,看下面的代码案例,先体验体验,回头再上来看看。下面后端代码都有注释,包看抱会(一半是正经说法的注释,一半个人理解写的注释,包看包会)

最快速了解:

        websocket是双向协议,服务器可以主动向客户端发送消息,基于这一点实现聊天功能。websocket有五个重要点:建立连接接收数据发送数据关闭连接连接异常

就是能建立连接,收、发数据。

 什么是websocket

         WebSocket 是一种网络通信协议。RFC6455定义了它的通信标准。
         WebSocket 是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
         HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。
        这种通信模型有一个弊端: HTTP 协议无法实现服务器主动向客户端发起消息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 AJAX 请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)  

 websocket协议握手、通信例图

websocket协议本协议有两部分:握手和数据传输。

        握手是基于http协议的,来自客户端的握手看起来像如下形式:

GET ws : / / loca7host/chat HTTP/1.12

Host: localhost

upgrade: websocket4

connection: upgrade

sec-websocket-Key: dGh1IHNhbXBSZSBub25jzQ==

sec-websocket-Extensions: permessage-deflate

sec-websocket-version: 13

        来自服务器的握手看起来像如下形式:

HTTP/1.1 101 switching Protocols
upgrade: websocket
connection: upgrade
sec-websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOO=

sec-websocket-Extensions : permessage-def1ate

         字段说明:

头名称

说明

connection:Upgrade

标识该HTTP请求是一个协议升级请求

Upgrade:websocket

协议升级为websocket协议

sec-websocket-version: 13

客户端支持websocket的版本
sec-websocket-Key:客户端采用base64编码的24位随机字符序列,服务器接受客户端HTTP协议升级的证明。要求服务端响应一个对应加密的sec-websocket-Accept头信息作为应答
sec-websocket-Extensions协议扩展类型

客户端实现

        实现webSockets的 web浏览器将通过websocket对象公开所有必需的客户端功能(主要指支持Html5的浏览器)。

用形如类似以下 API用于创建websocket对象:

var ws = new websocket(url);

(参数url格式说明: ws : / /ip地址:端口号/资源名称)

websocket事件

事件事件处理程序描述
openwebsocket对象.onopen连接建立时触发
messagewebsocket对象.onmessage客户端接收服务端数据时触发
errorwebsocket对象.onerror通信发生错误时触发
closewebsocket对象.onclose连接关闭时触发

websocket对象的相关方法

方法描述
send ( )使用连接发送数据

服务端实现

        omcat的7.0.5版本开始支持websocket,并且实现了Java webSocket规范(JSR356)。

        Java WebSocket应用由一系列的webSocketEndpoint组成。Endpoint是一个java对象,代表webSocket链接的一端,对于服务端,我们可以视为处理具体websocket消息的接口,就像Servlet之与http请求一样。

服务端类Endpoint

     可以通过两种方式定义Endpoint 

              第一种是编程式,即继承类javax.websocket.Endpoint并实现其方法。

              第二种是注解式,即定义一个PoJo,并添加@serverEndpoint相关注解。

        Endpoint实例在webSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下:

方法含义描述注解
onclose当会话关闭时调用。@onclose
onopen当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法@onopen
onError当连接过程中异常时调用。@onError

 

服务端接收客户端数据

        通过为客户端的 Session 添加MessagelTandler消息处理器来接收消息,当采用注解方式定义Endpoint时,我们还可以通过onMessage 注解指定接收消息的方法。

服务端推送数据给客户端

        发送消息则由RemoteEndpoint完成,其实例由session维护,根据使用情况session.getBasicRemote获取同步消息发送的实例,然后调用其sendXxx()方法就可以发送消息,可以通过session.getAsyncRemote 获取异步消息发送实例。

服务端客户端通信总体流程

实操部分

下面就展示一下我个人写的一个很简单、不完整、漏洞百出,但基础聊天功能(一个人同时与多个用户聊天)实现的demo,请多多指教

如果有用数据库记录聊天记录的朋友,可以看下最后一点

 导入依赖坐标:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

创建配置类:

package com.fjut.jjl3211311109.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
//websocket要做的配置类
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

java代码(有详细解释):

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fjut.jjl3211311109.Utils.SpringContextUtils;
import com.fjut.jjl3211311109.mapper.ChatMapper;
import com.fjut.jjl3211311109.pojo.Chat;
import com.fjut.jjl3211311109.pojo.Follow;
import com.fjut.jjl3211311109.pojo.Message;
import com.fjut.jjl3211311109.service.ChatService;
import com.fjut.jjl3211311109.service.FollowService;
import com.fjut.jjl3211311109.service.impl.ChatServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.util.CharsetMapper;
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.time.LocalDateTime;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
//这个value后面是建立连接的名字,后面可以跟参数,前端传送,用来做消息发送对象或者创建客户端连接的名字
@ServerEndpoint(value = "/websocket/{username}/{followName}")
@Component
public class ChatEndPoint {

    //用线程安全的map来保存当前用户
        //静态map,明白吧,整个程序运行中就这一个,当成全局变量
    private static Map<String,ChatEndPoint> onLineUsers = new ConcurrentHashMap<>();

    //声明一个session对象,通过该对象可以发送消息给指定用户,不能设置为静态,每个ChatEndPoint有一个session才能区分(websocket的session)
        //注意哦,这个session和登录的那个session可不是同一个哦
    private Session session;

    //保存当前登录浏览器的用户
        //就是本次创建连接客户端的名字啦
    private String username;

    //保存被发起方
        //对方名字咯
    private String followName;

    private static ChatMapper chatMapper;

    @Autowired
    public static void setChatMapper(ChatMapper chatMapper) {
        ChatEndPoint.chatMapper = chatMapper;
    }


    //建立连接时发送系统广播
    @OnOpen
    public void onOpen(Session session,  @PathParam(value="username")String username, @PathParam(value="followName")String followName){
        System.err.println("建立客户端连接");

        this.username = username;
        this.followName = followName;

        //将局部的session对象赋值给成员session
        this.session = session;

        //将对象存储到容器中
            //将用户名称以自己和对方的名字加起来命名,查找简单
        System.err.println("上线用户名称:"+username+followName);
        
        log.info("上线用户名称:"+username+followName)
;
            //新建立的客户端要保存起来哦
        onLineUsers.put(username+followName, this);
    }

    //获取当前登录的用户
    private Set<String> getNames(){
        return onLineUsers.keySet();
    }


    //用户之间的信息发送
    @OnMessage
    //接收到客户端数据被调用
    public void onMessage(String message, Session session){
        try{
            //将json字符串转对象,前端传送参数的时候也要注意哦
            Message msg = JSON.parseObject(message, Message.class);
                System.err.println(msg.getFrom()+msg.getToWho()+"向"+msg.getToWho()+msg.getFrom()+"发送: "+msg.getMessage());

            //从容器中获取bean
                //这个bean是因为在当前类里不能直接用IOC容器注入,用来将数据存入数据库用的,下次可以看见聊天记录,可以不写哦,即时聊天也是聊天hh
            ChatMapper chatMapper = (ChatMapper) SpringContextUtils.getBean(ChatMapper.class);

            //新建聊天记录
                //这个chat很容易理解吧,我就不放上来了
            Chat chat = new Chat();
                //发送人
            chat.setPromoter(msg.getFrom());
                //接收人
            chat.setRecipient(msg.getToWho());
                //消息
            chat.setMessage(msg.getMessage());
                //时间
            chat.setCreatDate(LocalDateTime.now().toString());

            //保存聊天记录,将这个数据存入数据库中
           chatMapper.insert(chat);

            //判断接收方是否在线,发送消息
                //对方是否在线的意思是,对方是否也创建了(他名字+你名字)客户端,没有的话不能发送消息哦,会报错
            if(onLineUsers.get(msg.getToWho()+msg.getFrom()) != null){   //若接收方在线,直接发送
                    onLineUsers.get(msg.getToWho() + msg.getFrom()).session.getBasicRemote().sendText(msg.getMessage());
            }else{   //若接收方不在线,添加提醒,不发送,只保存记录到数据库,接收方上线了就可以看到
                FollowService followService = (FollowService) SpringContextUtils.getBean(FollowService.class);

                //下面代码到方法结束可以不用注意,这个只是我写的一种用数据库查询查看是否有新消息的方式
                Follow one = followService.getOne(Wrappers.<Follow>lambdaQuery().eq(Follow::getFollowName, msg.getFrom()).eq(Follow::getUserName, msg.getToWho()));

                if(one.getRemind().equals("2")) {
                    followService.AddRemind(msg.getToWho(), msg.getFrom(),"3");
                } else {
                    followService.AddRemind(msg.getToWho(), msg.getFrom(), "1");
                }
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //用户断开连接的断后操作
    @OnClose
    //连接关闭时被调用
    public void onClose(Session session){
        log.info("离线用户:"+ username+followName);
        System.err.println(username+followName+": 断开");
        if (username+followName!= null){
            onLineUsers.remove(username+followName);
        }
    }
}

 前端代码(Vue)

(饶了我吧,这是我在gpt帮助下,能写出的最好的前端了(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ ))

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Interface</title>
    <link rel="stylesheet" href="chat.css">
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    
</head>
<body>
<div id="app">
    <p>{{followName}}</p>
    <button class="clean-button" @click="deleteChat" >清除聊天记录</button>
    <div class="chat-container">
        <div class="chat-box">
            <div v-for="message in messages" :key="message.id" class="message">
                <div v-if="message.isSent" class="sent-message align-right">
                    {{ message.content }}
                </div>
                <div v-else class="received-message align-left">
                    {{ message.content }}
                </div>
            </div>

        </div>
        <div class="input-box">
            <textarea class="input-field" v-model="inputMessage" @keyup.enter="sendME"></textarea>
            <button class="send-button" @click="sendM" >发送</button>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
    var username;
    var followName;
    var ws;
    new Vue( {
        el: '#app',
        data() {
            return {
                messages: [],
                inputMessage: '',
            }
        },
        created() {
            const urlParams = new URLSearchParams(window.location.search);
            followName = urlParams.get('followName')
            console.log(followName)
            this.getCurrentUser();
        },
        destroyed: function () { // 离开页面生命周期函数
            this.websocketclose();
        },
        methods: {
            sendM() {
                if (this.inputMessage) {
                    this.messages.push({
                        id: this.messages.length + 1,
                        content: this.inputMessage,
                        isSent: false
                    });
                    var json = {"from": username ,"toWho": followName ,"message": this.inputMessage};
                    ws.send(JSON.stringify(json))
                    this.scrollToBottom();
                    this.inputMessage = '';
                }
            },
            sendME(event) {
                if (event.shiftKey) {
                    event.preventDefault(); // 阻止默认的换行行为
                } else {
                    this.sendM();
                }
            },
            scrollToBottom() {
                //滑动到最下方
                this.$nextTick(() => {
                    const chatBox = document.querySelector(".chat-box");
                    chatBox.scrollTop = chatBox.scrollHeight;
                });
            },
            getCurrentUser: function() {
                axios.post("http://localhost:8083/user/getCurrentUser",).then(resp => {
                        Result = resp.data;
                        username=Result.msg;
                        console.log(username);
                        //初始化websocket
                        this.initWebSocket()
                        //获取聊天记录
                        this.getChatList();

                    }
                ).catch(e => {
                    console.log(e)
                })
            },
            deleteChat: function() {
                axios.post("http://localhost:8083/chat/delete?from="+username+"&toWho="+followName,).then(resp => {
                        this.messages=[];
                    }
                ).catch(e => {
                    console.log(e)
                })
            },
            getChatList: function() {
                axios.post("http://localhost:8083/chat/get?from="+username+"&toWho="+followName).then(resp => {
                    Result = resp.data;
                    console.log(Result);
                    // 遍历聊天记录
                    Result.data.forEach(chat => {
                        let isSent = false;
                        if (chat.promoter === username) {
                            isSent = false;
                        } else if (chat.recipient === username) {
                            isSent = true;
                        }
                        // 将聊天记录添加到 messages 数组
                        this.messages.push({
                            id: this.messages.length + 1,
                            content: chat.message,
                            isSent: isSent
                        });
                    });

                    //滑动到最新消息
                    this.scrollToBottom();

                }).catch(e => {
                    console.log(e);
                });
            },
            initWebSocket: function () { // 建立连接
                // WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
                var host = window.location.host;
                ws = new WebSocket("ws://"+host+"/websocket/"+username+"/"+followName);
                console.log(ws);
                ws.onopen = this.websocketonopen;
                ws.onerror = this.websocketonerror;
                ws.onmessage = this.websocketonmessage;
                ws.onclose = this.websocketclose;
            },
            // 连接成功后调用
            websocketonopen: function () {
                console.log("WebSocket连接成功");
            },
            // 发生错误时调用
            websocketonerror: function (e) {
                console.log("WebSocket连接发生错误");
            },
            // 接收服务端消息
            websocketonmessage: function (e) {
                var data = e.data;
                this.messages.push({
                    id: this.messages.length + 1,
                    content: data,
                    isSent: true
                });
                //滑动到最新消息
                this.scrollToBottom();
            },
            // 关闭连接时调用
            websocketclose: function (e) {
                console.log("WebSocket连接关闭");
            },

        }
    })
</script>

</body>
</html>

 css

body {
    background-image:url('980.jpg');
    background-color: #f2f2f2;
    margin: 0;
    padding: 0;
    font-family: Arial, sans-serif;
}

.chat-box {
    margin-top: 40px;
    width: 600px;
    height: 800px;
    background-color: #fff;
    border-radius: 20px;
    overflow-y: scroll;
    padding: 15px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.sent-message {
    background-color: #f3f3f3;
    padding: 15px;
    border-radius: 16px;
    color: #333;
    font-size: 16px;
    word-wrap: break-word; /* 自动换行 */
}
.sent-message.align-right {
    text-align: left;
    margin-left: 0;
    margin-right: auto;
}

.received-message {
    background-color: #d4f4ff;
    padding: 15px;
    border-radius: 16px;
    color: #333;
    font-size: 16px;
    word-wrap: break-word; /* 自动换行 */
}

.received-message.align-left {
    text-align: right;
    margin-right: 0;
    margin-left: auto;
}

.input-field {
    flex-grow: 1;
    border: none;
    outline: none;
    font-size: 16px; /* 更新字体大小值 */
    padding: 5px;
}
.send-button {
    background-color: #4e8aff;
    color: #fff;
    border: none;
    outline: none;
    padding: 8px 15px;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}
.send-button:hover {
    background-color: #1565c0;
}



.message {
    margin-bottom: 10px;
}

.input-box {
    width: 600px;
    margin-top: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #fff;
    border-radius: 16px;
    padding: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}





body {
    background-image:url('980.jpg');
    background-color: #f2f2f2;
    margin: 0;
    padding: 0;
    font-family: Arial, sans-serif;
}

.chat-container {
    height: 90vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}
.chat-box {
    margin-top: 40px;
    width: 600px;
    height: 700px;
    background-color: #fff;
    border-radius: 20px;
    overflow-y: scroll;
    padding: 15px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.sent-message {
    background-color: #f3f3f3;
    padding: 15px;
    border-radius: 16px;
    color: #333;
    font-size: 16px;
    word-wrap: break-word; /* 自动换行 */
}
.sent-message.align-right {
    text-align: left;
    margin-left: 0;
    margin-right: auto;
}

.received-message {
    background-color: #d4f4ff;
    padding: 15px;
    border-radius: 16px;
    color: #333;
    font-size: 16px;
    word-wrap: break-word; /* 自动换行 */
}

.received-message.align-left {
    text-align: right;
    margin-right: 0;
    margin-left: auto;
}

.input-field {
    flex-grow: 1;
    border: none;
    outline: none;
    font-size: 16px; /* 更新字体大小值 */
    padding: 5px;
}
.send-button {
    background-color: #4e8aff;
    color: #fff;
    border: none;
    outline: none;
    padding: 8px 15px;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}
.send-button:hover {
    background-color: #1565c0;
}

.clean-button {
    background-color: #cecece;
    color: #fff;
    border: none;
    outline: none;
    padding: 8px 15px;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}
.clean-button:hover {
    background-color: #b6b6b6;
}


.message {
    margin-bottom: 10px;
}

.input-box {
    width: 600px;
    margin-top: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #fff;
    border-radius: 16px;
    padding: 10px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

编写过程记录

前端:

        前端太难了,555,特别是实例、组件方法啥的,花了好久时间(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )

后端代码:

        将客户端的命名格式为:当前用户+聊天用户。可以避免当一个人同时与多个人聊天的时候,若只用当前用户名作为客户端名,会出现串聊的问题(用户A同时建立多个命名为A的客户端,多个客户端对的客户端A发消息,会对所有名字为A的客户端发消息,就串聊了),但是我突然意识到,每与一个人聊天,就创建一个客户端,这消耗。。。还是得以当前用户名创建一个客户端,至于怎么区分不同用户的消息可以再做判断(前端貌似能做)

目前版本能完成对单人同时与多人单独聊天,没做平台检测,要是同时登录多个会出错(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ ),而且一个浏览器只能同时登录一个账号。还有好多问题一时间没想起来

遇到的一个获取bean的问题:

我使用mybatisplus来访问存取数据库,使用bean注入的时候发现了问题,注入的bean都是空的,websocket类加上了@Component注解,但还是无法通过@Resource和@Autowire注入spring容器管理下的bean(但是websocket类可以被别的类注入),所以采用通过ApplicationContext接口的getBean()方法获取Bean对象,工具类代码如下。

package com.fjut.jjl3211311109.Utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtils implements ApplicationContextAware {

    /**
     * 上下文对象实例
     */
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    /**
     * 获取applicationContext
     */
    public static ApplicationContext getApplicationContext() {
        //判断是否为null
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextHolder.");
        }
        return applicationContext;
    }

    /**
     * 通过name获取Bean
     */
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    /**
     * 通过class获取Bean
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过name和class获取Bean
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

}

 最终效果展示

来看看最终成功吧~~

成功!可以一个人同时与多个人单独聊天!

参考:

【黑马程序员】WebSocket打造在线聊天室【配套资料+源码】_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值