springboot系列文章之集成WebSocket進行廣播式消息推送

前言

在springboot整合websocket之前,先簡單闡述下websocket的基本概念,以及與它相關的sockjs,stomp又是什麼。

WebSocket簡介

WebSocket協議是 HTML5新增的一種在單個TCP連接上進行全雙工通訊的協議,在 WebSocket API 中,瀏覽器和服務器只需要做一個握手的動作,然後,瀏覽器和服務器之間就形成一條快速通道,兩者之間就直接可以數據相互傳送了。

WebSocket與HTTP的不同之處在於:

WebSocket是一種全雙工通信協議,在建立連接後,WebSocket服務器和瀏覽器端都能夠主動的向對方發送消息,就像Socket一樣。而HTTP只能由客戶端發起請求,服務器返回查詢結果,做不到服務器主動向客戶端發送請求,如下圖所示

websocket

WebSocket的特點

這裏總結下WebSocket的特點:
- WebSocket服務器和瀏覽器都能夠主動向對方發送消息
- 建立在 TCP協議之上,服務器的實現比較容易
- 與HTTP 協議有着良好的兼容性,默認端口也是 80和443,並且握手階段採用HTTP協議,可以通過HTTP代理
- 數據格式比較輕量,性能開銷小,通信高效
- 可以發送文本,也可以發送二進制數據
- 沒有同源限制,客戶端可以與任意服務器通信
- 協議標識符是 ws(如果加密,則爲wss),服務器網址是URL

SockJS

SockJS是一個瀏覽器上運行的JavaScript庫,如果瀏覽器不支持 WebSocket,該庫可以模擬對 WebSocket的支持,實現瀏覽器和Web服務器之間的低延遲,全雙工,跨域的通訊通道

STOMP

STOMP即 Simple(or Streaming) Text Oriented Messaging Protocol 的簡稱,簡單(流)文本定向消息協議,它提供了一個可戶操作的連接格式,允許 STOMP 客戶端與任意 STOMP消息代理(Broker)進行交互,STOMP協議由於設計簡單,易於開發客戶端,因此在多種語言和多種平臺上得到廣泛應用

之前的介紹談到 WebSocket是基於 TCP協議的,直接使用WebSocket(或者SockJS)來編程就與直接使用TCP套接字來編程web應用類似,這會非常難受,因爲沒有高層協議,因此就需要我們定義應用間所發送消息的語義,還需要確保連接兩端都能遵循這些語義。

那麼現在STOMP就派上用場了,同HTTP在TCP套接字上添加請求-響應模型層一樣,STOMP在 WebSocket之上提供了一個基於幀的線路格式層,用來定義消息語義

STOMP幀

STOMP幀由命令,一個或多個頭消息以及負載所組成,如下所示是一個發送數據的STOMP幀:

   SEND
destination:/app/room-message
content-length:20

{\"message\":\"Hello!\"}

對上面分析如下:
- SEND: STOMP命令,表明會發送一些內容
- destination: 頭消息,用來表示消息發送到哪裏
- content-length: 頭信息,用來表示負載內容的大小
- 空行
- 幀內容(負載)內容

WebSocket、SockJS、STOMP的關係

簡單說就是,WebSocket是基於TCP的底層協議,SockJS是WebSocket的備選方案,用於那些不支持WebSocket的瀏覽器,也是底層協議,而STOMP是 WebSocket的上層協議,是高級協議

SpringBoot整合WebSocket

前面鋪墊了一些基礎知識過後,下面進入本篇文章的主題,使用SpringBoot+WebSocket+SockJS+STOMP搭建一個廣播式的WebSocket

導入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocket配置

@Configuration
@EnableWebSocketMessageBroker //啓用STOMP消息
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //註冊STOMP端點,即WebSocket客戶端需要連接到WebSocket握手端點
        //這是一個端點,客戶端在訂閱或發佈消息到目的地路徑前,要連接該端點
        registry.addEndpoint("/point")
                //跨域設置
                .setAllowedOrigins("*")
                //啓用SockJS功能
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //設置消息代理,所有目的地前綴爲"/topic","/queue"的消息都會發送到STOMP代理中
        registry.enableSimpleBroker("/topic", "/queue");
        //設置應用程序的目的地前綴爲"/app",當有以應用程序爲目的地的消息將會直接路由到帶有@MessageMapping註解的控制器方法
        registry.setApplicationDestinationPrefixes("/app");
    }
}

對上述程序程序進行分析:
- @EnableWebSocketMessageBroker註解不僅配置了WebSocket,還配置了基於代理的STOMP消息
- 重載了registerStompEndpoints方法,將”/point”註冊爲STOMP端點,客戶端需要先連接該端點
- 重載configureMessageBroker配置消息代理,同時設置應用程序的目的地前綴,當以應用程序爲目的地的消息將會直接路由到帶@MessageMapoping註解的控制器方法

下圖來自spring-websocket官方文檔,表示爲websocket的通訊模型圖
message

解讀一下模型圖:

對於同一個目標:/a,它的前綴將會決定消息該如何處理,分爲兩種:/app/a/topic/a,如果是爲 /topic/a,那麼可以直接將消息體發送到 簡單代理消息處理器上,而如果是 /app/a,那麼它會先將消息路由到應用程序內部帶有 @MessageMapping註解的控制器方法中,在控制器方法中進行處理,然後將處理結果發送到 brokeChannel,最後再將消息發送到簡單代理消息處理器上,兩種情況最後都是經由代理再發送到客戶端的目的地的。

請求消息類

public class RequestMessage {
    private String name;

    public String getName() {
        return name;
    }
}

響應消息類

public class ResponseMessage {
    private String responseMessage;

    public ResponseMessage(String responseMessage) {
        this.responseMessage = responseMessage;
    }

    public ResponseMessage() {
    }

    public String getResponseMessage() {
        return responseMessage;
    }

    public void setResponseMessage(String responseMessage) {
        this.responseMessage = responseMessage;
    }
}

處理來自客戶端的STOMP消息

藉助 @MessageMapping 註解在控制器中處理 STOMP消息,代碼如下:

@Controller
public class GreetingController {
    /**
     * 處理髮往 /app/greeting目的地的消息
     *
     * @param greeting
     * @return
     */
    @MessageMapping("/greeting")
//    @SendTo("/topic/say")
    public ResponseMessage handle(RequestMessage greeting) {
        //Spring的某一個消息轉換器會將STOMP消息的負載轉換爲 RequestMessage對象
        System.out.println(greeting.getName());
        return new ResponseMessage("welcome," + greeting.getName());
    }
}

代碼分析:
- handle方法處理客戶端發往目的地爲 /app/greeting的消息,/app爲隱含的,因爲在配置類中我們將其設置爲應用的目的地前綴
- 該方法有一個RequestMessage參數,實際上是Spring利用消息轉換器將消息負載轉換成了 RequestMessage對象
- 該方法返回一個 ResponseMessage實體,Spring使用消息轉換器將這個返回的ResponseMessage對象轉換爲消息負載
- 默認情況下,返回消息的目的地與客戶端發送消息的目的地想用,只不過會添加 /topic,當然我們也可以使用 @SendTo註解重載返回消息的目的地。

訂閱註解 @SubcribeMapping

當客戶端訂閱一個地址的時候,我們也可以使用@SubcribeMapping註解發送一條消息,作爲訂閱的迴應:

   @SubscribeMapping("/subscribe")
    public ResponseMessage subscribe() {
        ResponseMessage responseMessage = new ResponseMessage();
        responseMessage.setResponseMessage("歡迎訂閱");
        return responseMessage;
    }

這裏的註解 @SubcribeMapping 註解表明當客戶端訂閱 /app/subscribe(/app是應用目的地的前綴)目的地的時候,將會調用 subscribe()方法,並返回一個ResponseMessage對象

利用SimpMessagingTemplate

我們也可以使用SimpMessagingTemplate,Spring的SimpMessagingTemplate 能夠在應用的任何地方發送消息,甚至不需要首先接收一條消息作爲前提。

客戶端

客戶端編寫需要添加stomp.js和sock.js,下面是具體客戶端代碼:

<html>
<head>
    <meta charset="UTF-8"/>
    <title>廣播式WebSocket</title>
    <script src="js/sockjs.min.js"></script>
    <script src="js/stomp.js"></script>
    <script src="js/jquery-3.1.1.js"></script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">Sorry,瀏覽器不支持WebSocket</h2></noscript>
<div>
    <div>

        <button id="connect" onclick="connect();">連接</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">斷開連接</button>
    </div>

    <div id="conversationDiv">
        <label>輸入你的名字</label><input type="text" id="name"/>
        <button id="sendName" onclick="sendName();">發送</button>
        <p id="response"></p>
        <p id="callback"></p>
    </div>
</div>
<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';
        $("#response").html();
        $("#callback").html();
    }

    function connect() {
        <!--連接stomp端點-->
        var socket = new SockJS('http://localhost:9999/point');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected:' + frame);
            <!--訂閱/topic/greeting-->
            stompClient.subscribe('/topic/greeting', function (response) {
                showResponse(JSON.parse(response.body).responseMessage);
            });
            <!--訂閱/app/subscribe-->
            stompClient.subscribe('/app/subscribe', function (response) {
                showResponse(JSON.parse(response.body).responseMessage);
            });
        });
    }

    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log('Disconnected');
    }

    function sendName() {
        var name = $('#name').val();
        console.log('name:' + name);
        <!--向目的地/app/greeting發送消息,對應服務端@MessageMapping註解的方法來處理-->
        stompClient.send("/app/greeting", {}, JSON.stringify({'name': name}));
    }

    function showResponse(message) {
        $("#response").html(message);
    }
    function showCallback(message) {
        $("#callback").html(message);
    }
</script>
</body>
</html>

測試結果

test

頁面上點擊連接後,會先連接上 /point端點,然後同時訂閱 /topic/greeting/app/subscribe ,輸入名字點擊發送,將向 /greeting 的URL發送消息,然後服務器響應消息到 /topic/greeting

參考資料 & 鳴謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章