[超詳細]SpringBoot整合WebSocket

2023-08-28 18:00:59

1. 什麼是WebSocket?

WebSocket 是一種在單個 TCP 連線上進行全雙工通訊的協定,它允許在瀏覽器和伺服器之間進行實時的、雙向的通訊。相對於傳統的基於請求和響應的 HTTP 協定,WebSocket 提供了一種更有效、更實時的通訊方式,適用於需要實時更新、實時通知和實時互動的應用。

WebSocket 的一些關鍵特點包括:

  1. 全雙工通訊: WebSocket 允許伺服器和使用者端在同一連線上同時進行雙向通訊。這意味著伺服器可以隨時向用戶端推播資料,而不必等待使用者端傳送請求。
  2. 持久連線: WebSocket 連線一旦建立,會一直保持開啟狀態,不會像傳統的 HTTP 連線那樣在每次請求和響應之後關閉。這減少了每次連線建立和關閉的開銷,使通訊更加高效。
  3. 低延遲: 由於連線保持開啟狀態,WebSocket 通訊具有較低的延遲,適用於實時性要求較高的應用,如實時聊天、實時資料更新等。
  4. 少量的資料交換: 與 HTTP 請求和響應相比,WebSocket 資料交換的開銷較小。WebSocket 的幀頭相對較小,因此有效載荷的比例更高。
  5. 相容性: 現代瀏覽器和大多數伺服器支援 WebSocket。此外,WebSocket 協定還定義了一個子協定 STOMP(Streaming Text Oriented Messaging Protocol),用於更高階的訊息傳遞和訂閱功能。
  6. 安全性: 與其他網路通訊協定一樣,WebSocket 通訊也需要一些安全性的考慮。可以使用加密協定(如 TLS)來保護資料在網路傳輸中的安全性。

2. 程式碼實戰

2.1 SpringBoot匯入依賴

在pom.xml中匯入以下依賴,版本由SpringBoot管理

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

2.2 建立設定類

建立WebSocketConfig設定類,並將其注入到Bean容器中

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

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

2.3 建立WebSocketServer類

建立WebSocketServer類,並將其注入到Bean容器中

注意:@ServerEndpoint("/WebSocket"),該註解用於設定建立WebSocket連線的路徑,可以按需修改。

該類一般擁有以下功能:

  1. WebSocket 端點註冊: WebSocket 伺服器需要註冊一個或多個 WebSocket 端點(Endpoints)。每個端點對應一種處理邏輯,可以處理使用者端傳送過來的訊息,以及向用戶端傳送訊息。這些端點通過註解或設定來定義。
  2. 建立和維護連線: WebSocket 伺服器負責監聽使用者端的連線請求,一旦有使用者端連線,伺服器會建立一個 WebSocket 對談(Session)來管理這個連線。伺服器需要能夠維護這些連線,包括開啟、關閉、保持心跳等操作。
  3. 訊息處理: 一旦使用者端連線成功,WebSocket 伺服器需要處理使用者端傳送過來的訊息。這可以在 WebSocket 端點中的方法上定義處理邏輯。伺服器可以根據不同的業務需求處理不同型別的訊息。
  4. 處理異常: 與任何網路通訊一樣,WebSocket 連線可能會面臨各種異常情況,如斷開連線、網路問題等。WebSocket 伺服器需要能夠處理這些異常情況,進行適當的清理和處理。

可以將該類理解為WebSocket生命週期中會呼叫的方法。

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

@Slf4j
@Component
@ServerEndpoint("/WebSocket")
public class WebSocketServer {

    private Session session;

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        WebSocketManager.sentToUser(session, "WebSocket is connected!");
        WebSocketManager.addWebSocketServer(this);
        log.info("與SessionId:{}建立連線", session.getId());
    }

    @OnClose
    public void onClose() {
        WebSocketManager.removeWebSocketServer(this);
        log.info("WebSocket連線關閉");
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("來自SessionId:{}的訊息:{}", session.getId(), message);
    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.error("Session:{}的WebSocket發生錯誤", session.getId(), error);
    }

    public Session getSession() {
        return session;
    }

    public String getSessionId() {
        return session.getId();
    }
}

2.4 建立WebSocketServer管理類

該類用於管理WebSocketServer(其實主要是管理Session),如果不需要傳送訊息給特定使用者,那麼無需建立該類,在WebSocketServer類中維護一個類變數即可。

import lombok.extern.slf4j.Slf4j;

import javax.websocket.Session;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

@Slf4j
public class WebSocketManager {

    private final static CopyOnWriteArraySet<WebSocketServer> webSocketServerSet = new CopyOnWriteArraySet<>();

    private final static ConcurrentHashMap<String, WebSocketServer> webSocketServerMap = new ConcurrentHashMap<>();

    public static void addWebSocketServer(WebSocketServer webSocketServer){
        if (webSocketServer != null){
            webSocketServerSet.add(webSocketServer);
            webSocketServerMap.put(webSocketServer.getSessionId(), webSocketServer);
        }
    }

    public static void removeWebSocketServer(WebSocketServer webSocketServer){
        webSocketServerSet.remove(webSocketServer);
        webSocketServerMap.remove(webSocketServer.getSessionId());
    }

    /**
     * 通過SessionId傳送訊息給特定使用者
     * @param
     * @param msg
     */
    public static void sentToUser(String sessionId, String msg){
        Session session = webSocketServerMap.get(sessionId).getSession();
        sentToUser(session, msg);
    }

    /**
     * 通過Session傳送訊息給特定使用者
     * @param session
     * @param msg
     */
    public static void sentToUser(Session session, String msg){
        if (session == null){
            log.error("不存在該Session,無法傳送訊息");
            return;
        }
        session.getAsyncRemote().sendText(msg);
    }

    /**
     * 傳送訊息給所有使用者
     * @param msg
     */
    public static void sentToAllUser(String msg){
        for (WebSocketServer webSocketServer : webSocketServerSet) {
            sentToUser(webSocketServer.getSession(), msg);
        }
        log.info("向所有使用者傳送WebSocket訊息完畢,訊息:{}", msg);
    }
}

3. 測試

使用Postman等工具進行WebSocket連線,連線路徑為WebSocket(與前文的@ServerEndpoint內容一致)。建立連線後會收到"WebSocket is connected!"

4. 後續完善

如果你的專案中有登入功能,那麼應該在你的登入時將使用者的Session記錄下來。你可以將Session記錄在本地快取、Redis和Grava快取等能夠建立關聯關係的地方。