1. 程式人生 > >SpringBoot整合WebSocket,打造一個聊天室

SpringBoot整合WebSocket,打造一個聊天室

本文,我們來講下SpringBoot整合WebSocket,打造一個聊天室。

 

640?wx_fmt=png

WebSocket 是什麼?

 

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

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

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

因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。WebSocket 連線允許客戶端和伺服器之間進行全雙工通訊,以便任一方都可以通過建立的連線將資料推送到另一端。只需要建立一次連線,就可以一直保持連線狀態。這相比於輪詢方式的不停建立顯然效率要大大提高。

Web 瀏覽器和伺服器都必須實現 WebSockets 協議來建立和維護連線。由於 WebSockets 連線長期存在,與典型的HTTP連線不同,對伺服器有重要的影響。基於多執行緒或多程序的伺服器無法適用於 WebSockets,因為它旨在開啟連線,儘可能快地處理請求,然後關閉連線。任何實際的 WebSockets 伺服器端實現都需要一個非同步伺服器。

 

640?wx_fmt=png

使用idea建立SpringBoot專案

 

640?wx_fmt=gif

如果不使用上述方法匯入maven的,請使用以下程式碼:

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

 

640?wx_fmt=png

Spring注入Bean

 

package com.example.websocket.demo;
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();
    }
}

 

640?wx_fmt=png

編寫websocket服務類

 

package com.cloudkd.websocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;


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

//@ServerEndpoint("/websocket/{user}")
@ServerEndpoint("/websocket")
@Component
public class WebSocketServer {
    private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    //靜態變數,用來記錄當前線上連線數。應該把它設計成執行緒安全的。
    private static int onlineCount = 0;
    //concurrent包的執行緒安全Set,用來存放每個客戶端對應的MyWebSocket物件。
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
    //與某個客戶端的連線會話,需要通過它來給客戶端傳送資料
    private Session session;

    /**
     * 連線建立成功呼叫的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        //加入set中
        webSocketSet.add(this);
        //線上數加1
        addOnlineCount();
        log.info("有新連線加入!當前線上人數為" + getOnlineCount());
        try {
            sendMessage("連線成功");
        } catch (IOException e) {
            log.error("websocket IO異常");
        }
    }
    // //連線開啟時執行
    // @OnOpen
    // public void onOpen(@PathParam("user") String user, Session session) {
    //    currentUser = user;
    //    System.out.println("Connected ... " + session.getId());
    // }

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

    /**
     * 收到客戶端訊息後呼叫的方法
     *
     * @param message 客戶端傳送過來的訊息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("來自客戶端的訊息:" + message);
        //群發訊息
        for (WebSocketServer item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("發生錯誤");
        error.printStackTrace();
    }

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

    /**
     * 群發自定義訊息
     */
    public static void sendInfo(String message) {
        log.info(message);
        for (WebSocketServer item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException ignored) {
            }
        }
    }

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

    private static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    private static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

 

640?wx_fmt=png

編寫一個前端客戶端

 

圖中位置建立一個簡單的index.html頁面:

640?wx_fmt=png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        var websocket = null;
        //判斷當前瀏覽器是否支援WebSocket
        if ('WebSocket' in window) {
//這裡ws://192.168.1.111:8080/websocket 寫自己的ip和埠號
            websocket = new WebSocket("ws://192.168.1.111: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>
</head>
<body>
    <h3>Welcome</h3><br/>
    <input id="text" type="text"/>
    <button onclick="send()">Send</button>
    <button onclick="closeWebSocket()">Close</button>
    <div id="message"></div>
</body>
</html>

 

640?wx_fmt=png

啟動專案,做個測試

 

640?wx_fmt=png

點選啟動專案:

640?wx_fmt=png

啟動完成。訪問index.html測試一下:

640?wx_fmt=jpeg

okay,成功啟動。後臺日誌也有記錄:

640?wx_fmt=png

說個話試試:

640?wx_fmt=png

也可以多開幾個頁面測試下。好了,下面來完成服務端向客戶端推訊息。

 

640?wx_fmt=png

服務端向客戶端推訊息(後臺主動)

 

產生訊息的場景有多種,HTTP(s)、定時任務、MQ等,這裡我用一個HTTP請求的controller程式碼完成。

編寫一個pushWebController類:

package com.example.websocket.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class PushWebController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @GetMapping(value = "/pushWeb")
    public Map<String, Object> pushVideoListToWeb(String message) {
        Map<String, Object> result = new HashMap<String, Object>();
        try {
            WebSocketServer.sendInfo("有新客戶呼入,message:" + message);
            result.put("operationResult", true);
        } catch (Exception e) {
            result.put("operationResult", true);
        }
        return result;
    }
}

重新啟動專案。測試下:

640?wx_fmt=png

640?wx_fmt=png

成功了!到此位置,demo已經成功實現了。如果你恰好也有可以用WebSocket實現的類似場景,希望對你有幫助