程序新视界:一个“软实力”、“硬技术”同步成长的平台。
在上篇文章中我们学习了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中的简单流程图:
图中各个组件介绍:
生产者客户端(左上组件):发送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.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-websocketartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-thymeleafartifactId> dependency> <dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> <optional>trueoptional> 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"; }}
主要生成用户名,并返回对应的页面。
前端页面内容如下:
<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">群发消息 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的精品课程:《Spring Boot 2.x全家桶》系列视频教程,该系列视频教程预计20+章节,100+节课,20+小时的内容,涵盖市面上大多数的Spring Boot使用场景。目前录制过半,价格优惠,后期会逐步调整,可点击“阅读原文”,查看相关教程。https://edu.csdn.net/course/detail/20369
未完待续,持续保持每周更新5-7节课时的节奏中……