WebSocket的實現與應用

WebSocket的實現與應用

前言

說到websocket,就不得不提http協議的鏈接特色特色與交互模型。html

首先,http協議的特色是無狀態鏈接。即http的前一次鏈接與後一次鏈接是相互獨立的。前端

其次,http的交互模型是請求/應答模型。即交互是經過C/B端向S端發送一個請求,S端根據請求,返回一個響應。java

那麼這裏就有一個問題了--S端沒法主動向C/B端發送消息。而交互是雙方的事情,怎麼能限定一方發數據,另外一方接數據呢。web

傳統解決方案:

傳統的解決方案就倆字:輪詢。spring

長短鏈接輪詢就不詳細說了,就說說輪詢。大概的場景是這樣的:apache

客戶端(Request):有消息不?安全

服務端(Response):No服務器

客戶端(Request):有消息不?websocket

服務端(Response):No網絡

客戶端(Request):有消息不?

服務端(Response):No

客戶端(Request):有消息不?

服務端(Response):有了。你媽叫你回家吃飯。

客戶端(Request):有消息不?

服務端(Response):No

==================================> loop

看着都累,資源消耗那就更沒必要說了。尤爲有些對實時性要求高的數據,那可能就是1s請求一次。目測服務器已經淚奔。

websocket解決方案:

那麼websocket的解決方案,總結一下,就是:創建固定鏈接

說白了,就是C/B端與S端就一個websocket服務創建一個固定的鏈接,不斷開。

大概的場景是這樣的:

服務端:我創建了一個chat的websocket,歡迎你們鏈接。

客戶端:我要和你的chat的websocket鏈接,個人sid(惟一標識)是No.1

服務端:好的,我已經記住你了。若是有發往chat下No.1的消息,我會告訴你的。

客戶端:嗯。謝謝了哈。

==================================> 過了一段時間

(有一個請求調用了chat的websocket,而且指名是給No.1的消息)

服務端(發送消息給No.1):No.1,有你的消息。你媽媽叫你回家作做業。

客戶端(No.1):好的。我收到了。謝謝。

因爲此次只是簡單說一下websocket,因此就不深刻解讀網絡相關知識了。

應用場景

既然http沒法知足用戶的全部需求,那麼爲之誕生的websocket必然有其諸多應用場景。如:

  1. 實時顯示網站在線人數
  2. 帳戶餘額等數據的實時更新
  3. 多玩家網絡遊戲
  4. 多媒體聊天,如聊天室
  5. 。。。

其實總結一下,websocket的應用場景就倆字:實時

不管是多玩家網絡遊戲,網站在線人數等都是因爲實時性的需求,才用上了websocket(後面用縮寫ws)。

談幾個在我項目中用到的情景:

  1. 在線教育項目中的課件系統,經過ws實現學生端課件與教師端課件的實時交互
  2. 物聯網項目中的報警系統,經過ws實現報警信息的實時推送
  3. 大數據項目中的數據展現,經過ws實現數據的實時更新
  4. 物聯網項目中的硬件交互系統,經過ws實現硬件異步響應的展現

當你的項目中存在須要S端向C/B端發送數據的情形,那就能夠考慮上一個websocket了。

實現

服務端開發:

引入依賴:

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

添加配置:

忍不住想要吐槽,爲何不能夠如eureka等組件那樣,直接在啓動類寫一個註解就Ok了呢。看來還得之後本身動手,豐衣足食啊。

package com.renewable.center.warning.configuration;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    /**
     * Websocket的配置
     * 說白了就是引入Websocekt至spring容器
     */
    @Configuration
    public class WebSocketConfig {  
        
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {  
            return new ServerEndpointExporter();
        }  
      
    }

代碼實現:

WebSocketServer的實現:

package com.renewable.center.warning.controller.websocket;
    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.stereotype.Component;
    
    import javax.websocket.*;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    import java.io.IOException;
    import java.util.concurrent.CopyOnWriteArraySet;
    
    /**
     * @Description:
     * @Author: jarry
     */
    @Component
    @Slf4j
    @ServerEndpoint("/websocket/warning/{sid}")
    public class WarningWebSocketServer {
    
        // JUC包的線程安全Set,用來存放每一個客戶端對應的WarningWebSocketServer對象。
        // 用ConcurrentHashMap也是能夠的。說白了就是相似線程池中的BlockingQueue那樣做爲一個容器
        private static CopyOnWriteArraySet<WarningWebSocketServer> warningWebSocketSet = new CopyOnWriteArraySet<WarningWebSocketServer>();
    
        // 與某個客戶端的鏈接會話,須要經過它來給客戶端發送數據
        private Session session;
    
        // 接收sid
        private String sid="";
    
        /**
         * 創建websocket鏈接
         * 看起來很像JSONP的回調,由於前端那裏是Socket.onOpne()
         * @param session
         * @param sid
         */
        @OnOpen
        public void onOpen(Session session, @PathParam("sid") String sid){
            this.session = session;
            this.sid = sid;
            warningWebSocketSet.add(this);
    
            sendMessage("websocket connection has created.");
        }
    
        /**
         * 關閉websocket鏈接
         */
        @OnClose
        public void onClose(){
            warningWebSocketSet.remove(this);
            log.info("there is an wsConnect has close .");
        }
    
        /**
         * websocket鏈接出現問題時的處理
         */
        @OnError
        public void onError(Session session, Throwable error){
            log.error("there is an error has happen ! error:{}",error);
        }
    
        /**
         * websocket的server端用於接收消息的(目測是用於接收前端經過Socket.onMessage發送的消息)
         * @param message
         */
        @OnMessage
        public void onMessage(String message){
            log.info("webSocketServer has received a message:{} from {}", message, this.sid);
    
            // 調用消息處理方法(此時針對的WarningWebSocektServer對象,只是一個實例。這裏進行消息的單發)
            // 目前這裏尚未處理邏輯。故爲了便於前端調試,這裏直接返回消息
            this.sendMessage(message);
        }
    
        /**
         * 服務器主動推送消息的方法
         */
        public void sendMessage(String message){
            try {
                this.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.warn("there is an IOException:{}!",e.toString());
            }
        }
    
        public static void sendInfo(String sid, String message){
            for (WarningWebSocketServer warningWebSocketServerItem : warningWebSocketSet) {
                if (StringUtils.isBlank(sid)){
                    // 若是sid爲空,即羣發消息
                    warningWebSocketServerItem.sendMessage(message);
                    log.info("Mass messaging. the message({}) has sended to sid:{}.", message,warningWebSocketServerItem.sid);
                }
                if (StringUtils.isNotBlank(sid)){
                    if (warningWebSocketServerItem.sid.equals(sid)){
                        warningWebSocketServerItem.sendMessage(message);
                        log.info("single messaging. message({}) has sended to sid:{}.", message, warningWebSocketServerItem.sid);
                    }
                }
            }
        }
    
    }

WesocketController

爲了便於調試與展現效果,寫一個控制層,用於推送消息

package com.renewable.center.warning.controller.websocket;
    
    import com.renewable.terminal.terminal.common.ServerResponse;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.io.IOException;
    
    /**
     * @Description: 用於測試WebsocketServer
     * @Author: jarry
     */
    @Controller
    @RequestMapping("/websocket/test/")
    public class WarningWebsocketController {
    
        @GetMapping("link.do")
        @ResponseBody
        public ServerResponse link(@RequestParam(name = "sid") int sid){
            return ServerResponse.createBySuccessMessage("link : "+sid);
        }
    
        /**
         * 調用WarningWebsocketServer的消息推送方法,從而進行消息推送
         * @param sid 鏈接WarningWebsocketServer的前端的惟一標識。若是sid爲空,即表示向全部鏈接WarningWebsocketServer的前端發送相關消息
         * @param message 須要發送的內容主體
         * @return
         */
        @ResponseBody
        @RequestMapping("push.do")
        public ServerResponse pushToWeb(@RequestParam(name = "sid", defaultValue = "") String sid, @RequestParam(name = "message")  String message) {
            WarningWebSocketServer.sendInfo(sid, message);
            return ServerResponse.createBySuccessMessage(message+"@"+sid+" has send to target.");
        }
    }

WesocketTestIndex

這裏創建了一個B端頁面,用於與S端進行交互,演示。

<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>WebsocketTestIndex</title>
    </head>
    <body>
    
    <h1>Websocket Test</h1>
    <script>
        var socket;
        if(typeof(WebSocket) == "undefined") {
            console.log("Your browser not support WebSocket !");
        }else{
            console.log("Your browser support WebSocket");
            // 實例化WebSocket對象
            // 指定要鏈接的服務器地址與端口
            // 創建鏈接
            socket = new WebSocket("ws://localhost:10706/websocket/warning/2");
            // 打開事件
            socket.onopen = function() {
                console.log("You has connect to WebSocketServer");
            };
            // 得到消息事件
            socket.onmessage = function(msg) {
                // 打印接收到的消息
                console.log(msg.data);
            };
            // 關閉事件
            socket.onclose = function() {
                console.log("Socket has closed");
            };
            // 發生了錯誤事件
            socket.onerror = function() {
                alert("Socket happen an error !");
            }
        }
    </script>
    </body>
    </html>

效果展現

再次強調,圖片很大很清晰。若是看不清楚,請單獨打開圖片。

B端網頁初始化:

調用S端WarningWebsocketController下pushToWeb()接口,對sid=2的B端發送消息:

B端網頁接收到專門發給sid=2的消息後的效果:

調用S端WarningWebsocketController下pushToWeb()接口,全部鏈接該websocket的B端羣發消息:

B端網頁接收到羣發消息後的效果:

S端接收到消息後的日誌打印:

S端在B端關閉鏈接後的日誌打印:

總結

至此,websocket的應用就算入門了。至於實際的使用,其實就是服務端本身調用WebSocket的sendInfo接口。固然也能夠本身擴展更爲細緻的邏輯,方法等。

另外,須要注意的是,別忘了及時關閉webocket的鏈接。尤爲在負載較大的狀況下,更須要注意即便關閉沒必要要的鏈接。

架構的技術選型,須要的不是最好的,而是最適合的。

擴展:

若是想要了解更多概念上的細節,能夠看看這篇文章:

websocket的理解&應用&場景

相關文章
相關標籤/搜索