spring-boot之websocket · 下

前言
昨天我們提到,并不是所有的瀏覽器都支持websokcet協(xié)議,對于不支持的瀏覽器,我們要通過STOMP協(xié)議來進行兼容,今天我們就來看下如何通過STOMP來兼容websocket。
websocket兼容
STOMP的全稱是Simple (or Streaming) Text Orientated Messaging Protocol,中文的意思是簡單(流)文本定向消息協(xié)議,也就是說,我們其實使用了消息組件來兼容的。
配置類
對于不支持websocket的瀏覽器我們需要通過STOMP來兼容,兼容的解決方案涉及兩方面知識,一個是SockJs,一個就是WebSocketMessageBroker。SockJs一種讓前端可以支持socket通信的技術(shù)解決方案,WebSocketMessageBroker是基于消息組件實現(xiàn)的一種通信協(xié)議。
下面是我們的STOMP解決方案的配置類,注釋已經(jīng)夠詳細了,所以這里就不在贅述。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注冊服務器端點
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 增加一個聊天服務端點
registry.addEndpoint("/socket").withSockJS();
// 增加一個用戶服務端點
registry.addEndpoint("/wsuser").withSockJS();
}
/**
* 定義服務器端點請求和訂閱前綴
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客戶端訂閱路徑前綴
registry.enableSimpleBroker("/sub", "/queue");
// 服務端點請求前綴
registry.setApplicationDestinationPrefixes("/request");
}
}
消息接收接口
這里定義兩個接口,一個是接收通用消息的(/send),一個是發(fā)給指定用戶的(/sendToUser)。這里需要補充說明的是,@SendTo注解的作用是將接收到的消息發(fā)送到指定的路由目的地,所有訂閱該消息的用戶都能收到,屬于廣播。
@RestController
public class WebsocketController {
private final Logger logger = LoggerFactory.getLogger(WebsocketController.class);
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@Autowired
private WebSocketService webSocketService;
@MessageMapping("/send")
@SendTo("/sub/chat")
public String sendMessage(String value) {
logger.info("發(fā)送消息內(nèi)容:{}", value);
return value;
}
@MessageMapping("/sendToUser")
public void sendToUser(Principal principal, String body) {
String srcUser = principal.getName();
String[] args = body.split(": ");
String desUser = args[0];
String message = String.format("【%s】給你發(fā)來消息:%s", webSocketService.getNameMap().get(srcUser), args[1]);
// 發(fā)送到用戶和監(jiān)聽地址
simpMessagingTemplate.convertAndSendToUser(desUser, "/queue/customer", message);
}
}
前端頁面
普通消息發(fā)送
首先要引入jquery.js、stomp.js和sockjs.js,這個三個js就可以確保前端頁面也支持STOMP協(xié)議。
然后我們定義了三個方法:connect()、disconnect()和sendMessage()方法。
在connect方法內(nèi)部,我們通過SockJS初始化了stompClient實例,SockJS的節(jié)點地址就是我們配置類中定義的聊天服務節(jié)點,然后建立stomp連接。
發(fā)送消息的時候,我們直接調(diào)用stomp客戶端的send方法即可,這里需要指定發(fā)送消息的地址,要和消息接收方的地址一致。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket STOMP</title>
</head>
<body>
websocket兼容STOMP測試<br>
<div>
<div>
<button id = "connect" onclick="connect()">連接</button>
<button id = "disconnect" disabled="disabled" onclick="disconnect()">斷開連接</button>
</div>
<div id = "conversationDiv">
<p>
<label>發(fā)送消息內(nèi)容</label>
</p>
<p>
<textarea id="message" rows = "5"></textarea>
</p>
<p>
<button id = "sendMsg" onclick="sendMessage()">發(fā)送</button>
</p>
<p id = "response">
</p>
</div>
<a href="#" target="/websocket-receive">跳轉(zhuǎn)到消息接收頁</a>
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript">
var stompClient = null;
// 設置連接
function setConnected(connected) {
$("#connect").attr({"disabled": connected});
$("#disconnect").attr({"disabled": !connected});
if (connected) {
$("#conversationDiv").show();
} else {
$("#conversationDiv").hide();
}
$("#response").html("")
}
function connect() {
// 定義請求服務器的端點
var socket = new SockJS('/socket');
// stomp客戶端
stompClient = Stomp.over(socket);
// 連接服務器端點
stompClient.connect({}, function (frame) {
// 建立連接后的回調(diào)
setConnected(true);
})
}
// 斷開socket連接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 向/request/send服務端發(fā)送消息
function sendMessage() {
var message = $("#message").val();
// 發(fā)送消息到"/request/send",其中/request是服務器定義的前綴
// 而/send則是@MessageMapping所配置的路徑
stompClient.send("/request/send", {}, message);
}
connect();
</script>
</body>
</html>
普通文本消息接收
接收頁面和發(fā)送頁面對應,sockJS的地址必須一樣,因為是接收消息,所以這里執(zhí)行的是stompClient的subscribe(訂閱消息),這里的地址也必須和發(fā)送頁面一致,否則無法收到消息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket-stomp-receive</title>
</head>
<body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script type="text/javascript">
var noticeSocket = function () {
// 連接服務器端點
var s = new SockJS('/socket');
//客戶端
var stompClient = Stomp.over(s);
stompClient.connect({}, function () {
console.log("notice socket connected !");
// 訂閱消息地址
stompClient.subscribe('/sub/chat', function (data) {
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
<h1><span id="receive">等待接收消息</span></h1>
</body>
</html>
普通文本測試
我登陸了兩個賬號,用其中一個賬號發(fā)送消息,他自己以及另一個賬號都收到了發(fā)送的消息,說明我們的實例是ok的。

下面,我們看下如何給指定用戶發(fā)送消息。
給指定用戶發(fā)送消息
發(fā)送頁面沒有區(qū)別,只是js不一樣,所以這里只貼出js。
首先第一個不一樣的地方是服務端點不一樣了,我們這里的SockJS監(jiān)聽的是/wsuser,也就是給指定用戶發(fā)送消息的地址。
然后再就是發(fā)送消息的地址也變了,指定的是/request/sendToUser,對應的是指定用戶的發(fā)送消息的接口,剩下其他的都一模一樣。
<script type="text/javascript">
var stompClient = null;
// 設置連接
function setConnected(connected) {
$("#connect").attr({"disabled": connected});
$("#disconnect").attr({"disabled": !connected});
if (connected) {
$("#conversationDiv").show();
} else {
$("#conversationDiv").hide();
}
$("#response").html("")
}
function connect() {
// 定義請求服務器的端點
var socket = new SockJS('/wsuser');
// stomp客戶端
stompClient = Stomp.over(socket);
// 連接服務器端點
stompClient.connect({}, function (frame) {
// 建立連接后的回調(diào)
setConnected(true);
})
}
// 斷開socket連接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 向/request/send服務端發(fā)送消息
function sendMessage() {
var message = $("#message").val();
var user = $("#user").val();
// 發(fā)送消息到"/request/send",其中/request是服務器定義的前綴
// 而/send則是@MessageMapping所配置的路徑
var messageSend = user + ": " + message
stompClient.send("/request/sendToUser", {}, messageSend);
}
connect();
</script>
給指定用戶接收頁面
這里也只是js發(fā)生變化,節(jié)點名稱和發(fā)生頁面一致,訂閱地址和配置類中的一致。
<script type="text/javascript">
var noticeSocket = function () {
// 連接服務器端點
var s = new SockJS('/wsuser');
//客戶端
var stompClient = Stomp.over(s);
stompClient.connect({}, function () {
console.log("notice socket connected !");
// 訂閱消息地址
stompClient.subscribe('/user/queue/customer', function (data) {
$('#receive').html(data.body);
});
});
};
noticeSocket();
</script>
給指定用戶發(fā)送消息測試
這次我們用哪吒的賬號給女媧發(fā)了一條消息,最終的結(jié)果是只有女媧收到了消息,也和我們預期一致。

總結(jié)
相比于昨天我們直接通過websocket通信,通過STOMP通信,前端要稍過復雜一些,但總體來說,也不是特別復雜。
通篇來看,其實STOMP就是后端啟動一個消息池,然后將消息發(fā)送接口暴露給前端,前端調(diào)用發(fā)送消息接口發(fā)消息,消息由后端轉(zhuǎn)發(fā)到消息池中指定的隊列(類似消息中繼站),然后消費者(訂閱該隊列的消息接收方)接收并消費其中的消息。
如果知道了這點,那我們完全可以自己根據(jù)mq的相關文檔開發(fā)一套,而且現(xiàn)在好多mq都提供了對ajax的支持,比如activemq。
