SpringBoot整合WebSocket的两种方式及微服务网关Gateway配置

news2024/12/24 2:57:39

一、说明

项目中后台微服务需要向前端页面推送消息,因此不可避免的需要用到WebSocket技术。SpringBoot已经为WebSocket的集成提供了很多支持,只是WebSocket消息如何通过微服务网关Spring Cloud Gateway向外暴露接口,实际开发过程中遇到了很多问题。微服务框架本身是作为一个平台为各种服务提供支撑的,所以对常用的两种WebSocket实现方式都要能够适配,特别是用Stomp方式实现时要考虑WebSocket接口与Rest API接口共存时的跨域问题。查了很多资料,也稍微浏览了一下源码,总算成功的解决了问题。下面着重讲实现的过程,展示代码,原理就不详细介绍了,网上一大堆。

二、WebSocket基本原理

WebSocket 协议是基于 TCP 的一种新的网络协议,它实现了浏览器与服务器全双工(full-duplex)通信—允许服务器主动发送信息给客户端,这样就可以实现从客户端发送消息到服务器,而服务器又可以转发消息到客户端,这样就能够实现客户端之间的交互。对于 WebSocket 的开发,Spring 也提供了良好的支持,目前很多浏览器已经实现了 WebSocket 协议,但是依旧存在着很多浏览器没有实现该协议,为了兼容那些没有实现该协议的浏览器,往往还需要通过 STOMP 协议来完成这些兼容。

 三、常规实现方式

1、后台微服务代码

POM引入

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

WebSocket配置类

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

 WebSocket服务实现类

@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;
    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

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

    //接收sid
    private String sid="";

/**
 * 连接建立成功调用的方法*/
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        log.info("有新窗口开始监听:"+sid+",当前在线人数为" + getOnlineCount());
        this.sid=sid;
        try {
            sendMessage("连接成功");
        } catch (IOException e) {
            log.error("websocket IO异常");
        }
    }

/**
 * 连接关闭调用的方法
 */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1
        log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

/**
 * 收到客户端消息后调用的方法
 *
 * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自窗口"+sid+"的信息:"+message);
        //群发消息
        for (WebSocketServer item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

/**
 *
 * @param session
 * @param error
 */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

/**
 * 实现服务器主动推送
 */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

/**
 * 群发自定义消息
 * */
    public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException {
        log.info("推送消息到窗口"+sid+",推送内容:"+message);
        for (WebSocketServer item : webSocketSet) {
            try {
                //这里可以设定只推送给这个sid的,为null则全部推送
                if(sid==null) {
                    item.sendMessage(message);
                }else if(item.sid.equals(sid)){
                    item.sendMessage(message);
                }
            } catch (IOException e) {
                continue;
            }
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

    public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
        return webSocketSet;
    }
}

应用程序配置信息 application.yml

server:
  port: 9009

spring:
  application:
    name: test-service

2、Spring Cloud Gateway配置

这种方式的配置相对简单一些,只要把websocket的路径配置上就可以了。需要和REST API的路由信息分开进行配置。另外对于REST API有安全校验机制,在网关验证http请求携带的Token信息,对WebSocket连接就不检查了。

spring:
  cloud:
    gateway:
      globalcors:
        add-to-simple-url-handler-mapping: true
        corsConfigurations: #网关跨域配置
          '[/**]':
            allowedOriginPatterns: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true
            maxAge: 360000
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true
      routes:
        - id: testWS2 #websocket 路由配置
          uri: lb:ws://test-service
          predicates:
            - Path=/test-ws/**
          filters:
            - StripPrefix=1
        - id: test2 #REST API路由配置
          uri: lb://test-service
          predicates:
            - Path=/test2/**
          filters:
            - StripPrefix=1

security:
  ignore:
    whites:
      - /test-ws/** #websocket不检查http头中的token

3、前端代码

写了个最简单的Html页面进行验证

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"></meta>
    <title>Title</title>
</head>
<body>
<button id="connect" onclick="connect();">发送</button>
hello world!
</body>
<script src="jquery.min.js"></script>
<script>
    var socket;
    if(typeof(WebSocket) == "undefined") {
        console.log("您的浏览器不支持WebSocket");
    }else{
        console.log("您的浏览器支持WebSocket");
        //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
        //等同于
        index = new WebSocket("ws://localhost:38080/test-ws/websocket/99");
        //打开事件
        index.onopen = function() {
            console.log("Socket 已打开");
            //socket.send("这是来自客户端的消息" + location.href + new Date());
        };
        //获得消息事件
        index.onmessage = function(msg) {
            console.log(msg.data);
            //发现消息进入    开始处理前端触发逻辑
        };
        //关闭事件
        index.onclose = function() {
            console.log("Socket已关闭");
        };
        //发生了错误事件
        index.onerror = function() {
            alert("Socket发生了错误");
            //此时可以尝试刷新页面
        }
        //离开页面时,关闭socket
        //jquery1.8中已经被废弃,3.0中已经移除
        // $(window).unload(function(){
        //     socket.close();
        //});
    }
	
	function connect() {
	    $.ajax({
                "url":"http://localhost:38080/test2/system/socket/push/99",
                "type":"get",
                "data":"",
                "dataType":"json",
                "success":function(res){
                    console.log(res);
                }
            });
	}
</script>
</html>

4、实现效果

打开网页 F12,可以看到效果。

 

四、STOMP方式

STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

为什么需要STOMP呢:常规的websocket连接和普通的TCP基本上没有什么差别的。所以STOMP在websocket上提供了一中基于帧线路格式(frame-based wire format)。简单一点来说,就是在websocket(TCP)上面加了一层协议,使双方遵循这种协议来发送消息。

1、后台微服务代码

POM引入,与上面相同

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

webSocket配置类

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
    //配置消息代理(Message Broker)
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 订阅
        registry.enableSimpleBroker("/toAll");
    }

    //注册STOMP协议的节点(endpoint)
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 端点
        registry.addEndpoint("/device/point")
                //跨域,这儿必须加跨域,只在网关加不行
                .setAllowedOriginPatterns("*")
                //.setHandshakeHandler(customHandshakeHandler)
                .withSockJS(); // 使用sockJs
    }

    @Override
    public boolean configureMessageConverters(List<MessageConverter> messageConverters) { // 不要这个Bean好像也没问题
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setObjectMapper(objectMapper);
        DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
        resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
        converter.setContentTypeResolver(resolver);
        messageConverters.add(new StringMessageConverter());
        messageConverters.add(new ByteArrayMessageConverter());
        messageConverters.add(converter);
        return false;
    }
}

Controller层代码

@Controller
public class ExampleWebSocketController {
    @MessageMapping("/welcome")
    @SendTo("/toAll/getResponse")
    public String sendTopicMessage(String str) {
        System.out.println("后台广播推送!");

        return str;
    }
}

2、Spring Cloud Gateway配置

这儿的配置是试了很多方法才确定了可行的配置的。对WebSocket,需要配置两个路由。一个是http方式,一个是ws方式。

网关本身加了跨域设置,后台服务的websocket也加了跨域配置,所以得在路由上加上DedupeResponseHeader的过滤器,只保留一个跨域请求头。这儿是关键。

spring:
  cloud:
    gateway:
      globalcors:
        add-to-simple-url-handler-mapping: true
        corsConfigurations: #网关跨域配置
          '[/**]':
            allowedOriginPatterns: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
            allowCredentials: true
            maxAge: 360000
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true
      routes:
        - id: model #REST API 路由配置
          uri: lb://model-service
          predicates:
            - Path=/model/**
          filters:
            - StripPrefix=1
        - id: websocket1  #websokcet 路由配置
          uri: lb://model-service
          predicates:
            - Path=/model-ws/device/point/info/**
          filters:
            - StripPrefix=1
            - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
        - id: websocket1 #websokcet 路由配置
          uri: lb:ws://model-service
          predicates:
            - Path=/model-ws/device/point/**
          filters:
            - StripPrefix=1
            - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

security:
  ignore:
    whites:
      - /model-ws/**

3、前端代码

再写一个简单的HTML页面展示效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Spring Boot+WebSoc+广播式</title>
</head>
<body onload="disconnect()">
  <noscript><h2 style="color: #ff0000">貌似浏览器不支持WebSocket</h2></noscript>
<div>
  <div>
    <button id="connect" onclick="connect();">连接</button>
    <button id="disconnect" onclick="disconnect();">断开连接</button>
  </div>
  <div id="conversationDiv">
    <label>输入你的名字</label><input type="text" id="name"/>
    <button id="sendName" onclick="sendName();">发送</button>
    <p id="response"></p>
  </div>
  <script src="sockjs.min.js"></script>
  <script src="stomp.min.js"></script>
  <script src="jquery.min.js"></script>
  <script type="text/javascript">
    var stompClient  = null;
    function setConnected(connected) {
      document.getElementById('connect').disabled = connected;
      document.getElementById('disconnect').disabled = !connected;
      document.getElementById('conversationDiv').style.visibility = connected ? 'visible':'hidden';
      $("#response").html();
    }

    function connect() {
      //连接SockJS的endpoint名称为"/endpointWisely"
      var socket = new SockJS('http://localhost:38080/model-ws/device/point');
      //使用STOMP子协议的WebSocket客户端
      stompClient = Stomp.over(socket);
      //连接WebSocket服务端
      stompClient.connect({},function (frame) {
        setConnected(true);
        console.log('Connected:'+frame);
        //通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送消息
        //这个是在控制器的@SendTo中定义的
        stompClient.subscribe('/toAll/getResponse',function (response) {
		  console.log(response.body);
          showResponse(response.body);
        });
      });
    }

    function disconnect() {
      if (stompClient != null){
        stompClient.disconnect();
      }
      setConnected(false);
      console.log("Disconnected");
    }
    function sendName() {
      var name = $("#name").val();
      //通过stompClient.send向/welcome目标(destination)发送消息
      //这个是在控制器的@MessageMapping中定义的
      stompClient.send("/welcome",{}, name);
    }
    function showResponse(message) {
      var response = $("#response");
      response.html(message);
    }
  </script>
</div>
</body>
</html>

4、实现效果

打开网页,F12,可以看到效果

 

 

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

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

相关文章

【数据结构第四章】- 串的模式匹配算法(BF 算法和 KMP 算法/用 C 语言实现)

目录 一、前言 二、BF 算法 三、KMP 算法 3.2.1 - KMP 算法的原理 3.2.2 - KMP 算法的实现 3.2.3 - KMP 算法的优化 创作不易&#xff0c;可以点点赞&#xff0c;如果能关注一下博主就更好了~ 一、前言 子串的定位运算通常称为串的模式匹配或串匹配。此运算的应用非常广…

美国主机的带宽和网络速度究竟有多快?

在选择一个主机时&#xff0c;其带宽和网络速度是非常重要的考虑因素。而美国主机在带宽和网络速度方面有着明显的优势&#xff0c;成为了众多用户的首选。那么&#xff0c;美国主机的带宽和网络速度究竟有多快呢?本文将通过分析美国主机的网络基础设施和数据中心设施&#xf…

golang入门项目——打卡抽奖系统

功能介绍 用户加入群组之后&#xff0c;会在签到群组所设的签到地点进行签到和签退&#xff0c;并限制同一个设备只能签到一个用户&#xff0c;签到成功之后。会获取一定的限制在该群组使用的积分。该群组可以设置一些抽奖活动&#xff0c;用户可使用该群组内的积分来进行该群…

Python+mysql+php搭建另类免费代理池

文章目录 前言:思路&#xff1a;开干&#xff1a;php连接MySQL取ip和端口&#xff1a;效果图&#xff1a; 最后调用代理池&#xff1a;总结&#xff1a; 前言: 为什么说另类的&#xff0c;因为我完全是按照我自己的想法来的&#xff0c;比较鸡肋&#xff0c;但是能用&#xff…

短视频app开发:如何提高视频播放稳定性

简介 如今&#xff0c;短视频已经成为人们日常生活中不可或缺的一部分&#xff0c;而短视频app的开发也日益成为了人们热议的话题。在短视频app开发的过程中&#xff0c;如何提高视频播放稳定性是一个非常重要的问题。本文将从短视频源码角度出发&#xff0c;分享提高短视频ap…

如何优化语音交友app开发的搜索和匹配算法

语音交友app开发的挑战 在当今社交媒体行业中&#xff0c;语音交友app开发已经成为一个热门的领域。越来越多的人开始使用语音交友app来寻找新的朋友&#xff0c;这也为开发者们带来了许多机会。然而&#xff0c;这个领域也面临着一些挑战。其中一个最大的挑战是如何优化搜索和…

掏空腰包,日子难过,机缘转岗软件测试,这100个日夜的心酸只有自己知道...

我今年27岁&#xff0c;原本从事着土木工程相关的工作&#xff0c;19年开始有了转行的想法... 大学刚毕业那年&#xff0c;我由于学的是土木工程专业&#xff0c;自然而然的从事了和土木工程相关的工作&#xff0c;房贷、车贷&#xff0c;在经济的高压下&#xff0c;当代社会许…

大数据题目测试(一)

目录 一、环境要求 二、提交结果要求 三、数据描述 四、功能要求 1.数据准备 2.使用 Spark&#xff0c;加载 HDFS 文件系统 meituan_waimai_meishi.csv 文件&#xff0c;并分别使用 RDD和 Spark SQL 完成以下分析&#xff08;不用考虑数据去重&#xff09;。 (1)配置环境…

Java设计模式-day01

1&#xff0c;设计模式概述 1.1 软件设计模式的产生背景 "设计模式"最初并不是出现在软件设计中&#xff0c;而是被用于建筑领域的设计中。 1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫亚历山大&#xff08;Christopher Alexand…

React Native iOS打包详细步骤

一、在自己项目的iOS文件夹下新建一个文件夹取名bundle 二、将打包命令写到项目package.json文件里&#xff0c;终端执行 npm run bundle-ios 先添加如下&#xff08;注意&#xff1a;这里写的路径"./ios/bundle"就是上面bundle创建的文件夹&#xff09;&#xff1a…

C51单片机介绍

本文为学习51单片机的学习的基础&#xff0c;先介绍单片机是什么。所使用的单片机有什么资源。每一个功能的作用是什么。本文使用的是STC89C52RC 40I-PDIO40&#xff0c;故以此为基础研究学习。 C51单片机介绍 单片机的概述单片机的组成部分中央处理器程序存储器数据存储器定时…

图神经网络能做什么?

从概念上讲&#xff0c;我们可以将图神经网络的基本学习任务分为 5 个不同的方向&#xff1a; &#xff08;1&#xff09;图神 经网络方法&#xff1b; &#xff08;2&#xff09;图神经网络的理论理解&#xff1b; &#xff08;3&#xff09;图神经网络的可扩展性&#xff1b…

Git的进阶使用(二)

本篇文章旨在分享本人在学习Git时的随笔记&#x1f929; 文章目录 概述1、Git 分支1.1 主干分支1.2 其他分支1.2.1 创建分支1.2.2 查看分支1.2.3 切换分支1.2.4 删除分支 2、Git 合并2.1 主干分支2.2 其他分支2.3 合并分支 3、Git 冲突3.1 主干分支3.2 其他分支3.3 切换分支 -B…

Replika:AI智能聊天机器人

【产品介绍】 Replika&#xff0c;这个名字可能有点拗口&#xff0c;但如果你知道这是复制品Replica的同音变体&#xff0c;你即刻能明白这个产品的定位了。官方Luka公司定义它是你的AI朋友&#xff0c;默默学习你&#xff0c;最终成为你的复制品。它不像现在市面上各大厂的AI助…

《ChatGPT开发应用指南》,Datawhale开源了!

Datawhale发布 开源教程&#xff1a;HuggingLLM&#xff0c;Datawhale团队 随着ChatGPT的爆火&#xff0c;我们相信未来会有越来越多的大模型及类似OpenAI提供的服务出现&#xff0c;AI 正在逐渐平民化&#xff0c;将来每个人都可以利用大模型轻松地做出自己的AI产品。 Huggin…

【历史上的今天】3 月 23 日:网景创始人出生;FORMAC 语言的开发者诞生;PRMan 非商业版发布

整理 | 王启隆 透过「历史上的今天」&#xff0c;从过去看未来&#xff0c;从现在亦可以改变未来。 今天是 2023 年 3 月 23 日&#xff0c;在 141 年前的今天&#xff0c;1882 年 3 月 23 日&#xff0c;抽象代数之母艾米诺特&#xff08;Emmy Noether&#xff09;诞生。她的…

JUC-多线程(12. AQS-周阳)学习笔记

文章目录 1. 可重入锁1.1. 概述1.2. 可重入锁类型1.3. Synchronized 可重入实现机理 2. LockSupport2.1. LockSupport 是什么2.2. 3种线程等待唤醒的方法2.2.1 Object 的等待与唤醒2.2.2. Condition接口中的等待与唤醒2.2.3. 传统的 synchronized 和 Lock 实现等待唤醒通知的约…

本地搭建属于自己的ChatGPT:基于PyTorch+ChatGLM-6b+Streamlit+QDrant+DuckDuckGo

本地部署chatglm及缓解时效性问题的思路&#xff1a; 模型使用chatglm-6b 4bit&#xff0c;推理使用hugging face&#xff0c;前端应用使用streamlit或者gradio。 微调对显存要求较高&#xff0c;还没试验。可以结合LoRA进行微调。 缓解时效性问题&#xff1a;通过本地数据库…

YOLOv7如何提高目标检测的速度和精度,基于模型结构提高目标检测速度

目录 一、目标检测二、目标检测的速度和精度的权衡1、速度和精度的概念和定义2、如何评估目标检测算法的速度和精度3、速度和精度之间的权衡 三、基于模型结构提高目标检测速度1、Backbone网络的选择2、特征金字塔网络的设计3、通道注意力机制4、混合精度训练 一、目标检测 目…

光纤网卡传输速率和它的应用领域有哪些呢?通常会用到哪些型号网络变压器呢?

Hqst盈盛&#xff08;华强盛&#xff09;电子导读&#xff1a;常有客户问起光纤网卡该如何选用到合适的产品&#xff0c;选用时要注意到哪些事项&#xff0c;这节将结合配合到的网络变压器和大家一起探讨&#xff0c;希望对大家有些帮助。 1&#xff0e;光纤网卡传输速率与网络…