1. 程式人生 > >SpringBoot 整合 Tomcat 的 WebSocket 服務端

SpringBoot 整合 Tomcat 的 WebSocket 服務端

傳統 Tomcat 開發 WebSocket 回顧

     WebSocket 的出現是基於 Web 應用的實時性需要而產生的,在淘寶、京東等網頁客服、網頁賣家聊天等需求上應用廣泛。對於前端網頁可以使用 H5 開發 WebSocket 客戶端,也可以使用 SockJS 庫開發 WebSocket 客戶端。

     對於Java 開發者而言,後臺 WebSocket 服務端開發通常有以下常用的選擇:

Tomcat7 以後開始支援 websocket 協議

Spring4 以後開始支援 WebSocket

Netty3 以後支援開發 WebSocket

     Tomcat8 如下所示,自身已經支援 WebSocket 服務端開發,它的 lib 目錄下有自己實現 WebSocket 協議的開發包,如果是傳統的 Java Web 專案,則只需要將 tomcat-websocket.jar、websocket-api.jar 匯入應用中即可進行程式碼開發。

     Tomcat 自身也提供了 WebSocket 開發的示例,在 webapps/exampls下。

      一共提供了 4 個示例,可以啟動 Tomcat 進行訪問測試以及學習。

     對於傳統 Java Web 導包式應用開發,這裡不再過多進行說明,它的基本流程是:

1)新建 Java Web 應用後,匯入 Tomcat 伺服器 lib 目錄下的 websocket-api.jar 、tomcat-websocket.jar開發包,前者是瀏覽器 webSocket 規範的介面,後者是 Tomcat 對它的實現

2)建立後臺 webSocket 服務端類,標識 @ServerEndpoint( javax.websocket.server.ServerEndpoint)註解,表示當前類是 webSocket 服務終端,同時在裡面實現客戶端連線建立、傳送訊息、接收訊息等通訊業務。

2)自己實現 javax.websocket.server.ServerApplicationConfig 介面,掃描整個應用所有的 @ServerEndpoint 服務終端

SpringBoot 整合 Tomcat 的 WebSocket 開發

     本文的重點是 SpringBoot 專案如何再使用 Tomcat 的 webSocket 服務端開發。

    本文使用的是 springboot 2.1.0 版本

     既然是使用 Tomcat 的 webSocket ,所以應該參加 web 專案,而且要麼使用 SpringBoot 預設內建的 Tomcat 伺服器,要麼就是自己是有外接的 Servelt 容器(Tomcat)。專案結構如下:

pom.xml 引入 springboot websocket 元件

     預設情況 springboot 建立的 web 應用,在 web 啟動模組是已經依賴了 tomcat-websocket 模組的,所以不再需要自己在 pom.xml 檔案中重複匯入。

      但是必須匯入 springboot 的 websocket 啟動模組:

        <!-- Spring boot WebSocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

@ServerEndpoint 建立 websocket 服務終端

     建立後臺 webSocket 服務端類,標識 @ServerEndpoint( javax.websocket.server.ServerEndpoint)註解,表示當前類是 webSocket 服務終端,同時在裡面實現客戶端連線建立、傳送訊息、接收訊息等通訊業務。

     這與傳統導包式開發 Tomcat WebSocket 服務端是一樣的,區別就是:傳統方式 @ServerEndpoint 類上不需要加 @Component 交由 Spring 管理,而現在需要加上 @Component 將此元件交由 spring 管理。

package com.example.demo.tomcatWebSocket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * Created by Administrator on 2018/11/28 0028.
 * @ServerEndpoint :標識此類為 Tomcat 的 websocket 服務終端,/websocket/yy.action 是客戶端連線請求的路徑
 * @Component :將本類交由 spring IOC 容器管理
 */
@ServerEndpoint(value = "/websocket/yy.action")
@Component
public class ServerEnpoint {

    private static Logger logger = LoggerFactory.getLogger(ServerEnpoint.class);

    /**
     * 用 Set 來 儲存 客戶端 連線
     */
    private static Set<Session> sessionSet = new HashSet<>();

    /**
     * 連線成功後自動觸發
     *
     * @param session
     */
    @OnOpen
    public void afterConnectionEstablished(Session session) {
        /**
         * session 表示一個連線會話,整個連線會話過程中它都是固定的,每個不同的連線 session 不同
         * String queryString = session.getQueryString();//獲取請求地址中的查詢字串
         * Map<String, List<String>> parameterMap = session.getRequestParameterMap();//獲取請求地址中引數
         * Map<String, String> stringMap = session.getPathParameters();
         * URI uri = session.getRequestURI();
         */
        sessionSet.add(session);
        logger.info("新客戶端加入,session id=" + session.getId() + ",當前客戶端格個數為:" + sessionSet.size());

        /**
         * session.getBasicRemote().sendText(textMessage);同步傳送
         * session.getAsyncRemote().sendText(textMessage);非同步傳送
         */
        session.getAsyncRemote().sendText("我是伺服器,你連線成功!");
    }

    /**
     * 連線斷開後自動觸發,連線斷開後,應該清楚掉 session 集合中的值
     *
     * @param session
     */
    @OnClose
    public void afterConnectionClosed(Session session) {
        sessionSet.remove(session);
        logger.info("客戶端斷開,session id=" + session.getId() + ",當前客戶端格個數為:" + sessionSet.size());
    }

    /**
     * 收到客戶端訊息後自動觸發
     *
     * @param session
     * @param textMessage :客戶端傳來的文字訊息
     */
    @OnMessage
    public void handleMessage(Session session, String textMessage) {
        try {
            logger.info("接收到客戶端資訊,session id=" + session.getId() + ":" + textMessage);
            /**
             * 原樣回覆文字訊息
             * getBasicRemote:同步傳送
             * session.getAsyncRemote().sendText(textMessage);非同步傳送
             * */
            session.getBasicRemote().sendText(textMessage);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 訊息傳輸錯誤後
     *
     * @param session
     * @param throwable
     */
    @OnError
    public void handleTransportError(Session session, Throwable throwable) {
        System.out.println("shake client And server handleTransportError,session.getId()=" + session.getId() + " -- " + throwable.getMessage());
        logger.error("與客戶端 session id=" + session.getId() + " 通訊錯誤...");
    }
}

注入 ServerEndpointExporter

     注入 org.springframework.web.socket.server.standard.ServerEndpointExporter,這個 bean 會自動註冊使用了@ServerEndpoint 註解宣告的 Websocket endpoint 。

     如果使用獨立的 servlet 容器,而不是使用 springboot 的內建容器,就不要注入ServerEndpointExporter,因為它將由 Tomcat 容器自己提供和管理。

       因為傳統導包式 Tomcat websocket 開發時,是需要實現 javax.websocket.server.ServerApplicationConfig 介面的,然後由它去掃描整個應用中的 @ServerEndpoint,而現在這一步就由 springboot 的 ServerEndpointExporter 取代了。

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * Created by Administrator on 2018/11/28 0028.
 */
@Configuration
public class WebSocketConfig {
    /**
     * 建立 ServerEndpointExporter 元件,交由 spring IOC 容器管理,
     * 它會自動掃描註冊應用中所有的 @ServerEndpoint
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

前端 H5 webSocket 客戶端

     前端 webSocket 客戶端為了方便直接使用 H5 的 webSocket 方式,不清楚的可以參考《HTML5 WebSocket》。

頁面的 html 與 css 樣式就不做提供了,直接提供客戶端 webSocket 的 js 程式碼。

/**
 * web socket 繫結
 */
var ws = null;
function webSocketBind() {
    /**主流瀏覽器現在都支援 H5 d的 webSocket 通訊,但建議還是要判斷*/
    if ("WebSocket" in window) {
        /**建立 web socket 例項
         * 如果連線失敗,瀏覽器控制檯報錯,連線失敗
         * 字首 ws:// 必須正確,yyServer 是應用名稱,websocket/yy.action 是後臺訪問路徑
         * 192.168.1.20:websocket 伺服器地址
         * */
        ws = new WebSocket("ws://192.168.1.20/yyServer/websocket/yy.action");

        /**onopen:伺服器連線成功後,自動觸發*/
        ws.onopen = function () {
            /** Web Socket 已連線上,使用 send() 方法傳送資料*/
                //ws.send("connect success...");
            console.log("伺服器連線成功,併發送資料到後臺...");
        };

        /**伺服器傳送資料後,自動觸發此方法,客戶端進行獲取資料,使用 evt.data 獲取資料*/
        ws.onmessage = function (evt) {
            var received_msg = evt.data;
            console.log("接收到伺服器資料:" + received_msg);
            showClientMessage(received_msg);
        };

        /**客戶端與伺服器資料傳輸錯誤時觸發*/
        ws.onerror = function (evt) {
            console.log("客戶端 與 伺服器 資料傳輸錯誤...");
        };

        /**web Socket 連線關閉時觸發*/
        ws.onclose = function () {
            console.log("web scoket 連線關閉...");
        };
    } else {
        alert("您的瀏覽器不支援 WebSocket!");
    }
}

/**
 * 顯示伺服器傳送的訊息
 * @param message
 */
let showServerMessage = function (message) {
    if (message != undefined && message.trim() != "") {
        /**
         * 往伺服器傳送訊息
         */
        ws.send(message.trim());
        /**
         * scrollHeight:div 區域內文件的高度,只能 DOM 操作,JQuery 沒有提供相應的方法
         * @type {string}
         */
        let messageShow = "<div class='messageLine server'><div class='messageContent serverCon'>" + message + "</div><span>:我</span>";
        $(".centerTop").append(messageShow + "<br>");
        $(".messageArea").val("");

        let scrollHeight = $(".centerTop")[0].scrollHeight;
        $(".centerTop").scrollTop(scrollHeight - $(".centerTop").height());
    }
};

/**
 * 顯示客戶端的訊息
 * @param message
 */
let showClientMessage = function (message) {
    if (message != undefined && message.trim() != "") {
        /**
         * scrollHeight:div 區域內文件的高度,只能 DOM 操作,JQuery 沒有提供相應的方法
         * @type {string}
         */
        let messageShow = "<div class='messageLine client'><span>伺服器:</span><div class='messageContent clientCon'>" + message + "</div>";
        $(".centerTop").append(messageShow + "<br>");
        $(".messageArea").val("");

        let scrollHeight = $(".centerTop")[0].scrollHeight;
        $(".centerTop").scrollTop(scrollHeight - $(".centerTop").height());
    }
};

$(function () {
    /**初始化後清空訊息傳送區域*/
    $(".messageArea").val("");

    /**
     * 為 訊息 傳送按鈕繫結事件
     */
    $(".sendButton").click(function () {
        let message = $(".messageArea").val();
        showServerMessage(message);
    });

    /**
     * 繫結鍵盤敲擊事件 —— 用於按 回車鍵 傳送訊息
     */
    $(window).keydown(function (event) {
        if (event.keyCode === 13) {
            let message = $(".messageArea").val();
            showServerMessage(message);
        }
    });
    /**
     * 繫結 webSocket,連線 伺服器
     */
    webSocketBind();
});

執行測試