同事說他一直用輪詢解決數據實時性!來springboot整合websocket原生版|Java 開發實戰

這是我參與更文挑戰的第2天,活動詳情查看: 更文挑戰前端

本文正在參加「Java主題月 - Java 開發實戰」,詳情查看 活動連接java

[TOC]nginx

HTTP請求用於咱們開發以及用戶之間最爲普遍的一種協議,在HTTP中咱們能夠簡單的經過瀏覽器獲取到咱們須要的內容(頁面)。可是他也有他的侷限性。今天咱們的主角websocket將爲展示他的功能web

HTTP缺點

  • HTTP只能有client發起請求服務端作出響應返回結果。服務端是不能主動向客戶端發送信息的。因此有的網站在解決實時性上採用的是頁面的定時器功能。簡而言之是客戶端定時的向服務端發送請求

這樣多多少少的形成資源的浪費。spring

  • HTTP是無記憶的。每次請求服務端是沒法瞭解到客戶端以前的行爲的,可是咱們平時瀏覽器網站的時候感受瀏覽器是知道咱們以前作的事情的。這是網站在請求是添加的cookie這些服務端提供的數據。對咱們而言咱們感受是有記憶的。實則否則後端

  • HTTP1.1以後採用了短鏈接、長鏈接兩種方式。HTTP請求的發送每次也須要三次握手機制。因此每次的鏈接耗費資源。1.1後才必定時間內HTTP其實採用的是長鏈接,這樣能夠減小資源的開銷瀏覽器

  • 上述說道的長鏈接有人可能有疑問,其實HTTP協議是基於TCP協議開發的。因此天然有長鏈接的特性。緩存

HTTP websocket區別

  • HTTP由於短鏈接的特性因此是無記憶的。爲了解決這個問題每一個請求都是由General+Request Head+Request Paylaod+Response Headers組成的。其中Heads就是瀏覽器須要記住的東西,每次傳遞來傳遞去的非常耗費性能。安全

  • websocket因爲是長鏈接特性,一次鏈接就能夠一直的雙向通訊。從載體來講websocket關注的更少,只須要通訊當前須要的信息。歷史信息雙方都是有的。springboot

websocket原理

使用場景

springboot整合websocket

環境準備

  • 在springboot基礎上引入websocket的jar。由於咱們已經繼承了springboot因此咱們不須要添加版本號
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

複製代碼
  • 而後在項目中加入以下的配置,配置websocket須要的bean,交由spring管理
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

複製代碼
  • 而後咱們就能夠編寫websocket的接受和發送等事件了。 我在次基礎上封裝了一下,先抽象除一個websocket
public abstract class BaseWebSocket extends BaseController{
    /** * 靜態變量,用來記錄當前在線鏈接數。應該把它設計成線程安全的。 */
    private int onlineCount = 0;

    /** * concurrent包的線程安全Set,用來存放每一個客戶端對應的MyWebSocket對象。 */
    public CopyOnWriteArraySet<BaseWebSocket> webSocketSet = new CopyOnWriteArraySet<BaseWebSocket>();

    /** * 與某個客戶端的鏈接會話,須要經過它來給客戶端發送數據 */
    public Session session;

    private Logger log = LoggerFactory.getLogger("BaseWebSocket");


    /** * 鏈接創建成功調用的方法 * @param session */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) throws IOException {
        this.session = session;
        //加入set中
        webSocketSet.add(this);
        //在線數加1
        addOnlineCount();
        log.debug("有新鏈接加入!當前在線人數爲" + getOnlineCount());
        //發送信息
        MultiMap multiMap = new MultiMap();
        if (null!=session.getQueryString()&&!"".equals(session.getQueryString())) {
            UrlEncoded.decodeTo(session.getQueryString(), multiMap, "UTF-8");
        }
        sendInfo(defaultMessage(multiMap));
    }

    /** * 鏈接關閉調用的方法 */
    @OnClose
    public void onClose() {
        //從set中刪除
        webSocketSet.remove(this);
        //在線數減1
        subOnlineCount();
        log.info("有一鏈接關閉!當前在線人數爲" + getOnlineCount());
    }

    /** * 收到客戶端消息後調用的方法 * @param message 客戶端發送過來的消息 * @param session 緩存 * @throws IOException */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        this.session = session;
        try {
            Map paraMap = (Map) JSONObject.parse(message);
            handlerMessage(paraMap);
        } catch (JSONException e) {
            MultiMap multiMap = new MultiMap();
            UrlEncoded.decodeTo(message, multiMap, "UTF-8");
            handlerMessage(multiMap);
            //throw new BusinessException("傳遞消息格式錯誤(Json)");
        }
    }

    /** * 處理消息接受 * @param paraMap 接受到map類型的參數 */
    public void handlerMessage(Map paraMap) {
        try {
            sendInfo(defaultMessage(paraMap));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Object defaultMessage(Map<String, Object> paraMap) {
        Object obj = new Object();
        try {
            obj = defauleMessage(paraMap);
        } catch (BusinessException e) {
            return formatReturnAppData(e.getMessage());
        }
        return obj;
    }
    /** * 默認的發送數據 * @param paraMap 鏈接時傳遞的參數 * @return */
    public abstract Object defauleMessage(Map<String, Object> paraMap);

    public static boolean isJson(String content) {
        try {
            JSONObject.parse(content);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    /** * 發生錯誤時調用 @OnError **/
    public void onError(Session session, Throwable error) {
        log.error("onMessage方法異常"+error.toString());
        error.printStackTrace();
    }


    /** * 發送消息需注意方法加鎖synchronized,避免阻塞報錯 * 注意session.getBasicRemote()與session.getAsyncRemote()的區別 * @param message * @throws IOException */
    public synchronized void sendMessage(Object message) throws IOException {
// this.session.getBasicRemote().sendText(message);
        this.session.getAsyncRemote().sendText(JSONObject.toJSONString(message));
    }


    /** * 羣發自定義消息 * */
    public void sendInfo(Object message) throws IOException {
        for (BaseWebSocket item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                continue;
            }
        }
    }

    public synchronized int getOnlineCount() {
        return onlineCount;
    }

    public synchronized void addOnlineCount() {
        onlineCount++;
    }

    public synchronized void subOnlineCount() {
        onlineCount--;
    }
}

複製代碼
  • 而後咱們新增一個websocket的時候咱們只須要繼承一下這個抽象的websocket的類而後重現裏面的defauleMessage方法就好了。在類上須要加上以下註解
@ServerEndpoint(value = "/accident/getAccident")
@Component
public class AccidentGetAccident extends BaseWebSocket {

    AccidentController accidentController;


    @Override
    public Object defauleMessage(Map<String, Object> paraMap) {
        accidentController = ContextUtil.getApplicationContext().getBean(AccidentController.class);
        return accidentController.getAccident(paraMap);
    }
}

複製代碼

客戶端鏈接

  • 在前端只須要構造WebSocket這個對象,這個對象須要傳遞一個參數-鏈接地址
ws = new WebSocket(wsUrl);

複製代碼
  • 而後咱們就能夠重寫ws裏面的一些事件了。前端的事件咱們後端也是基本對應相同的事件的。這樣先後端就完成了事件的交互!
ws.onclose = function () {
    console.log('連接關閉');
};
ws.onerror = function() {
    console.log('發生異常了');
};
ws.onopen = function () {
    console.log('新建鏈接');
};
ws.onmessage = function (event) {
    console.log("接收到服務端反饋的信息了");
}

複製代碼
  • 可是咱們得考慮一種狀況就是,咱們的服務端由於某種因素形成服務宕機,這個時候客戶端捕獲到onclose事件,此次的鏈接就會結束,可是服務端可能在短期內搶修好了。這個時候咱們要客戶端進行寵幸刷新纔會進行重連。websocket正常都是用在大屏的時候,有時認爲刷新並非很方便,因此這個時候須要咱們的客戶端有重連機制。
var lockReconnect = false;//避免重複鏈接
    var wsUrl = "ws://127.0.0.1:8088/accident/getAccident?entId=zhonghuaxingzhong";
    var ws;
    var tt;
    function createWebSocket() {
        try {
            ws = new WebSocket(wsUrl);
            init();
        } catch(e) {
            console.log(e+'catch');
            reconnect(wsUrl);
        }
    }
    function init() {
        ws.onclose = function () {
            console.log('連接關閉');
            reconnect(wsUrl);
        };
        ws.onerror = function() {
            console.log('發生異常了');
            reconnect(wsUrl);
        };
        ws.onopen = function () {
            //心跳檢測重置
            heartCheck.start();
        };
        ws.onmessage = function (event) {
            setMessageInnerHTML(event.data);
            //拿到任何消息都說明當前鏈接是正常的
            console.log('接收到消息');
            heartCheck.start();
        }
    }
    function reconnect(url) {
        if(lockReconnect) {
            return;
        };
        lockReconnect = true;
        //沒鏈接上會一直重連,設置延遲避免請求過多
        tt && clearTimeout(tt);
        tt = setTimeout(function () {
            createWebSocket(url);
            lockReconnect = false;
        }, 4000);
    }
    //心跳檢測
    var heartCheck = {
        timeout: 3000,
        timeoutObj: null,
        serverTimeoutObj: null,
        start: function(){

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

複製代碼

總結

  • springboot將websocket作了層封裝。咱們只須要配置咱們websocket服務端點後就能夠與前端進行交互了。可是在nginx進行代理的時候咱們須要特殊注意下。由於websocket和http創建是有區別的。http是無狀態的請求響應以後就沒有後續了。可是websocket須要一段時間保持鏈接。還有一點是websocket也不會一直保持鏈接。在必定時間沒有進行交互就會自動斷開。這樣保證資源不被一直佔用

你們好!原創不易!springcloud系列+jvm系列正在創做中。。。。。

相關文章
相關標籤/搜索