草庐IT

SpringBoot——整合WebSocket(STOMP协议)

小波同学 2023-09-17 原文

前言

HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。

这种单向请求的缺点,如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。

一、为什么需要STOMP?

WebSocket 协议是一种相当低级的协议。它定义了如何将字节流转换为帧。帧可以包含文本或二进制消息。由于消息本身不提供有关如何路由或处理它的任何其他信息,因此很难在不编写其他代码的情况下实现更复杂的应用程序。幸运的是,WebSocket 规范允许在更高的应用程序级别上使用子协议。

另外,单单使用WebSocket完成群聊、私聊功能时,需要自己管理session信息,通过STOMP协议时,spring已经封装好,开发者只需要关注自己的主题、订阅关系即可。

二、STOMP详解

STOMP 中文为“面向消息的简单文本协议”,STOMP 提供了能够协作的报文格式,以至于 STOMP 客户端可以与任何 STOMP 消息代理(Brokers)进行通信,从而为多语言,多平台和 Brokers 集群提供简单且普遍的消息协作。STOMP 协议可以建立在WebSocket 之上,也可以建立在其他应用层协议之上。通过 Websocket建立 STOMP 连接,也就是说在 Websocket 连接的基础上再建立 STOMP 连接。最终实现如上图所示,这一点可以在代码中有一个良好的体现。

业界已经有很多优秀的 STOMP 的服务器/客户端的开源实现

  • STOMP 服务器:ActiveMQ、RabbitMQ、StompServer、…
  • STOMP 客户端库:stomp.js(javascript)

Stomp 的特点是客户端的实现很容易,服务端相当于消息队列的 broker 或者是 server,一般不需要我们去实现,所以重点关注一下客户端如何使用

  • CONNECT:启动与服务器的流或 TCP 连接
  • SEND:发送消息
  • SUBSCRIBE:订阅主题
  • UNSUBSCRIBE:取消订阅
  • BEGIN:启动事物
  • COMMIT:提交事物
  • ABORT:回滚事物
  • ACK:确认来自订阅的消息的消费
  • NACK:告诉服务器客户端没有消费该消息
  • DISCONNECT:断开连接

其实STOMP协议并不是为WS所设计的,它其实是消息队列的一种协议,和AMQP、JMS是平级的。 只不过由于它的简单性恰巧可以用于定义WS的消息体格式。 目前很多服务端消息队列都已经支持了STOMP,比如RabbitMQ、Apache ActiveMQ等。很多语言也都有STOMP协议的客户端解析库,像JAVA的Gozirra、C的libstomp、Python的pyactivemq、JavaScript的stomp.js等等。

STOMP协议官方文档

三、SpringBoot集成STOMP代码示例

3.1 架构图

3.2、服务端代码

  • 1、添加依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.10.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
    </dependency>

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

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>
  • 2、WebSocket的配置类
/**
 * 通过EnableWebSocketMessageBroker
 * 开启使用STOMP协议来传输基于代理(message broker)的消息,
 * 此时浏览器支持使用@MessageMapping 就像支持@RequestMapping一样。
 */

//WebSocket的配置类
@Configuration
//开启对WebSocket的支持
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{

    /**
     * 注册stomp的端点
     * 注册一个STOMP协议的节点,并映射到指定的URL
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //endPoint 注册协议节点,并映射指定的URl点对点-用
        //注册一个名字为"/endpointSocket" 的endpoint,并指定 SockJS协议。
        //允许使用socketJs方式访问,访问点为webSocketServer,允许跨域
        //连接前缀
        registry.addEndpoint("/endpointSocket")
                .setAllowedOrigins("*")  // 跨域处理
                .withSockJS();  //支持socketJs
    }


    /**
     * 配置消息代理(message broker)
     * 启用一个简单的基于内存的消息代理
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //订阅Broker名称:topic 代表发布广播,即群发
        //queue 代表点对点,即发指定用户
        registry.enableSimpleBroker("/queue", "/topic");
        
        //send命令时需要带上/app前缀
        // 全局使用的消息前缀(客户端订阅路径上会体现出来)
        config.setApplicationDestinationPrefixes("/app");
        
        //修改convertAndSendToUser方法前缀
        //点对点使用的订阅前缀(客户端订阅路径上会体现出来),
        // 不设置的话,默认也是/user/
        //config.setUserDestinationPrefix ("/user/");
    }
}

@EnableWebSocketMessageBroker注解启用 WebSocket 消息处理,由消息代理支持。

SockJS 有一些浏览器中缺少对 WebSocket 的支持,而 SockJS 是一个浏览器的 JavaScript库,它提供了一个类似于网络的对象,SockJS 提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和 Web 服务器之间创建了一个低延迟、全双工、跨域通信通道。SockJS 的一大好处在于提供了浏览器兼容性。即优先使用原生WebSocket,如果浏览器不支持 WebSocket,会自动降为轮询的方式。如果你使用 Java 做服务端,同时又恰好使用 Spring Framework 作为框架,那么推荐使用SockJS。

  • 3、控制器代码
@Slf4j
@RestController
public class TestController
{
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;
 
    @MessageMapping("/hello")
    @SendTo ("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(1000); // simulated delay
        return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
    }
 
    @MessageMapping("/topic/greetings")
    public Greeting greeting2(HelloMessage message) throws Exception {
        Thread.sleep(1000); // simulated delay
        log.info ("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
        return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
    }
    @GetMapping ("/hello2")
    public void greeting3(HelloMessage message) throws Exception {
        Thread.sleep(1000); // simulated delay
        simpMessagingTemplate.convertAndSend ("/topic/greetings",
                new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));
    }
 
 
    @MessageMapping("/sendToUser")
    public void sendToUser(HelloMessage message) throws Exception {
        Thread.sleep(1000); // simulated delay
        log.info ("userId:{},msg:{}",message.getUserId (),message.getName ());
//      simpMessagingTemplate.convertAndSendToUser (message.getUserId (),"/sendToUser",
//              new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));
//      simpMessagingTemplate.convertAndSend ("/user/1/sendToUser",
//              new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));
        simpMessagingTemplate.convertAndSend ("/topic/user/"+message.getUserId ()+"/sendToUser",
                new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));
    }
}
  • @MessageMapping:功能与RequestMapping注解类似。send指令发送信息时添加此注解。

  • @SendTo/@SendToUser:将信息输出到该主题。客户端订阅同样的主题后就会收到信息。

  • 在只有指定@MessageMapping时@MessageMapping == “/topic” + @SendTo。

  • 如果想使用rest接口发送消息。可以通过SimpMessagingTemplate进行发送。

  • 点对点聊天时,可以使用SimpMessagingTemplate.convertAndSendToUser方法发送。个人意味比注解@SendToUser更加容易理解,更加方便。

  • convertAndSendToUser方法和convertAndSend类似,区别在于convertAndSendToUser方法会在主题默认添加/user/为前缀。因此,示例代码中convertAndSend方法直接传入"/topic/user/"+message.getUserId ()+"/sendToUser" 也是点对点发送。topic其中是默认前缀。

  • 如果想修改convertAndSendToUser默认前缀可在配置类进行配置,可在WebSocketConfig类中查看。

四、SimpMessagingTemplate的作用

  • 1、SimpMessagingTemplate可以在应用的任意地方发送消息。
    Spring的SimpMessagingTemplate能够在应用的任何地方发送消息,甚至不必以首先接收一条消息作为前提。使用SimpMessagingTemplate的最简单方式是将它(或者其接口SimpMessageSendingOperations)自动装配到所需的对象中。

  • 2、SimpMessagingTemplate可以为指定的用户发送消息。
    SimpMessagingTemplate还提供了convertAndSendToUser()方法。convertAndSendToUser()方法能够让我们给特定用户发送消息。

simpMessageSendingOperations.convertAndSendToUser("1", "/message", "测试convertAndSendToUser");

stomp.subscribe('/users/1/message', function(message){ });

客户端接收一对一消息的主题是"/users/"+usersId+"/message",这里的用户Id可以是一个普通字符串,只要每个客户端都使用自己的Id并且服务器端知道每个用户的Id就行了。

五、神奇的@SendTo和@SendToUser

5.1 @SendTo

利用 @SendTo 注解,使方法的返回值推送到消息代理器中,由消息代理器广播到订阅路径中去。但并没有详细的介绍消息是怎样被Spring框架处理,最后发送广播出去的。

@MessageMapping("/hello") 
//使用MessageMapping注解来标识所有发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理. 
@SendTo("/topic/greetings") 
//使用SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic/greetings”. 
//传入的参数Message为客户端发送过来的消息,是自动绑定的。 
public Greeting greeting(HelloMessage message) throws Exception {
    Thread.sleep(1000); // 模拟处理延时 
    return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); //根据传入的信息,返回一个欢迎消息. 
} 

上面方法中的返回值,会被广播到 /topic/greetings 这个订阅路径中,只要客户端订阅了这个路径,都会接收到消息。Spring处理消息的主要类是 SimpleBrokerMessageHandler , 当需要发送广播消息时,最终会调用其中的 sendMessageToSubscribers() 方法:

方法内部会循环调用当前所有订阅此 Broker 的客户端 Session ,然后逐个发送消息。这里,入参 destination 就是 Broker 的地址,而 message,就是我们返回信息的封装,其他细节这里就不展开讲了。

那么如果我只是想用WebSocket向服务器发出查询请求,然后服务器你就把查询结果给我就行了,其他用户就不用你广播推送了,简单点,就是我请求,你就推送给我。这又该怎么办呢?是的, @SendToUser 就能解决这个问题。

5.2 @SendToUser

@MessageMapping("/hello") 
//使用MessageMapping注解来标识所有发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理. 
@SendToUser("/topic/greetings") 
//使用SendToUser注解来标识这个方法返回的结果,都会被发送到请求它的用户的destination. 
//传入的参数Message为客户端发送过来的消息,是自动绑定的。 
public Greeting greeting(HelloMessage message) throws Exception {
    Thread.sleep(1000); // 模拟处理延时 
    return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); //根据传入的信息,返回一个欢迎消息. 
} 

spring参考文档

websocket参考文档

到此这篇关于SpringBoot+STOMP协议实现就介绍到这了。

参考:
https://www.jb51.net/article/214655.htm

https://developer.aliyun.com/article/763747

https://www.codercto.com/a/22176.html

https://www.cnblogs.com/goloving/p/15023025.html

https://www.cnblogs.com/question-sky/p/9636756.html

http://events.jianshu.io/p/c9ea50050acd

有关SpringBoot——整合WebSocket(STOMP协议)的更多相关文章

  1. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  2. ruby - Faye WebSocket,关闭处理程序被触发后重新连接到套接字 - 2

    我有一个super简单的脚本,它几乎包含了FayeWebSocketGitHub页面上用于处理关闭连接的内容:ws=Faye::WebSocket::Client.new(url,nil,:headers=>headers)ws.on:opendo|event|p[:open]#sendpingcommand#sendtestcommand#ws.send({command:'test'}.to_json)endws.on:messagedo|event|#hereistheentrypointfordatacomingfromtheserver.pJSON.parse(event.d

  3. ruby - HTTP POST 上的 SSL 错误(未知协议(protocol)) - 2

    尝试通过SSL连接到ImgurAPI时出现错误。这是代码和错误:API_URI=URI.parse('https://api.imgur.com')API_PUBLIC_KEY='Client-ID--'ENDPOINTS={:image=>'/3/image',:gallery=>'/3/gallery'}#Public:Uploadanimage##args-Theimagepathfortheimagetoupload#defupload(image_path)http=Net::HTTP.new(API_URI.host)http.use_ssl=truehttp.verify

  4. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  5. C/C++好用的websocket库 - 2

    IntrductionLibwebsocketsisasimple-to-use,MIT-license,pureClibraryprovidingclientandserverforhttp/1,http/2,websockets,MQTTandotherprotocolsinasecurity-minded,lightweight,configurable,scalableandflexibleway.It’seasytobuildandcross-buildviacmakeandissuitablefortasksfromembeddedRTOSthroughmasscloudservi

  6. ruby-on-rails - Websocket-rails 不适用于 Nginx 和 Unicorn 的生产环境 - 2

    我有带有gemwebsocket-rails0.7的Rails3.2应用程序。在开发机上,一切正常在生产环境中,我使用Nginx/1.6作为代理服务器,Unicorn作为http服务器。Thin用于独立模式(在https://github.com/websocket-rails/websocket-rails/wiki/Standalone-Server-Mode之后)。nginx配置:location/websocket{proxy_passhttp://localhost:3001/websocket;proxy_http_version1.1;proxy_set_headerUp

  7. 网络实验之RIPV2协议(一) - 2

    一、RIPV2协议简介  RIP(RoutingInformationProtocol)路由协议是一种相对古老,在小型以及同介质网络中得到了广泛应用的一种路由协议。RIP采用距离向量算法,是一种距离向量协议。RIP-1是有类别路由协议(ClassfulRoutingProtocol),它只支持以广播方式发布协议报文。RIP-1的协议报文无法携带掩码信息,它只能识别A、B、C类这样的自然网段的路由,因此RIP-1不支持非连续子网(DiscontiguousSubnet)。RIP-2是一种无类别路由协议(ClasslessRoutingProtocol),支持路由标记,在路由策略中可根据路由标记对

  8. springboot定时任务 - 2

    如果您希望在Spring中启用定时任务功能,则需要在主类上添加 @EnableScheduling 注解。这样Spring才会扫描 @Scheduled 注解并执行定时任务。在大多数情况下,只需要在主类上添加 @EnableScheduling 注解即可,不需要在Service层或其他类中再次添加。以下是一个示例,演示如何在SpringBoot中启用定时任务功能:@SpringBootApplication@EnableSchedulingpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.ru

  9. 基于SpringBoot的线上日志阅读器 - 2

    软件特点部署后能通过浏览器查看线上日志。支持Linux、Windows服务器。采用随机读取的方式,支持大文件的读取。支持实时打印新增的日志(类终端)。支持日志搜索。使用手册基本页面配置路径配置日志所在的目录,配置后按回车键生效,下拉框选择日志名称。选择日志后点击生效,即可加载日志。windows路径E:\java\project\log-view\logslinux路径/usr/local/XX历史模式历史模式下,不会读取新增的日志。针对历史文件可以分页读取,配置分页大小、跳转。历史模式下,支持根据关键词搜索。目前搜索引擎使用的是jdk自带类库,搜索速度相对较低,优点是比较简单。2G日志全文搜

  10. WebSocket的那些事(1-概念篇) - 2

    目录一、什么是Websocket二、WebSocket部分header介绍三、HTTPVSWebSocket四、什么时候使用WebSockets五、关于SockJS和STOMP一、什么是Websocket根据RFC6455标准,Websocket协议提供了一种标准化的方式在客户端和服务端之间通过TCP连接建立全双工、双向通信渠道。它是一种不同于HTTP的TCP协议,但是被设计为在HTTP基础上运行。Websocket交互始于HTTP请求,该请求会通过HTTPUpgrade请求头去升级请求,进而切换到Websocket协议。请求报文如下:GET/spring-websocket-portfoli

随机推荐