【分布式WebSocket - 3】SpringBoot集成STOMP协议完成私聊、群聊
文章目录
- 一、为什么需要STOMP?
- 二、STOMP详解
- 三、SpringBoot集成STOMP代码示例
- 3.1、功能示例
- 3.2、架构图
- 3.3、服务端代码
- 3.4、h5代码
一、为什么需要STOMP?
WebSocket 协议是一种相当低级的协议。它定义了如何将字节流转换为帧。帧可以包含文本或二进制消息。由于消息本身不提供有关如何路由或处理它的任何其他信息,因此很难在不编写其他代码的情况下实现更复杂的应用程序。幸运的是,WebSocket 规范允许在更高的应用程序级别上使用子协议。
另外,单单使用WebSocket完成群聊、私聊功能时,需要自己管理session信息,通过STOMP协议时,spring已经封装好,开发者只需要关注自己的主题、订阅关系即可。
二、STOMP详解
STOMP 中文为“面向消息的简单文本协议”,STOMP 提供了能够协作的报文格式,以至于 STOMP 客户端可以与任何 STOMP 消息代理(Brokers)进行通信,从而为多语言,多平台和 Brokers 集群提供简单且普遍的消息协作。STOMP 协议可以建立在 WebSocket 之上,也可以建立在其他应用层协议之上。通过 Websocket建立 STOMP 连接,也就是说在 Websocket 连接的基础上再建立 STOMP 连接。最终实现如上图所示,这一点可以在代码中有一个良好的体现。
业界已经有很多优秀的 STOMP 的服务器/客户端的开源实现
- STOMP 服务器:ActiveMQ、RabbitMQ、StompServer、…
- STOMP 客户端库:stomp.js(javascript)
Stomp 的特点是客户端的实现很容易,服务端相当于消息队列的 broker 或者是 server,一般不需要我们去实现,所以重点关注一下客户端如何使用
CONNECT启动与服务器的流或 TCP 连接SEND发送消息SUBSCRIBE订阅主题UNSUBSCRIBE取消订阅BEGIN启动事物COMMIT提交事物ABORT回滚事物ACK确认来自订阅的消息的消费NACK告诉服务器客户端没有消费该消息DISCONNECT断开连接
其实STOMP协议并不是为WS所设计的, 它其实是消息队列的一种协议, 和AMQP,JMS是平级的。 只不过由于它的简单性恰巧可以用于定义WS的消息体格式。 目前很多服务端消息队列都已经支持了STOMP, 比如RabbitMQ, Apache ActiveMQ等。很多语言也都有STOMP协议的客户端解析库,像JAVA的Gozirra,C的libstomp,Python的pyactivemq,JavaScript的stomp.js等等。
STOMP协议官方文档
三、SpringBoot集成STOMP代码示例
3.1、功能示例
3.2、架构图

3.3、服务端代码
pom文件引入jar
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0modelVersion><groupId>org.examplegroupId><artifactId>websocket-demoartifactId><version>1.0-SNAPSHOTversion><parent><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-parentartifactId><version>2.3.10.RELEASEversion><relativePath/> parent><properties><java.version>1.8java.version>properties><dependencies><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-webartifactId>dependency><dependency><groupId>org.apache.commonsgroupId><artifactId>commons-lang3artifactId>dependency><dependency><groupId>org.projectlombokgroupId><artifactId>lombokartifactId><version>1.18.12version>dependency><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-websocketartifactId>dependency><dependency><groupId>org.webjarsgroupId><artifactId>webjars-locator-coreartifactId>dependency><dependency><groupId>org.webjarsgroupId><artifactId>sockjs-clientartifactId><version>1.0.2version>dependency><dependency><groupId>org.webjarsgroupId><artifactId>stomp-websocketartifactId><version>2.3.3version>dependency><dependency><groupId>org.webjarsgroupId><artifactId>bootstrapartifactId><version>3.3.7version>dependency><dependency><groupId>org.webjarsgroupId><artifactId>jqueryartifactId><version>3.1.0version>dependency><dependency><groupId>com.alibabagroupId><artifactId>fastjsonartifactId><version>1.2.62version>dependency>dependencies><build><plugins><plugin><groupId>org.apache.maven.pluginsgroupId><artifactId>maven-compiler-pluginartifactId><configuration><source>1.8source><target>1.8target>configuration>plugin>plugins>build>
project>
WebSocketMessageBroker配置类
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer
{// 启用一个简单的基于内存的消息代理@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {//通过/topic 开头的主题可以进行订阅config.enableSimpleBroker("/topic");//send命令时需要带上/app前缀config.setApplicationDestinationPrefixes("/app");//修改convertAndSendToUser方法前缀, 稍后解释作用
// config.setUserDestinationPrefix ("/myUserPrefix");}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {//连接前缀registry.addEndpoint("/gs-guide-websocket").setAllowedOrigins("*") // 跨域处理.withSockJS(); //支持socketJs}
}
@EnableWebSocketMessageBroker注解启用 WebSocket 消息处理,由消息代理支持。SockJS有一些浏览器中缺少对 WebSocket 的支持,而 SockJS 是一个浏览器的 JavaScript库,它提供了一个类似于网络的对象,SockJS 提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和 Web 服务器之间创建了一个低延迟、全双工、跨域通信通道。SockJS 的一大好处在于提供了浏览器兼容性。即优先使用原生WebSocket,如果浏览器不支持 WebSocket,会自动降为轮询的方式。如果你使用 Java 做服务端,同时又恰好使用 Spring Framework 作为框架,那么推荐使用SockJS。
控制器代码
@Slf4j
@RestController
public class TestController
{@Autowiredprivate SimpMessagingTemplate simpMessagingTemplate;@MessageMapping("/hello")@SendTo ("/topic/greetings")public Greeting greeting(HelloMessage message) throws Exception {Thread.sleep(1000); // simulated delayreturn new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");}@MessageMapping("/topic/greetings")public Greeting greeting2(HelloMessage message) throws Exception {Thread.sleep(1000); // simulated delaylog.info ("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");}@GetMapping ("/hello2")public void greeting3(HelloMessage message) throws Exception {Thread.sleep(1000); // simulated delaysimpMessagingTemplate.convertAndSend ("/topic/greetings",new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));}@MessageMapping("/sendToUser")public void sendToUser(HelloMessage message) throws Exception {Thread.sleep(1000); // simulated delaylog.info ("userId:{},msg:{}",message.getUserId (),message.getName ());
// simpMessagingTemplate.convertAndSendToUser (message.getUserId (),"/sendToUser",
// new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));
// simpMessagingTemplate.convertAndSend ("/user/1/sendToUser",
// new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));simpMessagingTemplate.convertAndSend ("/topic/user/"+message.getUserId ()+"/sendToUser",new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"));}
}
@MessageMapping功能与RequestMapping注解类似。send指令发送信息时添加此注解@SendTo/@SendToUser将信息输出到该主题。客户端订阅同样的主题后就会收到信息。- 在只有指定
@MessageMapping时@MessageMapping == “/topic” + @SendTo - 如果想使用rest接口发送消息。可以通过
SimpMessagingTemplate进行发送。 - 点对点聊天时,可以使用
SimpMessagingTemplate.convertAndSendToUser方法发送。个人意味比注解@SendToUser更加容易理解,更加方便 convertAndSendToUser方法和convertAndSend类似,区别在于convertAndSendToUser方法会在主题默认添加/user/为前缀。因此,示例代码中convertAndSend方法直接传入"/topic/user/"+message.getUserId ()+"/sendToUser"也是点对点发送。topic其中是默认前缀。- 如果想修改
convertAndSendToUser默认前缀可在配置类进行配置,可在WebSocketConfig类中查看。
3.4、h5代码
DOCTYPE html>
<html>
<head><title>Hello WebSockettitle><link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"><link href="/main.css" rel="stylesheet"><script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js">script><script src="/webjars/sockjs-client/sockjs.min.js">script><script src="/webjars/stomp-websocket/stomp.min.js">script><script src="/app.js">script>
head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript beingenabled. Please enableJavascript 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">Connectbutton><button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnectbutton>div>form>div><div class="col-md-6"><form class="form-inline"><div class="form-group"><label for="name">What is your name?label><input type="text" id="name" class="form-control" placeholder="Your name here..."><input type="text" id="userId" class="form-control" placeholder="userId">div><button id="send" class="btn btn-default" type="submit">Sendbutton><button id="send2" class="btn btn-default" type="submit">Send2button><button id="send3" class="btn btn-default" type="submit">SendToUserbutton>form>div>div><div class="row"><div class="col-md-12"><table id="conversation" class="table table-striped"><thead><tr><th>Greetingsth>tr>thead><tbody id="greetings">tbody>table>div>div>
div>
body>
html>
app.js
var stompClient = null;
var userId = null;
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('/gs-guide-websocket');stompClient = Stomp.over(socket);stompClient.connect({}, function (frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/topic/greetings', function (greeting) {showGreeting(JSON.parse(greeting.body).content);});//对应controller greeting2方法 注意,这儿有两个topicstompClient.subscribe('/topic/topic/greetings', function (greeting) {showGreeting(JSON.parse(greeting.body).content);});stompClient.subscribe('/topic/user/'+userId+'/sendToUser', function (greeting) {showGreeting(JSON.parse(greeting.body).content);});stompClient.subscribe('/user/'+userId+'/sendToUser', function (greeting) {showGreeting(JSON.parse(greeting.body).content);});});
}function disconnect() {if (stompClient !== null) {stompClient.disconnect();}setConnected(false);console.log("Disconnected");
}function sendName() {stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
// stompClient.send("/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
function sendName2() {stompClient.send("/app/topic/greetings", {}, JSON.stringify({'name': $("#name").val()}));
// stompClient.send("/topic/greetings", {}, JSON.stringify({'name': $("#name").val()}));
}
function sendName3() {stompClient.send("/app/sendToUser", {}, JSON.stringify({'userId':$("#userId").val(),'name': $("#name").val()}));
}function showGreeting(message) {$("#greetings").append("" + message + " ");
}
function GetQueryString(name) {var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配var context = "";if (r != null)context = r[2];reg = null;r = null;return context == null || context == "" || context == "undefined" ? "" : context;
}
$(function () {userId = GetQueryString("userId");$("form").on('submit', function (e) {e.preventDefault();});$( "#connect" ).click(function() { connect(); });$( "#disconnect" ).click(function() { disconnect(); });$( "#send" ).click(function() { sendName(); });$( "#send2" ).click(function() { sendName2(); });$( "#send3" ).click(function() { sendName3(); });});
一些无关紧要的类
public class Greeting
{private String content;public Greeting() {}public Greeting(String content) {this.content = content;}public String getContent() {return content;}
}
public class HelloMessage
{private String userId;private String name;// 省去get/set
}
Name3(); });});
spring参考文档
websocket参考文档
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
