WebSocket的實現與應用

  • 2019 年 10 月 3 日
  • 筆記

WebSocket的實現與應用

前言

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

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

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

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

傳統解決方案:

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

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

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

服務端(Response):No

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

服務端(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的理解&應用&場景