1. 程式人生 > >spring 使用WebSocket 和 STOMP 實現訊息功能

spring 使用WebSocket 和 STOMP 實現訊息功能

1)本文旨在 介紹如何 利用 WebSocket 和 STOMP 實現訊息功能;

2)要知道, WebSocket 是傳送和接收訊息的 底層API,而SockJS 是在 WebSocket 之上的 API;最後 STOMP(面向訊息的簡單文字協議)是基於 SockJS 的高階API

(乾貨——簡而言之,WebSocket 是底層協議,SockJS 是WebSocket 的備選方案,也是 底層協議,而 STOMP 是基於 WebSocket(SockJS) 的上層協議)

3)broker==經紀人,代理;

4)當然,你可以直接跳轉到 STOMP 知識(章節【3】);
 

【1】WebSocket

1)intro:WebSocket 協議提供了 通過一個套接字實現全雙工通訊的功能。也能夠實現 web  瀏覽器 和 server 間的 非同步通訊, 全雙工意味著 server 與 瀏覽器間 可以傳送和接收訊息。

【1.1】使用 spring 的低層級 WebSocket API

1)intro:為了在 spring 中 使用較低層級的 API 來處理訊息。有如下方案:

scheme1)我們必須編寫一個實現 WebSocketHandler:


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();

}

scheme2)當然,我們也可以擴充套件 AbstractWebSocketHandler(更加簡單一點);

// you can also extends TextWebSocketHandler

public class ChatTextHandler extends AbstractWebSocketHandler {


// handle text msg.

@Override

protected void handleTextMessage(WebSocketSession session,

TextMessage message) throws Exception {

session.sendMessage(new TextMessage("hello world."));

}

}

對以上程式碼的分析(Analysis): 當然了,我們還可以過載其他三個方法:    

handleBinaryMessage()

handlePongMessage()

handleTextMessage()

scheme3)也可以擴充套件 TextWebSocketHandler(文字 WebSocket 處理器), 不在擴充套件AbstractWebSocketHandler , TextWebSocketHandler 繼承 AbstractWebSocketHandler ;

2)你可能會關係建立和關閉連線感興趣。可以過載 afterConnectionEstablished() and afterConnectionClosed():

// 當新連線建立的時候,被呼叫;

public void afterConnectionEstablished(WebSocketSession session)

throws Exception {

logger.info("Connection established");

}


// 當連線關閉時被呼叫;

@Override

public void afterConnectionClosed(

WebSocketSession session, CloseStatus status) throws Exception {

logger.info("Connection closed. Status: " + status);

}
3)現在已經有了 message handler 類了,下面對其進行配置,配置到 springmvc 的執行環境中。

@Configuration

@EnableWebSocket

public class WebSocketConfig implements WebSocketConfigurer{

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

registry.addHandler(getTextHandler(), "/websocket/p2ptext");

} // 將 ChatTextHandler 處理器 對映到 /websocket/p2ptext 路徑下.


@Bean

public ChatTextHandler getTextHandler() {

return new ChatTextHandler();

}

}

對上述程式碼的分析(Analysis):registerWebSocketHandlers方法 是註冊訊息處理器的關鍵: 通過 呼叫 WebSocketHandlerRegistry .addHandler() 方法 來註冊資訊處理器;

Attention)server 端的 WebSocket 配置完畢,下面配置客戶端;

4)WebSocket 客戶端配置

4.1)client 傳送 一個文字到 server,他監聽來自 server 的文字訊息。下面程式碼 展示了 利用 js 開啟一個原始的 WebSocket 並使用它來發送訊息給server;

4.2)程式碼如下:


<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>


<html lang="zh-CN">

<head>

<meta charset="utf-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- 上述3個meta標籤*必須*放在最前面,任何其他內容都*必須*跟隨其後! -->

<title>web socket</title>


<link href="<c:url value="/"/>bootstrap/css/bootstrap.min.css"

rel="stylesheet">


<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->

<script src="<c:url value="/"/>bootstrap/jquery/jquery.min.js"></script>

<!-- Include all compiled plugins (below), or include individual files as needed -->

<script src="<c:url value="/"/>bootstrap/js/bootstrap.min.js"></script>


<script type="text/javascript">

$(document).ready(function() {

websocket_client();

});


function websocket_client() {

var hostaddr = window.location.host + "<c:url value='/websocket/p2ptext' />";

var url = 'ws://' + hostaddr;

var sock = new WebSocket(url);


// 以下的 open(), onmessage(), onclose()

// 對應到 ChatTextHandler 的

// afterConnectionEstablished(), handleTextMessage(), afterConnectionClosed();


sock.open = function() {

alert("open successfully.");

sayMarco();

};


sock.onmessage = function(e) {

alert("onmessage");

alert(e);

};


sock.onclose = function() {

alert("close");

};


function sayMarco() {

sock.send("this is the websocket client.");

}

}

</script>

</head>


<body>

<div id="websocket">

websocket div.

</div>

</body>

</html>

error)這樣配置後, WebSocket 無法正常執行;

【2】應對不支援 WebSocket 的場景(引入 SockJS)

1)problem+solutions:

1.1)problem:許多瀏覽器不支援 WebSocket 協議;

1.2)solutions: SockJS 是 WebSocket 技術的一種模擬。SockJS 會 儘可能對應 WebSocket API,但如果 WebSocket 技術 不可用的話,就會選擇另外的 通訊方式協議;

2)SockJS 會優先選擇 WebSocket 協議,但是如果 WebSocket協議不可用的話,他就會從如下 方案中挑選最優可行方案:

  1. XHR streaming

  2. XDR streaming

  3. iFrame event source

  4. iFrame HTML file

  5. XHR polling

  6. XDR polling

  7. iFrame XHR polling

  8. JSONP polling

3)如何在 server 端配置 SockJS :新增 withSockJS() 方法;

// 將 ChatTextHandler 對映到 /chat/text 路徑下.

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

registry.addHandler(getTextHandler(), "/websocket/p2ptext").withSockJS();

// withSockJS() 方法宣告我們想要使用 SockJS 功能,如果WebSocket不可用的話,會使用 SockJS;

}

4)客戶端配置 SockJS, 想要確保 載入了 SockJS 客戶端;

4.1)具體做法是 依賴於 JavaScript 模組載入器(如 require.js or curl.js) 還是簡單使用 <script> 標籤載入 JavaScript 庫。最簡單的方法是 使用 <script> 標籤從 SockJS CDN 中進行載入,如下所示:

<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>

Attention)用 WebJars 解析 Web資源(可選,有興趣的童鞋可以嘗試下)

A1)在springmvc 配置中搭建一個 資源處理器,讓它負責解析路徑以 "webjars/**" 開頭的請求,這也是 WebJars 的標準路徑:

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/webjars/**")

.addResourceLocations("classpath:/META-INF/resources/webjars/");

}

A2)在這個資源處理器 準備就緒後,我們可以在 web 頁面中使用 如下的 <script> 標籤載入 SockJS 庫;

<script src="sockjs.min.js}"> </script>

5)處理載入 SockJS 客戶端庫以外,還要修改 兩行程式碼:


var url = 'p2ptext';

var sock = new SockJS(url);

對以上程式碼的分析(Analysis): 

A1)SockJS 所處理的URL 是 "http://" 或 "https://" 模式,而不是 "ws://" or  "wss://" ;

A2)其他的函式如 onopen, onmessage, and  onclose ,SockJS 客戶端與 WebSocket 一樣;

6)SockJS 為 WebSocket 提供了 備選方案。但無論哪種場景,對於實際應用來說,這種通訊形式層級過低。下面看一下如何 在 WebSocket 之上使用 STOMP協議,來為瀏覽器 和 server間的 通訊增加適當的訊息語義;(乾貨——引入 STOMP—— Simple Text Oriented Message Protocol——面向訊息的簡單文字協議)

【3】使用 STOMP訊息

1)intro: 如何理解 STOMP 與 WebSocket 的關係:

1.1)假設 HTTP 協議 並不存在,只能使用 TCP 套接字來 編寫 web 應用,你可能認為這是一件瘋狂的 事情;

1.2)不過 幸好,我們有 HTTP協議,它解決了 web 瀏覽器發起請求以及 web 伺服器響應請求的細節;

1.3)直接使用 WebSocket(SockJS) 就很類似於 使用 TCP 套接字來編寫 web 應用;因為沒有高層協議,因此就需要我們定義應用間所傳送訊息的語義,還需要確保 連線的兩端都能遵循這些語義;

1.4)同 HTTP 在 TCP 套接字上新增 請求-響應 模型層一樣,STOMP 在 WebSocket 之上提供了一個基於 幀的線路格式層,用來定義訊息語義;(乾貨——STOMP 在 WebSocket 之上提供了一個基於 幀的線路格式層,用來定義訊息語義)

2)STOMP 幀:該幀由命令,一個或多個 頭資訊 以及 負載所組成。如下就是傳送 資料的一個 STOMP幀:(乾貨——引入了 STOMP幀格式)


SEND

destination:/app/marco

content-length:20


{\"message\":\"Marco!\"}

對以上程式碼的分析(Analysis):

A1)SEND:STOMP命令,表明會發送一些內容;

A2)destination:頭資訊,用來表示訊息傳送到哪裡;

A3)content-length:頭資訊,用來表示 負載內容的 大小;

A4)空行:

A5)幀內容(負載)內容:

3)STOMP幀 資訊 最有意思的是 destination頭資訊了: 它表明 STOMP 是一個訊息協議,類似於 JMS 或 AMQP。訊息會發送到 某個 目的地,這個 目的地實際上可能真的 有訊息代理作為 支撐。另一方面,訊息處理器 也可以監聽這些目的地,接收所傳送過來的訊息;

【3.1】啟用STOMP 訊息功能

1)intro:spring 的訊息功能是基於訊息代理構建的,因此我們必須要配置一個 訊息代理 和 其他的一些訊息目的地;(乾貨——spring 的訊息功能是基於訊息代理構建的)

2)如下程式碼展現了 如何通過 java配置 啟用基於代理的的web 訊息功能;

(乾貨——@EnableWebSocketMessageBroker 註解的作用: 能夠在 WebSocket 上啟用 STOMP)

package com.spring.spittr.web;


import org.springframework.context.annotation.Configuration;

import org.springframework.messaging.handler.annotation.MessageMapping;

import org.springframework.messaging.simp.config.MessageBrokerRegistry;

import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;

import org.springframework.web.socket.config.annotation.StompEndpointRegistry;


@Configuration

@EnableWebSocketMessageBroker

public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

@Override

public void configureMessageBroker(MessageBrokerRegistry config) {

config.enableSimpleBroker("/topic", "/queue");

config.setApplicationDestinationPrefixes("/app");

// 應用程式以 /app 為字首,而 代理目的地以 /topic 為字首.

// js.url = "/spring13/app/hello" -> @MessageMapping("/hello") 註釋的方法.

}


@Override

public void registerStompEndpoints(StompEndpointRegistry registry) {

registry.addEndpoint("/hello").withSockJS();

// 在網頁上我們就可以通過這個連結 /server/hello ==<c:url value='/hello'></span> 來和伺服器的WebSocket連線

}

}

對以上程式碼的分析(Analysis):

A1)EnableWebSocketMessageBroker註解表明: 這個配置類不僅配置了 WebSocket,還配置了 基於代理的 STOMP 訊息;

A2)它過載了 registerStompEndpoints() 方法:將 "/hello" 路徑 註冊為 STOMP 端點。這個路徑與之前傳送和接收訊息的目的路徑有所不同, 這是一個端點,客戶端在訂閱或釋出訊息 到目的地址前,要連線該端點,即 使用者傳送請求 url='/server/hello' 與 STOMP server 進行連線,之後再轉發到 訂閱url;(server== name of your springmvc project (乾貨——端點的作用——客戶端在訂閱或釋出訊息 到目的地址前,要連線該端點)

A3)它過載了 configureMessageBroker() 方法:配置了一個 簡單的訊息代理。如果不過載,預設case下,會自動配置一個簡單的 記憶體訊息代理,用來處理 "/topic" 為字首的訊息。但經過過載後,訊息代理將會處理字首為 "/topic" and "/queue" 訊息。

A4)之外:傳送應用程式的訊息將會帶有 "/app" 字首,下圖展現了 這個配置中的 訊息流;

對上述處理step的分析(Analysis):

A1)應用程式的目的地 以 "/app" 為字首,而代理的目的地以 "/topic" 和 "/queue" 作為字首;

A2)以應用程式為目的地的訊息將會直接路由到 帶有 @MessageMapping 註解的控制器方法中;(乾貨—— @MessageMapping的作用)

A3)而傳送到 代理上的訊息,包括 @MessageMapping註解方法的返回值所形成的訊息,將會路由到 代理上,並最終傳送到 訂閱這些目的地客戶端;

(乾貨——client 連線地址和 傳送地址是不同的,以本例為例,前者是/server/hello, 後者是/server/app/XX,先連線後傳送)

【3.1.1】啟用 STOMP 代理中繼

1)intro:在生成環境下,可能會希望使用 真正支援 STOMP 的代理來支援 WebSocket 訊息,如RabbitMQ 或 ActiveMQ。這樣的代理提供了可擴充套件性和健壯性更好的訊息功能,當然,他們也支援 STOMP 命令;

2)如何 使用 STOMP 代理來替換記憶體代理,程式碼如下:


@Override

public void configureMessageBroker(MessageBrokerRegistry registry) {

// 啟用了 STOMP 代理中繼功能,並將其代理目的地字首設定為 /topic and /queue .

registry.enableStompBrokerRelay("/queue", "/topic")

.setRelayPort(62623);

registry.setApplicationDestinationPrefixes("/app"); // 應用程式目的地.

}

對以上程式碼的分析(Analysis):(乾貨——STOMP代理字首和 應用程式字首的意義)

A1)方法第一行啟用了 STOMP 代理中繼功能: 並將其目的地字首設定為 "/topic" or "/queue" ;spring就能知道 所有目的地字首為 "/topic" or "/queue" 的訊息都會發送到 STOMP 代理中;

A2)方法第二行設定了 應用的字首為 "app":所有目的地以 "/app" 打頭的訊息(傳送訊息url not 連線url)都會路由到 帶有 @MessageMapping 註解的方法中,而不會發布到 代理佇列或主題中;

3)下圖闡述了 代理中繼如何 應用於 spring 的 STOMP 訊息處理之中。與 上圖的 關鍵區別在於: 這裡不再模擬STOMP 代理的功能,而是由 代理中繼將訊息傳送到一個 真正的訊息代理來進行處理;

Attention)

A1)enableStompBrokerRelay() and setApplicationDestinationPrefixes() 方法都可以接收變長 引數;

A2)預設情況下: STOMP 代理中繼會假設 代理監聽 localhost 的61613 埠,並且 client 的 username 和password 均為 guest。當然你也可以自行定義;


@Override

public void configureMessageBroker(MessageBrokerRegistry registry) {

registry.enableStompBrokerRelay("/topic", "/queue")

.setRelayHost("rabbit.someotherserver")

.setRelayPort(62623)

.setClientLogin("marcopolo")

.setClientPasscode("letmein01");

registry.setApplicationDestinationPrefixes("/app", "/foo");

} // setXXX()方法 是可選的

【3.2】 處理來自客戶端的 STOMP 訊息

1)藉助 @MessageMapping 註解能夠 在 控制器中處理 STOMP 訊息


package com.spring.spittr.web;


import org.springframework.messaging.handler.annotation.MessageMapping;

import org.springframework.messaging.handler.annotation.SendTo;

import org.springframework.stereotype.Controller;


import com.spring.pojo.Greeting;

import com.spring.pojo.HelloMessage;


@Controller

public class GreetingController {


@MessageMapping("/hello")

@SendTo("/topic/greetings")

public Greeting greeting(HelloMessage message) throws Exception {

System.out.println("receiving " + message.getName());

System.out.println("connecting successfully.");

return new Greeting("Hello, " + message.getName() + "!");

}

}

對以上程式碼的分析(Analysis):

A1)@MessageMapping註解:表示 handleShout()方法能夠處理 指定目的地上到達的訊息;

A2)這個目的地(訊息傳送目的地url)就是 "/server/app/hello",其中 "/app" 是 隱含的 ,"/server" 是 springmvc 專案名稱;

2)因為我們現在處理的 不是 HTTP,所以無法使用 spring 的 HttpMessageConverter 實現 將負載轉換為Shout 物件。Spring 4.0 提供了幾個訊息轉換器如下:(Attention, 如果是傳輸json資料的話,定要新增 Jackson jar 包到你的springmvc 專案中,不然連線不會成功的)

【3.2.1】處理訂閱(@SubscribeMapping註解)

1)@SubscribeMapping註解 的方法:當收到 STOMP 訂閱訊息的時候,帶有 @SubscribeMapping 註解 的方法將會觸發;其也是通過 AnnotationMethodMessageHandler 來接收訊息的;

2)@SubscribeMapping註解的應用場景:實現 請求-迴應模式。在請求-迴應模式中,客戶端訂閱一個目的地,然後預期在這個目的地上 獲得一個一次性的 響應;(乾貨——引入了@SubsribeMapping註解實現 請求-迴應模式)

2.1)看個荔枝:

@SubscribeMapping({"/marco"})

public Shout handleSubscription() {

Shout outgoing = new Shout();

outgoing.setMessage("Polo!");

return outgoing;

}

對以上程式碼的分析(Analysis):

A1)@SubscribeMapping註解 的方法來處理 對 "/app/macro" 目的地訂閱(與 @MessageMapping類似,"/app" 是隱含的 );

A2)請求-迴應模式與 HTTP GET 的全球-響應模式差不多: 關鍵區別在於, HTTP GET 請求是同步的,而訂閱的全球-迴應模式是非同步的,這樣客戶端能夠在迴應可用時再去處理,而不必等待;(乾貨——HTTP GET 請求是同步的,而訂閱的請求-迴應模式是非同步的)

【3.2.2】編寫 JavaScript 客戶端

1)intro:藉助 STOMP 庫,通過 JavaScript傳送訊息


<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';

document.getElementById('response').innerHTML = '';

}


function connect() {

var socket = new SockJS("<c:url value='/hello'/>");

stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {

setConnected(true);

console.log('Connected: ' + frame);

stompClient.subscribe('/topic/greetings', function(greeting){

showGreeting(JSON.parse(greeting.body).content);

});

});

}


function disconnect() {

if (stompClient != null) {

stompClient.disconnect();

}

setConnected(false);

console.log("Disconnected");

}


function sendName() {

var name = document.getElementById('name').value;

stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));

}


function showGreeting(message) {

var response = document.getElementById('response');

var p = document.createElement('p');

p.style.wordWrap = 'break-word';

p.appendChild(document.createTextNode(message));

response.appendChild(p);

}

</script>

對以上程式碼的 分析(Analysis): 以上程式碼連線“/hello” 端點併發送 ”name“;

2)stompClient.send("/app/hello", {}, JSON.stringify({'name':name})): 第一個引數:json 負載訊息傳送的 目的地; 第二個引數:是一個頭資訊的Map,它會包含在 STOMP 幀中;第三個引數:負載訊息;

(乾貨—— stomp client 連線地址 和 傳送地址不一樣的,連線地址為 <c:url value='/hello'/> ==localhost:8080/springmvc_project_name/hello , 而 傳送地址為 '/app/hello',這裡要當心)


<script src="<c:url value="/resources/sockjs-1.1.1.js" />"></script>

<script src="<c:url value="/resources/stomp.js" />"></script>
//this line.

function connect() {

var socket = new SockJS("<c:url value='/hello'/>");

stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {

setConnected(true);

console.log('Connected: ' + frame);

stompClient.subscribe('/topic/greetings', function(greeting){

showGreeting(JSON.parse(greeting.body).content);

});


stompClient.subscribe('/app/macro',function(greeting){

alert(JSON.parse(greeting.body).content);

showGreeting(JSON.parse(greeting.body).content);

});

});

}


function sendName() {

var name = document.getElementById('name').value;

stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));

}

  1. package com.spring.spittr.web;
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import org.springframework.messaging.handler.annotation.MessageMapping;
    
    import org.springframework.messaging.handler.annotation.SendTo;
    
    import org.springframework.messaging.simp.SimpMessageSendingOperations;
    
    import org.springframework.messaging.simp.SimpMessagingTemplate;
    
    import org.springframework.messaging.simp.annotation.SubscribeMapping;
    
    import org.springframework.stereotype.Controller;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import org.springframework.web.bind.annotation.RequestParam;
    
    
    import com.spring.pojo.Greeting;
    
    import com.spring.pojo.HelloMessage;
    
    
    @Controller
    
    public class GreetingController {
    
    
    // @MessageMapping defines the sending addr for client.
    
    // 訊息傳送地址: /server/app/hello
    
    @MessageMapping("/hello")
    
    @SendTo("/topic/greetings")
    
    public Greeting greeting(HelloMessage message) throws Exception {
    
    System.out.println("receiving " + message.getName());
    
    System.out.println("connecting successfully.");
    
    return new Greeting("Hello, " + message.getName() + "!");
    
    }
    
    
    @SubscribeMapping("/macro")
    
    public Greeting handleSubscription() {
    
    System.out.println("this is the @SubscribeMapping('/marco')");
    
    Greeting greeting = new Greeting("i am a msg from SubscribeMapping('/macro').");
    
    return greeting;
    
    }
    
    
    /*@MessageMapping("/feed")
    
    @SendTo("/topic/feed")
    
    public Greeting greetingForFeed(HelloMessage message) throws Exception {
    
    System.out.println("receiving " + message.getName());
    
    System.out.println("connecting successfully.");
    
    return new Greeting("i am /topic/feed, hello " + message.getName() + "!");
    
    }*/
    
    
    // private SimpMessagingTemplate template;
    
    // SimpMessagingTemplate implements SimpMessageSendingOperations.
    
    private SimpMessageSendingOperations template;
    
    
    @Autowired
    
    public GreetingController(SimpMessageSendingOperations template) {
    
    this.template = template;
    
    }
    
    
    @RequestMapping(path="/feed", method=RequestMethod.POST)
    
    public void greet(
    
    @RequestParam String greeting) {
    
    String text = "you said just now " + greeting;
    
    this.template.convertAndSend("/topic/feed", text);
    
    }
    
    }
    
    
    package com.spring.spittr.web;
    
    
    import org.springframework.context.annotation.Configuration;
    
    import org.springframework.messaging.handler.annotation.MessageMapping;
    
    import org.springframework.messaging.simp.config.MessageBrokerRegistry;
    
    import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
    
    import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
    
    import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
    
    
    @Configuration
    
    @EnableWebSocketMessageBroker
    
    public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    
    @Override
    
    public void configureMessageBroker(MessageBrokerRegistry config) {
    
    config.enableSimpleBroker("/topic", "/queue");
    
    config.setApplicationDestinationPrefixes("/app");
    
    // 應用程式以 /app 為字首,而 代理目的地以 /topic 為字首.
    
    // js.url = "/spring13/app/hello" -> @MessageMapping("/hello") 註釋的方法.
    
    }
    
    
    @Override
    
    public void registerStompEndpoints(StompEndpointRegistry registry) {
    
    registry.addEndpoint("/hello").withSockJS();
    
    // 在網頁上我們就可以通過這個連結 /server/hello 來和伺服器的WebSocket連線
    
    }
    
    }
    
    
    package com.spring.spittr.web;
    
    
    import java.io.IOException;
    
    
    import org.springframework.context.MessageSource;
    
    import org.springframework.context.annotation.Bean;
    
    import org.springframework.context.annotation.ComponentScan;
    
    import org.springframework.context.annotation.Configuration;
    
    import org.springframework.context.annotation.Import;
    
    import org.springframework.context.support.ResourceBundleMessageSource;
    
    import org.springframework.core.io.FileSystemResource;
    
    import org.springframework.web.multipart.MultipartResolver;
    
    import org.springframework.web.multipart.commons.CommonsMultipartResolver;
    
    import org.springframework.web.servlet.ViewResolver;
    
    import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
    
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    import org.springframework.web.servlet.view.InternalResourceViewResolver;
    
    import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
    
    import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
    
    
    @Configuration
    
    @ComponentScan(basePackages = { "com.spring.spittr.web" })
    
    @EnableWebMvc
    
    @Import({WebSocketConfig.class})
    
    public class WebConfig extends WebMvcConfigurerAdapter {
    
    
    @Bean
    
    public TilesConfigurer tilesConfigurer() {
    
    TilesConfigurer tiles = new TilesConfigurer();
    
    tiles.setDefinitions(new String[] { "/WEB-INF/layout/tiles.xml" });
    
    tiles.setCheckRefresh(true);
    
    return tiles;
    
    }
    
    
    // config processing for static resources.
    
    @Override
    
    public void configureDefaultServletHandling(
    
    DefaultServletHandlerConfigurer configurer) {
    
    configurer.enable();
    
    }
    
    
    // InternalResourceViewResolver
    
    @Bean
    
    public ViewResolver viewResolver1() {
    
    TilesViewResolver resolver = new TilesViewResolver();
    
    return resolver;
    
    }
    
    
    @Bean
    
    public ViewResolver viewResolver2() {
    
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    
    resolver.setPrefix("/WEB-INF/views/");
    
    resolver.setSuffix(".jsp");
    
    resolver.setExposeContextBeansAsAttributes(true);
    
    resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class);
    
    return resolver;
    
    }
    
    
    @Bean
    
    public MessageSource messageSource() {
    
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    
    messageSource.setBasename("messages");
    
    return messageSource;
    
    }
    
    
    @Bean
    
    public MultipartResolver multipartResolver() throws IOException {
    
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    
    multipartResolver.setUploadTempDir(new FileSystemResource("/WEB-INF/tmp/spittr/uploads"));
    
    multipartResolver.setMaxUploadSize(2097152);
    
    multipartResolver.setMaxInMemorySize(0);
    
    return multipartResolver;
    
    }
    
    }
    
    【3.3】傳送訊息到客戶端
    
    1)intro:spring提供了兩種 傳送資料到 client 的方法:
    
    method1)作為處理訊息 或處理訂閱的附帶結果;
    
    method2)使用訊息模板;
    
    
    
    【3.3.1】在處理訊息後,傳送訊息(server 對 client 請求的 響應訊息)
    
    1)intro:如果你想要在接收訊息的時候,在響應中傳送一條訊息,修改方法簽名 不是void 型別即可, 如下:
    
    
    @MessageMapping("/hello")
    
    @SendTo("/topic/greetings") //highlight line.
    
    public Greeting greeting(HelloMessage message) throws Exception {
    
    System.out.println("receiving " + message.getName());
    
    System.out.println("connecting successfully.");
    
    return new Greeting("Hello, " + message.getName() + "!");
    
    }

對以上程式碼的分析(Analysis):返回的物件將會進行轉換(通過訊息轉換器) 並放到 STOMP 幀的負載中,然後傳送給訊息代理(訊息代理分為 STOMP代理中繼 和 記憶體訊息代理)

2)預設情況下:幀所發往的目的地會與 觸發 處理器方法的目的地相同。所以返回的物件 會寫入到 STOMP 幀的負載中,併發布到 "/topic/stomp" 目的地。不過,可以通過 @SendTo 註解,過載目的地;(乾貨——註解 @SendTo 註解的作用)
程式碼同上。
對以上程式碼的分析(Analysis):
訊息將會發布到 /topic/hello, 所有訂閱這個主題的應用都會收到這條訊息;

3)@SubscriptionMapping 註解標註的方式也能傳送一條訊息,作為訂閱的迴應。

3.1)看個荔枝: 通過為 控制器新增如下的方法,當客戶端訂閱的時候,將會發送一條 shout 資訊:


@SubscribeMapping("/macro") // defined in Controller. attention for addr '/macro' in server.

public Greeting handleSubscription() {

System.out.println("this is the @SubscribeMapping('/marco')");

Greeting greeting = new Greeting("i am a msg from SubscribeMapping('/macro').");

return greeting;

}


function connect() {

var socket = new SockJS("<c:url value='/hello'/>");

stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {

setConnected(true);

console.log('Connected: ' + frame);

stompClient.subscribe('/topic/greetings', function(greeting){

showGreeting(JSON.parse(greeting.body).content);

});

// starting line.

stompClient.subscribe('/app/macro',function(greeting){

alert(JSON.parse(greeting.body).content);

showGreeting(JSON.parse(greeting.body).content);

}); // ending line. attention for addr '/app/macro' in client.

});

}

對以上程式碼的分析(Analysis): 

A0)這個SubscribeMapping annotation標記的方法,是在訂閱的時候呼叫的,也就是說,基本是隻執行一次的方法,client 呼叫定義在server 的 該 Annotation 標註的方法,它就會返回結果,不過經過代理。

A1)這裡的 @SubscribeMapping 註解表明當 客戶端訂閱 "/app/macro" 主題的時候("/app"是應用目的地的字首,注意,這裡沒有加springmvc 專案名稱字首), 將會呼叫 handleSubscription 方法。它所返回的shout 物件 將會進行轉換 併發送回client;

A2)SubscribeMapping註解的區別在於: 這裡的 Shout 訊息將會直接傳送給 client,不用經過 訊息代理;但,如果為方法新增 @SendTo 註解的話,那麼 訊息將會發送到指定的目的地,這樣就會經過代理;(乾貨——SubscribeMapping註解返回的訊息直接傳送到 client,不經過代理,而 @SendTo 註解的路徑,就會經過代理,然後再發送到 目的地)

【3.3.2】 在應用的任意地方傳送訊息

1)intro:spring 的 SimpMessagingTemplate 能夠在應用的任何地方傳送訊息,不必以接收一條訊息為 前提;

2)看個荔枝: 讓首頁訂閱一個 STOMP主題,在 Spittle 建立的時候,該主題能夠收到 Spittle 更新時的 feed;

2.1)JavaScript 程式碼:


<script>

var sock = new SockJS('spittr');

var stomp = Stomp.over(sock);

stomp.connect('guest', 'guest', function(frame) {

console.log('Connected');

stomp.subscribe("/topic/spittlefeed", handleSpittle); // highlight.

});

function handleSpittle(incoming) {

var spittle = JSON.parse(incoming.body);

console.log('Received: ', spittle);

var source = $("#spittle-template").html();

var template = Handlebars.compile(source);

var spittleHtml = template(spittle);

$('.spittleList').prepend(spittleHtml);

}

</script>

對以上程式碼的分析(Analysis): 在連線到 STMOP 代理後,我們訂閱了 "/topic/spittlefeed" 主題,並指定當訊息到達的是,由 handleSpittle()函式來處理 Spittle 更新。

2.2) server 端程式碼:使用 SimpMessagingTemplate 將所有新建立的 Spittle 以訊息的形式釋出到 "/topic/feed" 主題上;


@Service

public class SpittleFeedServiceImpl implements SpittleFeedService {

private SimpMessageSendingOperations messaging;

@Autowired

public SpittleFeedServiceImpl(

SimpMessageSendingOperations messaging) { // 注入訊息模板.

this.messaging = messaging;

}

public void broadcastSpittle(Spittle spittle) {

messaging.convertAndSend("/topic/spittlefeed", spittle); // 傳送訊息.

}

}

對以上程式碼的分析(Analysis): 

A1)配置 spring 支援 stomp 的一個附帶功能是 在spring應用上下文中已經包含了 Simple

A2)在釋出訊息給 STOMP 主題的時候,所有訂閱該主題的客戶端都會收到訊息。但有的時候,我們希望將訊息傳送給指定使用者;

【4】 為目標使用者傳送訊息

1)intro:在使用 srping 和 STOMP 訊息功能的時候,有三種方式來利用認證使用者:

way1)@MessageMapping and @SubscribeMapping 註解標註的方法 能夠使用 Principal 來獲取認證使用者;

way2)@MessageMapping, @SubscribeMapping, and @MessageException 方法返回的值能夠以 訊息的形式傳送給 認證使用者;

way3)SimpMessagingTemplate 能夠傳送訊息給特定使用者;

【4.1】在控制器中處理使用者的 訊息

1)看個荔枝: 編寫一個控制器方法,根據傳入的訊息建立新的Spittle 物件,併發送一個迴應,表明 物件建立成功;(這種 REST也可以實現,不過它是同步的,而這裡是非同步的);

1.1)程式碼如下:它會處理傳入的訊息並將其儲存我 Spittle:


@MessageMapping("/spittle")

@SendToUser("/queue/notifications")

public Notification handleSpittle(

Principal principal, SpittleForm form) {

Spittle spittle = new Spittle(

principal.getName(), form.getText(), new Date());

spittleRepo.save(spittle);

return new Notification("Saved Spittle");

}

1.2)該方法最後返回一個 新的 Notificatino,表明物件儲存成功;

1.3)該方法使用了 @MessageMapping("/spittle") 註解,所以當有發往 "/app/spittle" 目的地的訊息 到達時,該方法就會觸發;如果使用者已經認證的話,將會根據 STOMP 幀上的頭資訊得到 Principal 物件;

1.4)@SendToUser註解: 指定了 Notification 要傳送的 目的地 "/queue/notifications";

1.5)表明上, "/queue/notifications" 並不會與 特定使用者相關聯,但因為 這裡使用的是 @SendToUser註解, 而不是 @SendTo,所以 就會發生更多的事情了;

2)看一下針對 控制器方法釋出的 Notificatino 物件的目的地,客戶端該如何進行訂閱。

2.1)看個荔枝:考慮如下的 JavaScript程式碼,它訂閱了一個 使用者特定的 目的地:

stomp.subscribe("/user/queue/notifications", handleNotifications);

對以上程式碼的分析(Analysis):這個目的地使用了 "/user" 作為字首,在內部,以"/user" 為字首的訊息將會通過 UserDestinationMessageHandler 進行處理,而不是 AnnotationMethodMessageHandler 或  SimpleBrokerMessageHandler or StompBrokerRelayMessageHandler,如下圖所示:

Attention)UserDestinationMessageHandler 的主要任務: 是 將使用者訊息重新路由到 某個使用者獨有的目的地上。 在處理訂閱的時候,它會將目標地址中的 "/user" 字首去掉,並基於使用者 的會話新增一個字尾。如,對  "/user/queue/notifications" 的訂閱最後可能路由到 名為 "/queue/notifacations-user65a4sdfa" 目的地上;

【4.2】為指定使用者傳送訊息

1)intro:SimpMessagingTemplate還提供了 convertAndSendToUser() 方法,該方法能夠讓 我們給特定使用者傳送訊息;

2)我們在 web 應用上新增一個特性: 當其他使用者提交的 Spittle 提到某個使用者時,將會提醒該使用者(乾貨——這難道不是 微博的 @ 功能嗎)

2.1)看個荔枝:如果Spittle 文字中包含 "@tangrong",那麼我們就應該傳送一條訊息給 使用 tangrong 使用者名稱登入的client,程式碼例項如下:


@Service

public class SpittleFeedServiceImpl implements SpittleFeedService {

private SimpMessagingTemplate messaging;

// 實現使用者提及功能的正則表示式

private Pattern pattern = Pattern.compile("\\@(\\S+)");


@Autowired

public SpittleFeedServiceImpl(SimpMessagingTemplate messaging) {

this.messaging = messaging;

}

public void broadcastSpittle(Spittle spittle) {

messaging.convertAndSend("/topic/spittlefeed", spittle);

Matcher matcher = pattern.matcher(spittle.getMessage());

if (matcher.find()) {

String username = matcher.group(1);

// 傳送提醒給使用者.

messaging.convertAndSendToUser(

username, "/queue/notifications",

new Notification("You just got mentioned!"));

}

}

}

【5】處理訊息異常

1)intro:我們也可以在 控制器方法上新增 @MessageExceptionHandler 註解,讓它來處理 @MessageMapping 方法所丟擲的異常;

2)看個荔枝:它會處理 訊息方法所丟擲的異常;


@MessageExceptionHandler

public void handleExceptions(Throwable t) {

logger.error("Error handling message: " + t.getMessage());

}

3)我們也可以以 引數的形式宣告它所能處理的異常;

@MessageExceptionHandler(SpittleException.class) // highlight line.

public void handleExceptions(Throwable t) {

logger.error("Error handling message: " + t.getMessage());

}

// 或者:

@MessageExceptionHandler( {SpittleException.class, DatabaseException.class}) // highlight line.

public void handleExceptions(Throwable t) {

logger.error("Error handling message: " + t.getMessage());

}

4)該方法還可以迴應一個錯誤:

@MessageExceptionHandler(SpittleException.class)

@SendToUser("/queue/errors")

public SpittleException handleExceptions(SpittleException e) {

logger.error("Error handling message: " + e.getMessage());

return e;

}

// 如果丟擲 SpittleException 的話,將會記錄這個異常,並將其返回.

// 而 UserDestinationMessageHandler 會重新路由這個訊息到特定使用者所對應的 唯一路徑;