1. 程式人生 > >Spring使用WebSocket、SockJS、STOMP實現訊息功能

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就行了。

以上只是學習所做的筆記,如有錯誤請指正。謝謝啦!!!