SpringBoot+STOMP協(xié)議實現私聊、群聊
一、為什么需要STOMP?
WebSocket 協(xié)議是一種相當低級的協(xié)議。它定義了如何將字節(jié)流轉換為幀。幀可以包含文本或二進制消息。由于消息本身不提供有關如何路由或處理它的任何其他信息,因此很難在不編寫其他代碼的情況下實現更復雜的應用程序。幸運的是,WebSocket 規(guī)范允許在更高的應用程序級別上使用子協(xié)議。
另外,單單使用WebSocket完成群聊、私聊功能時,需要自己管理session信息,通過STOMP協(xié)議時,spring已經封裝好,開發(fā)者只需要關注自己的主題、訂閱關系即可。
二、STOMP詳解

STOMP 中文為“面向消息的簡單文本協(xié)議”,STOMP 提供了能夠協(xié)作的報文格式,以至于 STOMP 客戶端可以與任何 STOMP 消息代理(Brokers)進行通信,從而為多語言,多平臺和 Brokers 集群提供簡單且普遍的消息協(xié)作。STOMP 協(xié)議可以建立在 WebSocket 之上,也可以建立在其他應用層協(xié)議之上。通過 Websocket建立 STOMP 連接,也就是說在 Websocket 連接的基礎上再建立 STOMP 連接。最終實現如上圖所示,這一點可以在代碼中有一個良好的體現。
業(yè)界已經有很多優(yōu)秀的 STOMP 的服務器/客戶端的開源實現
- STOMP 服務器:ActiveMQ、RabbitMQ、StompServer、…
- STOMP 客戶端庫:stomp.js(javascript)
Stomp 的特點是客戶端的實現很容易,服務端相當于消息隊列的 broker 或者是 server,一般不需要我們去實現,所以重點關注一下客戶端如何使用
CONNECT啟動與服務器的流或 TCP 連接SEND發(fā)送消息SUBSCRIBE訂閱主題UNSUBSCRIBE取消訂閱BEGIN啟動事物COMMIT提交事物ABORT回滾事物ACK確認來自訂閱的消息的消費NACK告訴服務器客戶端沒有消費該消息DISCONNECT斷開連接
其實STOMP協(xié)議并不是為WS所設計的, 它其實是消息隊列的一種協(xié)議, 和AMQP,JMS是平級的。 只不過由于它的簡單性恰巧可以用于定義WS的消息體格式。 目前很多服務端消息隊列都已經支持了STOMP, 比如RabbitMQ, Apache ActiveMQ等。很多語言也都有STOMP協(xié)議的客戶端解析庫,像JAVA的Gozirra,C的libstomp,Python的pyactivemq,JavaScript的stomp.js等等。
三、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 服務器之間創(chuàng)建了一個低延遲、全雙工、跨域通信通道。SockJS 的一大好處在于提供了瀏覽器兼容性。即優(yōu)先使用原生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指令發(fā)送信息時添加此注解@SendTo/@SendToUser將信息輸出到該主題。客戶端訂閱同樣的主題后就會收到信息。- 在只有指定
@MessageMapping時@MessageMapping == “/topic” + @SendTo - 如果想使用rest接口發(fā)送消息。可以通過
SimpMessagingTemplate進行發(fā)送。 - 點對點聊天時,可以使用
SimpMessagingTemplate.convertAndSendToUser方法發(fā)送。個人意味比注解@SendToUser更加容易理解,更加方便 convertAndSendToUser方法和convertAndSend類似,區(qū)別在于convertAndSendToUser方法會在主題默認添加/user/為前綴。因此,示例代碼中convertAndSend方法直接傳入"/topic/user/"+message.getUserId ()+"/sendToUser"也是點對點發(fā)送。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(); });
});
到此這篇關于SpringBoot+STOMP協(xié)議實現私聊、群聊的文章就介紹到這了,更多相關SpringBoot STOMP私聊、群聊內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot通過Filter實現整個項目接口的SQL注入攔截詳解
這篇文章主要介紹了SpringBoot通過Filter實現整個項目接口的SQL注入攔截詳解,SQL注入是比較常見的網絡攻擊方式之一,在客戶端在向服務器發(fā)送請求的時候,sql命令通過表單提交或者url字符串拼接傳遞到后臺持久層,最終達到欺騙服務器執(zhí)行惡意的SQL命令,需要的朋友可以參考下2023-12-12
SpringBoot2 整合MinIO中間件實現文件便捷管理功能
這篇文章主要介紹了SpringBoot2 整合MinIO中間件,實現文件便捷管理,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
java利用Future實現多線程執(zhí)行與結果聚合實例代碼
這篇文章主要給大家介紹了關于java利用Future實現多線程執(zhí)行與結果聚合的相關資料,Future模式的核心,去除了主函數的等待時間,并使得原本需要等待的時間段可以用于處理其他業(yè)務邏輯,需要的朋友可以參考下2021-12-12

