SpringBoot+STOMP協議實現私聊、群聊
一、為什麼需要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
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <groupId>org.example</groupId> <artifactId>websocket-demo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.10.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.3.7</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
WebSocketMessageBroker配置類
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 啟用一個簡單的基於內存的消息代理 @Override public void configureMessageBroker(MessageBrokerRegistry config) { //通過/topic 開頭的主題可以進行訂閱 config.enableSimpleBroker("/topic"); //send命令時需要帶上/app前綴 config.setApplicationDestinationPrefixes("/app"); //修改convertAndSendToUser方法前綴, 稍後解釋作用 // config.setUserDestinationPrefix ("/myUserPrefix"); } @Override public 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 { @Autowired private SimpMessagingTemplate simpMessagingTemplate; @MessageMapping("/hello") @SendTo ("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); } @MessageMapping("/topic/greetings") public Greeting greeting2(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay log.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 delay simpMessagingTemplate.convertAndSend ("/topic/greetings", new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!")); } @MessageMapping("/sendToUser") public void sendToUser(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay log.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 WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="external nofollow" rel="stylesheet"> <link href="/main.css" rel="external nofollow" 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 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">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">Send</button> <button id="send2" class="btn btn-default" type="submit">Send2</button> <button id="send3" class="btn btn-default" type="submit">SendToUser</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> </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方法 註意,這兒有兩個topic stompClient.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("<tr><td>" + message + "</td></tr>"); } 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參考文檔
到此這篇關於SpringBoot+STOMP協議實現私聊、群聊的文章就介紹到這瞭,更多相關SpringBoot STOMP私聊、群聊內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- SpringBoot的WebSocket實現單聊群聊
- SpringBoot+WebSocket實現多人在線聊天案例實例
- Springboot Websocket Stomp 消息訂閱推送
- 詳解springboot集成websocket的兩種實現方式
- spring boot集成WebSocket日志實時輸出到web頁面