spring-boot之websocket · 下

前言
昨天我們提到,并不是所有的瀏覽器都支持websokcet協(xié)議,對(duì)于不支持的瀏覽器,我們要通過(guò)STOMP協(xié)議來(lái)進(jìn)行兼容,今天我們就來(lái)看下如何通過(guò)STOMP來(lái)兼容websocket。
websocket兼容
STOMP的全稱(chēng)是Simple (or Streaming) Text Orientated Messaging Protocol,中文的意思是簡(jiǎn)單(流)文本定向消息協(xié)議,也就是說(shuō),我們其實(shí)使用了消息組件來(lái)兼容的。
配置類(lèi)
對(duì)于不支持websocket的瀏覽器我們需要通過(guò)STOMP來(lái)兼容,兼容的解決方案涉及兩方面知識(shí),一個(gè)是SockJs,一個(gè)就是WebSocketMessageBroker。SockJs一種讓前端可以支持socket通信的技術(shù)解決方案,WebSocketMessageBroker是基于消息組件實(shí)現(xiàn)的一種通信協(xié)議。
下面是我們的STOMP解決方案的配置類(lèi),注釋已經(jīng)夠詳細(xì)了,所以這里就不在贅述。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注冊(cè)服務(wù)器端點(diǎn)
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 增加一個(gè)聊天服務(wù)端點(diǎn)
registry.addEndpoint("/socket").withSockJS();
// 增加一個(gè)用戶(hù)服務(wù)端點(diǎn)
registry.addEndpoint("/wsuser").withSockJS();
}
/**
* 定義服務(wù)器端點(diǎn)請(qǐng)求和訂閱前綴
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客戶(hù)端訂閱路徑前綴
registry.enableSimpleBroker("/sub", "/queue");
// 服務(wù)端點(diǎn)請(qǐng)求前綴
registry.setApplicationDestinationPrefixes("/request");
}
}
消息接收接口
這里定義兩個(gè)接口,一個(gè)是接收通用消息的(/send),一個(gè)是發(fā)給指定用戶(hù)的(/sendToUser)。這里需要補(bǔ)充說(shuō)明的是,@SendTo注解的作用是將接收到的消息發(fā)送到指定的路由目的地,所有訂閱該消息的用戶(hù)都能收到,屬于廣播。
@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ā)來(lái)消息:%s", webSocketService.getNameMap().get(srcUser), args[1]);
// 發(fā)送到用戶(hù)和監(jiān)聽(tīng)地址
simpMessagingTemplate.convertAndSendToUser(desUser, "/queue/customer", message);
}
}
前端頁(yè)面
普通消息發(fā)送
首先要引入jquery.js、stomp.js和sockjs.js,這個(gè)三個(gè)js就可以確保前端頁(yè)面也支持STOMP協(xié)議。
然后我們定義了三個(gè)方法:connect()、disconnect()和sendMessage()方法。
在connect方法內(nèi)部,我們通過(guò)SockJS初始化了stompClient實(shí)例,SockJS的節(jié)點(diǎn)地址就是我們配置類(lèi)中定義的聊天服務(wù)節(jié)點(diǎn),然后建立stomp連接。
發(fā)送消息的時(shí)候,我們直接調(diào)用stomp客戶(hù)端的send方法即可,這里需要指定發(fā)送消息的地址,要和消息接收方的地址一致。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket STOMP</title>
</head>
<body>
websocket兼容STOMP測(cè)試<br>
<div>
<div>
<button id = "connect" onclick="connect()">連接</button>
<button id = "disconnect" disabled="disabled" onclick="disconnect()">斷開(kāi)連接</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)到消息接收頁(yè)</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;
// 設(shè)置連接
function setConnected(connected) {
$("#connect").attr({"disabled": connected});
$("#disconnect").attr({"disabled": !connected});
if (connected) {
$("#conversationDiv").show();
} else {
$("#conversationDiv").hide();
}
$("#response").html("")
}
function connect() {
// 定義請(qǐng)求服務(wù)器的端點(diǎn)
var socket = new SockJS('/socket');
// stomp客戶(hù)端
stompClient = Stomp.over(socket);
// 連接服務(wù)器端點(diǎn)
stompClient.connect({}, function (frame) {
// 建立連接后的回調(diào)
setConnected(true);
})
}
// 斷開(kāi)socket連接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
// 向/request/send服務(wù)端發(fā)送消息
function sendMessage() {
var message = $("#message").val();
// 發(fā)送消息到"/request/send",其中/request是服務(wù)器定義的前綴
// 而/send則是@MessageMapping所配置的路徑
stompClient.send("/request/send", {}, message);
}
connect();
</script>
</body>
</html>
普通文本消息接收
接收頁(yè)面和發(fā)送頁(yè)面對(duì)應(yīng),sockJS的地址必須一樣,因?yàn)槭墙邮障?,所以這里執(zhí)行的是stompClient的subscribe(訂閱消息),這里的地址也必須和發(fā)送頁(yè)面一致,否則無(wú)法收到消息
<!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 () {
// 連接服務(wù)器端點(diǎn)
var s = new SockJS('/socket');
//客戶(hù)端
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>
普通文本測(cè)試
我登陸了兩個(gè)賬號(hào),用其中一個(gè)賬號(hào)發(fā)送消息,他自己以及另一個(gè)賬號(hào)都收到了發(fā)送的消息,說(shuō)明我們的實(shí)例是ok的。

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

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