前言
在springboot整合websocket之前,先簡單闡述下websocket的基本概念,以及與它相關的sockjs,stomp又是什麼。
WebSocket簡介
WebSocket協議是 HTML5新增的一種在單個TCP連接上進行全雙工通訊的協議,在 WebSocket API 中,瀏覽器和服務器只需要做一個握手的動作,然後,瀏覽器和服務器之間就形成一條快速通道,兩者之間就直接可以數據相互傳送了。
WebSocket與HTTP的不同之處在於:
WebSocket是一種全雙工通信協議,在建立連接後,WebSocket服務器和瀏覽器端都能夠主動的向對方發送消息,就像Socket一樣。而HTTP只能由客戶端發起請求,服務器返回查詢結果,做不到服務器主動向客戶端發送請求,如下圖所示
WebSocket的特點
這裏總結下WebSocket的特點:
- WebSocket服務器和瀏覽器都能夠主動向對方發送消息
- 建立在 TCP協議之上,服務器的實現比較容易
- 與HTTP 協議有着良好的兼容性,默認端口也是 80和443,並且握手階段採用HTTP協議,可以通過HTTP代理
- 數據格式比較輕量,性能開銷小,通信高效
- 可以發送文本,也可以發送二進制數據
- 沒有同源限制,客戶端可以與任意服務器通信
- 協議標識符是 ws
(如果加密,則爲wss
),服務器網址是URL
SockJS
SockJS是一個瀏覽器上運行的JavaScript庫,如果瀏覽器不支持 WebSocket,該庫可以模擬對 WebSocket的支持,實現瀏覽器和Web服務器之間的低延遲,全雙工,跨域的通訊通道
STOMP
STOMP即 Simple(or Streaming) Text Oriented Messaging Protocol 的簡稱,簡單(流)文本定向消息協議,它提供了一個可戶操作的連接格式,允許 STOMP 客戶端與任意 STOMP消息代理(Broker)進行交互,STOMP協議由於設計簡單,易於開發客戶端,因此在多種語言和多種平臺上得到廣泛應用
之前的介紹談到 WebSocket是基於 TCP協議的,直接使用WebSocket(或者SockJS)來編程就與直接使用TCP套接字來編程web應用類似,這會非常難受,因爲沒有高層協議,因此就需要我們定義應用間所發送消息的語義,還需要確保連接兩端都能遵循這些語義。
那麼現在STOMP就派上用場了,同HTTP在TCP套接字上添加請求-響應模型層一樣,STOMP在 WebSocket之上提供了一個基於幀的線路格式層,用來定義消息語義。
STOMP幀
STOMP幀由命令,一個或多個頭消息以及負載所組成,如下所示是一個發送數據的STOMP幀:
SEND
destination:/app/room-message
content-length:20
{\"message\":\"Hello!\"}
對上面分析如下:
- SEND: STOMP命令,表明會發送一些內容
- destination: 頭消息,用來表示消息發送到哪裏
- content-length: 頭信息,用來表示負載內容的大小
- 空行
- 幀內容(負載)內容
WebSocket、SockJS、STOMP的關係
簡單說就是,WebSocket是基於TCP的底層協議,SockJS是WebSocket的備選方案,用於那些不支持WebSocket的瀏覽器,也是底層協議,而STOMP是 WebSocket的上層協議,是高級協議
SpringBoot整合WebSocket
前面鋪墊了一些基礎知識過後,下面進入本篇文章的主題,使用SpringBoot+WebSocket+SockJS+STOMP搭建一個廣播式的WebSocket
導入依賴
<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>
WebSocket配置
@Configuration
@EnableWebSocketMessageBroker //啓用STOMP消息
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//註冊STOMP端點,即WebSocket客戶端需要連接到WebSocket握手端點
//這是一個端點,客戶端在訂閱或發佈消息到目的地路徑前,要連接該端點
registry.addEndpoint("/point")
//跨域設置
.setAllowedOrigins("*")
//啓用SockJS功能
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//設置消息代理,所有目的地前綴爲"/topic","/queue"的消息都會發送到STOMP代理中
registry.enableSimpleBroker("/topic", "/queue");
//設置應用程序的目的地前綴爲"/app",當有以應用程序爲目的地的消息將會直接路由到帶有@MessageMapping註解的控制器方法
registry.setApplicationDestinationPrefixes("/app");
}
}
對上述程序程序進行分析:
- @EnableWebSocketMessageBroker
註解不僅配置了WebSocket,還配置了基於代理的STOMP消息
- 重載了registerStompEndpoints
方法,將”/point”註冊爲STOMP端點,客戶端需要先連接該端點
- 重載configureMessageBroker
配置消息代理,同時設置應用程序的目的地前綴,當以應用程序爲目的地的消息將會直接路由到帶@MessageMapoping
註解的控制器方法
下圖來自spring-websocket官方文檔,表示爲websocket的通訊模型圖
解讀一下模型圖:
對於同一個目標:/a,它的前綴將會決定消息該如何處理,分爲兩種:
/app/a
和/topic/a
,如果是爲/topic/a
,那麼可以直接將消息體發送到 簡單代理消息處理器上,而如果是/app/a
,那麼它會先將消息路由到應用程序內部帶有@MessageMapping
註解的控制器方法中,在控制器方法中進行處理,然後將處理結果發送到brokeChannel
,最後再將消息發送到簡單代理消息處理器上,兩種情況最後都是經由代理再發送到客戶端的目的地的。
請求消息類
public class RequestMessage {
private String name;
public String getName() {
return name;
}
}
響應消息類
public class ResponseMessage {
private String responseMessage;
public ResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
public ResponseMessage() {
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
}
處理來自客戶端的STOMP消息
藉助 @MessageMapping 註解在控制器中處理 STOMP消息,代碼如下:
@Controller
public class GreetingController {
/**
* 處理髮往 /app/greeting目的地的消息
*
* @param greeting
* @return
*/
@MessageMapping("/greeting")
// @SendTo("/topic/say")
public ResponseMessage handle(RequestMessage greeting) {
//Spring的某一個消息轉換器會將STOMP消息的負載轉換爲 RequestMessage對象
System.out.println(greeting.getName());
return new ResponseMessage("welcome," + greeting.getName());
}
}
代碼分析:
- handle方法處理客戶端發往目的地爲 /app/greeting
的消息,/app
爲隱含的,因爲在配置類中我們將其設置爲應用的目的地前綴
- 該方法有一個RequestMessage
參數,實際上是Spring利用消息轉換器將消息負載轉換成了 RequestMessage對象
- 該方法返回一個 ResponseMessage
實體,Spring使用消息轉換器將這個返回的ResponseMessage對象轉換爲消息負載
- 默認情況下,返回消息的目的地與客戶端發送消息的目的地想用,只不過會添加 /topic
,當然我們也可以使用 @SendTo註解重載返回消息的目的地。
訂閱註解 @SubcribeMapping
當客戶端訂閱一個地址的時候,我們也可以使用@SubcribeMapping
註解發送一條消息,作爲訂閱的迴應:
@SubscribeMapping("/subscribe")
public ResponseMessage subscribe() {
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setResponseMessage("歡迎訂閱");
return responseMessage;
}
這裏的註解 @SubcribeMapping 註解表明當客戶端訂閱 /app/subscribe
(/app是應用目的地的前綴)目的地的時候,將會調用 subscribe()方法,並返回一個ResponseMessage對象
利用SimpMessagingTemplate
我們也可以使用SimpMessagingTemplate,Spring的SimpMessagingTemplate 能夠在應用的任何地方發送消息,甚至不需要首先接收一條消息作爲前提。
客戶端
客戶端編寫需要添加stomp.js和sock.js,下面是具體客戶端代碼:
<html>
<head>
<meta charset="UTF-8"/>
<title>廣播式WebSocket</title>
<script src="js/sockjs.min.js"></script>
<script src="js/stomp.js"></script>
<script src="js/jquery-3.1.1.js"></script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">Sorry,瀏覽器不支持WebSocket</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">連接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">斷開連接</button>
</div>
<div id="conversationDiv">
<label>輸入你的名字</label><input type="text" id="name"/>
<button id="sendName" onclick="sendName();">發送</button>
<p id="response"></p>
<p id="callback"></p>
</div>
</div>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
document.getElementById("conversationDiv").style.visibility = connected ? 'visible' : 'hidden';
$("#response").html();
$("#callback").html();
}
function connect() {
<!--連接stomp端點-->
var socket = new SockJS('http://localhost:9999/point');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected:' + frame);
<!--訂閱/topic/greeting-->
stompClient.subscribe('/topic/greeting', function (response) {
showResponse(JSON.parse(response.body).responseMessage);
});
<!--訂閱/app/subscribe-->
stompClient.subscribe('/app/subscribe', function (response) {
showResponse(JSON.parse(response.body).responseMessage);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log('Disconnected');
}
function sendName() {
var name = $('#name').val();
console.log('name:' + name);
<!--向目的地/app/greeting發送消息,對應服務端@MessageMapping註解的方法來處理-->
stompClient.send("/app/greeting", {}, JSON.stringify({'name': name}));
}
function showResponse(message) {
$("#response").html(message);
}
function showCallback(message) {
$("#callback").html(message);
}
</script>
</body>
</html>
測試結果
頁面上點擊連接後,會先連接上 /point
端點,然後同時訂閱 /topic/greeting
和 /app/subscribe
,輸入名字點擊發送,將向 /greeting
的URL發送消息,然後服務器響應消息到 /topic/greeting