SpringBoot進階教程(七十七)WebSocket

来源:https://www.cnblogs.com/toutou/archive/2023/10/01/springboot_websocket.html
-Advertisement-
Play Games

WebSocket是一種在單個TCP連接上進行全雙工通信的協議。WebSocket使得客戶端和伺服器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,併進行雙向數據傳輸。 ...


WebSocket是一種在單個TCP連接上進行全雙工通信的協議。WebSocket使得客戶端和伺服器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,併進行雙向數據傳輸。

v原理

很多網站為了實現推送技術,所用的技術都是輪詢。輪詢是在特定的時間間隔(如每1秒),由瀏覽器對伺服器發出HTTP請求,然後由伺服器返回最新的數據給客戶端的瀏覽器。這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向伺服器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。

而比較新的技術去做輪詢的效果是Comet。這種技術雖然可以雙向通信,但依然需要反覆發出請求。而且在Comet中,普遍採用的長鏈接,也會消耗伺服器資源。

在這種情況下,HTML5定義了WebSocket協議,能更好的節省伺服器資源和帶寬,並且能夠更實時地進行通訊。

v架構搭建

添加maven引用

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

配置應用屬性

server.port=8300
spring.thymeleaf.mode=HTML
spring.thymeleaf.cache=true
spring.thymeleaf.prefix=classpath:/web/
spring.thymeleaf.encoding: UTF-8
spring.thymeleaf.suffix: .html
spring.thymeleaf.check-template-location: true
spring.thymeleaf.template-resolver-order: 1

添加WebSocketConfig

package com.test.config;

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

/**
 * @Author chen bo
 * @Date 2023/10
 * @Des
 */
@Configuration
public class WebSocketConfig {
    /**
     * bean註冊:會自動掃描帶有@ServerEndpoint註解聲明的Websocket Endpoint(端點),註冊成為Websocket bean。
     * 要註意,如果項目使用外置的servlet容器,而不是直接使用springboot內置容器的話,就不要註入ServerEndpointExporter,因為它將由容器自己提供和管理。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

添加WebSocket核心類

因為WebSocket是類似客戶端服務端的形式(採用ws協議),那麼這裡的WebSocketServer其實就相當於一個ws協議的Controller

直接@ServerEndpoint("/imserver/{userId}")@Component啟用即可,然後在裡面實現@OnOpen開啟連接,@onClose關閉連接,@onMessage接收消息等方法。

新建一個ConcurrentHashMap用於接收當前userId的WebSocket或者Session信息,方便IM之間對userId進行推送消息。單機版實現到這裡就可以。集群版(多個ws節點)還需要藉助 MySQL或者 Redis等進行訂閱廣播方式處理,改造對應的 sendMessage方法即可。

package com.test.util;

import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import com.google.gson.JsonObject;

/**
 * WebSocket的操作類
 * html頁面與之關聯的介面
 * var reqUrl = "http://localhost:8300/websocket/" + cid;
 * socket = new WebSocket(reqUrl.replace("http", "ws"));
 */
@Component
@Slf4j
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {

    /**
     * 靜態變數,用來記錄當前線上連接數,線程安全的類。
     */
    private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0);

    /**
     * 存放所有線上的客戶端
     */
    private static Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();

    /**
     * 連接sid和連接會話
     */
    private String sid;
    private Session session;

    /**
     * 連接建立成功調用的方法。由前端<code>new WebSocket</code>觸發
     *
     * @param sid     每次頁面建立連接時傳入到服務端的id,比如用戶id等。可以自定義。
     * @param session 與某個客戶端的連接會話,需要通過它來給客戶端發送消息
     */
    @OnOpen
    public void onOpen(@PathParam("sid") String sid, Session session) {
        /**
         * session.getId():當前session會話會自動生成一個id,從0開始累加的。
         */
        log.info("連接建立中 ==> session_id = {}, sid = {}", session.getId(), sid);
        //加入 Map中。將頁面的sid和session綁定或者session.getId()與session
        //onlineSessionIdClientMap.put(session.getId(), session);
        onlineSessionClientMap.put(sid, session);

        //線上數加1
        onlineSessionClientCount.incrementAndGet();
        this.sid = sid;
        this.session = session;
        sendToOne(sid, "上線了");
        log.info("連接建立成功,當前線上數為:{} ==> 開始監聽新連接:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
    }

    /**
     * 連接關閉調用的方法。由前端<code>socket.close()</code>觸發
     *
     * @param sid
     * @param session
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid, Session session) {
        //onlineSessionIdClientMap.remove(session.getId());
        // 從 Map中移除
        onlineSessionClientMap.remove(sid);

        //線上數減1
        onlineSessionClientCount.decrementAndGet();
        log.info("連接關閉成功,當前線上數為:{} ==> 關閉該連接信息:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), sid);
    }

    /**
     * 收到客戶端消息後調用的方法。由前端<code>socket.send</code>觸發
     * * 當服務端執行toSession.getAsyncRemote().sendText(xxx)後,前端的socket.onmessage得到監聽。
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        /**
         * html界面傳遞來得數據格式,可以自定義.
         * {"sid":"user","message":"hello websocket"}
         */
        JsonObject jsonObject = JsonParser.parseString(message).getAsJsonObject();
        String toSid = jsonObject.get("sid").getAsString();
        String msg = jsonObject.get("message").getAsString();
        log.info("服務端收到客戶端消息 ==> fromSid = {}, toSid = {}, message = {}", sid, toSid, message);

        /**
         * 模擬約定:如果未指定sid信息,則群發,否則就單獨發送
         */
        if (toSid == null || toSid == "" || "".equalsIgnoreCase(toSid)) {
            sendToAll(msg);
        } else {
            sendToOne(toSid, msg);
        }
    }

    /**
     * 發生錯誤調用的方法
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket發生錯誤,錯誤信息為:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 群發消息
     *
     * @param message 消息
     */
    private void sendToAll(String message) {
        // 遍歷線上map集合
        onlineSessionClientMap.forEach((onlineSid, toSession) -> {
            // 排除掉自己
            if (!sid.equalsIgnoreCase(onlineSid)) {
                log.info("服務端給客戶端群發消息 ==> sid = {}, toSid = {}, message = {}", sid, onlineSid, message);
                toSession.getAsyncRemote().sendText(message);
            }
        });
    }

    /**
     * 指定發送消息
     *
     * @param toSid
     * @param message
     */
    private void sendToOne(String toSid, String message) {
        // 通過sid查詢map中是否存在
        Session toSession = onlineSessionClientMap.get(toSid);
        if (toSession == null) {
            log.error("服務端給客戶端發送消息 ==> toSid = {} 不存在, message = {}", toSid, message);
            return;
        }
        // 非同步發送
        log.info("服務端給客戶端發送消息 ==> toSid = {}, message = {}", toSid, message);
        toSession.getAsyncRemote().sendText(message);
        /*
        // 同步發送
        try {
            toSession.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("發送消息失敗,WebSocket IO異常");
            e.printStackTrace();
        }*/
    }

}

添加controller

package com.test.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;

/**
 * @Author chen bo
 * @Date 2023/10
 * @Des
 */
@Controller
public class HomeController {
    /**
     * 跳轉到websocketDemo.html頁面,攜帶自定義的cid信息。
     * http://localhost:8300/demo/toWebSocketDemo/user
     *
     * @param cid
     * @param model
     * @return
     */
    @GetMapping("/demo/toWebSocketDemo/{cid}")
    public String toWebSocketDemo(@PathVariable String cid, Model model) {
        model.addAttribute("cid", cid);
        return "index";
    }

    @GetMapping("hello")
    @ResponseBody
    public String hi(HttpServletResponse response) {
        return "Hi";
    }
}

添加html

註意:html文件添加在application.properties配置的對應目錄中。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>聊天視窗</title>
</head>
<body>
<div>
我的用戶名:
<input type="text" th:value="${cid}" readonly="readonly" id="cid"/>
</div>
<div id="chat-windows" style="width: 400px; height: 300px;overflow: scroll;border: blue 1px solid;"></div>
<div>收消息人用戶名:<input id="toUserId" name="toUserId" type="text"></div>
<div>輸入你要說的話:<input id="contentText" name="contentText" type="text"></div>
<div>
    <button type="button" onclick="sendMessage()">發送消息</button>
</div>
</body>

<script type="text/javascript">
    var socket;
    if (typeof (WebSocket) == "undefined") {
        alert("您的瀏覽器不支持WebSocket");
    } else {
        console.log("您的瀏覽器支持WebSocket");
        //實現化WebSocket對象,指定要連接的伺服器地址與埠  建立連接

        var cid = document.getElementById("cid").value;
        console.log("cid-->" + cid);
        var reqUrl = "http://localhost:8300/websocket/" + cid;
        socket = new WebSocket(reqUrl.replace("http", "ws"));
        //打開事件
        socket.onopen = function () {
            console.log("Socket 已打開");
            //socket.send("這是來自客戶端的消息" + location.href + new Date());
        };
        //獲得消息事件
        socket.onmessage = function (msg) {
            console.log("onmessage--" + msg.data);
            //發現消息進入    開始處理前端觸發邏輯
            var chatWindows = document.getElementById("chat-windows");
            var pElement = document.createElement('p')
            pElement.innerText = msg.data;
            chatWindows.appendChild(pElement);
        };
        //關閉事件
        socket.onclose = function () {
            console.log("Socket已關閉");
        };
        //發生了錯誤事件
        socket.onerror = function () {
            alert("Socket發生了錯誤");
            //此時可以嘗試刷新頁面
        }
        //離開頁面時,關閉socket
        //jquery1.8中已經被廢棄,3.0中已經移除
        // $(window).unload(function(){
        //     socket.close();
        //});
    }

    function sendMessage() {
        if (typeof (WebSocket) == "undefined") {
            alert("您的瀏覽器不支持WebSocket");
        } else {
            var toUserId = document.getElementById('toUserId').value;
            var contentText = document.getElementById('cid').value + ":" + document.getElementById('contentText').value;
            var msg = '{"sid":"' + toUserId + '","message":"' + contentText + '"}';
            console.log(msg);
            var chatWindows = document.getElementById("chat-windows");
            var chatWindows = document.getElementById("chat-windows");
            var pElement = document.createElement('p');
            pElement.innerText = "我:" + document.getElementById('contentText').value;
            chatWindows.appendChild(pElement);
            socket.send(msg);
        }
    }

</script>
</html>

1對1模擬演練

啟動項目後,在瀏覽器訪問http://localhost:8300/demo/toWebSocketDemo/{cid} 跳轉到對應頁面,其中cid是用戶名。

為了便於1對1測試,這裡我們啟動兩個瀏覽器視窗。

http://localhost:8300/demo/toWebSocketDemo/陽光男孩

http://localhost:8300/demo/toWebSocketDemo/水晶女孩

按照要求輸入對方用戶信息之後,便可以輸入你要說的話,暢快聊起來了。

效果圖如下:

請叫我頭頭哥

當然,如果收消息人用戶名是自己的話,也可以自己給自己發送數據的。

群發模擬演練

為了便於群發測試,這裡我們啟動3個瀏覽器視窗。

http://localhost:8300/demo/toWebSocketDemo/陽光男孩

http://localhost:8300/demo/toWebSocketDemo/水晶女孩

http://localhost:8300/demo/toWebSocketDemo/路人A

由於sendToAll方法中定義群發的條件為:當不指定 toUserid時,則為群發。

效果圖如下:

請叫我頭頭哥

項目架構圖如下:

請叫我頭頭哥

v源碼地址

https://github.com/toutouge/javademosecond

其他參考/學習資料:


作  者:請叫我頭頭哥
出  處:http://www.cnblogs.com/toutou/
關於作者:專註於基礎平臺的項目開發。如有問題或建議,請多多賜教!
版權聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
特此聲明:所有評論和私信都會在第一時間回覆。也歡迎園子的大大們指正錯誤,共同進步。或者直接私信
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角推薦一下。您的鼓勵是作者堅持原創和持續寫作的最大動力!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 Hyper-V安裝文檔:在 Windows 10 上安裝 Hyper-V CentOS 系統下載:CentOS 國內鏡像源 8.5.2111 作者:易墨 發佈時間:2023.10.01 原文地址:https://www.cnblogs.com/morang/p/devops-hyperv-ce ...
  • 1. 複製切換 1.1. 複製是高可用性的基礎 1.1.1. 總是保留一份持續更新的副本數據,會讓災難恢復更簡單 1.2. “切換副本”(promoting a replica)和“故障切換”(failing over)是同義詞 1.2.1. 意味著源伺服器不再接收寫入,並將副本提升為新的源伺服器 ...
  • [webpack中文文檔](概念 | webpack 中文文檔 | webpack中文文檔 | webpack中文網 (webpackjs.com)): 本質上,webpack 是一個用於現代 JavaScript 應用程式的 靜態模塊打包工具。當 webpack 處理應用程式時,它會在內部從一個或 ...
  • 1 StyleSheet 一張 StyleSheet 由一系列 Rules 組成,這些 Rules 可以分成 2 大類: 1 Style Rule 2 At-Rule 下麵的例子展示了 Style Rule 和 At-Rule: // Style Rule div { background-colo ...
  • 代碼中在使用JUC、消息隊列、回調函數、消息中間件等提高程式性能的方式進行非同步處理時,一定要分清主次,哪些邏輯必須在主線程執行,哪些邏輯可以非同步處理。 ...
  • 前面幾天的學習,我們瞭解了Dart語言的特性(基礎語法概覽、迭代集合、非同步編程和Mixin高級特性)。今天我們深入學習Dart的變數,包括:空安全(Null safety)、變數預設值、延遲變數(late)、final變數和const常量…… ...
  • Java 21引入了兩個語言核心功能: 未命名的Java類你說 新的啟動協議:該協議允許更簡單地運行Java類,並且無需太多樣板 下麵一起來看個例子。通常,我們初學Java的時候,都會寫類似下麵這樣的 Hello World 程式: public class HelloWorld { public ...
  • 【中秋國慶不斷更】HarmonyOS對通知類消息的管理與發佈通知(上) 一、 通知概述 通知簡介 應用可以通過通知介面發送通知消息,終端用戶可以通過通知欄查看通知內容,也可以點擊通知來打開應用。 通知常見的使用場景: ​ ● 顯示接收到的短消息、即時消息等。 ​ ● 顯示應用的推送消息,如廣告、版本 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...