一、什么是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 客户端发送到服务端
都是建立在连接成功的情况下, 客户端想发送到服务端
- 服务端通过setApplicationDestinationPrefixes("/全局路径"); 设置路径
- 服务端编写controller代码 @MessageMapping("/路径") 可供调用
- 客户端通过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 测试连接
连接成功!!
四、总结
这个对代码的侵入性比较高 但是涉及到有业务逻辑相关的 用这个也还行