首頁 > 軟體

SpringBoot+WebSocket實現即時通訊的方法詳解

2022-05-19 19:02:12

環境資訊

名稱版本號
Spring Boot2.4.5
Idea2021.3.2

伺服器端實現

匯入依賴

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

注意:Spring Boot在父工程中已經管理了websocket的版本資訊,所以不用指定版本號也是可以的

建立設定類

package com.fenzhichuanmei.configuration;

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

/**
 * @author Yi Dai 484201132@qq.com
 * @since 2022/5/13 11:34
 */

@Configuration
public class WebsocketConfiguration {

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

}

建立此設定類的目的只是為了把ServerEndpointExporter 這個類的範例交給spring 容器進行管理,您可以用任意一種方式交給容器,如使用@Import(ServerEndpointExporter.class)這種方式等進行操作;此處只是我的編碼風格如此;並非必須這樣操作

建立一個註解式的端點並在其中通過配套註解宣告回撥方法

package com.fenzhichuanmei.websocket;

import com.fenzhichuanmei.websocket.utils.SessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

/**
 * @author Yi Dai 484201132@qq.com
 * @since 2022/3/7 15:47
 */

@Slf4j
@Component
@ServerEndpoint("/arcticFoxServerEndpoint/{websocketClientType}")
public class ArcticFoxServerEndpoint {

    private static SessionManager sessionManager;

    @Resource
    public void setProcessor(SessionManager sessionManager) {
        ArcticFoxServerEndpoint.sessionManager = sessionManager;
    }

    /**
     * 建立連線成功的回撥方法
     *
     * @param session             對談物件
     * @param websocketClientType 此引數就是路徑中{websocketClientType}位置傳入的引數
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("websocketClientType") int websocketClientType) {
        sessionManager.onOpen(session, websocketClientType);
    }

    /**
     * 當對談關閉時執行的回撥方法
     *
     * @param session             對談物件
     * @param websocketClientType 此引數就是路徑中{websocketClientType}位置傳入的引數
     */
    @OnClose
    public void onClose(Session session, @PathParam("websocketClientType") int websocketClientType) {
        sessionManager.onClose(session, websocketClientType);
    }

    /**
     * 當收到使用者端資訊時執行的回撥方法
     *
     * @param session             對談物件
     * @param message             使用者端傳遞過來的資訊
     * @param websocketClientType 此引數就是路徑中{websocketClientType}位置傳入的引數
     */
    @OnMessage
    public void onMessage(Session session, String message, @PathParam("websocketClientType") int websocketClientType) {
        sessionManager.onMessage(session, message, websocketClientType);
    }

    /**
     * 當發生錯誤時的回撥方法
     *
     * @param session             對談物件
     * @param e                   異常物件
     * @param websocketClientType 此引數就是路徑中{websocketClientType}位置傳入的引數
     */
    @OnError
    public void onError(Session session, Throwable e, @PathParam("websocketClientType") int websocketClientType) {
        sessionManager.onError(session, e, websocketClientType);
    }

}

@ServerEndpoint註解標註此類為一個伺服器端的端點類,此註解有一個必須的引數,用於指定使用者端存取的地址,本案例中為:/arcticFoxServerEndpoint,而路徑後面的/{websocketClientType}這個是路徑中引數的預留位置,有點類似與Spring Mvc中Rest介面和@PathVariable註解的作用

注意事項: 一定要將此類交給spring 容器進行管理!!還有一個坑就是,此類的範例時非單例的,所以如果要在此類中注入其他的bean,不能使直接在屬性上使用@Resource註解或者@Autowired等註解進行注入,否則會報錯。正確操作應該是把要注入的欄位設定為靜態的,然後通過非靜態的set方法進行注入,具體程式碼請看上方範例

伺服器端主動傳送訊息給使用者端

通過上面的程式碼我們可以知道每個回撥方法中都會收到一個Session物件,正如您所想,要向用戶端傳送訊息正是要藉助此物件;Session物件有一個getAsyncRemote方法,呼叫此方法可以得到一個RemoteEndpoint.Async物件,檢視此物件,發現有很多send打頭的方法;

是的,這些方法就是傳送訊息的方法,博主這個專案中主要是通過JSON來進行互動的,所以我使用了sendText方法,範例程式碼:

RemoteEndpoint.Async asyncRemote = session.getAsyncRemote();
asyncRemote.sendText(jsonString);

很顯然中轉變數asyncRemote 沒什麼太大的用處,不如直接寫成:

session.getAsyncRemote().sendText(jsonString);

通過方法名看到,似乎還可以傳送物件,二進位制序列等,博主沒有深入研究,有興趣的小夥伴可以嘗試嘗試

使用者端實現

一般來講使用者端應該是用Java Script實現,但是博主這個專案比較特殊,需要用Java來實現使用者端,下面博主先以Java使用者端說明其實現細節,然後再說再前端如何實現

Java使用者端實現

匯入依賴

<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.5.3</version>
</dependency>

其實Java中實現WebSocket的第三方包還有很多,博主這個地方使用的是Java-WebSocket,有興趣的小夥伴可以試試其他的包

建立連線和處理回撥

package com.fenzhichuanmei.websocket;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fenzhichuanmei.components.PaymentComponent;
import com.fenzhichuanmei.pojo.Instructions;
import com.fenzhichuanmei.utils.WebsocketClientType;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.enums.ReadyState;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * @author Yi Dai 484201132@qq.com
 * @since 2022/5/13 10:16
 */

@Slf4j
@Component
public class ArcticFoxWebSocketClient {

    @Resource
    private ObjectMapper objectMapper;

    @Resource
    private PaymentComponent paymentComponent;

    @Resource
    private ArcticFoxWebSocketClientProperties properties;

    public void establishConnection() throws URISyntaxException {
        WebSocketClient webSocketClient = new WebSocketClient(new URI(String.format("%s/%d", properties.getWebSocketServerUrl(), WebsocketClientType.PAYMENT_DEVICE))) {
            @Override
            public void onOpen(ServerHandshake serverHandshake) {
                log.info("WebSocketClient: onOpen : {}", serverHandshake);
            }

            @Override
            public void onMessage(String jsonString) {
                try {
                    Instructions instructions = objectMapper.readValue(jsonString, Instructions.class);
                    if (instructions.getType() == Instructions.NOTICE_PAYMENT) {
                        paymentComponent.queryAnUnpaidOrdersAndPay();
                    } else {
                        throw new RuntimeException("錯誤的指令型別");
                    }
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onClose(int i, String s, boolean b) {
                log.info("WebSocketClient: onClose : i:{},s:{},b:{}", i, s, b);
                try {
                    Thread.sleep(1000 * 20);
                    establishConnection();
                } catch (InterruptedException | URISyntaxException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onError(Exception e) {
                log.error("WebSocketClient: onError {}", e.getMessage());
            }
        };
        webSocketClient.connect();
        while (!(webSocketClient.getReadyState() == ReadyState.OPEN)) {
            try {
                Thread.sleep(1000 * 2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.info("WebSocketClient: connection established successfully");
    }


    @Data
    @Component
    @ConfigurationProperties("arctic-fox-web-socket-client.properties")
    public static class ArcticFoxWebSocketClientProperties {

        private String webSocketServerUrl;

    }
}

程式碼解釋: 其實我的establishConnection方法中上來就範例化了一個WebSocketClient 類的範例,請注意,此類是個抽象類,我在這裡用匿名實現類的方式實現的,此類有幾個抽象方法需要實現,也就是onOpen,onMessage,onClose,onError四個方法,其作用其實已經是很見名知意了,和伺服器端的回撥方法一樣,就不過多解釋;範例化此類需要傳入一個URI物件,這個URI物件其實就是封裝了對伺服器端連線的地址,由於博主不希望把伺服器端的地址給寫死了,所以我設定到了組態檔中,然後通過String.format靜態方法配合預留位置拼接url地址和引數;路徑的規則是:協定名://IP地址(或域名):埠號/伺服器端宣告的地址/引數;舉個例子:

ws://192.168.88.88:8080/arcticFoxServerEndpoint/1

ws://localhost:8080/arcticFoxServerEndpoint/2

ws://為協定;範例化WebSocketClient 類的範例之後,呼叫其connect()方法即開始建立連線,呼叫getReadyState()方法可以獲得其狀態;由於我的伺服器端可能隨時都連不上,所以我在使用者端的onClose回撥函數中進行了一個遞迴(20秒後),用於重新連線。

使用者端向伺服器端傳送訊息

通過WebSocketClient 類的範例,我們可以看到有以下方法,很明顯send方法就是用來傳送訊息使用的

範例程式碼:

//判斷一下是否為空
if (Objects.nonNull(webSocketClient)) {
    try {
    	//通過jackson將物件轉換為json字串(非必須)
        String jsonString = objectMapper.writeValueAsString(feedback);
        //傳送資訊
        webSocketClient.send(jsonString);
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }
} else {
    log.warn("no connection established");
}

在前端環境(vue)中使用websocket

安裝reconnecting-websocket包(非必須)

npm i --save reconnecting-websocket

安裝這個包是為了websocket能在斷線之後重新連線,其實不使用這個包也是可以用原生Java Script實現的;但是他和原生的api幾乎一樣;

範例程式碼:

import ReconnectingWebSocket from "reconnecting-websocket";

export default function initializationWebsocket() {
    let reconnectingWebSocket = new ReconnectingWebSocket(`ws://localhost:8080/arcticFoxServerEndpoint/${2}`);
    reconnectingWebSocket.onopen = event => {
        console.log("on open :", event);
    };

    reconnectingWebSocket.onmessage = event => {
    	//event物件中data儲存的就是伺服器端傳送過來的訊息
        let parse = JSON.parse(event.data);
        console.log("webSocket on message :", parse);
    };

    reconnectingWebSocket.onclose = event => {
        console.log(event);
    };

    reconnectingWebSocket.onerror = event => {
        console.log(event);
    };
	
	//視窗關閉時斷開連線
    window.onbeforeunload = function () {
        reconnectingWebSocket.close();
    }
}

在前端中實現websocket就比較簡單了,就上面的幾行程式碼即可,不用呼叫其他函數進行連線,範例化之後就開始連線了

想伺服器端傳送資訊

在前端中傳送資訊就更簡單了,直接呼叫reconnectingWebSocketsend方法,傳入要傳送的資料即可

到此這篇關於SpringBoot+WebSocket實現即時通訊的方法詳解的文章就介紹到這了,更多相關SpringBoot WebSocket即時通訊內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com