一、什么是STOMP?

STOMP源于需要通过脚本语言(例如Ruby,Python和Perl)连接到企业消息代理的需求。在这样的环境中,通常在逻辑上执行简单的操作,例如“可靠地发送单个消息并断开连接”或“在给定目的地上消耗所有消息”。

它是其他开放消息协议(例如AMQP)和JMS代理(例如OpenWire)中使用的实现特定有线协议的替代。它通过覆盖一小部分常用消息传递操作而不提供全面的消息传递API来与众不同。

STOMP协议是和AMQP,JMS消息协议是平级的是应用层的协议,STOMP是可以建立在WebSocket协议上面的,

一个STOMP帧由三部分组成: 命令,Header(头信息),Body(消息体)

一个STOMP客户端是一个可以以两种模式运行的用户代理,可能是同时运行两种模式。

  • 作为生产者,通过SEND框架将消息发送给服务器的某个服务
  • 作为消费者,通过SUBSCRIBE制定一个目标服务,通过MESSAGE框架,从服务器接收消息。

二、STOMP 命令

在客户端和服务端连接发送消息的命令有以下几种

  • SEND 发送
  • SUBSCRIBE 订阅
  • UNSUBSCRIBE 退订
  • BEGIN 开始
  • COMMIT 提交
  • ABORT 取消
  • ACK 确认
  • DISCONNECT 断开
2.1 客户端发送到服务端

都是建立在连接成功的情况下, 客户端想发送到服务端

  1. 服务端通过setApplicationDestinationPrefixes("/全局路径"); 设置路径
  2. 服务端编写controller代码 @MessageMapping("/路径") 可供调用
  3. 客户端通过stompClient.send(/全局路径/路径,body) 发送数据
2.2 服务端发送到客户端

连接成功的情况下 服务端想发送到客户端

  • 客户端需要去订阅服务器地址
  • 如果是公共的 /topic/路径
  • 如果是私有的 /queue/userId/路径
  • 服务端发送指定的路径到客户端,直接发送到订阅的路径就可以了
  • 可以通过 @SendTo(路径)
  • 可以通过 simpMessagingTemplate.convertAndSend("/queue/5214521/路径",“谢谢你hello”);

三、springboot 和stomp 最佳实践

3.1 添加依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
        </dependency>
</dependencies>
3.2 配置websocket stomp
package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
// @EnableWebSocketMessageBroker注解用于开启使用STOMP协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)
// 开始支持@MessageMapping,就像是使用@requestMapping一样。
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册一个 Stomp 的节点(endpoint),并指定使用 SockJS 协议。
        registry.addEndpoint("/sendStomp").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 广播式配置名为 topic 和 queue
        // topic一般用于广播推送
        // queue用于点对点推送
        registry.enableSimpleBroker("/topic","/queue");
        // 如果设置 前段请求过来必须带上 带上这个才会被 controller 拦截 (发送路径上体现)
        registry.setApplicationDestinationPrefixes("/server");
        // 点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
        // registry.setUserDestinationPrefix("/user/");

    }
}
3.3 实体类
package com.example.demo.request;

import lombok.Data;

/**
 * @Classname ClientRequest
 * @Description 
 * @Date 2021/3/19 14:35
 * @Created by dongzhiwei
 */
@Data
public class ClientRequest {

    /**
     * 用户名
     */
    private String name;

}

response

package com.example.demo.response;

import lombok.Data;

/**
 * @Classname ServerResponse
 * @Description 服务端返回
 * @Date 2021/3/19 14:35
 * @Created by dongzhiwei
 */
@Data
public class ServerResponse {

    public ServerResponse() {
    }

    public ServerResponse(String message) {
        this.message = message;
    }

    private String message;
}
3.4 控制层
package com.example.demo.controller;

import com.example.demo.request.ClientRequest;
import com.example.demo.response.ServerResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;

/**
 * @Classname WebSocketController
 * @Description ws 的controller
 * @Date 2021/3/19 14:33
 * @Created by dongzhiwei
 */
@Controller
@Slf4j
public class WebSocketController {

    /**
     * 这个模板 可以发送到 客户端订阅的路径上,
     * 也就是调用这个发 必须要客户端先通过 subscribe 订阅这个地址
     */
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;


    @MessageMapping("/hello") // @MessageMapping 和 @RequestMapping 功能类似,浏览器向服务器发起请求时,映射到该地址。
    public ServerResponse say(ClientRequest message) throws Exception {
        Thread.sleep(1000);
        simpMessagingTemplate.convertAndSend("/queue/5214521/subscribeTest","谢谢你hello");
        return new ServerResponse("Hello,"+message);
    }

    /**
     * 订阅点
     *
     */
    @SubscribeMapping("/subscribeTest")
    public ServerResponse sub() {
        log.info("XXX用户订阅了我。。。");
        return new ServerResponse("感谢你订阅了我。。。");
    }


}
3.5 编写客户端

如果spring boot 可以访问 web页面 可以通过 thymeleaf 实现 加依赖

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

application.properties 里面配置访问路径

spring.thymeleaf.prefix=classpath:/public/

页面的代码

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <link href="https://cdn.bootcss.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">WebSocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">siteId:</label>
                    <input type="text" id="name" class="form-control" placeholder="Your siteId here...">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</button>
            </form>


        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Greetings</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>

<script>
    var stompClient = null;
    var userId = '00ff94c5-ae8a-44b3-8065-04b0d577b43c';
    function setConnected(connected) {
        $("#connect").prop("disabled", connected);
        $("#disconnect").prop("disabled", !connected);
        if (connected) {
            $("#conversation").show();
        }
        else {
            $("#conversation").hide();
        }
        $("#greetings").html("");
    }

    function connect() {
        var socket = new SockJS('/sendStomp');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                stompClient.subscribe('/queue/5214521/subscribeTest', function (data) {
                    console.log('返回信息: ' + data);

                    // showGreeting(JSON.parse(data.body).content);
                    showGreeting(data.body);
                });
            },
            function (error) {
                // 连接失败时(服务器响应 ERROR 帧)的回调方法
                console.log('connect fails: ' + error);
            }
        );
    }

    function disconnect() {
        if (stompClient !== null) {
            stompClient.disconnect({"login":"userId","login2":"userId2"});
        }
        setConnected(false);
        console.log("Disconnected");
    }

    function sendName() {
        stompClient.send("/server/hello", {}, JSON.stringify({'name': $("#name").val()}));

    }

    function showGreeting(message) {
        $("#greetings").append("<tr><td>" + message + "</td></tr>");
    }

    $(function () {
        $("form").on('submit', function (e) {
            e.preventDefault();
        });
        $( "#connect" ).click(function() { connect(); });
        $( "#disconnect" ).click(function() { disconnect(); });
        $( "#send" ).click(function() { sendName(); });
    });
</script>
</html>
3.6 测试连接

spring Boot适用seata saga spring boot stomp_STOMP


连接成功!!

四、总结

这个对代码的侵入性比较高 但是涉及到有业务逻辑相关的 用这个也还行