Java使用WebStocket实现前后端互发消息

news2024/11/20 20:43:47

记录一下自己使用WebStocket实现服务器主动发消息的过程和踩得雷。

需求:车牌识别系统识别到车牌后,持续向前端推送车牌信息,直到前端回复收到。

测试需求:新增 客户后,持续向前端推送客户信息,直到前端收到消息,并且回复收到。

1.引入WebStocket的依赖

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

2.创建配置类 WebScoketConfig

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

/**
 * 开启WebSocket支持
 */
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

新增客户的业务层

  这里实现了新增 客户后,持续向前端推送客户信息。

  实现思路:本来是打算 让前端接收到客户信息,回复后端的时候,后端修改数据库中此条客户的 接收状态的字段,然后每次后端往前端发送消息的时候都去数据库查询一次 客户信息的接收状态,如果已经接收到了就不往前端推送。但是好像会造成一边读数据库,一边修改数据库,会出现脏读的问题,而且我在 WebScoketConfigServer 中并不能创建Service层的对象,总是报空指针。

  最后,决定使用 static修饰的静态变量来实现对前端是否接受到消息和是否发送的是同一条重复的消息进行判断。然后根据返回的结果决定是否继续往前端推送消息。

import io.recycle.modules.rest.api.dto.system.CustomerDto;
import io.recycle.modules.rest.api.dto.system.CustomerQueryDto;
import io.recycle.modules.rest.api.dto.weigh.CardDto;
import io.recycle.modules.rest.api.dto.weigh.CustomerParam;
import io.recycle.modules.rest.api.dto.weigh.CustomerWeighDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

import io.recycle.modules.rest.api.dao.RecycleCustomerDao;
import io.recycle.modules.rest.api.entity.RecycleCustomerEntity;
import io.recycle.modules.rest.api.service.RecycleCustomerService;



@Service("recycleCustomerService")
public class RecycleCustomerServiceImpl implements RecycleCustomerService{

	private static int count=0;
	@Autowired
	private NoticeWebsocket noticeWebsocket;
	@Autowired
	private RecycleCustomerDao recycleCustomerDao;

	@Override
	public void save(RecycleCustomerEntity recycleCustomer){
		recycleCustomerDao.save(recycleCustomer);
		//测试webstocket,实现新增客户往前端推送消息,直到前端回复
//		//测试webstocket
		boolean flag = false;
		do{
			try {
				Thread.sleep(300);    //休息300毫秒
			} catch (InterruptedException e) {
				e.printStackTrace();
				System.out.println("休息时出错000000");
			}
			//往前端发送消息
			System.out.println("count===="+count);
			boolean resultFlag = noticeWebsocket.sendMessage("新增了用户:" + recycleCustomer.toString(),count);

			flag = resultFlag;

			if (resultFlag){
				System.out.println("停止往前端发送数据,因为 resultFlag 为: "+resultFlag+"==说明前端已接收的消息");
			}else {
				System.out.println("往前端发送数据,因为 resultFlag 为: "+resultFlag+"==说明前端还没接收到消息");
			}


		}while ( !flag );
		System.out.println("停止往前端发送数据,因为 delFlag 为: "+flag);
		count = count +1;


	}
}

3.创建WebScoketConfigServer

在websocket协议下,后端服务器相当于ws里的客户端,需要用@ServerEndpoint指定访问的路径,并使用@Component注入容器。

这里实现了新增 客户后,持续向前端推送客户信息,直到前端收到消息,并且回复收到。

实现思路:

import com.alibaba.fastjson.JSONObject;
import io.recycle.modules.rest.api.dao.RecycleCustomerDao;
import io.recycle.modules.rest.api.dto.websocket.NoticeWebsocketResp;
import io.recycle.modules.rest.api.entity.RecycleCustomerEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */
@ServerEndpoint("/chepaisend")
@Component
@Slf4j
public class NoticeWebsocket {

    //记录连接的客户端
    public static Map<String, Session> clients = new ConcurrentHashMap<>();

    /**
     * userId关联sid(解决同一用户id,在多个web端连接的问题)
     */
    public static Map<String, Set<String>> conns = new ConcurrentHashMap<>();

    private String sid = null;




    //一些记录发送消息状态
    private static int initFlag =0;

    private static int tempFlag =0;
    //区分新旧消息的变量
    private static int sum=0;
    /**
     * 连接成功后调用的方法
     * @param session
     *
     */
    @OnOpen
    public void onOpen(Session session) {
        this.sid = UUID.randomUUID().toString();

        clients.put(this.sid, session);

        log.info(this.sid + "连接开启!");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        log.info(this.sid + "连接断开!");
        clients.remove(this.sid);
    }

    /**
     * 判断是否连接的方法
     * @return
     */
    public static boolean isServerClose() {
        if (NoticeWebsocket.clients.values().size() == 0) {
            log.info("已断开");
            return true;
        }else {
            log.info("已连接");
            return false;
        }
    }

    /**
     * 发送给所有用户
     * @param noticeType
     */

    public static boolean sendMessage(String noticeType,int count){
        //判断是否是新的新增客户
        System.out.println("count= "+count+",sum= "+sum+",initFlag= "+initFlag+",tempFlag= "+tempFlag);
        if (sum != count){
            NoticeWebsocketResp noticeWebsocketResp = new NoticeWebsocketResp();
            noticeWebsocketResp.setNoticeType(noticeType);
            sendMessage(noticeWebsocketResp);
            sum = count;
        }
        //判断前端是否 回复了 收到消息  相等没收到,不相等 收到
        if (initFlag==tempFlag){
            NoticeWebsocketResp noticeWebsocketResp = new NoticeWebsocketResp();
            noticeWebsocketResp.setNoticeType(noticeType);
            sendMessage(noticeWebsocketResp);
        }else {
            //收到消息了,别发同一个消息了
            tempFlag = initFlag;
            return true;
        }
        tempFlag = initFlag;
        //没收到消息继续发
        return false;
    }


    /**
     * 发送给所有用户
     * @param noticeWebsocketResp
     */
    public static void sendMessage(NoticeWebsocketResp noticeWebsocketResp){
        String message = JSONObject.toJSONString(noticeWebsocketResp);
        for (Session session1 : NoticeWebsocket.clients.values()) {
            try {
                session1.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 根据用户id发送给某一个用户
     * **/
    public static void sendMessageByUserId(String userId, NoticeWebsocketResp noticeWebsocketResp) {
        if (!StringUtils.isEmpty(userId)) {
            String message = JSONObject.toJSONString(noticeWebsocketResp);
            Set<String> clientSet = conns.get(userId);
            if (clientSet != null) {
                Iterator<String> iterator = clientSet.iterator();
                while (iterator.hasNext()) {
                    String sid = iterator.next();
                    Session session = clients.get(sid);
                    if (session != null) {
                        try {
                            session.getBasicRemote().sendText(message);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }


    /**
     * 收到客户端消息后调用的方法
     * @param message
     * @param session
     */

    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自窗口"+"的信息:"+message);
        if ("已接收到消息".equals(message)){
            //收到消息,改变flag的值
            System.out.println("前端已经收到消息,开始改变 initFlag的值,此时initFlag= "+initFlag);
            initFlag = initFlag +1;
            System.out.println("前端已经收到消息,已经改变 initFlag的值,此时initFlag== "+initFlag);
        }

    }

    /**
     * 发生错误时的回调函数
     * @param error
     */
    @OnError
    public void onError(Throwable error) {
        log.info("错误");
        error.printStackTrace();
    }

}

封装的发送消息的对象

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel("ws通知返回对象")
public class NoticeWebsocketResp<T> {

    @ApiModelProperty(value = "通知类型")
    private String noticeType;

    @ApiModelProperty(value = "通知内容")
    private T noticeInfo;

}

4.WebSocket调用

用户端调用此接口,主动将消息发送给后端,后端接收到消息后再主动推送给指定/全部用户,可以实现消息的私聊和群发功能。

import io.recycle.common.utils.R;
import io.recycle.modules.rest.api.service.impl.NoticeWebsocket;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {
	@GetMapping("/test")
    public R test() {
    	NoticeWebsocket.sendMessage("你好,WebSocket",1);
        return R.ok();
    }
}

前端WebSocket连接

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SseEmitter</title>
</head>
<body>
<div id="message"></div>
</body>
<script>
    var limitConnect = 0;
    init();
    function init() {
        //开启webstocket服务的ip地址  ws:// + ip地址 + 访问路径
        var ws = new WebSocket('ws://127.0.0.1:8191/double-win/chepaisend');
// 获取连接状态
        console.log('ws连接状态:' + ws.readyState);
//监听是否连接成功
        ws.onopen = function () {
            console.log('ws连接状态:' + ws.readyState);
            limitConnect = 0;
            //连接成功则发送一个数据
            ws.send('我们建立连接啦');
        }
// 接听服务器发回的信息并处理展示
        ws.onmessage = function (data) {
            console.log('接收到来自服务器的消息:');
            console.log(data);
            //接收到 消息后给后端发送的 确认收到消息,后端接收到后 不再重复发消息
            ws.send('已接收到消息');
            //完成通信后关闭WebSocket连接
            // ws.close();
        }
// 监听连接关闭事件
        ws.onclose = function () {
            // 监听整个过程中websocket的状态
            console.log('ws连接状态:' + ws.readyState);
            reconnect();

        }
// 监听并处理error事件
        ws.onerror = function (error) {
            console.log(error);
        }
    }
    function reconnect() {
        limitConnect ++;
        console.log("重连第" + limitConnect + "次");
        setTimeout(function(){
            init();
        },2000);

    }
</script>
</html>

项目启动后,打开写好的前端页面后控制台打印连接信息

 新增客户后,前端接收到,并回复收到了

  新增客户后,前端接收到,并回复收到了,后端停止推送

前端接收到,但是骗后端没收到,或者说后端不知道 前端已经接收到消息。 

后端展示,后端一直往前端推送

 

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

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

相关文章

type=“module“ 你了解,但 type=“importmap“ 你知道吗

新出了一个系列&#xff1a;Vue2与Vue3 技巧小册 有梦想&#xff0c;有干货&#xff0c;微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。 本文 GitHub https://github.com/qq449245884/xiaozhi 已收录&#xff0c;有一线大厂面试完整考点、资料以及我的系列文章。 当E…

css的rotate3d实现炫酷的圆环转动动画

1.实现效果 2.实现原理 2.1 rotate3d rotate3d&#xff1a;rotate3d() CSS 函数定义一个变换&#xff0c;它将元素围绕固定轴移动而不使其变形。运动量由指定的角度定义; 如果为正&#xff0c;运动将为顺时针&#xff0c;如果为负&#xff0c;则为逆时针。 语法&#xff1a; …

【面试题】redux及中间件相关面试题解析

1、什么是Redux&#xff1f; Redux就是一个js容器&#xff0c;用于全局的状态管理 2、为什么在React项目中要使用Redux&#xff1f; 因为React本质上就是一个UI库&#xff0c;它是单向数据流的&#xff0c;就是说数据只能从父组件通过props流向子组件&#xff0c;但如果子组…

Vue 无感刷新token

关于无感刷新的理解: 实现token无感刷新对于前端来说是一项非常常用的技术,其本质是为了优化用户体验,当token过期时不需要用户跳回登录页重新登录,而是当token失效时,进行拦截,发送刷新token的请求,获取最新的token进行覆盖,让用户感受不到token已经过期 刷新token的一些方案…

零基础JavaScript学习【第六期】

博主有话说:这个礼拜有些忙,离上一次更新已经过了亿天,这几天较忙所以更新会有点慢,谢谢大家的支持。(っ•̀ω•́)っ✎⁾⁾ 我爱学习 个人空间:GUIDM的个人空间 专栏内容:零基础JavaScript学习 基础还是关键。 欢迎大家的一键三连。 前情回顾:第一期https://blog.cs…

uni-app - 最详细 H5 网页接入微信支付功能,提供从详细的示例代码与注释(移动端网页实现微信支付能力,微信公众号前端支付 JSAPI / JS-SDK 详细示例源码)官方最新超级详细教程

前言 关于 uni-app 项目中接入微信支付的文章鱼龙混杂,各种 JSAPI / JS-SDK 乱代码、过时、没注释、不讲流程原理,非常难用。 本文实现了 uni-app H5 移动端网页项目,实现微信支付功能,详细讲解接入流程及超详细示例代码, 从 0-1 完全站在小白的角度,让您无需太多知识也…

【React】React入门--生命周期

&#x1f380;个人主页&#xff1a;努力学习前端知识的小羊 感谢你们的支持&#xff1a;收藏&#x1f384; 点赞&#x1f36c; 加关注&#x1fa90; 文章目录React生命周期初始化阶段运行中阶段销毁阶段老生命周期的问题新生命周期代替性能优化的方案重要的勾子即将废弃的勾子R…

【ElementUI样式优化1】el-table 修改斑马格样式、修改滚动条样式、添加表头边框、删除表格边框划线

重要的不是过去&#xff0c;而是你怎末看待过去&#xff0c;而我们对过去的看法&#xff0c;是可以改变的。 效果预览&#xff1a; &#xff08;1&#xff09;删除表格外框&#xff0c;内框。 &#xff08;2&#xff09;添加表头边框&#xff0c;修改表头文字大小、颜色 &…

猿创征文 |【高级篇】Java 进阶之JVM实战

文章目录⚡前言一、面试题解析二、JVM 理论详解⛅JVM的位置❄️JVM的体系结构⏳类加载器三、JVM 双亲委派机制四、Native 关键字五、PC寄存器 与 方法区六、栈与堆七、三种JVM、新生区、老年区、永久区⛵小结⚡前言 JVM 是 Java 实现 跨平台的基础&#xff0c;所有的Java 程序…

[遇到的问题-已解决]Cannot resolve plugin org.apache.maven.plugins:maven-compiler-plugin:3.1

如上图所示&#xff0c;这是我解决好的&#xff0c;刚开始的时候爆红有这些&#xff1a; 我按照在网上查找的方法&#xff0c;一一试了。 首先&#xff0c;maven 安装的路径和和本地仓库的目录必须要保持一致 打开setting-Build,Excution,Deployment-Build Tools-Maven&#…

vue框架介绍

概述 Vue 是一套用于构建用户界面的渐进式框架 对渐进式的理解&#xff1a; 每个框架都不可避免会有自己的一些特点&#xff0c;从而会对使用者有一定的要求&#xff0c;这些要求有强有弱&#xff0c;它的强势程度会影响在业务开发中的使用方式。 vue的定位&#xff1a; 我在做…

【Vue入门必备知识篇03】--- 生命周期 数据共享

前言❤️ 过好自律的生活&#xff0c;美好才会在路上不期而遇 ❤️【Vue入门必备知识篇03】--- 生命周期 & 数据共享一、生命周期 & 数据共享&#xff08;1&#xff09;组件的生命周期1.1 生命周期 & 生命周期函数1.2 组件生命周期函数的分类1.3 生命周期图示&…

记录Chrome插件从V2版本升级到V3版本的过程中遇到的问题

总结一下自己在把Chrome V2版本的插件升级到V3版本的过程中&#xff0c;遇到的一些问题&#xff0c;之前也有发布一章V3版本的manifest.json配置项参数说明&#xff0c;基本也涵盖了下面提到的几个配置项的改动&#xff0c;传送门>> 总结分了两大块&#xff0c;一块是ma…

【Node.js】深度解析常用核心模块-fs模块

✅ 作者简介&#xff1a;一名将要迈入大三的大学生&#xff0c;致力于提高前端开发能力 ✨ 个人主页&#xff1a;前端小白在前进的主页 &#x1f525; 系列专栏 &#xff1a; node.js学习专栏 ⭐️ 个人社区 : 个人交流社区 &#x1f525;前言 在文章&#x1f449;Node.js — 前…

React脚手架工具创建项目的详细介绍

文章目录React脚手架工具脚手架工具解析create-react-app创建React项目目录的结构分析从零编写代码React脚手架工具 脚手架工具解析 如果我们只是开发几个小的demo程序&#xff0c;那么永远不需要考虑一些复杂的问题: 比如目录结构如何组织划分; 比如如何管理文件之间的相互依…

Python lxml库的安装和使用

lxml 是 Python 的第三方解析库&#xff0c;完全使用 Python 语言编写&#xff0c;它对 Xpath 表达式提供了良好的支持&#xff0c;因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 安装lxml库 lxml 属于 Python 第三方库&#xff0c;因此需要…

全网多种方式解决Unchecked runtime.lastError: The message port closed before a response was received的错误

文章目录1. 文章引言2. 分析问题3. 解决问题4. 解决该错误的其他方法1. 文章引言 今天启动项目后访问Knife4j接口文档&#xff0c;却报出下图错误&#xff1a; 在报出Knife4j文档请求异常错误时&#xff0c;赶紧打开控制台&#xff0c;如下所示&#xff1a; 即Unchecked runti…

CSS过渡动画

css中实现动画有两种方式&#xff1a;transition过渡动画、 animation自定义动画。 transition 是 css3 新增的⼀个功能&#xff0c;可以实现元素不同状态间的平滑过渡&#xff08;当元素从⼀个状态进⼊到另⼀个状态时&#xff09;&#xff0c;经常⽤来制作⼀些动画效果。 之…

前端项目面试核心问题(持续更新)

本文有配套视频教程 项目面核心问题回答思路 说说你最近的项目 记叙文的六要素&#xff1a;时间、人物、地点、起因、经过、结果&#xff1b;时间&#xff1a;研发周期&#xff1b;人物&#xff1a;团队成员、分工、我负责哪几个模块&#xff1b;起因&#xff1a;项目背景、…

Vue项目实战——【基于 Vue3.x + Vant UI】实现一个多功能记账本(开发导航栏及公共部分)

基于 Vue3.x Vant UI 的多功能记账本&#xff08;三&#xff09; 文章目录基于 Vue3.x Vant UI 的多功能记账本&#xff08;三&#xff09;项目演示开发导航栏1、底部导航栏2、测试底部导航栏3、公共头部写到最后&#xff08;附源码&#xff09;系列内容参考链接基于 Vue3.x …