Spring使用WebSocket、SockJS、STOMP實現訊息功能
WebSocket
概述
WebSocket協議提供了通過一個套接字實現全雙工通訊的功能。除了其他的功能之外,它能夠實現Web瀏覽器和伺服器之間的非同步通訊。全雙工意味著伺服器可以傳送訊息給瀏覽器,瀏覽器也可以傳送訊息給伺服器。
使用Spring的低層級WebSocketAPI
按照其最簡單的形式,WebSocket只是兩個應用之間通訊的通道。位於WebSocket一端的應用傳送訊息,另一端接收訊息。因為它是全雙工的,所以每一端都可以傳送和處理訊息。
WebSocket通訊可以應用於任何型別的應用中,但是WebSocket最常見的應用場景是實現伺服器和基於瀏覽器的應用之間的通訊。
編寫簡單的WebSocket樣例(基於JavaScript的客戶端與伺服器的一個無休止的“Marco Polo”遊戲)
為了在Spring使用較底層級的API來處理訊息,我們必須編寫一個實現WebSocketHandler的類。
WebSocketHandler.java
public interface WebSocketHandler { void afterConnectionEstablished(WebSocketSession session) throws Exception; void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception; void handleTransportError(WebSocketSession session, Throwable exception) throws Exception; void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception; boolean supportsPartialMessages(); }
不過更為簡單的方法是擴充套件AbstractWebSocketHandler,這是WebSocketHandler的一個抽象實現。
MarcoHandler.java
public class MarcoHandler extends AbstractWebSocketHandler { protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { System.out.println("Received message: " + message.getPayload()); Thread.sleep(2000); session.sendMessage(new TextMessage("Polo!")); } @Override public void afterConnectionEstablished(WebSocketSession session) { System.out.println("Connection established!"); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { System.out.println("Connection closed. Status: " + status); }
儘管AbstractWebSocketHandler是一個抽象類,但是它並不要求我們必須過載任何特定的方法。相反,它讓我們來決定該過載哪一個方法。除了過載WebSocketHandler中定義的五個方法以外,我們還可以過載AbstractWebSocketHandler中所定義的三個方法:
- handleBinaryMessage()
- handlePongMessage()
- handleTextMessage()
這三個方法只是handleMessage()方法的具體化,每個方法對應於某一種特定型別的訊息。
所以沒有過載的方法都由AbstractWebSocketHandler以空操作的方式進行。這意味著MarcoHandler也能處理二進位制和pong訊息,只是對這些訊息不進行任何操作而已。
另外一種方案我們可以擴充套件TextWebSocketHandler,TextWebSocketHandler是AbstractWebSocketHandler的子類,它會拒絕處理二進位制訊息。它過載了handleBinaryMessage()方法,如果收到二進位制訊息,將會關閉WebSocket連線。與之類似,BinaryWebSocketHandler也是AbstractWebSocketHandler的子類,它過載了handleTextMessage()方法,如果收到文字訊息的話,將會關閉連線。
public class MarcoHandler extends TextWebSocketHandler {
...
}
public class MarcoHandler extends BinaryWebSocketHandler{
...
}
WebSocketConfig.java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(marcoHandler(), "/marco"); //註冊資訊管理器,將MarcoHandler對映到"/marco"
}
@Bean
public MarcoHandler marcoHandler() {
return new MarcoHandler();
}
}
WebAppInitializer.java
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {WebSocketConfig.class};
}
JavaScript客戶端程式碼
<script>
var url = 'ws://' + window.location.host + '/yds(你的專案名稱)/marco';
var sock = new WebSocket(url); //開啟WebSocket
sock.onopen = function() { //處理連線開啟事件
console.log('Opening');
sock.send('Marco!');
};
sock.onmessage = function(e) { //處理資訊
console.log('Received Message: ', e.data);
setTimeout(function() {
sayMarco()
}, 2000);
};
sock.onclose = function() { //處理連線關閉事件
console.log('Closing');
};
function sayMarco() { //傳送資訊函式
console.log('Sending Marco!');
sock.send('Marco!');
}
</script>
在本例中,URL使用了ws://字首,表明這是一個基本的WebSocket連線,如果是安全WebSocket的話,協議的字首將會是wss://。
注意: jar包一定要導正確,我是用的Spring5.0、jackson2.9.3。一些老版本的jar包老是報各種NoSuchMethodException,又或者Spring與jackson版本不相容
WebSocket簡單示例
個人感覺上面的那種太複雜了,如果只是簡單的通訊的話,可以像下面這樣寫:
<script>
if('WebSocket' in window)
{
var url = 'ws://' + window.location.host + '/TestsWebSocket(專案名)/websocket(服務端定義的端點)';
var sock = new WebSocket(url); //開啟WebSocket
}else
{
alert("你的瀏覽器不支援WebSocket");
}
sock.onopen = function() { //處理連線開啟事件
console.log('Opening');
sock.send('start');
};
sock.onmessage = function(e) { //處理資訊
e = e || event; //獲取事件,這樣寫是為了相容IE瀏覽器
console.log(e.data);
};
sock.onclose = function() { //處理連線關閉事件
console.log('Closing');
};
</script>
import java.io.IOException;
import java.util.Date;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint(value = "/websocket") //宣告這是一個Socket服務
public class MyWebSocket {
//session為與某個客戶端的連線會話,需要通過它來給客戶端傳送資料
private Session session;
/**
* 連線建立成功呼叫的方法
* @param session 可選的引數
* @throws Exception
*/
@OnOpen
public void onOpen(Session session) throws Exception {
this.session = session;
System.out.println("Open");
}
/**
* 連線關閉呼叫的方法
* @throws Exception
*/
@OnClose
public void onClose() throws Exception {
System.out.println("Close");
}
/**
* 收到訊息後呼叫的方法
* @param message 客戶端傳送過來的訊息
* @param session 可選的引數
* @throws Exception
*/
@OnMessage
public void onMessage(String message, Session session) throws Exception {
if (message != null){
switch (message) {
case "start":
System.out.println("接收到資料"+message);
sendMessage("哈哈哈哈哈哈哈哈");
break;
case "question":
case "close":
System.out.println("關閉連線");
onClose();
default:
break;
}
}
}
/**
* 發生錯誤時呼叫
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 傳送訊息方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message); //向客戶端傳送資料
}
}
執行,瀏覽器與服務端的輸出如圖:
SockJS
概述
WebSocket是一個相對比較新的規範,在Web瀏覽器和應用伺服器上沒有得到一致的支援。所以我們需要一種WebSocket的備選方案。
而這恰恰是SockJS所擅長的。SockJS是WebSocket技術的一種模擬,在表面上,它儘可能對應WebSocket API,但是在底層非常智慧。如果WebSocket技術不可用的話,就會選擇另外的通訊方式。
使用SockJS
WebSocketConfig.java
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(marcoHandler(), "/marco").withSockJS();
}
只需加上withSockJS()方法就能宣告我們想要使用SockJS功能,如果WebSocket不可用的話,SockJS的備用方案就會發揮作用。
JavaScript客戶端程式碼
要在客戶端使用SockJS,需要確保載入了SockJS客戶端庫。
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
除了載入SockJS客戶端庫外,要使用SockJS只需要修改兩行程式碼即可:
var url = 'marco';
var sock = new SockJS(url); //SockJS所處理的URL是http://或https://,不再是ws://和wss://
//使用相對URL。例如,如果包含JavaScript的頁面位於"http://localhost:8080/websocket"的路徑下
// 那麼給定的"marco"路徑將會形成到"http://localhost:8080/websocket/marco"的連線
執行效果一樣,但是客戶端–伺服器之間通訊的方式卻有了很大的變化。
使用STOMP訊息
概述
STOMP在WebSocket之上提供了一個基於幀的線路格式層,用來定義訊息的語義。STOMP幀由命令、一個或多個頭資訊以及負載所組成。例如如下就是傳送資料的一個STOMP幀:
>>> SEND
destination:/app/marco
content-length:20
{"message":"Maeco!"}
在這個簡單的樣例中,STOMP命令是SEND,表明會發送一些內容。緊接著是兩個頭資訊:一個用來表示訊息要傳送到哪裡的目的地,另外一個則包含了負載的大小。然後,緊接著是一個空行,STOMP幀的最後是負載內容。
STOMP幀中最有意思的是destination頭資訊了。它表明STOMP是一個訊息協議。訊息會發布到某個目的地,這個目的地實際上可能真的有訊息代理作為支撐。另一方面,訊息處理器也可以監聽這些目的地,接收所傳送過來的訊息。
啟用STOMP訊息功能
WebSocketStompConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer{
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/marcopolo").withSockJS();//為/marcopolo路徑啟用SockJS功能
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry)
{
//表明在topic、queue、users這三個域上可以向客戶端發訊息。
registry.enableSimpleBroker("/topic","/queue","/users");
//客戶端向服務端發起請求時,需要以/app為字首。
registry.setApplicationDestinationPrefixes("/app");
//給指定使用者傳送一對一的訊息字首是/users/。
registry.setUserDestinationPrefix("/users/");
}
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {WebSocketStompConfig.class,WebConfig.class};
}
WebSocketStompConfig 過載了registerStompEndpoints()方法,將/marcopolo註冊為STOMP端點。這個路徑與之前接收和傳送訊息的目的地路徑有所不同。這是一個端點,客戶端在訂閱或釋出訊息到目的地前,要連線該端點。
WebSocketStompConfig還通過過載configureMessageBroker()方法配置了一個簡單的訊息代理。這個方法是可選的,如果不過載它的話,將會自動配置一個簡單的記憶體訊息代理,用它來處理以“/topic”為字首的訊息。
處理來自客戶端的STOMP訊息
testConroller.java
@Controller
public class testConroller {
@MessageMapping("/marco")
public void handleShout(Shout incoming)
{
System.out.println("Received message:"+incoming.getMessage());
}
@SubscribeMapping("/subscribe")
public Shout handleSubscribe()
{
Shout outing = new Shout();
outing.setMessage("subscribes");
return outing;
}
}
@MessageMapping註解,表明handleShout()方法能夠處理指定目的地上到達的訊息。本例中目的地也就是“/app/marco”。(“/app”字首是隱含 的,因為我們將其配置為應用的目的地字首)
@SubscribeMapping註解,與@MessageMapping註解相似,當收到了STOMP訂閱訊息的時候,帶有@SubscribeMapping註解的方法將會被觸發。
Shout.java
public class Shout {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
客戶端JavaScript程式碼
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>
<script>
var url = 'http://'+window.location.host+'/yds/marcopolo';
var sock = new SockJS(url); //建立SockJS連線。
var stomp = Stomp.over(sock);//建立STOMP客戶端例項。實際上封裝了SockJS,這樣就能在WebSocket連線上傳送STOMP訊息。
var payload = JSON.stringify({'message':'Marco!'});
stomp.connect('guest','guest',function(frame){
stomp.send("/app/marco",{},payload);
stomp.subscribe('/app/subscribe', function(message){
});
});
</script>
Received message:Marco!
傳送訊息到客戶端
如果你想要在接收訊息的時候,同時在響應中傳送一條訊息,那麼需要做的僅僅是將內容返回就可以了。
@MessageMapping("/marco")
public Shout handleShout(Shout incoming) {
System.out.println("Received message:"+incoming.getMessage());
Shout outing = new Shout();
outing.setMessage("Polo");
return outing;
}
當@MessageMapping註解標示的方法有返回值的時候,返回的物件將會進行轉換(通過訊息轉換器)並放到STOMP幀的負載中,然後發給訊息代理。
預設情況下,幀所發往的目的地會與觸發處理器方法的目的地相同,只不過會加上“/topic”字首。
stomp.subscribe('/topic/marco', function(message){ 訂閱後將會接收到訊息。
});
不過我們可以通過為方法新增@SendTo註解,過載目的地:
@MessageMapping("/marco")
@SendTo("/queue/marco")
public Shout handleShout(Shout incoming) {
System.out.println("Received message:"+incoming.getMessage());
Shout outing = new Shout();
outing.setMessage("Polo");
return outing;
}
stomp.subscribe('/queue/marco', function(message){
});
在應用的任意地方傳送訊息
Spring的SimpMessagingTemplate能夠在應用的任何地方傳送訊息,甚至不必以首先接收一條訊息作為前提。
使用SimpMessagingTemplate的最簡單方式是將它(或者其介面SimpMessageSendingOperations)自動裝配到所需的物件中。
@Autowired
private SimpMessageSendingOperations simpMessageSendingOperations;
@RequestMapping("/test")
public void sendMessage()
{
simpMessageSendingOperations.convertAndSend("/topic/test", "測試SimpMessageSendingOperations ");
}
訪問/test後:
為目標使用者傳送訊息
使用@SendToUser註解,表明它的返回值要以訊息的形式傳送給某個認證使用者的客戶端。
@MessageMapping("/message")
@SendToUser("/topic/sendtouser")
public Shout message()
{
Shout outing = new Shout();
outing.setMessage("SendToUser");
return outing;
}
stomp.subscribe('/users/topic/sendtouser', function(message){//給指定使用者傳送一對一的訊息字首是/users/。
});
這個目的地使用了/users作為字首,以/users作為字首的目的地將會以特殊的方式進行處理。以/users為字首的訊息將會通過UserDestinationMessageHandler進行處理。
UserDestinationMessageHandler的主要任務是將使用者訊息重新路由到某個使用者獨有的目的地上。在處理訂閱的時候,它會將目標地址中的/users字首去掉,並基於使用者的會話新增一個字尾。
為指定使用者傳送訊息
SimpMessagingTemplate還提供了convertAndSendToUser()方法。convertAndSendToUser()方法能夠讓我們給特定使用者傳送訊息。
simpMessageSendingOperations.convertAndSendToUser("1", "/message", "測試convertAndSendToUser");
stomp.subscribe('/users/1/message', function(message){
});
客戶端接收一對一訊息的主題是"/users/"+usersId+"/message",這裡的使用者Id可以是一個普通字串,只要每個客戶端都使用自己的Id並且伺服器端知道每個使用者的Id就行了。
以上只是學習所做的筆記,如有錯誤請指正。謝謝啦!!!