客户端和服务端使用STOMP协议的坑

Posted £漫步 云端彡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了客户端和服务端使用STOMP协议的坑相关的知识,希望对你有一定的参考价值。

STOMP帧由命令,一个或多个头信息,一个空行及负载(文本或字节)组成;其中可用的命令有:connect、send、subscribe、unsubscribe、begin、commit、abort、ack、nack、disconnect。
客户端连接首先得引入js文件

<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>

服务端使用SpringBoot,引入pom.xml

<!-- https://mvnrepository.com/artifact/javax.websocket/javax.websocket-api -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>

  1. 建立连接:
    连接常用的有几种方法:
    使用webSocket建立连接:
var url = "ws://localhost:61614/stomp";
var client = Stomp.client(url);

使用SocketJS间接建立连接,需要引入sockjs.js

var ws = new SockJS(url);
var client = Stomp.over(ws);

如果在nodejs环境下引入stompjs时,可使用一下两种方式建立连接:

// 安装
npm install stompjs

var Stomp = require('stompjs');
// 第一种引入方式:Stomp.overTCP(host,port)
var client = Stomp.overTCP('localhost', 61613);
// 第二种引入方式:Stomp.overWS(url)
var client = Stomp.overWS('ws://localhost:61614/stomp');
  1. 向服务器发起websocket连接
// headers是头部信息,没有可以传递{},connectCallback连接成功后的回调,errorCallback连接失败后的回调
client.connect(headers, connectCallback);
client.connect(headers, connectCallback, errorCallback);
  1. 断开连接
//断开后,有一个回调函数,可以做后续处理
client.disconnect(function() {
  alert("See you next time!");
};
  1. 发送消息
// url表示发送路径,路径后续会详细说明
client.send(url[, headers[, body]]);

// 例:
client.send("/queue/test", {priority: 9}, "Hello, STOMP");
client.send("/queue/test", {}, "Hello, STOMP");
  1. 订阅消息(接收消息)、取消订阅
// 订阅消息,url,消息回调,头部
client.subscribe(url, callback[, headers])

// 例:
var subscription = client.subscribe("/queue/test",function (message){});
// 取消订阅
subscription.unsubscribe();
  1. 发送JSON数据,可使用JSON.stringify发送,JSON.parse来解析数据
var quote = {symbol: 'APPL', value: 195.46};
client.send("/topic/stocks", {}, JSON.stringify(quote));
client.subcribe("/topic/stocks", function(message) {
  var quote = JSON.parse(message.body);
  alert(quote.symbol + " is at " + quote.value);
};
  1. 其余请参考详细介绍

服务端这里首先创建一个配置类,实现WebSocketMessageBrokerConfigurer接口,重写registerStompEndpoints和configureMessageBroker方法,其余不是很重要。registerStompEndpoints注册一个节点路径,configureMessageBroker配置消息代理。同时该配置类需要开启EnableWebSocketMessageBroker注解,之后就可以使用@MessageMapping类似于@RequsetMapping一样。代码示例如下:
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;

/**
 * STOMP使用
 * 1. @EnableWebSocketMessageBroker 注解用于开启使用STOMP协议来传输基于代理(MessageBroker)的消息,
 * 这时候控制器(controller)开始支持@MessageMapping,就像是使用@RequestMapping一样。
 * 2. AbstractWebSocketMessageBrokerConfigurer已过时,目前使用WebSocketMessageBrokerConfigurer
 */
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {
    /**
     * 注册一个节点(url),并开启SockJS协议
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/app")
                .setAllowedOrigins("http://127.0.0.1:8848")
                //开启SockJS协议
                .withSockJS();
    }

    /**
     * 配置消息代理
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //服务端向客户端发送消息的前缀
        registry.enableSimpleBroker("/allSend","/specified");
        //客户端向服务端发送消息的前缀
        registry.setApplicationDestinationPrefixes("/api");
        //给指定客户端发送一对一的消息前缀是/users ,不设置默认是/user/
//        registry.setUserDestinationPrefix("/user");
    }

}

另外需要封装一个消息接收对象进行接收消息。

/**
 * 创建一个消息实体
 */
public class Message{
    private String message;

    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

简单举例如下:
示例①:客户端发送消息,服务端订阅消息。

// 客户端
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>

<script>
	var sock = new SockJS('http://192.168.2.39:8085/app'); //创建SockJS连接。
	var stomp = Stomp.over(sock); //创建STOMP客户端实例。实际上封装了SockJS,这样就能在WebSocket连接上发送STOMP消息。
	var payload = JSON.stringify({
		'message': '你好!'
	});
	stomp.connect({}, function(frame) {
		stomp.send("/api/test1", {}, payload);
	});
</script>
// 服务端
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;

/**
 * STOMP控制器
 */
@Controller
public class StompController {
    /**
     * 订阅消息 MessageMapping注解用与订阅消息使用,参数为路径
     * @param mess
     */
    @MessageMapping("/test1")
    public void test1(Message mess){
        System.out.println("test1: 接收到消息:" + mess.getMessage());
    }
}

由于服务端配置后消息代理,所以客户端发送服务端路径就为前缀+消息映射,即/api/test1。运行结果如下图:

示例②:服务端发送消息,客户端订阅消息。

// 客户端
stomp.connect({}, function(frame) {
	stomp.subscribe('/api/test2', function(message) {
		console.log(message)
	});
});
// 服务端 SubscribeMapping注解用于发送消息,参数为路径
@SubscribeMapping("/test2")
public Message test2() {
    Message outing = new Message();
    outing.setMessage("test2: Hello");
    return outing;
}

与示例①,客户端订阅服务端路径就为前缀+消息映射,即/api/test1。运行结果如下图:

示例③:客户端发送消息之后,服务端又返回消息。

// 客户端
stomp.connect({}, function(frame) {
	stomp.send("/api/test3", {}, payload); 
	stomp.subscribe('/allSend/test3', function(message) {
		console.log(message)
	});});
// 服务端 SubscribeMapping注解用于发送消息,参数为路径
/**
* 订阅并且发送,SendTo注解用于设置转发消息的路径,如果有配置enableSimpleBroker,发送路径必须是其中的一个作为前缀+任意路径
* @param mess
* @return
* @throws InterruptedException
*/
@MessageMapping("/test3")
//必须加前缀
@SendTo("/allSend/test3")
public Message test3(Message mess) throws InterruptedException {
    System.out.println("test3接收到消息:" + mess.getMessage());
    Thread.sleep(1000);
    Message result = new Message();
    result.setMessage("Hello");
    return result;
}

此时客户端订阅的路径就和之前不一样了,订阅路径为@SendTo参数,为/allSend/test3。注意:此时当不同客户端都订阅/allSend/test3时,所有的客户端都接收到@SendTo转发的消息,属于群发。

示例④:服务端指定用户发送消息。

// 客户端
stomp.connect({}, function(frame) {
	stomp.send("/api/test5", {}, payload);

	stomp.subscribe('/user/specified/test6', function(message) {
		console.log(message)
	});
});
// 服务端 SubscribeMapping注解用于发送消息,参数为路径
/**
 * 指定用户发送,如果没有配置setUserDestinationPrefix,默认前缀为/user
 * spring webscoket默认会把消息推送到同一个帐号不同的session,
 * 你可以利用broadcast = false把避免推送到所有的session中
 * @param mess
 * @return
 * @throws InterruptedException
*/
@MessageMapping("/test5")
//必须加前缀
@SendToUser(value = "/specified/test6",broadcast = false)
public Message test5(Message mess) throws InterruptedException {
    System.out.println("test3接收到消息:" + mess.getMessage());
    Thread.sleep(1000);
    Message result = new Message();
    result.setMessage("Hello");
    return result;
}

指定用户发送时,订阅路径有所不同,有配置enableSimpleBroker时,@SendToUser的路径首先得加上enableSimpleBroker配置过的前缀(非setUserDestinationPrefix过的前缀。默认的也不行),然后自定义路径。客户端订阅的路径默认会再加一个前缀/user,如果有配置setUserDestinationPrefix时,路径就是自定义前缀了。所以上述来说客户端订阅路径为:/user/specified/test6。如果没有配置enableSimpleBroker,则不管默认与否,需要配置setUserDestinationPrefix才能生效。路径为:配置前缀路径+@SendToUser设置的value,此时就会指定发送至当前用户了。

以上实现消息转发的情况都需要连接之后一端发送消息进行触发才可使用。Spring的SimpMessagingTemplate能够在应用的任何地方发送消息,甚至不必以首先接收一条消息作为前提。使用SimpMessagingTemplate的最简单方式是将它(或者其接口SimpMessageSendingOperations)自动装配到所需的对象中。可使用自动注入的方式注入SimpMessagingTemplate。

// 群发
simpMessagingTemplate.convertAndSend("/topic/test", "测试SimpMessageSendingOperations ");
//订阅
stomp.subscribe('/topic/test', function(message) {
	console.log(message)
});
//指定用户发送,三个参数分别是用户名称,随意指定皆可,指定url,数据
simpMessagingTemplate.convertAndSendToUser("name", "/message", "测试convertAndSendToUser");
//订阅
stomp.subscribe('/user/name/message, function(message) {
	console.log(message)
});
  • 使用convertAndSendToUser注意:enableSimpleBroker和setUserDestinationPrefix配置前缀中需要指定相同的前缀路径,默认点对点发送的前缀路径为/user,也就是说需要在enableSimpleBroker添加/user路径,convertAndSendToUser才能生效。此时订阅路径就是:/user/name/message。
  • 特别注意的是,此时@SendToUser便不会起作用了。
  • 此时使用convertAndSendToUser还会时而接收消息,时而收不到消息。
  • 使用convertAndSendToUser时,enableSimpleBroker配置需要包含点对点发送前缀,使用@SendToUser时,enableSimpleBroker不能包含点对点发送前缀。
  • 最好配置不加前缀,使用@SendToUser注解,点对点发送。

以上是关于客户端和服务端使用STOMP协议的坑的主要内容,如果未能解决你的问题,请参考以下文章

STOMP原理与应用开发详解

SpringBoot集成STOMP协议完成私聊群聊

用于持久订阅的 stomp 协议常规序列

Spring stomp - 使用 SimpMessagingTemplate 从服务器发送消息

向非 Stomp/Websocket 消费者发送消息

分布式WebSocket - 4SpringBoot集成STOMP协议,RabbitMQ为消息代理