Spring中配置WebSocket

Frank-Hao發表於2016-09-24

專案中需要在瀏覽器中實時檢視 docker 容器內框架的執行日誌, 在伺服器上可以通過 docker 提供的 RESTful 介面, 獲取 tail -f 的執行結果 InputStream 流. 而瀏覽器 HTTP 協議是一個請求 - 響應的協議, 要想獲得資料, 就必須發起一次請求, 顯然和 Java 的 InputStream 的概念是不能協作的, 伺服器無法實時的將日誌流推送到瀏覽器. 使用 AJAX 技術可以以一定的間隔時間非同步方式發起請求. 在瀏覽器沒有發起 AJAX 的時間間隙內, 伺服器需要維持日誌流的快取, 如果有很多不同的頁面檢視日誌, 那麼就需要維持多個佇列, 還有一個問題就是如果一個日誌頁面關閉了, 如何清除這個佇列快取?

所以考慮使用 WebSocket 來實時獲取日誌流.

WebSocket簡介

WebSocket為瀏覽器提供了一個真正的瀏覽器和伺服器之間的全雙工Channel. WebSocket使用HTTP的request protocol upgrade頭部來進行請求建立, 服務端返回101表示協議切換成功, 底層的TCP Channel就會一直保持開啟. 一旦通道建立, 瀏覽器端使用send()向伺服器傳送資料, 通過onmessage事件handler來接收伺服器資料, 且每次傳送資料時, 不需要再次傳輸HTTP Header, 大大減少了資料傳輸量, 適用於瀏覽器和伺服器間進行高頻率低延遲的資料交換. 同時實現了真正的伺服器推送資料到瀏覽器.

瀏覽器端:
目前主流的瀏覽器都支援WebSocket.

瀏覽器 支援的版本
Chrome Supported in version 4+
Firefox Supported in version 4+
Internet Explorer Supported in version 10+
Opera Supported in version 10+
Safari Supported in version 5+

服務端:
JavaEE 7 JSR356提供了對WebSocket的支援, Tomcat從7.047版本開始提供了JSR356的支援, Spring從4.0版本開始支援WebSocket.

Java 實現方法

在 Spring 端可以有以下幾種方法使用 WebSocket
1. 使用 Java EE7 的方式
2. 使用 Spring 提供的介面
3. 使用 STOMP 協議以及 Spring 的 MVC

第三種方式見 Spring 的官方文件, 基於 webSocket, 使用 Simple Text Oriented Message Protocol(STOMP) 協議:Using WebSocket to build an interactive web application

STOM 協議工作在 Socket 之上, 類似於 HTTP 協議, 為面向於文字訊息的中介軟體而設計, 是一種語言無關的協議, 符合該協議的 client 和 broker 之間都能通訊, 無論是使用何種語言開發.
STOM 協議介紹

這裡我將著重介紹使用 Spring 提供的介面開發的方式.
主要分為以下幾個類, 第一個是 WebSocket 連線建立前的攔截類 HandshakeInterceptor, 類似於 Spring MVC 的 HandlerInteceptor, 第二個 WebSocket 的處理類, 負責對生命週期進行管理, 第三個是配置類, 將 websocket 請求與對應的 handler 類進行對映.
**1.HandshakeInterceptor
2.WebSocketHandler
3.WebSocketConfigurer**

依賴

Spring WebSocket 依賴於以下包, 版本為 4.0 版本及以上, Tomcat 7.047 以上版本, Java EE 7

<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-messaging</artifactId>
      <version>4.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-websocket</artifactId>
    <version>8.0.23</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
</dependency>

HandshakeInterceptor

這個類類似於 Spring MVC 中用於攔截 HTTP 請求的 HandlerInteceptor. 它有兩個方法,

public interface HandshakeInterceptor {

    boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
            WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception;

    void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
            WebSocketHandler wsHandler, Exception exception);

}

如果 beforeHandshake 返回 false, 那麼該 WebSocket 請求被拒絕連線.
其中 request 可以轉換成 ServletServerHttpRequest, 並獲取其中包裝的 HttpServletRequest, 以獲得 http 請求中 parameter 等資訊.

WebSocketHandler

作為 WebSocket 連線建立後的處理介面, 主要有以下方法.

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

}

對於每個 WebSocket 連線, 傳入的引數 session 都有一個唯一的 id, 通過 getId() 方法獲取, 可用於標記這個 WebSocket 連線.
常用的類有 TextWebSocketHandler 和 BinaryWebSocketHandler. 這兩個類繼承自 AbstractWebSocketHandler, 其接收 message 時, 對 message 型別進行了判斷, 並對不同型別的訊息進行了分發. 使用時我們需要分別覆蓋 handleTextMessage 方法或 handleBinaryMessage 方法.

    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        if (message instanceof TextMessage) {
            handleTextMessage(session, (TextMessage) message);
        }
        else if (message instanceof BinaryMessage) {
            handleBinaryMessage(session, (BinaryMessage) message);
        }
        else if (message instanceof PongMessage) {
            handlePongMessage(session, (PongMessage) message);
        }
        else {
            throw new IllegalStateException("Unexpected WebSocket message type: " + message);
        }
    }

WebSocketConfigurer

上面我們定義了握手時需要的 HandlerShakeInteceptor 和對 WebSocket 進行業務處理的 WebSocketHandler, 我們還需要將訪問連線與對應的 handler 以及攔截器關聯起來.

@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebsocketEndPoint implements WebSocketConfigurer {
    @Autowired
    LogWebSocketHandler handler;
    @Autowired
    HandlerShakerInceptor handlerShakerInceptor;

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(handler, "/logs.do").setAllowedOrigins("*").addInterceptors(handlerShakerInceptor);
    }
}

@Configuration 是 Spring 基於 Java 類的配置, 可以將 Spring 的 Bean 轉換為 Spring 的配置.
其等效的 XML 配置如下:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:websocket="http://www.springframework.org/schema/websocket"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/websocket
       http://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:handlers allowed-origins="*">
        <websocket:mapping path="/log.do" handler="myHandler"/>
        <websocket:handshake-interceptors>
            <ref bean="myHandlerShakeInceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>
    <bean id="myHandlerShakeInceptor" class="com.haoyifen.myHandlerShakeInceptor"/>
    <bean id="myHandler" class="com.haoyifen.myHandler"/>
</beans>

對於 java configuration 中的 setAllowedOrigins 和 xml 中的 allowed-origins, 解釋一下, 這是用於設定跨域的, 簡單的說就是一般瀏覽器對於 AJAX 和 WebSocket 是禁止跨域請求, 需要我們在伺服器端進行設定, 允許其他域 (比如主機名不同或者埠不同) 上的網頁與你的 WebSocket 發起連線. “*” 表示任何域上的網頁請求都可以連線, 請設定為信任的域名.

客戶端

1.Chrome 的 Dark WebSocket Terminal 應用
2.Java-WebSocket 包中的 client 類
3.JS

   $(document).ready(function () {
        var websocket = new WebSocket('ws://localhost:8081/logs.do?path=/web/logs/hadoop/hadoop-hadoop-namenode-vm10244.log');
        websocket.onmessage = function (event) {
        ....
        };
        websocket.onclose=function (event) {
        ....
        }
        websocket.onerror=function (event) {
        ...
        };
    });

相關文章