2018年11月22日 星期四

SpringBoot WebSocket Broadcast 範例

  WebSocket 如其名,即是 Socket over Web。可以做到使用者的 Browser 與 Server 直接建立 Socket 連線,不像傳統  HTTP Request/Response 一來一回,傳完 TCP/IP 連線就結束。WebSocket 同 Socket 可以做到 Connection 不中斷,Server 持續不斷的拋送訊息給 Browser 端,並且在 UI 的上不斷更新資料。不再需像過去使用週期性的javascript timeout polling方式向 Server 要最新狀態,週期設太寬,使用者端就比較晚得知數值狀態的改變,設太密又造成 Server Loading。



  本篇範例是建立一個 WebSocket 通道,連上此 WebSocket 的所有 Client 端,每5秒會收到 Server 廣播的訊息。使用 Spring Boot 1.4.2 版本,gradle設定如下:


dependencies {
      // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket
      compile group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '1.4.2.RELEASE'

      // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
      compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '1.4.2.RELEASE'

      // https://mvnrepository.com/artifact/org.slf4j/slf4j-api
      compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
}


  於 WebSocketConfiguration 中綁定 WebSocketHandler 與 URI 之關係,本例的 WebSocket URI 為 ws://IP:Port/greet。Client須知此URI,以建立 WebSocket 連線。
@Configuration
public class WebSocketConfiguration implements WebSocketConfigurer {

      @Autowired
      private GreetWebSocketHandler greetWebSocketHandler;

      public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            registry.addHandler(greetWebSocketHandler, "/greet").setAllowedOrigins("*");
      }

}


  WebSocketHandler用來處理 WebSocket連線的相關 Event,Event 包括 Client 建立連線、關閉連線、傳輸錯誤、Client送來訊息等 @Component
public class GreetWebSocketHandler extends TextWebSocketHandler {

      private final Logger logger = LoggerFactory.getLogger(GreetWebSocketHandler.class);

      //Map: 儲存Client Session。
      public ConcurrentMap sessions = new ConcurrentHashMap<>();

      @Override
      public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {

            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            exception.printStackTrace(pw);

            logger.error("TransportError:" + session.getId());
            logger.error(sw.toString());
      }

      @Override
      public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
            logger.info("SessionClosed: " + session.getId());
            if(sessions.containsKey(session.getId())) {
                  //將 Session 自 Map 刪除
                  sessions.remove(session.getId());
            }
      }

      @Override
      public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            logger.info("SessionCreated: " + session.getId());
            //將 Session 加到 Map
            sessions.put(session.getId(), session);
      }

      @Override
      protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

            logger.info("Message: " + message.getPayload());
            session.sendMessage(new TextMessage("Hi, " + message.getPayload()));
      }
}


啟動 Scheduler,每 5 秒送訊息給所有 Client 。 @Component
public class GreetingTaskScheduler {

      @Autowired
      private GreetingTask task;

      @Autowired
      private ThreadPoolTaskScheduler scheduler;

      @PostConstruct
      private void start() {
            scheduler.setPoolSize(1);
            scheduler.setThreadNamePrefix("GreetingTaskScheduler");
            scheduler.scheduleAtFixedRate(task, 5000);

      }

      @Component
      public class GreetingTask implements Runnable {

            private Logger logger = LoggerFactory.getLogger(GreetingTask.class);
            private SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

            @Autowired
            GreetWebSocketHandler handler;

            @Override
            public void run() {
                  String msg = "The time is now " + dateFormat.format(new Date());

                  WebSocketSession[] sessions = handler.sessions.values().toArray(new WebSocketSession[]{});
                  //Broadcast訊息給所有Client
                  for(WebSocketSession session: sessions) {
                        try {
                              session.sendMessage(new TextMessage(msg));
                              logger.info(session.getId() + ": The time is now {}", dateFormat.format(new Date()));
                        } catch (IOException e) {
                        }
                  }
            }
      }
}


Client 端測試:
使用 Chrome 的擴充功能 "Simple WebSocket Client"進行測試。
連到 ws://IP:PORT/greet後,每 5 秒收到 Server 廣播的 Server Side 時間。
多開幾個頁籤進行連線,每個頁籤在 Server 端視為不同 Session (session id 不同),皆可收到廣播的訊息。



















另外,javascript部份也蠻簡單,

可參考 https://www.tutorialspoint.com/html5/html5_websocket.htm
<!DOCTYPE HTML>
<html>
  <head>
    <script type = "text/javascript">
      function WebSocketTest() {

         if ("WebSocket" in window) {
           console.log("WebSocket is supported by your Browser!");

           // Let us open a web socket
           var ws = new WebSocket("ws://localhost:8080/greet");

           ws.onopen = function() {

               // Web Socket is connected, send data using send()
               ws.send("Message to send");
               console.log("Message to send");
           };

           ws.onmessage = function (evt) {
               var received_msg = evt.data;
               console.log(received_msg);
           };

           ws.onclose = function() {

               // websocket is closed.
               console.log("closed");
           };
        } else {
           // The browser doesn't support WebSocket
           console.log("WebSocket NOT supported by your Browser!");
        }
      }
    </script>
  </head>

  <body>
    <div id = "sse">
      <a href = "javascript:WebSocketTest()">Run WebSocket</a>
    </div>
 </body>
</html>

 

以上~


沒有留言: