前端加springboot实现Web Socket连接通讯以及测试流程(包括后端实现心跳检测)

news2024/12/29 14:17:32

【2023】前端加springboot实现Web Socket连接通讯(包括后端实现心跳检测)

  • 一级目录
    • 二级目录
      • 三级目录
  • 前言
  • 一、Web Socket 简绍
    • 1 为什么用 websocket?
  • 二、代码实现
    • 1、前端(html)
      • 1.1、无前端向后端发送消息
      • 1.2、有前端向后端发送消息
    • 2、后端具体代码(spring boot)
    • 2.1、maven依赖
    • 2.2、配置类
    • 3、Web Socket连接工具类
    • 2.3、Controller用于测试主动发送消息
    • 2.4、定时任务,用于调用主动向客户端发送心跳
  • 三、测试
    • 1、 测试消息发送
      • 1.1、前端日志
      • 1.2、后端日志
    • 2、测试客户端异常断开,服务器通过心跳检测自动剔除掉异常对话。

一级目录

二级目录

三级目录

前言

写这个项目主要是有有个项目需要后端有数据实话返回前端,一开始采用前端轮询的方式,后面觉得及时性上有些不行,然后改为使用websocket ,具体实现demo以及测试流程发出来提供交流学习,

一、Web Socket 简绍

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

1 为什么用 websocket?

换句话说,websocket 解决了什么问题?答案是,解决了两个主要问题:

  • 只能客户端发送请求
  • 一段时间内的频繁信息发送

假设现在需要设计一个实时预警系统的通知模块,那么作为工程师我们应该怎么设计通知的这个功能呢?因为这些系统的数据来源,一般他通过硬件设备采集到后台的,如果我们现在只有 http 协议,那么我们只能让客户端不断地轮询服务器,轮询的时间间隔越小越能接近实时的效果。可是,轮询的效率低,又浪费资源。针对这样的场景,websocket 应运而生。

在这里插入图片描述
特点:
(1)建立在 TCP 协议之上,服务器端的实现比较容易,是一个可靠的传输协议。
(2)与 HTTP协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

二、代码实现

1、前端(html)

1.1、无前端向后端发送消息

在这里插入图片描述

uid实际开发中应该使用唯一值作为当前对话的key

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
消息展示区:<br/>
<div id="textArea"></div>
 
</body>
<script>
    var textArea = document.getElementById('textArea');
 
 
    var websocket = null;
    //如果浏览器支持websocket就建立一个websocket,否则提示浏览器不支持websocket 
                //uid应该要用唯一标识,为了测试方便看
    if('websocket' in window){
        websocketPage = new WebSocket('ws://localhost:8080/websocket/' + 99);
    }else{
        alert('浏览器不支持websocket!');
    }
    //建立websocket时自动调用
    websocketPage.onopen = function (event) {
        console.log('建立连接');
    }
    //关闭webscoket时自动调用
    websocketPage.oncolse = function (event){
        console.log('关闭连接');
    }
    //websocket接收到消息时调用
    websocketPage.onmessage = function (event){
        //将接收到的消息展示在消息展示区  (心跳响应回来的消息不显示)
        if (event.data !== "conn_success"){
            textArea.innerText += event.data;
            textArea.innerHTML += "<br/>";
        }
    }
    //websocket出错自动调用
    websocketPage.onerror = function () {
        alert('websocket出错');
    }
    //关闭窗口前关闭websocket连接
    window.onbeforeunload = function (){
        websocketPage.close();
    }
 
</script>
</html>

1.2、有前端向后端发送消息

在这里插入图片描述

<!DOCTYPE html>
<html>

	<head>
		<meta charset="utf-8">
		<title>Java后端WebSocket的Tomcat实现</title>
		<script type="text/javascript" src="js/jquery.min.js"></script>
	</head>

	<body>
		
		Welcome<br/><input id="text" type="text" />
		<button onclick="send()">发送消息</button>
		<hr/>
		<button onclick="closeWebSocket()">关闭WebSocket连接</button>
		<hr/>
		<div id="message"></div>
	</body>
	<script type="text/javascript">
		var websocket = null;
		//判断当前浏览器是否支持WebSocket
		if('WebSocket' in window) {
			//改成你的地址
			websocket = new WebSocket("ws://localhost:8080/websocket/100");
		} else {
			alert('当前浏览器 Not support websocket')
		}

		//连接发生错误的回调方法
		websocket.onerror = function() {
			setMessageInnerHTML("WebSocket连接发生错误");
		};

		//连接成功建立的回调方法
		websocket.onopen = function() {
			setMessageInnerHTML("WebSocket连接成功");
		}
		var U01data, Uidata, Usdata
		//接收到消息的回调方法
		websocket.onmessage = function(event) {
			console.log(event);
			if (event.data !== "conn_success"){
				setMessageInnerHTML("接收消息:"+event.data);
				// setMessageInnerHTML(event);
				setechart()
			}
		}

		//连接关闭的回调方法
		websocket.onclose = function() {
			setMessageInnerHTML("WebSocket连接关闭");
		}

		// //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
		window.onbeforeunload = function() {
			closeWebSocket();
		}

		//将消息显示在网页上
		function setMessageInnerHTML(innerHTML) {
			document.getElementById('message').innerHTML += innerHTML + '<br/>';
		}

		//关闭WebSocket连接
		function closeWebSocket() {
			websocket.close();
		}

		//发送消息
		function send() {
			var message = document.getElementById('text').value;
			websocket.send('{"msg":"' + message + '"}');
			setMessageInnerHTML("--------------发送消息:"+message + "");
		}
	</script>
</html>

2、后端具体代码(spring boot)

2.1、maven依赖

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

– yml没有东西,只有一个默认端口

2.2、配置类

需要加一个 WebSocket 端点暴露 的bean 和定时器注解

@EnableScheduling  //定时器
@SpringBootApplication
public class WebSocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebSocketApplication.class, args);
    }



    /** 
     * 服务器端点导出 
     * @author zhengfuping
     * @date 2023/8/22 
     * @return ServerEndpointExporter 
     */
    @Bean
    public ServerEndpointExporter getServerEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

3、Web Socket连接工具类

@Slf4j
@Service
@ServerEndpoint("/websocket/{uid}")
public class WebSocketServer2 {

    //连接建立时长
    private static final long sessionTimeout = 60000;

    // 用来存放每个客户端对应的WebSocketServer对象
    private static Map<String, WebSocketServer2> webSocketMap = new ConcurrentHashMap<>();

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    // 接收id
    private String uid;

    /**
     * 连接建立成功调用的方法
     * @author zhengfuping
     * @date 2023/8/22
     * @param session
     * @param uid
     */
    @OnOpen
    public void onOpen(Session session , @PathParam("uid") String uid){
        session.setMaxIdleTimeout(sessionTimeout);
        this.session = session;
        this.uid = uid;
        if (webSocketMap.containsKey(uid)){
            webSocketMap.remove(uid);
        }
        webSocketMap.put(uid,this);
        log.info("websocket连接成功编号uid: " + uid + ",当前在线数: " + getOnlineClients());

        try{
        // 响应客户端实际业务数据!
            sendMessage("conn_success");
        }catch (Exception e){
            log.error("websocket发送连接成功错误编号uid: " + uid + ",网络异常!!!");
        }
    }

    /**
     * 连接关闭调用的方法
     * @author zhengfuping
     * @date 2023/8/22
     */
    @OnClose
    public void onClose(){
        try {
            if (webSocketMap.containsKey(uid)){
                webSocketMap.remove(uid);
            }
            log.info("websocket退出编号uid: " + uid + ",当前在线数为: " + getOnlineClients());
        } catch (Exception e) {
            log.error("websocket编号uid连接关闭错误: " + uid + ",原因: " + e.getMessage());
        }
    }


    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            WebSocketServer2.sendInfo(message);
            log.info("websocket收到客户端编号uid消息: " + uid + ", 报文: " + message);
        } catch (Exception e) {
            log.error("websocket发送消息失败编号uid为: " + uid + ",报文: " + message);
        }

    }

    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("websocket编号uid错误: " + this.uid + "原因: " + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     * @author yingfeng
     * @date 2023/8/22 10:11
     * @Param * @param null
     * @return
     */

    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 获取客户端在线数
     * @author zhengfuping
     * @date 2023/8/22 10:11
     * @param
     */
    public static synchronized int getOnlineClients() {
        if (Objects.isNull(webSocketMap)) {
            return 0;
        } else {
            return webSocketMap.size();
        }
    }




    /**
     * 单机使用,外部接口通过指定的客户id向该客户推送消息
     * @param key
     * @param message
     * @return boolean
     */
    public static boolean sendMessageByWayBillId(@NotNull String key, String message) {
        WebSocketServer2 webSocketServer = webSocketMap.get(key);
        if (Objects.nonNull(webSocketServer)) {
            try {
                webSocketServer.sendMessage(message);
                log.info("websocket发送消息编号uid为: " + key + "发送消息: " + message);
                return true;
            } catch (Exception e) {
                log.error("websocket发送消息失败编号uid为: " + key + "消息: " + message);
                return false;
            }
        } else {
            log.error("websocket未连接编号uid号为: " + key + "消息: " + message);
            return false;
        }
    }

    /**
     * 群发自定义消息
     * @author zhengfuping
     * @date 2023/8/22 9:52
     * @param message
     */
    public static void sendInfo(String message) {
        webSocketMap.forEach((k, v) -> {
            WebSocketServer2 webSocketServer = webSocketMap.get(k);
            try {
                webSocketServer.sendMessage(message);
                log.info("websocket群发消息编号uid为: " + k + ",消息: " + message);
            } catch (IOException e) {
                log.error("群发自定义消息失败: " + k + ",message: " + message);
            }
        });
    }
    /**
     * 服务端群发消息-心跳包
     * @author zhengfuping
     * @date 2023/8/22 10:09
     * @param message 推送数据
     * @return int 连接数
     */
    public static synchronized int sendPing(String message){
        if (webSocketMap.size() == 0)
            return 0;
        StringBuffer uids = new StringBuffer();
        AtomicInteger count = new AtomicInteger();
        webSocketMap.forEach((uid,server)->{
            count.getAndIncrement();

            if (webSocketMap.containsKey(uid)){
                WebSocketServer2 webSocketServer = webSocketMap.get(uid);
                try {
                    if (Integer.valueOf(uid) ==101){
                        Integer i=1/0;
                    }

                    webSocketServer.sendMessage(message);
                    if (count.equals(webSocketMap.size() - 1)){
                        uids.append("uid");
                        return;

                    }
                    uids.append(uid).append(",");
                } catch (Exception e) {
                    webSocketMap.remove(uid);
                    log.info("客户端心跳检测异常移除: " + uid + ",心跳发送失败,已移除!");

                }
            }else {
                log.info("客户端心跳检测异常不存在: " + uid + ",不存在!");

            }
        });
        log.info("客户端心跳检测结果: " + uids + "连接正在运行");
        return webSocketMap.size();
    }
    /**
     * 连接是否存在
     * @param uid
     * @return boolean
     */
    public static boolean isConnected(String uid) {
        if (Objects.nonNull(webSocketMap) && webSocketMap.containsKey(uid)) {
            return true;
        } else {
            return false;
        }
    }
}

2.3、Controller用于测试主动发送消息

@RestController
@RequestMapping("/test")
public class WebSocketController{
    /**
     * 检验连接
     * @date 2023/8/22
     * @Param * @param webSocketId
     * @return * @return String
     */
    @GetMapping("/webSocketIsConnect/{webSocketId}")
    public String webSocketIsConnect(@PathVariable("webSocketId") String webSocketId){
        if (WebSocketServer2.isConnected(webSocketId)) {
            return webSocketId+"正在连接";
        }
        return webSocketId+"连接断开!";
    }

    /**
     * 单发 消息
     * @author zhengfuping
     * @date 2023/8/22 10:25
     * @param webSocketId  指定 连接
     * @param message  数据
     * @param pwd 验证密码
     * @return String
     */
    @GetMapping("/sendMessageByWayBillId")
    public String sendMessageByWayBillId(String webSocketId, String message, String pwd) {
        boolean flag = false;

            flag = WebSocketServer2.sendMessageByWayBillId(webSocketId, message);

        if (flag) {
            return "发送成功!";
        }
        return "发送失败!";
    }

    /**
     * 群发
     * @author zhengfuping
     * @date 2023/8/22 10:26
     * @param message
     * @param pwd
     */
    @GetMapping("/broadSendInfo")
    public void sendInfo(String message, String pwd) {
            WebSocketServer2.sendInfo(message);
    }
}

2.4、定时任务,用于调用主动向客户端发送心跳

每10秒调用一次,主动检测,查看客户端连接是否异常断开,如果异常断开,则把该会话从集合中剔除掉,避免无限积压。

@Component
@Slf4j
public class WebSocketTask {
    
    @Scheduled(cron = "0/10 * * * * ?")
    public void clearOrders(){
        int num = 0;
        try {
            num = WebSocketServer2.sendPing("conn_success");
        } finally {
            log.info("websocket心跳检测结果,共【" + num + "】个连接");
        }
    }
}

三、测试

1、 测试消息发送

1.1、前端日志

在这里插入图片描述

1.2、后端日志

在这里插入图片描述

2、测试客户端异常断开,服务器通过心跳检测自动剔除掉异常对话。

因为测试不方便,只能通过断点实现效果

  1. 前端需要把主动关闭会话的注释掉,不让主动关闭
    在这里插入图片描述

  2. 先在连接关闭的地方和心跳检测地方打上断点,断点需要设置成Thread,要不然没法异步
    在这里插入图片描述

  3. 然后关闭掉一个前端页面让他把会话关闭,就会进入该断点位置,通过断点让它停住不让他去正常关闭

在这里插入图片描述4. 然后选择执行该心跳检测的断点代码
在这里插入图片描述
5. 进入心跳的循环给每个会话发送心跳检测,此时前端已经异常断开了
在这里插入图片描述
6. 因为前端已经关闭会话了,则发送心跳会失败,会直接进入catch块,然后把该会话从集合中剔除掉
在这里插入图片描述
最终日志
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/913197.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

当下软件测试员的求职困境

从去年被裁到现在&#xff0c;自由职业的我已经有一年没有按部就班打卡上班了。期间也面试了一些岗位&#xff0c;有首轮就挂的&#xff0c;也有顺利到谈薪阶段最后拿了offer的&#xff0c;不过最后选择了拒绝。 基于自己近一年的面试求职经历&#xff0c;我想聊聊当下大家在求…

半导体投资前景蒙阴,投资降幅或创下近十年最大纪录 | 百能云芯

对经济增速放缓的担忧&#xff0c;正逐渐影响半导体行业&#xff0c;使得一度火热的投资热情开始变得谨慎起来。预计2023年&#xff0c;全球前十大半导体制造商的设备投资额将首次在过去四年中出现下降趋势&#xff0c;而且这个下降幅度可能创下近十年来的最大纪录。 最近&…

AD域证书导入JDK

一、安装证书 服务器管理器找到仪表盘&#xff0c;点击添加角色和功能 点击下一步 再次点击下一步 下一步 选择Active Directory证书服务 点击添加功能&#xff0c;然后点击下一步 点击下一步 下一步 按照下图所示选择&#xff0c;默认证书颁发机构已经选择&#xff0c…

Vue3.X 创建简单项目(一)

一、环境安装与检查 首先&#xff0c;我们要确保我们安装了构建vue框架的环境&#xff0c;不会安装的请自行百度&#xff0c;有很多安装教程。检查环境 node -v # 如果没有安装nodejs请安装&#xff0c;安装教程自行百度 vue -V# 没有安装&#xff0c;请执行npm install -g v…

2023年如何运营TikTok账号?这些技巧你一定要知道

Tik Tok目前的全球月活已经突破7亿。作为全球最受欢迎的应用程序之一&#xff0c;它不仅为用户提供了记录分享生活中美好时刻、交流全球创意的阵地&#xff0c;也给全球的企业提供了一个直接触达用户的平台。 一、保持视频内容的真实性 当我们站在用户的角度去考虑时&#xf…

PMP如何备考?学习方式这里有

预习阶段&#xff1a;强烈建议跟着习课视频学习&#xff08;自己看书真的很难看懂&#xff09;&#xff0c;初步了解PMBOK&#xff0c;有个大致印象&#xff1b; 精讲阶段&#xff1a;这个时候就需要静下心来深入了解各个知识模块&#xff0c;不仅是看PMBOK&#xff0c;还要尽…

走进湖南大学麒麟信安|openEuler 嵌入式Meetup议程硬核来袭!

9月8日&#xff0c;openEuler社区将联合湖南大学、麒麟信安和湖南欧拉生态创新中心举办嵌入式Meetup&#xff0c;这将是一场集结智慧的开发者交流与愉快学习的盛宴&#xff0c;让我们的技术激情熊熊燃起&#xff01; 01 活动亮点 嵌入式技术新契机&#xff1a;本次Meetup将为…

深度学习入门(四):经典网络架构(Alexnet、Vgg、Resnet)

一、经典网络架构-Alexnet 2012年ImageNet竞赛冠军 8层神经网络、5层卷积层、3层全连接 二、经典网络架构-Vgg 2014 年ImageNet 竞赛冠军 VGG 最大的特点就是它在之前的网络模型上&#xff0c;通过比较彻底地采用 3x3 尺寸的卷积核来堆叠神经网络&#xff0c;从而加深整个神…

怎么样才能开期权账户

为了保护投资者权益&#xff0c;上交所设定了50万的准入门槛&#xff0c;挡着了很多想入手期权交易的小伙伴&#xff0c;如果资金不够50万&#xff0c;那么有什么办法能零门槛参与期权呢&#xff0c;下文给大家介绍怎么样才能开期权账户的知识点。本文来自&#xff1a;期权酱 一…

【玩三层交换机,关键得有这几个思路。】

今天要分享的主题还是跟交换机有关&#xff01;三层交换机。 当我们需要在不同的 LAN 或 VLAN 之间传输数据时&#xff0c;二层交换机就无法满足了。 这时&#xff0c;需要三层交换机登场了哈。 很多新来的朋友总在问&#xff0c;三层交换机到底要怎么配置&#xff0c;有哪些…

全国城市内涝排涝模拟技术及在市政、规划设计中应用教程

详情点击链接&#xff1a;全国城市内涝排涝模拟技术及在市政、规划设计中应用教程 一&#xff0c;数据准备 通过标准化的步骤&#xff0c;利用CAD数据、GIS数据&#xff0c;在建模的不同阶段发挥不同软件的优势&#xff0c;实现高效的数据处理、准确的参数赋值、模型的快速建…

Docker-Consul

Docker-Consul 一、介绍1.什么是服务注册与发现2.什么是consul3.consul提供的一些关键特性&#xff1a; 二、consul 部署1.环境准备2.consul服务器3.查看集群信息4.通过 http api 获取集群信息 三、registrator服务器1.安装 Gliderlabs/Registrator2.测试服务发现功能是否正常3…

开源TTS+gtx1080+cuda11.7+conda+python3.9吊打百度TTS

简介 开源项目&#xff0c;文本提示的生成音频模型 https://github.com/suno-ai/bark Bark是由Suno创建的基于变换器的文本到音频模型。Bark可以生成极为逼真的多语种演讲以及其他音频 - 包括音乐、背景噪音和简单的声音效果。该模型还可以产生非言语沟通&#xff0c;如笑声…

【数据结构】 单链表面试题讲解->叁

文章目录 &#x1f340;[相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/description/)&#x1f384;题目描述&#x1f38d;示例&#x1f6a9;示例一&#x1f6a9;示例二&#x1f6a9;示例三 &#x1f38b;解法思路&#x1f6a9;相关变量的建立&…

Elasticsearch(十三)搜索---搜索匹配功能④--Constant Score查询、Function Score查询

一、前言 之前我们学习了布尔查询&#xff0c;知道了filter查询只在乎查询条件和文档的匹配程度&#xff0c;但不会根据匹配程度对文档进行打分&#xff0c;而对于must、should这两个布尔查询会对文档进行打分&#xff0c;那如果我想在查询的时候同时不去在乎文档的打分&#…

多传感器分布式融合算法——加权最小二乘WLS融合/简单凸组合SCC融合

加权最小二乘WLS融合/简单凸组合SCC融合——多传感器分布式融合算法 原创不易&#xff0c;路过的各位大佬请点个赞 主要讲解算法&#xff1a; 加权最小二乘融合WLS 简单凸组合融合SCC 应用于: 多传感器网络协同目标跟踪/定位/导航 联系WX: ZB823618313 目…

【Python】理解作用域:内置、全局、局部

原文作者&#xff1a;我辈李想 版权声明&#xff1a;文章原创&#xff0c;转载时请务必加上原文超链接、作者信息和本声明。 文章目录 一、基础数据类型1.整型1.函数中加global&#xff08;2&#xff09;函数中不加global&#xff08;3&#xff09;报错 二、组合数据类型1.列表…

AI让儿童绘画动起来-程序员带娃必备

项目效果演示 项目描述 很多小朋友在学习绘画的过程中&#xff0c;创作出来很多比较有创意的作品&#xff0c;那么怎么让这些作品&#xff0c;动起来&#xff0c;甚至是和拍摄的视频进行互动呢&#xff0c;今天分享的这个项目&#xff0c;能够完美解决这个问题。 项目地址http:…

ASEMI整流桥KBP210和2W10能代换吗

编辑-Z 在电子世界中&#xff0c;整流桥是最常见和最重要的组件之一。它是将交流电转换为直流电的重要设备。在这篇文章中&#xff0c;我们将深入了解两款常见的整流桥&#xff1a;KBP210和2W10&#xff0c;以及它们是否可以互换使用。 首先&#xff0c;我们需要关注的是这两种…

网上可做的兼职副业,分享3个靠谱办法,快来收藏

不知道从何时起&#xff0c;越来越多的人开始在网上做兼职&#xff0c;兼职逐渐的成为上班族和大学生的第二件事。相比线上&#xff0c;他们不愿意做传统的发传单、或者体力活兼职。线上兼职也是在互联网和智能手机的影响下被广泛使用了吧。但是网上的兼职这么多&#xff0c;那…