在上篇文章中我们学习了WebSocket的基础概念,依旧相关的实战项目。在本篇文章中,我们来学习一下如何基于STOMP协议来进行WebSocket协议的实现。

也就是说基于WebSocket协议有多种实现,基于STOMP来实现是其中的一种,也是Spring Boot推荐的一种。

学习完WebSocket协议,我们知道它并没有规定其消息发送的详细格式。那就意味着每个使用WebSocket的开发者,都需要在服务端和客户端定义一套规则,用来传输信息。而STOMP的出现,正好解决了该问题。

STOMP协议

STOMP:原文Simple Text Orientated Messaging Protocol,是一个简单的文本消息传输协议,属于WebSocket的子协议。

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

STOMP协议并不是为WebSocket所设计的,它其实是消息队列的一种协议,与AMQP,JMS是平级的。只不过由于它的简单性恰巧可以用于定义Websocket的消息体格式。目前很多服务端消息队列都已经支持了STOMP,比如RabbitMQ,Apache ActiveMQ等。

STOMP协议构成

STOMP是基于Text的,也允许传输二进制数据,默认编码是UTF-8。

STOMP是一种基于帧的协议。一帧由一个命令,一组可选的Header和一个可选的Body组成。

COMMAND        -- 注释:命令
header1:value1 -- 注释:一组可选Header之一
header2:value2 -- 注释:一组可选Header之一
-- 注释:一个空行
Body^@ -- 注释:一个可选的Body

上述协议结构中,命令(COMMAND)对应的有SEND、SUBSCRIBE、MESSAGE、CONNECT、CONNECTED等。

Header类似HTTP的Header,有content-length,content-type等。

Body可以是二进制也可以是文本。Body与Header间通过一个空行(EOL)来分隔。

具体实例如下:

SEND
destination:/welcome
content-length:12

{"name":"1"}

STOMP服务端

STOMP服务端可以接收客户端发送的一组目标地址。

地址的格式在协议中并没有具体定义。使用/topic/a、/queue/a、queue-a等格式都是可以的。

这样的好处就是可以通过自定义不同的格式来表示不同的含义。比如,以/topic开头的为发布订阅模式,所有消费端都可以接收到消息;以/user开头的为点对点模式,只会被一个消费者客户端收到。

STOMP客户端

STOMP的客户端可以同时扮演两种角色:消息生产者和消息消费者。

作为生产者时通过SEND帧发送消息到指定的地址。

作为消费者时通过发送SUBSCRIBE帧到已知地址来进行消息订阅,当有生产者发送消息到对应的订阅地址时,作为消费者便会接收到对应的消息。

Spring Boot中的STOMP

首先看STOMP在Spring Boot中的简单流程图:

Spring Boot基于STOMP实战_java

图中各个组件介绍:

  • 生产者客户端(左上组件):发送SEND命令到目的地址(destination)。

  • 消费者客户端(左下组件):订阅地址(destination),并接收此目的地址所推送过来的消息。

  • request channel:一组用来接收生产者型客户端所推送过来的消息的线程池。

  • response channel:一组用来推送消息给消费者型客户端的线程池。

  • broker: 消息队列管理者,也称消息代理。接收客户端的订阅指令,并记录订阅者与目的地址(destination)的关系。

  • SimpAnnotatonMethod:发送到达broker之前,会被该组件拦截,可先处理一些业务逻辑。

  • SimpleBroker:直接转到broker。不会被应用拦截。

整个流程如下:

  • 生产者客户端发送SEND命令消息到指定地址;

  • 服务端request channel接收到消息进行判断;

  • 如果目的地址是应用(/app)目的地址则转到SimpAnnotatonMethod中定义的业务方法进行处理。然后,再转到broker(SimpleBroker)。

  • 如果目的地址是非应用目的地址则直接转到broker。broker构建MESSAGE命令消息, 通过response channel推送给所有订阅对应地址的消费者

     

 

上面我们讲解了STOMP的基本原理和在Spring Boot中的处理流程。下面以具体的实例带大家了解如何在Spring Boot中使用STOMP协议。

实例场景

用户访问页面,进入“技术交流群”页面,自动登录会话,可发送消息、离开等操作。同时,服务器端会定时推送“计数”到前端。

实战代码

引入依赖

在Spring Boot项目中引入以下依赖:

  •  
<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency></dependencies>

其中最核心的便是websocket的依赖,上节课中我们演示WebSocket实例时引入的同样是该依赖。

开启STOMP代理配置

通过配置文件来配置STOMP代理以及注册STOMP节点:

  •  
package com.secbro.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;
/** * * 注解@EnableWebSocketMessageBroker开启使用STOMP协议来传输基于代理的消息 * @author sec * @version 1.0 * @date 2020/2/26 9:06 AM **/@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/** * 注册STOMP协议的节点,并指定映射的URL */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册STOMP协议节点 registry.addEndpoint("/simple") // 解决跨域问题 .setAllowedOrigins("*") // 指定端点使用SockJS协议 .withSockJS(); }
/** * 配置消息代理 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 由于是实现推送功能,这里的消息代理是/topic // 启动简单Broker,消息的发送的地址符合配置的前缀来的消息才发送到这个broker registry.enableSimpleBroker("/topic"); }
}

 

实体类

创建用于接收消息和返回消息的实体类:

@Data
public class RequestMessage {
private String name;
}
@Data
public class ResponseMessage {
private String message;
}

定义消息订阅地址

下面便是定义具体的消息订阅和发送地址:

  •  
  •  
@Slf4j@RestControllerpublic class StompController {
private static AtomicInteger index = new AtomicInteger();
@Resource private SimpMessagingTemplate messagingTemplate;
/** * 注解@MessageMapping@RequestMapping类似,定位请求地址; * 注解@SendTo,指定当服务器有消息需要推送的时候,订阅了@SendTo中路径的客户端发送消息。 */ @MessageMapping("/hello") @SendTo("/topic/hello") public ResponseMessage hello(RequestMessage message) { ResponseMessage resp = new ResponseMessage(); String hello = "welcome," + message.getName() + " !"; log.info("ResponseMessage:{}", hello); resp.setMessage(hello); return resp; }
/** * 定时推送消息,每隔两秒钟返回一次消息给订阅"/topic/callback"的客户端 */ @Scheduled(fixedRate = 5000) public void callback() { // 发送消息 messagingTemplate.convertAndSend("/topic/callback", "index: " + index.getAndIncrement()); }}

其中@MessageMapping("/hello"),与@RequestMapping类似,定位请求地址。注解@SendTo,指定当服务器有消息需要推送的时候,订阅了@SendTo中路径的客户端发送消息。

callback方法通过定时任务向订阅了/topic/callback地址的客户端定时发送消息。

由于开启了定时任务,因此在spring boot启动类上使用了@EnableScheduling注解。

  •  
@EnableScheduling@SpringBootApplicationpublic class SpringBootMainApplication {
public static void main(String[] args) { SpringApplication.run(SpringBootMainApplication.class, args);  }}

前端页面

访问前端页面的Controller如下:

  •  
@Controllerpublic class AssistantController {
private AtomicInteger idProducer = new AtomicInteger();
@RequestMapping("/") public String index(Model model) {
model.addAttribute("username","user" + idProducer.getAndIncrement()); return "index"; }
}

主要生成用户名,并返回对应的页面。

前端页面内容如下:

  •  
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head>    <meta charset="UTF-8">    <title>基于STOMP模式实现</title>    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css">    <script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>    <script src="js/stomp.js"></script>    <script src="js/sockjs.min.js"></script></head><body class="container" style="width: 70%"><h4>基于STOMP模式实现</h4><div class="form-group">    <label for="content"></label>    <textarea id="content" class="form-control" readonly="readonly" cols="80" rows="15"></textarea></div><div class="form-group">    <label for="message">群发消息 &nbsp;</label>    <input id="message" value="" class="form-control"/>    <div style="margin-top: 10px">        <button id="user_exit" class="btn btn-danger">离开</button>        <button id="toSend" class="btn btn-info">发送消息</button>        <input id="username" th:value="${username}" style="display: none"/>    </div></div></body>
<script type="text/javascript">
$(document).ready(function () { var stompClient; var userName = $('#username').val();
// WebSocket的连接地址 var socket = new SockJS('http://localhost:8080/simple'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { console.log('Connected:' + frame);
// 客户端订阅消息的目的地址 stompClient.subscribe('/topic/hello', function (response) { $('#content').append(JSON.parse(response.body).message + '\n'); });
// 客户端消息发送的目的地址,注册定时任务接收 stompClient.subscribe('/topic/callback', function (response) { $('#content').append(response.body + '\n'); }); });
// 客户端发送消息到服务器 $('#toSend').click(function () { sendMsg(); });
$(document).keyup(function(event){ // 回车键事件 if(event.keyCode==13){ sendMsg(); } });
function sendMsg(){ stompClient.send("/hello", {}, JSON.stringify({'name': $('#message').val()})); }
// 退出 $('#user_exit').click(function () { if (stompClient != null) { stompClient.disconnect(); } $('#content').append('[' + userName + '] 已离开!'); console.log('Disconnected'); }); })</script>
</html>

在该页面中引入了bootstrap样式、jQuery、stomp.js和sockjs.js,其中stomp用来实现stomp协议,sockjs是websocket协议的实现,增加了对浏览器不支持websocket的时候的兼容支持。

整体流程:页面初始化完成,执行js进行stomp的连接,并进行地址的订阅;订阅完成,服务器便会定时发送消息给客户端,客户端可以发送消息给服务器,也可以点击“离开”,断开连接。

实例效果如下: Spring Boot基于STOMP实战_java_02