SpringBoot2 整合 WebSocket 簡單實現聊天室功能

DoubleFJ發表於2018-09-11

一個很簡單的 Demo,可以用 WebSocket 實現簡易的聊天室功能
個人部落格 : DoubleFJ の Blog

一反常態,我們先來看一下效果,如下:

嫌麻煩的可以直接去我的 GitHub 獲取完整無碼 Demo。

概述

  • WebSocket 是什麼?

WebSocket 是一種網路通訊協議。RFC6455 定義了它的通訊標準。

WebSocket 是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議。

  • 為什麼需要 WebSocket ?

瞭解計算機網路協議的人,應該都知道:HTTP 協議是一種無狀態的、無連線的、單向的應用層協議。它採用了請求/響應模型。通訊請求只能由客戶端發起,服務端對請求做出應答處理。

這種通訊模型有一個弊端:HTTP 協議無法實現伺服器主動向客戶端發起訊息。

這種單向請求的特點,註定瞭如果伺服器有連續的狀態變化,客戶端要獲知就非常麻煩。大多數 Web 應用程式將通過頻繁的非同步 JavaScript 和 XML(AJAX)請求實現長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連線,或者 HTTP 連線始終開啟)。

  • WebSocket 如何工作?

Web瀏覽器和伺服器都必須實現 WebSockets 協議來建立和維護連線。由於 WebSockets 連線長期存在,與典型的 HTTP 連線不同,對伺服器有重要的影響。

基於多執行緒或多程式的伺服器無法適用於 WebSockets,因為它旨在開啟連線,儘可能快地處理請求,然後關閉連線。任何實際的 WebSockets 伺服器端實現都需要一個非同步伺服器。

實現

首先去 start.spring.io 快速下載一個 springboot Demo,記得選中 Websocket 依賴。

然後將專案匯入你的 IDE 中。

新建一個 config 類用來註冊我們的 websocket bean。

我的是 WebSocketConfig.java :

@Configuration
public class WebSocketConfig {

    /**
     * 自動註冊使用了@ServerEndpoint註解宣告的Websocket endpoint
     * 
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

該加上的註解別忘了加,專案啟動時 springboot 會自動去掃描註解的類。

然後是訊息接收處理 websocket 連線、關閉等鉤子。

MyWebSocket.java :

@ServerEndpoint(value = "/websocket")
@Component
public class MyWebSocket {

    // 靜態變數,用來記錄當前線上連線數。應該把它設計成執行緒安全的。
    private static int onlineCount = 0;

    // concurrent包的執行緒安全Set,用來存放每個客戶端對應的MyWebSocket物件。
    private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();

    // 與某個客戶端的連線會話,需要通過它來給客戶端傳送資料
    private Session session;

    /**
     * 連線建立成功呼叫的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this); // 加入set中
        addOnlineCount(); // 線上數加1
        System.out.println("有新連線加入!當前線上人數為 : " + getOnlineCount());
        try {
            sendMessage("您已成功連線!");
        } catch (IOException e) {
            System.out.println("IO異常");
        }
    }

    /**
     * 連線關閉呼叫的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this); // 從set中刪除
        subOnlineCount(); // 線上數減1
        System.out.println("有一連線關閉!當前線上人數為 : " + getOnlineCount());
    }

    /**
     * 收到客戶端訊息後呼叫的方法
     *
     * @param message
     *            客戶端傳送過來的訊息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("來自客戶端的訊息:" + message);

        // 群發訊息
        for (MyWebSocket item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 發生錯誤時呼叫
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("發生錯誤");
        error.printStackTrace();
    }

    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
        // this.session.getAsyncRemote().sendText(message);
    }

    /**
     * 群發自定義訊息
     */
    public static void sendInfo(String message) throws IOException {
        for (MyWebSocket item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                continue;
            }
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        MyWebSocket.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        MyWebSocket.onlineCount--;
    }
}

關鍵就是@OnOpen@OnClose等這幾個註解了。每個物件有著各自的 session,其中可以存放個人資訊。當收到一個客戶端訊息時,往所有維護著的物件迴圈 send 了訊息,這就簡單實現了聊天室的聊天功能了。

其中 websocket session 傳送文字訊息有兩個方法:getAsyncRemote()和 getBasicRemote()。 getAsyncRemote 是非阻塞式的,getBasicRemote 是阻塞式的。

然後我用了 Controller 來簡單跳轉測試頁面,也可以直接訪問頁面。

InitController.java :

@Controller
public class InitController {

    @RequestMapping("/websocket")
    public String init() {
        return "websocket.html";
    }

}

websocket.html :

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My WebSocket Test</title>
</head>
<body>

Welcome<br/>
<input id="text" type="text" />
<button onclick="send()">Send</button>
<button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>

</body>

<script type="text/javascript">

    var websocket = null;

    //判斷當前瀏覽器是否支援WebSocket
    if('WebSocket' in window){
        websocket = new WebSocket("ws://localhost:8080/websocket");
    }
    else{
        alert('Not support websocket')
    }

    //連線發生錯誤的回撥方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //連線成功建立的回撥方法
    websocket.onopen = function(event){
        setMessageInnerHTML("open");
    }

    //接收到訊息的回撥方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //連線關閉的回撥方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //監聽視窗關閉事件,當視窗關閉時,主動去關閉websocket連線,防止連線還沒斷開就關閉視窗,server端會拋異常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //將訊息顯示在網頁上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //關閉連線
    function closeWebSocket(){
        websocket.close();
    }

    //傳送訊息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>
</html>

要注意,這裡沒有用到任何模板引擎,所有直接把 websocket.html 放在 static 資料夾下就可以訪問了。

所有的這些搞好就可以執行了,一個簡單的效果就能出來。

End.

參考

相關文章